Compare commits
44 Commits
bb27092e3b
...
main
Author | SHA1 | Date | |
---|---|---|---|
881e16b8d7 | |||
5174453261 | |||
be5b561799 | |||
943a51ec5e | |||
ac4268c0c7 | |||
56b717cfab | |||
fc74957bb7 | |||
d88a85233a | |||
09549aabea | |||
![]() |
f2423b34a8 | ||
d2c4210757 | |||
![]() |
a62ba324f2 | ||
ce3084f5ff | |||
ca0f9fbd68 | |||
2ffed940c0 | |||
52efb51a3c | |||
7cd5107c8a | |||
aab0e0b780 | |||
a0eddece86 | |||
c74500adfd | |||
c8d5868c60 | |||
f7647829bd | |||
ea65f51704 | |||
965ba4c80b | |||
de01d2507f | |||
2583544779 | |||
ff72f7c9fa | |||
0327892f02 | |||
![]() |
e7a8b314ac | ||
6d021e8d6b | |||
4ea9068850 | |||
2e4f45054a | |||
fb1bb429c3 | |||
998248acb7 | |||
84ad3432c9 | |||
161f4f4073 | |||
e120d19dc8 | |||
5755109dc8 | |||
652ea26b32 | |||
e991818c7d | |||
67d861325a | |||
8a6a35269c | |||
cf1866af69 | |||
76e1c767ae |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@
|
|||||||
.history
|
.history
|
||||||
/frontend/node_modules/*
|
/frontend/node_modules/*
|
||||||
db.sqlite
|
db.sqlite
|
||||||
|
config.toml
|
||||||
|
421
Cargo.lock
generated
421
Cargo.lock
generated
@@ -17,6 +17,41 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aead"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aes"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cipher",
|
||||||
|
"cpufeatures",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aes-gcm"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
|
||||||
|
dependencies = [
|
||||||
|
"aead",
|
||||||
|
"aes",
|
||||||
|
"cipher",
|
||||||
|
"ctr",
|
||||||
|
"ghash",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
@@ -53,17 +88,6 @@ version = "1.7.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "async-trait"
|
|
||||||
version = "0.1.88"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atoi"
|
name = "atoi"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@@ -156,22 +180,6 @@ dependencies = [
|
|||||||
"tower-service",
|
"tower-service",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "axum-messages"
|
|
||||||
version = "0.8.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d67ce6e7bc1e1e71f2a4e86d418045a29c63c4ebb631f3d9bb2f81c4958ea391"
|
|
||||||
dependencies = [
|
|
||||||
"axum-core",
|
|
||||||
"http",
|
|
||||||
"parking_lot",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tower",
|
|
||||||
"tower-sessions-core",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backtrace"
|
name = "backtrace"
|
||||||
version = "0.3.75"
|
version = "0.3.75"
|
||||||
@@ -290,6 +298,16 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cipher"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"inout",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@@ -311,7 +329,11 @@ version = "0.18.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aes-gcm",
|
||||||
|
"base64",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
|
"rand",
|
||||||
|
"subtle",
|
||||||
"time",
|
"time",
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
@@ -387,9 +409,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
|
"rand_core",
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ctr"
|
||||||
|
version = "0.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
||||||
|
dependencies = [
|
||||||
|
"cipher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
version = "0.7.10"
|
version = "0.7.10"
|
||||||
@@ -408,7 +440,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -509,20 +540,6 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures"
|
|
||||||
version = "0.3.31"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
|
||||||
dependencies = [
|
|
||||||
"futures-channel",
|
|
||||||
"futures-core",
|
|
||||||
"futures-io",
|
|
||||||
"futures-sink",
|
|
||||||
"futures-task",
|
|
||||||
"futures-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -567,17 +584,6 @@ version = "0.3.31"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-macro"
|
|
||||||
version = "0.3.31"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -598,7 +604,6 @@ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-macro",
|
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -640,6 +645,16 @@ dependencies = [
|
|||||||
"wasi 0.14.2+wasi-0.2.4",
|
"wasi 0.14.2+wasi-0.2.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ghash"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
|
||||||
|
dependencies = [
|
||||||
|
"opaque-debug",
|
||||||
|
"polyval",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gimli"
|
name = "gimli"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
@@ -661,8 +676,8 @@ dependencies = [
|
|||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"bstr",
|
"bstr",
|
||||||
"log",
|
"log",
|
||||||
"regex-automata",
|
"regex-automata 0.4.9",
|
||||||
"regex-syntax",
|
"regex-syntax 0.8.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -963,7 +978,7 @@ dependencies = [
|
|||||||
"globset",
|
"globset",
|
||||||
"log",
|
"log",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-automata",
|
"regex-automata 0.4.9",
|
||||||
"same-file",
|
"same-file",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
@@ -979,6 +994,15 @@ dependencies = [
|
|||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inout"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "io-uring"
|
name = "io-uring"
|
||||||
version = "0.7.9"
|
version = "0.7.9"
|
||||||
@@ -1061,7 +1085,6 @@ checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
"serde",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1070,6 +1093,15 @@ version = "0.4.27"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchers"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||||
|
dependencies = [
|
||||||
|
"regex-automata 0.1.10",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchit"
|
name = "matchit"
|
||||||
version = "0.8.4"
|
version = "0.8.4"
|
||||||
@@ -1161,6 +1193,16 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nu-ansi-term"
|
||||||
|
version = "0.46.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
||||||
|
dependencies = [
|
||||||
|
"overload",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint-dig"
|
name = "num-bigint-dig"
|
||||||
version = "0.8.4"
|
version = "0.8.4"
|
||||||
@@ -1229,6 +1271,18 @@ version = "1.21.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opaque-debug"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "overload"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
@@ -1312,6 +1366,18 @@ version = "0.3.32"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "polyval"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"opaque-debug",
|
||||||
|
"universal-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -1419,8 +1485,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-automata",
|
"regex-automata 0.4.9",
|
||||||
"regex-syntax",
|
"regex-syntax 0.8.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||||
|
dependencies = [
|
||||||
|
"regex-syntax 0.6.29",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1431,9 +1506,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-syntax",
|
"regex-syntax 0.8.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.6.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
@@ -1524,7 +1605,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"siphasher",
|
"siphasher",
|
||||||
"toml",
|
"toml 0.8.23",
|
||||||
"triomphe",
|
"triomphe",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1646,6 +1727,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@@ -1693,6 +1783,15 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sharded-slab"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
@@ -2024,6 +2123,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thread_local"
|
||||||
|
version = "1.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.41"
|
version = "0.3.41"
|
||||||
@@ -2140,11 +2248,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned 0.6.9",
|
||||||
"toml_datetime",
|
"toml_datetime 0.6.11",
|
||||||
"toml_edit",
|
"toml_edit",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned 1.0.0",
|
||||||
|
"toml_datetime 0.7.0",
|
||||||
|
"toml_parser",
|
||||||
|
"toml_writer",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "0.6.11"
|
version = "0.6.11"
|
||||||
@@ -2154,6 +2277,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.22.27"
|
version = "0.22.27"
|
||||||
@@ -2162,18 +2294,33 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned 0.6.9",
|
||||||
"toml_datetime",
|
"toml_datetime 0.6.11",
|
||||||
"toml_write",
|
"toml_write",
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_parser"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10"
|
||||||
|
dependencies = [
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_write"
|
name = "toml_write"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_writer"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@@ -2190,22 +2337,6 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tower-cookies"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36"
|
|
||||||
dependencies = [
|
|
||||||
"axum-core",
|
|
||||||
"cookie",
|
|
||||||
"futures-util",
|
|
||||||
"http",
|
|
||||||
"parking_lot",
|
|
||||||
"pin-project-lite",
|
|
||||||
"tower-layer",
|
|
||||||
"tower-service",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-http"
|
name = "tower-http"
|
||||||
version = "0.6.6"
|
version = "0.6.6"
|
||||||
@@ -2244,57 +2375,6 @@ version = "0.3.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tower-sessions"
|
|
||||||
version = "0.14.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "43a05911f23e8fae446005fe9b7b97e66d95b6db589dc1c4d59f6a2d4d4927d3"
|
|
||||||
dependencies = [
|
|
||||||
"async-trait",
|
|
||||||
"http",
|
|
||||||
"time",
|
|
||||||
"tokio",
|
|
||||||
"tower-cookies",
|
|
||||||
"tower-layer",
|
|
||||||
"tower-service",
|
|
||||||
"tower-sessions-core",
|
|
||||||
"tower-sessions-memory-store",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tower-sessions-core"
|
|
||||||
version = "0.14.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ce8cce604865576b7751b7a6bc3058f754569a60d689328bb74c52b1d87e355b"
|
|
||||||
dependencies = [
|
|
||||||
"async-trait",
|
|
||||||
"axum-core",
|
|
||||||
"base64",
|
|
||||||
"futures",
|
|
||||||
"http",
|
|
||||||
"parking_lot",
|
|
||||||
"rand",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror",
|
|
||||||
"time",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tower-sessions-memory-store"
|
|
||||||
version = "0.14.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fb05909f2e1420135a831dd5df9f5596d69196d0a64c3499ca474c4bd3d33242"
|
|
||||||
dependencies = [
|
|
||||||
"async-trait",
|
|
||||||
"time",
|
|
||||||
"tokio",
|
|
||||||
"tower-sessions-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.41"
|
version = "0.1.41"
|
||||||
@@ -2325,6 +2405,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"valuable",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-log"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-subscriber"
|
||||||
|
version = "0.3.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
||||||
|
dependencies = [
|
||||||
|
"matchers",
|
||||||
|
"nu-ansi-term",
|
||||||
|
"once_cell",
|
||||||
|
"regex",
|
||||||
|
"sharded-slab",
|
||||||
|
"smallvec",
|
||||||
|
"thread_local",
|
||||||
|
"tracing",
|
||||||
|
"tracing-core",
|
||||||
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2377,6 +2487,16 @@ version = "0.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
|
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "universal-hash"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unsafe-libyaml"
|
name = "unsafe-libyaml"
|
||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
@@ -2418,6 +2538,12 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "valuable"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
@@ -2543,15 +2669,18 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"axum-extra",
|
"axum-extra",
|
||||||
"axum-messages",
|
|
||||||
"chrono",
|
"chrono",
|
||||||
"maud",
|
"maud",
|
||||||
|
"rand",
|
||||||
"rust-i18n",
|
"rust-i18n",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"toml 0.9.5",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tower-sessions",
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2565,6 +2694,22 @@ dependencies = [
|
|||||||
"wasite",
|
"wasite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-i686-pc-windows-gnu",
|
||||||
|
"winapi-x86_64-pc-windows-gnu",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-util"
|
name = "winapi-util"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
@@ -2574,6 +2719,12 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
|
@@ -5,14 +5,17 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
axum-extra = { version = "0.10", features = ["cookie"] }
|
axum-extra = { version = "0.10", features = ["cookie-private", "cookie"] }
|
||||||
axum-messages = "0.8"
|
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
maud = { version = "0.27", features = ["axum"] }
|
maud = { version = "0.27", features = ["axum"] }
|
||||||
|
rand = "0.8"
|
||||||
rust-i18n = "3.1"
|
rust-i18n = "3.1"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "macros", "chrono"] }
|
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "macros", "chrono"] }
|
||||||
|
time = "0.3"
|
||||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
||||||
|
toml = "0.9"
|
||||||
tower-http = { version = "0.6", features = ["fs"] }
|
tower-http = { version = "0.6", features = ["fs"] }
|
||||||
tower-sessions = "0.14"
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
uuid = { version = "1.17", features = ["v4", "serde"] }
|
uuid = { version = "1.17", features = ["v4", "serde"] }
|
||||||
|
468
bad/bad-list.txt
Normal file
468
bad/bad-list.txt
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
2g1c
|
||||||
|
2 girls 1 cup
|
||||||
|
acrotomophilia
|
||||||
|
alabama hot pocket
|
||||||
|
alaskan pipeline
|
||||||
|
anal
|
||||||
|
anilingus
|
||||||
|
anus
|
||||||
|
apeshit
|
||||||
|
arsehole
|
||||||
|
ass
|
||||||
|
asshole
|
||||||
|
assmunch
|
||||||
|
auto erotic
|
||||||
|
autoerotic
|
||||||
|
babeland
|
||||||
|
baby batter
|
||||||
|
baby juice
|
||||||
|
ball gag
|
||||||
|
ball gravy
|
||||||
|
ball kicking
|
||||||
|
ball licking
|
||||||
|
ball sack
|
||||||
|
ball sucking
|
||||||
|
bangbros
|
||||||
|
bangbus
|
||||||
|
bareback
|
||||||
|
barely legal
|
||||||
|
barenaked
|
||||||
|
bastard
|
||||||
|
bastardo
|
||||||
|
bastinado
|
||||||
|
bbw
|
||||||
|
bdsm
|
||||||
|
beaner
|
||||||
|
beaners
|
||||||
|
beaver cleaver
|
||||||
|
beaver lips
|
||||||
|
beastiality
|
||||||
|
bestiality
|
||||||
|
big black
|
||||||
|
big breasts
|
||||||
|
big knockers
|
||||||
|
big tits
|
||||||
|
bimbos
|
||||||
|
birdlock
|
||||||
|
bitch
|
||||||
|
bitches
|
||||||
|
black cock
|
||||||
|
blonde action
|
||||||
|
blonde on blonde action
|
||||||
|
blowjob
|
||||||
|
blow job
|
||||||
|
blow your load
|
||||||
|
blue waffle
|
||||||
|
blumpkin
|
||||||
|
bollocks
|
||||||
|
bondage
|
||||||
|
boner
|
||||||
|
boob
|
||||||
|
boobs
|
||||||
|
booty call
|
||||||
|
brown showers
|
||||||
|
brunette action
|
||||||
|
bukkake
|
||||||
|
bulldyke
|
||||||
|
bullet vibe
|
||||||
|
bullshit
|
||||||
|
bung hole
|
||||||
|
bunghole
|
||||||
|
busty
|
||||||
|
butt
|
||||||
|
buttcheeks
|
||||||
|
butthole
|
||||||
|
camel toe
|
||||||
|
camgirl
|
||||||
|
camslut
|
||||||
|
camwhore
|
||||||
|
carpet muncher
|
||||||
|
carpetmuncher
|
||||||
|
chocolate rosebuds
|
||||||
|
cialis
|
||||||
|
circlejerk
|
||||||
|
cleveland steamer
|
||||||
|
clit
|
||||||
|
clitoris
|
||||||
|
clover clamps
|
||||||
|
clusterfuck
|
||||||
|
cock
|
||||||
|
cocks
|
||||||
|
coprolagnia
|
||||||
|
coprophilia
|
||||||
|
cornhole
|
||||||
|
coon
|
||||||
|
coons
|
||||||
|
creampie
|
||||||
|
cum
|
||||||
|
cumming
|
||||||
|
cumshot
|
||||||
|
cumshots
|
||||||
|
cunnilingus
|
||||||
|
cunt
|
||||||
|
darkie
|
||||||
|
date rape
|
||||||
|
daterape
|
||||||
|
deep throat
|
||||||
|
deepthroat
|
||||||
|
dendrophilia
|
||||||
|
dick
|
||||||
|
dildo
|
||||||
|
dingleberry
|
||||||
|
dingleberries
|
||||||
|
dirty pillows
|
||||||
|
dirty sanchez
|
||||||
|
doggie style
|
||||||
|
doggiestyle
|
||||||
|
doggy style
|
||||||
|
doggystyle
|
||||||
|
dog style
|
||||||
|
dolcett
|
||||||
|
domination
|
||||||
|
dominatrix
|
||||||
|
dommes
|
||||||
|
donkey punch
|
||||||
|
double dong
|
||||||
|
double penetration
|
||||||
|
dp action
|
||||||
|
dry hump
|
||||||
|
dvda
|
||||||
|
eat my ass
|
||||||
|
ecchi
|
||||||
|
ejaculation
|
||||||
|
erotic
|
||||||
|
erotism
|
||||||
|
escort
|
||||||
|
eunuch
|
||||||
|
fag
|
||||||
|
faggot
|
||||||
|
fecal
|
||||||
|
felch
|
||||||
|
fellatio
|
||||||
|
feltch
|
||||||
|
female squirting
|
||||||
|
femdom
|
||||||
|
figging
|
||||||
|
fingerbang
|
||||||
|
fingering
|
||||||
|
fisting
|
||||||
|
foot fetish
|
||||||
|
footjob
|
||||||
|
frotting
|
||||||
|
fuck
|
||||||
|
fuck buttons
|
||||||
|
fuckin
|
||||||
|
fucking
|
||||||
|
fucktards
|
||||||
|
fudge packer
|
||||||
|
fudgepacker
|
||||||
|
futanari
|
||||||
|
gangbang
|
||||||
|
gang bang
|
||||||
|
gay sex
|
||||||
|
genitals
|
||||||
|
giant cock
|
||||||
|
girl on
|
||||||
|
girl on top
|
||||||
|
girls gone wild
|
||||||
|
goatcx
|
||||||
|
goatse
|
||||||
|
god damn
|
||||||
|
gokkun
|
||||||
|
golden shower
|
||||||
|
goodpoop
|
||||||
|
goo girl
|
||||||
|
goregasm
|
||||||
|
grope
|
||||||
|
group sex
|
||||||
|
g-spot
|
||||||
|
guro
|
||||||
|
hand job
|
||||||
|
handjob
|
||||||
|
hard core
|
||||||
|
hardcore
|
||||||
|
hentai
|
||||||
|
homoerotic
|
||||||
|
honkey
|
||||||
|
hooker
|
||||||
|
horny
|
||||||
|
hot carl
|
||||||
|
hot chick
|
||||||
|
how to kill
|
||||||
|
how to murder
|
||||||
|
huge fat
|
||||||
|
humping
|
||||||
|
incest
|
||||||
|
intercourse
|
||||||
|
jack off
|
||||||
|
jail bait
|
||||||
|
jailbait
|
||||||
|
jelly donut
|
||||||
|
jerk off
|
||||||
|
jigaboo
|
||||||
|
jiggaboo
|
||||||
|
jiggerboo
|
||||||
|
jizz
|
||||||
|
juggs
|
||||||
|
kike
|
||||||
|
kinbaku
|
||||||
|
kinkster
|
||||||
|
kinky
|
||||||
|
knobbing
|
||||||
|
leather restraint
|
||||||
|
leather straight jacket
|
||||||
|
lemon party
|
||||||
|
livesex
|
||||||
|
lolita
|
||||||
|
lovemaking
|
||||||
|
make me come
|
||||||
|
male squirting
|
||||||
|
masturbate
|
||||||
|
masturbating
|
||||||
|
masturbation
|
||||||
|
menage a trois
|
||||||
|
milf
|
||||||
|
missionary position
|
||||||
|
mong
|
||||||
|
motherfucker
|
||||||
|
mound of venus
|
||||||
|
mr hands
|
||||||
|
muff diver
|
||||||
|
muffdiving
|
||||||
|
nambla
|
||||||
|
nawashi
|
||||||
|
negro
|
||||||
|
neonazi
|
||||||
|
nigga
|
||||||
|
nigger
|
||||||
|
nig nog
|
||||||
|
nimphomania
|
||||||
|
nipple
|
||||||
|
nipples
|
||||||
|
nsfw
|
||||||
|
nsfw images
|
||||||
|
nude
|
||||||
|
nudity
|
||||||
|
nutten
|
||||||
|
nympho
|
||||||
|
nymphomania
|
||||||
|
octopussy
|
||||||
|
omorashi
|
||||||
|
one cup two girls
|
||||||
|
one guy one jar
|
||||||
|
orgasm
|
||||||
|
orgy
|
||||||
|
paedophile
|
||||||
|
paki
|
||||||
|
panties
|
||||||
|
panty
|
||||||
|
pedobear
|
||||||
|
pedophile
|
||||||
|
pegging
|
||||||
|
penis
|
||||||
|
phone sex
|
||||||
|
piece of shit
|
||||||
|
pikey
|
||||||
|
pissing
|
||||||
|
piss pig
|
||||||
|
pisspig
|
||||||
|
playboy
|
||||||
|
pleasure chest
|
||||||
|
pole smoker
|
||||||
|
ponyplay
|
||||||
|
poof
|
||||||
|
poon
|
||||||
|
poontang
|
||||||
|
punany
|
||||||
|
poop chute
|
||||||
|
poopchute
|
||||||
|
porn
|
||||||
|
porno
|
||||||
|
pornography
|
||||||
|
prince albert piercing
|
||||||
|
pthc
|
||||||
|
pubes
|
||||||
|
pussy
|
||||||
|
queaf
|
||||||
|
queef
|
||||||
|
quim
|
||||||
|
raghead
|
||||||
|
raging boner
|
||||||
|
rape
|
||||||
|
raping
|
||||||
|
rapist
|
||||||
|
rectum
|
||||||
|
reverse cowgirl
|
||||||
|
rimjob
|
||||||
|
rimming
|
||||||
|
rosy palm
|
||||||
|
rosy palm and her 5 sisters
|
||||||
|
rusty trombone
|
||||||
|
sadism
|
||||||
|
santorum
|
||||||
|
scat
|
||||||
|
schlong
|
||||||
|
scissoring
|
||||||
|
semen
|
||||||
|
sex
|
||||||
|
sexcam
|
||||||
|
sexo
|
||||||
|
sexy
|
||||||
|
sexual
|
||||||
|
sexually
|
||||||
|
sexuality
|
||||||
|
shaved beaver
|
||||||
|
shaved pussy
|
||||||
|
shemale
|
||||||
|
shibari
|
||||||
|
shit
|
||||||
|
shitblimp
|
||||||
|
shitty
|
||||||
|
shota
|
||||||
|
shrimping
|
||||||
|
skeet
|
||||||
|
slanteye
|
||||||
|
slut
|
||||||
|
s&m
|
||||||
|
smut
|
||||||
|
snatch
|
||||||
|
snowballing
|
||||||
|
sodomize
|
||||||
|
sodomy
|
||||||
|
spastic
|
||||||
|
spic
|
||||||
|
splooge
|
||||||
|
splooge moose
|
||||||
|
spooge
|
||||||
|
spread legs
|
||||||
|
spunk
|
||||||
|
strap on
|
||||||
|
strapon
|
||||||
|
strappado
|
||||||
|
strip club
|
||||||
|
style doggy
|
||||||
|
suck
|
||||||
|
sucks
|
||||||
|
suicide girls
|
||||||
|
sultry women
|
||||||
|
swastika
|
||||||
|
swinger
|
||||||
|
tainted love
|
||||||
|
taste my
|
||||||
|
tea bagging
|
||||||
|
threesome
|
||||||
|
throating
|
||||||
|
thumbzilla
|
||||||
|
tied up
|
||||||
|
tight white
|
||||||
|
tit
|
||||||
|
tits
|
||||||
|
titties
|
||||||
|
titty
|
||||||
|
tongue in a
|
||||||
|
topless
|
||||||
|
tosser
|
||||||
|
towelhead
|
||||||
|
tranny
|
||||||
|
tribadism
|
||||||
|
tub girl
|
||||||
|
tubgirl
|
||||||
|
tushy
|
||||||
|
twat
|
||||||
|
twink
|
||||||
|
twinkie
|
||||||
|
two girls one cup
|
||||||
|
undressing
|
||||||
|
upskirt
|
||||||
|
urethra play
|
||||||
|
urophilia
|
||||||
|
vagina
|
||||||
|
venus mound
|
||||||
|
viagra
|
||||||
|
vibrator
|
||||||
|
violet wand
|
||||||
|
vorarephilia
|
||||||
|
voyeur
|
||||||
|
voyeurweb
|
||||||
|
voyuer
|
||||||
|
vulva
|
||||||
|
wank
|
||||||
|
wetback
|
||||||
|
wet dream
|
||||||
|
white power
|
||||||
|
whore
|
||||||
|
worldsex
|
||||||
|
wrapping men
|
||||||
|
wrinkled starfish
|
||||||
|
xx
|
||||||
|
xxx
|
||||||
|
yaoi
|
||||||
|
yellow showers
|
||||||
|
yiffy
|
||||||
|
zoophilia
|
||||||
|
analritter
|
||||||
|
arsch
|
||||||
|
arschficker
|
||||||
|
arschlecker
|
||||||
|
arschloch
|
||||||
|
bimbo
|
||||||
|
bratze
|
||||||
|
bumsen
|
||||||
|
bonze
|
||||||
|
dödel
|
||||||
|
fick
|
||||||
|
ficken
|
||||||
|
flittchen
|
||||||
|
fotze
|
||||||
|
fratze
|
||||||
|
hackfresse
|
||||||
|
hure
|
||||||
|
hurensohn
|
||||||
|
ische
|
||||||
|
kackbratze
|
||||||
|
kacke
|
||||||
|
kacken
|
||||||
|
kackwurst
|
||||||
|
kampflesbe
|
||||||
|
kanake
|
||||||
|
kimme
|
||||||
|
lümmel
|
||||||
|
MILF
|
||||||
|
möpse
|
||||||
|
morgenlatte
|
||||||
|
möse
|
||||||
|
mufti
|
||||||
|
muschi
|
||||||
|
nackt
|
||||||
|
neger
|
||||||
|
nigger
|
||||||
|
nippel
|
||||||
|
nutte
|
||||||
|
onanieren
|
||||||
|
orgasmus
|
||||||
|
penis
|
||||||
|
pimmel
|
||||||
|
pimpern
|
||||||
|
pinkeln
|
||||||
|
pissen
|
||||||
|
pisser
|
||||||
|
popel
|
||||||
|
poppen
|
||||||
|
porno
|
||||||
|
reudig
|
||||||
|
rosette
|
||||||
|
schabracke
|
||||||
|
schlampe
|
||||||
|
scheiße
|
||||||
|
scheisser
|
||||||
|
schiesser
|
||||||
|
schnackeln
|
||||||
|
schwanzlutscher
|
||||||
|
schwuchtel
|
||||||
|
tittchen
|
||||||
|
titten
|
||||||
|
vögeln
|
||||||
|
vollpfosten
|
||||||
|
wichse
|
||||||
|
wichsen
|
||||||
|
wichser
|
130
bad/create.sh
130
bad/create.sh
@@ -1,130 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Fetch and merge text and JSON files from URLs
|
|
||||||
# Usage: ./create.sh [output_file]
|
|
||||||
|
|
||||||
OUTPUT_FILE="${1:-merged_output.txt}"
|
|
||||||
TEMP_DIR=$(mktemp -d)
|
|
||||||
FINAL_TEMP="$TEMP_DIR/combined.txt"
|
|
||||||
|
|
||||||
# Color codes for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# CONFIGURE YOUR URLS AND COMMENTS HERE
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
URLS_AND_COMMENTS=(
|
|
||||||
"https://raw.githubusercontent.com/dsojevic/profanity-list/refs/heads/main/en.txt"
|
|
||||||
"https://raw.githubusercontent.com/dsojevic/profanity-list/refs/heads/main/emoji.txt"
|
|
||||||
"https://raw.githubusercontent.com/LDNOOBW/List-of-Dirty-Naughty-Obscene-and-Otherwise-Bad-Words/refs/heads/master/de"
|
|
||||||
"https://raw.githubusercontent.com/LDNOOBW/List-of-Dirty-Naughty-Obscene-and-Otherwise-Bad-Words/refs/heads/master/en"
|
|
||||||
"https://raw.githubusercontent.com/zacanger/profane-words/refs/heads/master/words.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
# Check if jq is available for JSON processing
|
|
||||||
if ! command -v jq &> /dev/null; then
|
|
||||||
echo -e "${YELLOW}Warning: jq not found. JSON files will be skipped.${NC}"
|
|
||||||
echo -e "${YELLOW}Install jq with: apt-get install jq (Ubuntu/Debian) or brew install jq (macOS)${NC}"
|
|
||||||
HAS_JQ=false
|
|
||||||
else
|
|
||||||
HAS_JQ=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${GREEN}Text/JSON File Merger${NC}"
|
|
||||||
echo "Processing $(echo "${URLS_AND_COMMENTS[@]}" | grep -c 'https://' || true) URLs..."
|
|
||||||
|
|
||||||
# Process URLs and comments
|
|
||||||
for line in "${URLS_AND_COMMENTS[@]}"; do
|
|
||||||
# Handle comments
|
|
||||||
if [[ "$line" =~ ^[[:space:]]*# ]]; then
|
|
||||||
echo "$line" >> "$FINAL_TEMP"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Skip lines that don't look like URLs
|
|
||||||
if [[ ! "$line" =~ ^https?:// ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${YELLOW}Fetching: $line${NC}"
|
|
||||||
|
|
||||||
# Create temporary file for this URL
|
|
||||||
URL_TEMP="$TEMP_DIR/url_content.tmp"
|
|
||||||
|
|
||||||
# Fetch the URL
|
|
||||||
if curl -s -f "$line" -o "$URL_TEMP" 2>/dev/null; then
|
|
||||||
echo -e "${GREEN}✓ Successfully fetched${NC}"
|
|
||||||
|
|
||||||
# Check if the content is JSON
|
|
||||||
if [ "$HAS_JQ" = true ] && jq empty "$URL_TEMP" 2>/dev/null; then
|
|
||||||
echo -e "${BLUE} → Detected JSON format, extracting strings...${NC}"
|
|
||||||
|
|
||||||
# Try to extract strings from JSON array or object
|
|
||||||
# Handle different JSON structures
|
|
||||||
if jq -e 'type == "array"' "$URL_TEMP" >/dev/null 2>&1; then
|
|
||||||
# JSON array - extract all string values
|
|
||||||
jq -r '.[] | select(type == "string")' "$URL_TEMP" >> "$FINAL_TEMP" 2>/dev/null || {
|
|
||||||
echo -e "${RED} ✗ Failed to parse JSON array${NC}"
|
|
||||||
echo "# ERROR: Could not parse JSON from $line" >> "$FINAL_TEMP"
|
|
||||||
}
|
|
||||||
elif jq -e 'type == "object"' "$URL_TEMP" >/dev/null 2>&1; then
|
|
||||||
# JSON object - extract all string values
|
|
||||||
jq -r 'recurse | select(type == "string")' "$URL_TEMP" >> "$FINAL_TEMP" 2>/dev/null || {
|
|
||||||
echo -e "${RED} ✗ Failed to parse JSON object${NC}"
|
|
||||||
echo "# ERROR: Could not parse JSON from $line" >> "$FINAL_TEMP"
|
|
||||||
}
|
|
||||||
else
|
|
||||||
echo -e "${RED} ✗ Unsupported JSON structure${NC}"
|
|
||||||
echo "# ERROR: Unsupported JSON structure from $line" >> "$FINAL_TEMP"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# Regular text file - append as-is
|
|
||||||
echo -e "${BLUE} → Processing as text file...${NC}"
|
|
||||||
cat "$URL_TEMP" >> "$FINAL_TEMP"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Clean up URL temp file
|
|
||||||
rm -f "$URL_TEMP"
|
|
||||||
else
|
|
||||||
echo -e "${RED}✗ Failed to fetch: $line${NC}"
|
|
||||||
echo "# ERROR: Could not fetch $line" >> "$FINAL_TEMP"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Check if we have any content
|
|
||||||
if [ ! -s "$FINAL_TEMP" ]; then
|
|
||||||
echo -e "${RED}No content to process${NC}"
|
|
||||||
rm -rf "$TEMP_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${YELLOW}Processing content...${NC}"
|
|
||||||
|
|
||||||
# Remove duplicates, sort alphabetically, and save to output file
|
|
||||||
# Keep comments at the top, sort the rest
|
|
||||||
{
|
|
||||||
grep '^#' "$FINAL_TEMP" 2>/dev/null || true
|
|
||||||
grep -v '^#' "$FINAL_TEMP" 2>/dev/null | grep -v '^[[:space:]]*$' | sort -u
|
|
||||||
} > "$OUTPUT_FILE"
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
rm -rf "$TEMP_DIR"
|
|
||||||
|
|
||||||
# Show results
|
|
||||||
line_count=$(wc -l < "$OUTPUT_FILE")
|
|
||||||
echo -e "${GREEN}✓ Complete! Merged content saved to: $OUTPUT_FILE${NC}"
|
|
||||||
echo -e "${GREEN}Total lines: $line_count${NC}"
|
|
||||||
|
|
||||||
# Show a preview of the content
|
|
||||||
if [ -s "$OUTPUT_FILE" ]; then
|
|
||||||
echo -e "${YELLOW}Preview (first 10 non-comment lines):${NC}"
|
|
||||||
grep -v '^#' "$OUTPUT_FILE" | head -10
|
|
||||||
fi
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
7020
bad/test-common-names.txt
Normal file
7020
bad/test-common-names.txt
Normal file
File diff suppressed because it is too large
Load Diff
104
locales/de.yml
104
locales/de.yml
@@ -14,21 +14,30 @@ artists_easter_egg: ", Marie Birner (Couch)"
|
|||||||
project_quote: "Digital Shadows konfrontiert Besucher*innen mit ihrem digitalen Selbst – kopiert, vermessen, analysiert. Ein Experiment über Datenmacht, Sichtbarkeit und Kontrolle im digitalen Zeitalter."
|
project_quote: "Digital Shadows konfrontiert Besucher*innen mit ihrem digitalen Selbst – kopiert, vermessen, analysiert. Ein Experiment über Datenmacht, Sichtbarkeit und Kontrolle im digitalen Zeitalter."
|
||||||
project_quote_attribution: "— Digital Shadows Team"
|
project_quote_attribution: "— Digital Shadows Team"
|
||||||
project_description: "Digital Shadows lädt die Teilnehmer*innen ein, Fragen digitaler und physischer Identität, Datensicherheit und Kontrolle zu erleben. In immersiven Zonen, die mit choreografischen Elementen verwoben sind, begegnen Besucher*innen sich selbst gespiegelt, kopiert, vermessen und verlieren sich gleichzeitig in einem System, das mehr über sie weiß, als sie preisgeben wollen. Zwischen Spiel und Analyse, Verbergen und Transparenz entsteht eine Reflexion über Identität im Zeitalter von Gesichtserkennung, Deepfakes und algorithmischer Profilierung. Wie täuscht man eine Kamera? Wie sichtbar möchte ich sein? Wem gehört, was ich hinterlasse, und wer profitiert davon? Dieses Experiment ist ein kollaboratives Unterfangen zwischen Wissenschaft und Kunst, das Macht, Sichtbarkeit und Selbstbestimmung im digitalen Raum greifbar macht. Durch eine Erkundung digitaler Materialität und algorithmischer Intelligenz entfaltet sich eine manchmal absurde, immer unmittelbare Reflexion über unsere Rolle in datengetriebenen Welten, bis wir unserem digitalen Dilemma gegenüberstehen und die Wahl noch immer bei uns liegt."
|
project_description: "Digital Shadows lädt die Teilnehmer*innen ein, Fragen digitaler und physischer Identität, Datensicherheit und Kontrolle zu erleben. In immersiven Zonen, die mit choreografischen Elementen verwoben sind, begegnen Besucher*innen sich selbst gespiegelt, kopiert, vermessen und verlieren sich gleichzeitig in einem System, das mehr über sie weiß, als sie preisgeben wollen. Zwischen Spiel und Analyse, Verbergen und Transparenz entsteht eine Reflexion über Identität im Zeitalter von Gesichtserkennung, Deepfakes und algorithmischer Profilierung. Wie täuscht man eine Kamera? Wie sichtbar möchte ich sein? Wem gehört, was ich hinterlasse, und wer profitiert davon? Dieses Experiment ist ein kollaboratives Unterfangen zwischen Wissenschaft und Kunst, das Macht, Sichtbarkeit und Selbstbestimmung im digitalen Raum greifbar macht. Durch eine Erkundung digitaler Materialität und algorithmischer Intelligenz entfaltet sich eine manchmal absurde, immer unmittelbare Reflexion über unsere Rolle in datengetriebenen Welten, bis wir unserem digitalen Dilemma gegenüberstehen und die Wahl noch immer bei uns liegt."
|
||||||
what_to_do_title: "Was tun mit diesen Informationen?"
|
what_to_do_title: "Unsere Aktivitäten"
|
||||||
visit_booth_title: "Besuche unseren Stand"
|
|
||||||
visit_booth_description: "Wir freuen uns darauf, dich an unserem Stand in der Post City Linz zu sehen, wo unser Team dir zeigt, was passiert, wenn dein digitaler Schatten allmächtig wird. "
|
|
||||||
jku_link_title: "Zur JKU Informationsseite"
|
|
||||||
find_out_more: "Erfahre mehr"
|
find_out_more: "Erfahre mehr"
|
||||||
location_postcity: "Wo: Postcity Linz"
|
location_postcity: "Wo: Postcity Linz"
|
||||||
play_game_title: "Spiele unser Spiel"
|
play_game_title: "Finde die Überwachungskameras"
|
||||||
play_game_description: "Schon mal durch Linz gewandert mit dem Ziel, (versteckte) Kameras zu finden? Nun, wenn du so jemand bist, dann ist unser 'Kameras entdecken'-Spiel ideal für dich! "
|
play_game_description: "Schon mal durch Linz gewandert mit dem Ziel, (versteckte) Kameras zu finden? Nun, wenn du so jemand bist, dann ist unser 'Kameras entdecken'-Spiel ideal für dich! "
|
||||||
game_link_title: "Zur Spiel-Seite"
|
game_link_title: "Zur Spiel-Seite"
|
||||||
location_linz: "Wo: überall in Linz"
|
location_linz: "Wo: überall in Linz"
|
||||||
|
tour_aef_title: "Anmeldung zu <q>Finde deinen perfekten Partner</q>"
|
||||||
|
tour_aef_description: "Digital Shadows lädt dich ein, Fragen nach digitaler und physischer Identität, Datensicherheit und Kontrolle hautnah zu erleben. In immersiven Zonen mit choreografischen Elementen begegnen dir Spiegelungen, Kopien und Messungen deiner selbst – während du dich zugleich in einem System verlierst, das mehr über dich weiß, als du preisgeben willst.<br /><br/>Zwischen Spiel und Analyse, zwischen Verbergen und Offenlegen entsteht eine eindringliche Reflexion über Identität im Zeitalter von Gesichtserkennung, Deepfakes und algorithmischem Profiling. Wie täuscht man eine Kamera? Wie sichtbar will ich sein? Wem gehören meine digitalen Spuren – und wer profitiert davon?<br /><br />Dieses Experiment, eine Zusammenarbeit von Wissenschaft und Kunst, macht Machtverhältnisse, Sichtbarkeit und Selbstbestimmung im digitalen Raum greifbar. In der Auseinandersetzung mit digitaler Materialität und algorithmischer Intelligenz entfaltet sich ein mitunter absurdes, stets unmittelbares Spiegelbild unserer Rolle in datengetriebenen Welten – bis wir unserem eigenen digitalen Dilemma gegenüberstehen und die Entscheidung noch immer bei uns liegt."
|
||||||
|
tour_register: "Jetzt anmelden"
|
||||||
|
tour_register_title: "Zur Tour anmelden"
|
||||||
|
more_infos: "Mehr Infos"
|
||||||
|
new_header: "Laufende Ausstellung • Performances • Immersive Theater Tour"
|
||||||
|
scientific_background: "Wissenschaftlicher Hintergrund zu Digital Shadows"
|
||||||
|
calendar: "Kalender"
|
||||||
|
cal_options: "Wähle wie du den Kalender anzeigen willst"
|
||||||
|
cal_new_tab: "In neuen Tab öffnen"
|
||||||
|
cal_show_here: "Hier anzeigen <small>(Google Inhalt laden)</small>"
|
||||||
|
|
||||||
# Game page
|
# Game page
|
||||||
game_title: "Wer findet die meisten Kameras?"
|
game_title: "Wer findet die meisten Kameras?"
|
||||||
game_explanation_todo: "Willkommen zu unserem Überwachungsbewusstseinsspiel! Als Teil unserer Digital Shadows Ausstellung beim Ars Electronica Festival haben wir QR-Codes bei Überwachungskameras in ganz Linz platziert. Deine Mission: Entdecke die Kameras, scanne unsere Codes und finde heraus, wie allgegenwärtig öffentliche Überwachung wirklich ist. Wir sind aber nur Menschen – wir haben nur einen kleinen Teil aller Kameras erfasst, die unsere Stadt beobachten. Wer beobachtet wen in unseren öffentlichen Räumen? Die Jagd beginnt jetzt! 🕵️"
|
game_explanation_todo: "Willkommen zu unserem Überwachungsbewusstseinsspiel! Als Teil unserer Digital Shadows Ausstellung beim Ars Electronica Festival haben wir QR-Codes bei Überwachungskameras in ganz Linz platziert. Deine Mission: Entdecke die Kameras, scanne unsere Codes und finde heraus, wie allgegenwärtig öffentliche Überwachung wirklich ist. Wir sind aber nur Menschen – wir haben nur einen kleinen Teil aller Kameras erfasst, die unsere Stadt beobachten. Wer beobachtet wen in unseren öffentlichen Räumen? Die Jagd beginnt jetzt! 🕵️"
|
||||||
save_button: "Speichern"
|
save_button: "Speichern"
|
||||||
|
amount_participants: "Aktuell gibt es insgesamt %{amount} Teilnehmer:innen (die mindestens 1 Kamera gefunden haben)."
|
||||||
cameras_found: "Du hast %{found}/%{total} Kameras gefunden:"
|
cameras_found: "Du hast %{found}/%{total} Kameras gefunden:"
|
||||||
highscore_title: "Bestenliste"
|
highscore_title: "Bestenliste"
|
||||||
not_found_title: "ups"
|
not_found_title: "ups"
|
||||||
@@ -48,7 +57,86 @@ received_characters: "Erhaltene Zeichen"
|
|||||||
see_ranking: "Sieh deine Platzierung"
|
see_ranking: "Sieh deine Platzierung"
|
||||||
new_name_title: "Neuer Name!"
|
new_name_title: "Neuer Name!"
|
||||||
new_name_message: "Fühlt es sich viel anders an?"
|
new_name_message: "Fühlt es sich viel anders an?"
|
||||||
footer_text: "Footer "
|
|
||||||
footer_todo: "noch zu vervollständigen"
|
|
||||||
footer_links: "mit Links"
|
|
||||||
impressum: "Impressum"
|
impressum: "Impressum"
|
||||||
|
|
||||||
|
# Privacy Policy
|
||||||
|
privacy_policy: "Datenschutz"
|
||||||
|
privacy_policy_title: "Daten<wbr/>schutz<wbr/>erklärung"
|
||||||
|
data_controller: "Datenverantwortlicher"
|
||||||
|
data_controller_info: "Johannes Kepler Universität Linz<br>Institut für Netzwerke und Sicherheit<br>Science Park 3, 2. Stock<br>Altenberger Straße 69, 4040 Linz, Austria<br>https://www.ins.jku.at/<br>+43 732 2468-4120<br>office@digidow.eu<br>Umsatzsteuer-Identifikationsnummer (UID) der JKU: ATU57515567"
|
||||||
|
overview: "Überblick"
|
||||||
|
privacy_overview: "Diese Datenschutzerklärung erklärt, wie Daten auf dieser Website gesammelt und verarbeitet werden. Diese Website ist Teil der <a href='https://www.jku.at/ars-electronica-2025-panic-yes-no/digital-shadows/' target='_blank'>Digital Shadows Ausstellung vom Ars Electronica Festival 2025</a> - einem künstlerisch-wissenschaftlichen Projekt, das sich mit der Allgegenwart digitaler Überwachung auseinandersetzt.<br><br>Unser interaktives Spiel lädt Sie ein, die versteckten Überwachungskameras in Linz zu entdecken. Während Sie durch die Stadt wandern und QR-Codes an Kameras scannen, sammeln Sie Punkte und werden Teil einer spielerischen Reflexion über öffentliche Überwachung. Das Spiel macht sichtbar, wie alltäglich und unsichtbar Kameras in unserem urbanen Raum geworden sind. Durch das Sammeln und Vergleichen mit anderen Teilnehmer*innen entsteht ein Bewusstsein für die Dichte des Überwachungsnetzes, in dem wir uns täglich bewegen.<br><br>Da dieses Projekt den bewussten Umgang mit persönlichen Daten thematisiert, legen wir besonderen Wert auf Transparenz bezüglich der Datenverarbeitung auf dieser Website."
|
||||||
|
data_we_collect: "Daten, die wir sammeln"
|
||||||
|
cookies: "Cookies"
|
||||||
|
cookies_description: "Wir verwenden nur zwei Cookies auf dieser Website:"
|
||||||
|
cookie_client_id: "(notwendig): Ein automatisch generierter, für jede Person eindeutiger Identifikator, der benötigt wird, um zu verfolgen, welche Kameras Sie gefunden haben. Dieser Cookie ist für die Spielfunktionalität notwendig."
|
||||||
|
cookie_lang: "(optional): Speichert Ihre Spracheinstellung, wenn Sie eine Sprache auswählen. Dieser Cookie wird nur gesetzt, wenn Sie aktiv eine Spracheinstellung wählen."
|
||||||
|
game_data: "Spieldaten"
|
||||||
|
game_data_description: "Wenn Sie an unserem Überwachungsbewusstseinsspiel teilnehmen, sammeln wir:"
|
||||||
|
chosen_name: "Standardmäßig wird ein Name zufällig ausgewählt. Wenn Sie optional einen Namen vergeben wird dieser gespeichert. Hinweis: Der Name wird gemeinsam mit der Anzahl der Kameras die Sie gefunden haben öffentlich in der Bestenliste angezeigt."
|
||||||
|
game_progress: "Spielfortschritt: Welche Kameras Sie entdeckt haben und wann Sie sie gefunden haben"
|
||||||
|
random_client_id: "Die zufällig generierte <em>client_id</em> (Cookie, siehe oben) um Ihre Spielsitzungen zu verknüpfen"
|
||||||
|
purpose_legal_basis: "Zweck und Rechtsgrundlage"
|
||||||
|
game_functionality: "Spielfunktionalität: Wir verarbeiten Ihre Daten, um das Kamera-Entdeckungsspiel zu betreiben und die Bestenliste anzuzeigen (Einwilligung nach Art. 6(1)(a) DSGVO)"
|
||||||
|
language_preference: "Spracheinstellung: Wir speichern Ihre Sprachwahl basierend auf Ihrer Einwilligung (Art. 6(1)(a) DSGVO), damit Sie nur einmal die Sprache einstellen müssen."
|
||||||
|
statistical_analysis: "Statistische Auswertung: Am Ende des Festivals werden statistische, nicht-personenbezogene Auswertungen über die Nutzung der Website gespeichert."
|
||||||
|
data_retention: "Datenspeicherung"
|
||||||
|
data_retention_description: "Ihre Spieldaten werden in unserer Datenbank bis zum Ende der Ars Electronica Festival Ausstellungszeit gespeichert. Die Cookies verfallen automatisch zum Festivalende oder wenn Sie Ihre Cookies löschen."
|
||||||
|
data_sharing: "Datenweitergabe"
|
||||||
|
data_sharing_description: "Wir teilen, verkaufen oder übertragen Ihre Daten nicht an Dritte. Daten werden ausschließlich für den Betrieb des Kamera-Entdeckungsspiels verwendet."
|
||||||
|
your_rights_gdpr: "Ihre Rechte unter der DSGVO"
|
||||||
|
rights_description: "Sie haben das Recht auf:"
|
||||||
|
right_access: "Auskunft: Anfrage, welche Daten wir über Sie haben"
|
||||||
|
right_rectification: "Berichtigung: Korrektur ungenauer Daten"
|
||||||
|
right_erasure: "Löschung: Anfrage zur Löschung Ihrer Daten"
|
||||||
|
right_restriction: "Einschränkung: Beschränkung der Verarbeitung Ihrer Daten"
|
||||||
|
right_portability: "Datenübertragbarkeit: Erhalt Ihrer Daten in einem strukturierten Format"
|
||||||
|
how_to_exercise_rights: "Wie Sie Ihre Rechte ausüben können"
|
||||||
|
clear_cookies: "Browser-Cookies löschen, um gespeicherte Identifikatoren zu entfernen"
|
||||||
|
contact_us: "Kontaktieren Sie uns an unserem Postcity Linz Stand oder <a href='https://digidow.eu/impressum' target='_blank'>per E-Mail</a>"
|
||||||
|
|
||||||
|
# Additional privacy policy sections
|
||||||
|
server_logfiles: "Serverlogfiles"
|
||||||
|
server_logfiles_description: "Unser Webserver legt ausschließlich anonymisierte Server-Logfiles an, in denen keine IP-Adressen oder sonstigen personenbezogenen Daten gespeichert werden."
|
||||||
|
data_security: "Datensicherheit"
|
||||||
|
data_security_description: "Alle Daten, die zwischen Ihrem Browser und unseren Servern übertragen werden, sind per SSL mit aktuellen Verschlüsselungsstandards gesichert und bieten eine hohe Übertragungssicherheit. Unsere Server werden an der JKU Linz betrieben und regelmäßig gewartet."
|
||||||
|
minors: "Minderjährige"
|
||||||
|
minors_description: "Diese Website darf nur von Personen ab 14 Jahren genutzt werden. Nutzer unter 14 Jahren benötigen die ausdrückliche Einverständniserklärung ihrer Erziehungsberechtigten."
|
||||||
|
data_protection_officer: "Datenschutzbeauftrager"
|
||||||
|
data_protection_officer_contact_full: "Stabsstelle Datenschutz der Johannes Kepler Universität Linz<br>Altenberger Straße 69, 4040 Linz<br>+43 732 2468 3802<br>datenschutz@jku.at"
|
||||||
|
data_collection_timing: "Wann werden Daten gesammelt"
|
||||||
|
data_collection_timing_description: "Daten werden gesammelt, wenn die Website besucht wird und QR Codes gescannt werden."
|
||||||
|
delete_personal_data: "Persönliche Daten löschen"
|
||||||
|
delete_data_description: "Sie können die vollständige Löschung aller Ihrer auf unseren Servern gespeicherten persönlichen Daten beantragen. Dies umfasst Ihren gewählten Namen, Spielfortschritt und alle Sichtungen. Diese Aktion kann nicht rückgängig gemacht werden."
|
||||||
|
delete_my_data: "Meine Daten löschen"
|
||||||
|
delete_confirmation: "Sind Sie sicher, dass Sie alle Ihre persönlichen Daten löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden und Sie verlieren Ihren gesamten Spielfortschritt."
|
||||||
|
data_deletion_success_title: "Daten erfolgreich gelöscht"
|
||||||
|
data_deletion_success_body: "Alle Ihre persönlichen Daten wurden erfolgreich von unseren Servern entfernt. Ihr Sitzungs-Cookie wurde ebenfalls zerstört."
|
||||||
|
|
||||||
|
# Camera demonstration page
|
||||||
|
cam_title: "Gesichts<wbr/>erkennung"
|
||||||
|
cam_title2: "@ Ars Electronica Festival 2025"
|
||||||
|
cam_subtitle: "Bildungsdemonstrationen allgegenwärtiger Überwachungstechnologie"
|
||||||
|
cam_description: "Erleben Sie hautnah, wie Gesichtserkennungssysteme Ihre biometrischen Daten in vernetzten Umgebungen verfolgen und verarbeiten."
|
||||||
|
cam_project_by: "Ein Forschungs- und Sensibilisierungsprojekt vom "
|
||||||
|
cam_institute: "Institut für Netzwerke und Sicherheit, Johannes Kepler Universität"
|
||||||
|
cam_mission_quote: "Das Ziel dieses Projekts ist es, die Öffentlichkeit über allgegenwärtige Gesichtserkennungstechnologien und deren Auswirkungen auf die Privatsphäre zu informieren, indem Festivalbesucher*innen persönlich erfahren können, wie solche Systeme funktionieren und welche Daten verarbeitet werden."
|
||||||
|
cam_mission_attribution: "Projekt Mission Statement"
|
||||||
|
cam_project_description: "Dieses zeitlich begrenzte Forschungs- und Sensibilisierungsprojekt konzentriert sich auf die Verarbeitung biometrischer Daten zu Forschungs-, Bewusstseins- und künstlerischen Zwecken und hilft Besucher*innen, die allgegenwärtige Natur der Gesichtserkennung in unserem täglichen Leben zu verstehen."
|
||||||
|
cam_how_it_works: "Wie es funktioniert"
|
||||||
|
cam_tech_setup_title: "Technische Ausstattung"
|
||||||
|
cam_tech_setup_p1: "Das System besteht aus einer Hauptkamera und bis zu 10 kleineren Sensorstationen, die an verschiedenen Festivalstandorten positioniert sind. Diese Kameras erfassen Bilder und nutzen Gesichtserkennung, um Besucher*innen zu identifizieren und zu verfolgen, während sie sich zwischen den Stationen bewegen."
|
||||||
|
cam_tech_setup_p2: "Das System verarbeitet biometrische Merkmale (gespeichert als \"Embeddings\"), Zeitstempel, Standortdaten und optional benutzerzugewiesene Pseudonyme, um zu demonstrieren, wie moderne Überwachungssysteme funktionieren."
|
||||||
|
cam_data_processing_title: "Daten<wbr/>verarbeitung"
|
||||||
|
cam_data_processing_p1: "<strong>Wichtig ist, dass die tatsächlichen Bilder nicht gespeichert werden</strong> - nur die extrahierten biometrischen Daten und zugehörigen Metadaten werden verarbeitet und vorübergehend auf einem sicheren Server an der JKU gespeichert."
|
||||||
|
cam_festival_details: "Festival-Details"
|
||||||
|
cam_when_where_title: "Wann & Wo"
|
||||||
|
cam_festival_info: "Ars Electronica Festival 2025"
|
||||||
|
cam_festival_dates: "3. bis 7. September 2025"
|
||||||
|
cam_festival_location: "Verschiedene Standorte im gesamten Festivalgelände"
|
||||||
|
cam_legal_compliance: "Rechtliche Konformität"
|
||||||
|
cam_legal_description: "Wir haben bei der österreichischen Datenschutzbehörde die Genehmigung für diesen experimentellen Aufbau beantragt und die Genehmigung wurde am 28. Juli 2025 erteilt."
|
||||||
|
cam_legal_request: "Genehmigter Antrag"
|
||||||
|
cam_legal_decision: "Behördenbescheid"
|
||||||
|
cam_legal_request_title: "Genehmigungsantrag ansehen"
|
||||||
|
cam_legal_decision_title: "Behördenbescheid ansehen"
|
||||||
|
103
locales/en.yml
103
locales/en.yml
@@ -14,21 +14,31 @@ artists_easter_egg: ", Marie Birner (Couch)"
|
|||||||
project_quote: "Digital Shadows confronts visitors with their digital self – copied, measured, analyzed. An experiment on data power, visibility, and control in the digital age."
|
project_quote: "Digital Shadows confronts visitors with their digital self – copied, measured, analyzed. An experiment on data power, visibility, and control in the digital age."
|
||||||
project_quote_attribution: "— Digital Shadows Team"
|
project_quote_attribution: "— Digital Shadows Team"
|
||||||
project_description: "Digital Shadows invites the participants to experience questions of digital and physical identity, data security, and control. In immersive zones woven with choreographic elements, visitors encounter themselves mirrored, copied, measured and simultaneously lose themselves in a system that knows more about them than they intend to reveal. Between play and analysis, concealment and transparency, a reflection emerges on identity in the age of facial recognition, deepfakes, and algorithmic profiling. How does one fool a camera? How visible do I want to be? Who owns what I leave behind, and who profits from it? This experiment is a collaborative endeavor between science and art, making power, visibility, and self-determination in digital space tangible. Through an exploration of digital materiality and algorithmic intelligence, a sometimes absurd, always immediate reflection unfolds on our role in data-driven worlds until we face our digital dilemma, and the choice is still ours to make."
|
project_description: "Digital Shadows invites the participants to experience questions of digital and physical identity, data security, and control. In immersive zones woven with choreographic elements, visitors encounter themselves mirrored, copied, measured and simultaneously lose themselves in a system that knows more about them than they intend to reveal. Between play and analysis, concealment and transparency, a reflection emerges on identity in the age of facial recognition, deepfakes, and algorithmic profiling. How does one fool a camera? How visible do I want to be? Who owns what I leave behind, and who profits from it? This experiment is a collaborative endeavor between science and art, making power, visibility, and self-determination in digital space tangible. Through an exploration of digital materiality and algorithmic intelligence, a sometimes absurd, always immediate reflection unfolds on our role in data-driven worlds until we face our digital dilemma, and the choice is still ours to make."
|
||||||
what_to_do_title: "What to do with this information?"
|
what_to_do_title: "Our Activities"
|
||||||
visit_booth_title: "Visit our booth"
|
|
||||||
visit_booth_description: "We will be delighted to see you at our booth in the Post City Linz, where our team will show you, what happens when your Digital Shadow becomes allmighty. "
|
|
||||||
jku_link_title: "Go to JKU Information Page"
|
|
||||||
find_out_more: "Find out more"
|
find_out_more: "Find out more"
|
||||||
location_postcity: "Where: Postcity Linz"
|
location_postcity: "Where: Postcity Linz"
|
||||||
play_game_title: "Play our game"
|
play_game_title: "Play our game"
|
||||||
play_game_description: "Ever wandered through Linz with the aim to find (hidden) cameras? Well, if you are that kind of person than our 'Discover cameras' game will be ideal for you! "
|
play_game_description: "Ever wandered through Linz with the aim to find (hidden) cameras? Well, if you are that kind of person than our 'Discover cameras' game will be ideal for you! "
|
||||||
game_link_title: "Go to Game Page"
|
game_link_title: "Go to Game Page"
|
||||||
location_linz: "Where: all over Linz"
|
location_linz: "Where: all over Linz"
|
||||||
|
tour_aef_title: "Registration for <q>Find Your Perfect Partner</q>"
|
||||||
|
tour_aef_description: "Digital Shadows invites you to explore pressing questions about digital and physical identity, data security, and control in an immediate and tangible way. In immersive zones with choreographic elements, you encounter reflections, copies, and measurements of yourself—while at the same time losing yourself in a system that knows more about you than you may wish to reveal.
|
||||||
|
Between play and analysis, between concealment and disclosure, an intense reflection emerges on identity in the age of facial recognition, deepfakes, and algorithmic profiling. How can one deceive a camera? How visible do I want to be? Who owns my digital traces—and who benefits from them?<br /><br />This experiment, a collaboration between science and art, makes power structures, visibility, and self-determination in digital spaces tangible. In engaging with digital materiality and algorithmic intelligence, an at times absurd yet always immediate mirror of our role in data-driven worlds unfolds—until we face our own digital dilemma, with the decision still in our hands"
|
||||||
|
tour_register: "Register now"
|
||||||
|
tour_register_title: "Register a slot"
|
||||||
|
more_infos: "More infos"
|
||||||
|
new_header: "Ongoing Exhibition • Performances • Immersive Tour"
|
||||||
|
scientific_background: "Scientific background to Digital Shadows"
|
||||||
|
calendar: "Calendar"
|
||||||
|
cal_options: "Choose how you'd like to access the calendar"
|
||||||
|
cal_new_tab: "Open in New Tab"
|
||||||
|
cal_show_here: "Show here <small>(load Google content)</small>"
|
||||||
|
|
||||||
# Game page
|
# Game page
|
||||||
game_title: "Who finds the most cameras?"
|
game_title: "Who finds the most cameras?"
|
||||||
game_explanation_todo: "Welcome to our public surveillance awareness game! As part of our Digital Shadows exhibition at Ars Electronica Festival, we've placed QR codes near surveillance cameras throughout Linz. Your mission: spot the cameras, scan our codes, and discover how pervasive public monitoring really is. We're only human though – we've mapped just a small subset of all the cameras watching our city. Who's watching whom in our public spaces? The hunt begins now! 🕵️"
|
game_explanation_todo: "Welcome to our public surveillance awareness game! As part of our Digital Shadows exhibition at Ars Electronica Festival, we've placed QR codes near surveillance cameras throughout Linz. Your mission: spot the cameras, scan our codes, and discover how pervasive public monitoring really is. We're only human though – we've mapped just a small subset of all the cameras watching our city. Who's watching whom in our public spaces? The hunt begins now! 🕵️"
|
||||||
save_button: "Save"
|
save_button: "Save"
|
||||||
|
amount_participants: "In total there are %{amount} participants so far (with at least 1 camera)."
|
||||||
cameras_found: "You have found %{found}/%{total} cameras:"
|
cameras_found: "You have found %{found}/%{total} cameras:"
|
||||||
highscore_title: "Highscore"
|
highscore_title: "Highscore"
|
||||||
not_found_title: "uups"
|
not_found_title: "uups"
|
||||||
@@ -48,7 +58,86 @@ received_characters: "Received characters"
|
|||||||
see_ranking: "See your ranking"
|
see_ranking: "See your ranking"
|
||||||
new_name_title: "New name!"
|
new_name_title: "New name!"
|
||||||
new_name_message: "Does it feel much different?"
|
new_name_message: "Does it feel much different?"
|
||||||
footer_text: "Footer "
|
|
||||||
footer_todo: "to be completed"
|
|
||||||
footer_links: "with links"
|
|
||||||
impressum: "Impressum"
|
impressum: "Impressum"
|
||||||
|
|
||||||
|
# Privacy Policy
|
||||||
|
privacy_policy: "Privacy Policy"
|
||||||
|
privacy_policy_title: "Privacy Policy"
|
||||||
|
data_controller: "Data controller"
|
||||||
|
data_controller_info: "Johannes Kepler Universität Linz<br>Institut für Netzwerke und Sicherheit<br>Science Park 3, 2nd Floor<br>Altenberger Straße 69, 4040 Linz, Austria<br>https://www.ins.jku.at/<br>+43 732 2468-4120<br>office@digidow.eu<br>VAT identification number (UID) of JKU: ATU57515567"
|
||||||
|
overview: "Overview"
|
||||||
|
privacy_overview: "This privacy policy explains how we collect and process data on this website, which is part of the <a href='https://www.jku.at/ars-electronica-2025-panic-yes-no/digital-shadows/' target='_blank'>Digital Shadows exhibition of the Ars Electronica Festival 2025</a>."
|
||||||
|
data_we_collect: "Data we collect"
|
||||||
|
cookies: "Cookies"
|
||||||
|
cookies_description: "We use only two cookies on this website:"
|
||||||
|
cookie_client_id: "(essential): An automatically generated unique identifier that allows us to tracks which cameras you've found. This cookie is necessary for the game functionality."
|
||||||
|
cookie_lang: "(optional): Stores your language preference when you select a language. This cookie is only set if you actively choose a language setting."
|
||||||
|
game_data: "Game data"
|
||||||
|
game_data_description: "When you participate in our surveillance awareness game, we collect:"
|
||||||
|
chosen_name: "Your chosen name (optional): The display name you enter to display in the highscore list"
|
||||||
|
game_progress: "Game progress: Which cameras you've discovered and when you found them"
|
||||||
|
random_client_id: "The randomly generated <em>client_id</em> to link your game sessions"
|
||||||
|
purpose_legal_basis: "Purpose and legal basis"
|
||||||
|
game_functionality: "Game functionality: We process your data to operate the camera discovery game (legitimate interest under Art. 6(1)(f) GDPR)"
|
||||||
|
language_preference: "Language preference: We store your language choice based on your consent (Art. 6(1)(a) GDPR)"
|
||||||
|
statistical_analysis: "Statistical analysis: At the end of the festival, statistical, non-personal analyses of website usage will be stored."
|
||||||
|
data_retention: "Data retention"
|
||||||
|
data_retention_description: "Your game data is stored in our database until the end of the Ars Electronica Festival exhibition period. The cookies expire automatically after the festival or when you clear your cookies. There is no long-term storage of any data."
|
||||||
|
data_sharing: "Data sharing"
|
||||||
|
data_sharing_description: "We do not share, sell, or transfer your data to third parties. Data is used exclusively for operating the camera discovery game."
|
||||||
|
your_rights_gdpr: "Your rights under GDPR"
|
||||||
|
rights_description: "You have the right to:"
|
||||||
|
right_access: "Access: Request what data we have about you"
|
||||||
|
right_rectification: "Rectification: Correct inaccurate data"
|
||||||
|
right_erasure: "Erasure: Request deletion of your data"
|
||||||
|
right_restriction: "Restriction: Limit how we process your data"
|
||||||
|
right_portability: "Data portability: Receive your data in a structured format"
|
||||||
|
how_to_exercise_rights: "How to exercise your rights"
|
||||||
|
clear_cookies: "Clear browser cookies to remove stored identifiers"
|
||||||
|
contact_us: "Contact us at our Postcity Linz booth or <a href='https://digidow.eu/impressum' target='_blank'>via mail</a>"
|
||||||
|
|
||||||
|
# Additional privacy policy sections
|
||||||
|
server_logfiles: "Server logfiles"
|
||||||
|
server_logfiles_description: "Our web server creates only anonymized server log files, in which no IP addresses or other personal data are stored."
|
||||||
|
data_security: "Data security"
|
||||||
|
data_security_description: "All data transmitted between your browser and our servers is secured by SSL with current encryption standards and provides high transmission security. Our servers are operated at JKU Linz and regularly maintained."
|
||||||
|
minors: "Minors"
|
||||||
|
minors_description: "This website may only be used by persons aged 14 and over. Users under 14 require the express consent of their parents or guardians."
|
||||||
|
data_protection_officer: "Data Protection Officer"
|
||||||
|
data_protection_officer_contact_full: "Data Protection Office of Johannes Kepler University Linz<br>Altenberger Straße 69, 4040 Linz<br>+43 732 2468 3802<br>datenschutz@jku.at"
|
||||||
|
data_collection_timing: "When data is collected"
|
||||||
|
data_collection_timing_description: "Data is collected when the website is visited and QR codes are scanned."
|
||||||
|
delete_personal_data: "Delete Personal Data"
|
||||||
|
delete_data_description: "You can request the complete deletion of all your personal data stored on our servers. This includes your chosen name, game progress, and all sightings. This action cannot be undone."
|
||||||
|
delete_my_data: "Delete My Data"
|
||||||
|
delete_confirmation: "Are you sure you want to delete all your personal data? This action cannot be undone and you will lose all your game progress."
|
||||||
|
data_deletion_success_title: "Data Successfully Deleted"
|
||||||
|
data_deletion_success_body: "All your personal data has been successfully removed from our servers. Your session cookie has also been destroyed."
|
||||||
|
|
||||||
|
# Camera demonstration page
|
||||||
|
cam_title: "Face recognition"
|
||||||
|
cam_title2: "@ Ars Electronica Festival 2025"
|
||||||
|
cam_subtitle: "Educational Demonstration of Omnipresent Surveillance Technology"
|
||||||
|
cam_description: "Experience firsthand how facial recognition systems track and process your biometric data across interconnected environments."
|
||||||
|
cam_project_by: "A research and sensitization project by the "
|
||||||
|
cam_institute: "Institute for Networks and Security, Johannes Kepler University"
|
||||||
|
cam_mission_quote: "The goal of this project is to educate the public about omnipresent facial recognition technologies and their impact on privacy by allowing festival-goers to personally experience how such systems function and what data is processed."
|
||||||
|
cam_mission_attribution: "Project Mission Statement"
|
||||||
|
cam_project_description: "This time-limited research and sensitization project focuses on biometric data processing for research, awareness, and artistic purposes, helping visitors understand the pervasive nature of facial recognition in our daily lives."
|
||||||
|
cam_how_it_works: "How It Works"
|
||||||
|
cam_tech_setup_title: "Technology Setup"
|
||||||
|
cam_tech_setup_p1: "The system consists of a main camera and up to 10 smaller sensor-stations positioned at different festival locations. These cameras capture images and use facial recognition to identify and track visitors as they move between stations."
|
||||||
|
cam_tech_setup_p2: "The system processes biometric features (stored as \"Embeddings\"), timestamps, location data, and optionally, user-assigned pseudonyms to demonstrate how modern surveillance systems function."
|
||||||
|
cam_data_processing_title: "Data Processing"
|
||||||
|
cam_data_processing_p1: "<strong>Importantly, the actual images are not stored</strong> - only the extracted biometric data and associated metadata are processed and temporarily stored on a secure server at JKU."
|
||||||
|
cam_festival_details: "Festival Details"
|
||||||
|
cam_when_where_title: "When & Where"
|
||||||
|
cam_festival_info: "Ars Electronica Festival 2025"
|
||||||
|
cam_festival_dates: "September 3rd to 7th, 2025"
|
||||||
|
cam_festival_location: "Various locations throughout the festival grounds"
|
||||||
|
cam_legal_compliance: "Legal Compliance"
|
||||||
|
cam_legal_description: "We requested approval for this experimental setup from the Austrian Data Protection Authority and the request was approved on July 28, 2025."
|
||||||
|
cam_legal_request: "Approved Request (Antrag)"
|
||||||
|
cam_legal_decision: "Authority Decision (Bescheid)"
|
||||||
|
cam_legal_request_title: "View approval request"
|
||||||
|
cam_legal_decision_title: "View authority decision"
|
||||||
|
@@ -24,3 +24,8 @@ CREATE TABLE sightings (
|
|||||||
-- Create indexes for better performance on foreign key lookups
|
-- Create indexes for better performance on foreign key lookups
|
||||||
CREATE INDEX idx_sightings_client_uuid ON sightings(client_uuid);
|
CREATE INDEX idx_sightings_client_uuid ON sightings(client_uuid);
|
||||||
CREATE INDEX idx_sightings_camera_id ON sightings(camera_id);
|
CREATE INDEX idx_sightings_camera_id ON sightings(camera_id);
|
||||||
|
|
||||||
|
CREATE TABLE banned_names (
|
||||||
|
name TEXT PRIMARY KEY NOT NULL,
|
||||||
|
banned_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
433
src/admin.rs
Normal file
433
src/admin.rs
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
use crate::{language::language, page::Page, AppState};
|
||||||
|
use axum::{
|
||||||
|
extract::{Path, Query, State},
|
||||||
|
http::HeaderMap,
|
||||||
|
response::{IntoResponse, Redirect, Response},
|
||||||
|
routing::{get, post},
|
||||||
|
Form, Router,
|
||||||
|
};
|
||||||
|
use axum_extra::extract::{
|
||||||
|
cookie::{Cookie, Expiration},
|
||||||
|
CookieJar, PrivateCookieJar,
|
||||||
|
};
|
||||||
|
use maud::{html, Markup};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct LoginForm {
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct CameraForm {
|
||||||
|
uuid: String,
|
||||||
|
name: String,
|
||||||
|
desc: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct DeleteCameraForm {
|
||||||
|
uuid: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct EditCameraForm {
|
||||||
|
name: String,
|
||||||
|
desc: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn login_page(cookies: CookieJar, headers: HeaderMap) -> Markup {
|
||||||
|
let lang = language(&cookies, &headers);
|
||||||
|
rust_i18n::set_locale(lang.to_locale());
|
||||||
|
|
||||||
|
Page::new(lang).content(html! {
|
||||||
|
h1 { "Admin Login" }
|
||||||
|
form method="POST" action="/admin/login" {
|
||||||
|
fieldset {
|
||||||
|
label for="password" { "Password:" }
|
||||||
|
input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
id="password"
|
||||||
|
required;
|
||||||
|
input type="submit" value="Login";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn login(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
private_cookies: PrivateCookieJar,
|
||||||
|
Form(form): Form<LoginForm>,
|
||||||
|
) -> Response {
|
||||||
|
if form.password == state.admin_password {
|
||||||
|
// Set secure admin session cookie
|
||||||
|
let expiration_date = OffsetDateTime::now_utc() + time::Duration::days(30);
|
||||||
|
let mut cookie = Cookie::new("admin_session", "authenticated");
|
||||||
|
cookie.set_expires(Expiration::DateTime(expiration_date));
|
||||||
|
cookie.set_http_only(true);
|
||||||
|
cookie.set_secure(true);
|
||||||
|
cookie.set_path("/");
|
||||||
|
|
||||||
|
let updated_cookies = private_cookies.add(cookie);
|
||||||
|
(updated_cookies, Redirect::to("/protected")).into_response()
|
||||||
|
} else {
|
||||||
|
// Invalid password, redirect back to login
|
||||||
|
Redirect::to("/admin/login").into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn logout(private_cookies: PrivateCookieJar) -> Response {
|
||||||
|
// Remove admin session cookie
|
||||||
|
let expired_cookie = Cookie::build(("admin_session", ""))
|
||||||
|
.expires(Expiration::DateTime(
|
||||||
|
OffsetDateTime::now_utc() - time::Duration::days(1),
|
||||||
|
))
|
||||||
|
.http_only(true)
|
||||||
|
.secure(true)
|
||||||
|
.path("/")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let updated_cookies = private_cookies.add(expired_cookie);
|
||||||
|
(updated_cookies, Redirect::to("/")).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn protected_page(
|
||||||
|
private_cookies: PrivateCookieJar,
|
||||||
|
cookies: CookieJar,
|
||||||
|
headers: HeaderMap,
|
||||||
|
) -> Response {
|
||||||
|
// Check if admin is authenticated
|
||||||
|
if private_cookies.get("admin_session").is_none() {
|
||||||
|
return Redirect::to("/admin/login").into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let lang = language(&cookies, &headers);
|
||||||
|
rust_i18n::set_locale(lang.to_locale());
|
||||||
|
|
||||||
|
let markup = Page::new(lang).content(html! {
|
||||||
|
h1 { "Protected Admin Area" }
|
||||||
|
p { "Welcome to the admin area! This is a protected route." }
|
||||||
|
p { "Only authenticated administrators can access this page." }
|
||||||
|
|
||||||
|
h2 { "Camera Management" }
|
||||||
|
p { "Manage cameras in the system." }
|
||||||
|
a href="/admin/cameras/add" { "Add Camera" }
|
||||||
|
" | "
|
||||||
|
a href="/admin/cameras" { "Manage Cameras" }
|
||||||
|
|
||||||
|
form method="POST" action="/admin/logout" {
|
||||||
|
input type="submit" value="Logout" class="secondary";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
markup.into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_camera_page(
|
||||||
|
private_cookies: PrivateCookieJar,
|
||||||
|
cookies: CookieJar,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Query(params): Query<HashMap<String, String>>,
|
||||||
|
) -> Response {
|
||||||
|
// Check if admin is authenticated
|
||||||
|
if private_cookies.get("admin_session").is_none() {
|
||||||
|
return Redirect::to("/admin/login").into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let lang = language(&cookies, &headers);
|
||||||
|
rust_i18n::set_locale(lang.to_locale());
|
||||||
|
|
||||||
|
// Get pre-filled UUID from query params
|
||||||
|
let prefilled_uuid = params.get("uuid").unwrap_or(&String::new()).clone();
|
||||||
|
|
||||||
|
let markup = Page::new(lang).content(html! {
|
||||||
|
h1 { "Add Camera" }
|
||||||
|
@if !prefilled_uuid.is_empty() {
|
||||||
|
p.text-muted { "Auto-detected missing camera with UUID: " strong { (prefilled_uuid) } }
|
||||||
|
}
|
||||||
|
form method="POST" action="/admin/cameras/add" {
|
||||||
|
fieldset {
|
||||||
|
label for="uuid" { "Camera UUID:" }
|
||||||
|
input
|
||||||
|
type="text"
|
||||||
|
name="uuid"
|
||||||
|
id="uuid"
|
||||||
|
placeholder="e.g., 123e4567-e89b-12d3-a456-426614174000"
|
||||||
|
value=(prefilled_uuid)
|
||||||
|
required;
|
||||||
|
|
||||||
|
label for="name" { "Camera Name:" }
|
||||||
|
input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
id="name"
|
||||||
|
placeholder="e.g., Front Entrance Camera"
|
||||||
|
required;
|
||||||
|
|
||||||
|
label for="desc" { "Description (optional):" }
|
||||||
|
textarea
|
||||||
|
name="desc"
|
||||||
|
id="desc"
|
||||||
|
placeholder="e.g., Camera monitoring the main entrance" {};
|
||||||
|
|
||||||
|
input type="submit" value="Add Camera";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
a href="/protected" { "← Back to Admin Dashboard" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
markup.into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn manage_cameras_page(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
private_cookies: PrivateCookieJar,
|
||||||
|
cookies: CookieJar,
|
||||||
|
headers: HeaderMap,
|
||||||
|
) -> Response {
|
||||||
|
// Check if admin is authenticated
|
||||||
|
if private_cookies.get("admin_session").is_none() {
|
||||||
|
return Redirect::to("/admin/login").into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let lang = language(&cookies, &headers);
|
||||||
|
rust_i18n::set_locale(lang.to_locale());
|
||||||
|
|
||||||
|
let cameras = state.backend.get_all_cameras().await;
|
||||||
|
|
||||||
|
let markup = Page::new(lang).content(html! {
|
||||||
|
h1 { "Manage Cameras" }
|
||||||
|
p { "Total cameras: " strong { (cameras.len()) } }
|
||||||
|
|
||||||
|
@if cameras.is_empty() {
|
||||||
|
p.text-muted { "No cameras found in the system." }
|
||||||
|
} @else {
|
||||||
|
table {
|
||||||
|
thead {
|
||||||
|
tr {
|
||||||
|
th { "UUID" }
|
||||||
|
th { "Name" }
|
||||||
|
th { "Description" }
|
||||||
|
th { "Actions" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody {
|
||||||
|
@for camera in &cameras {
|
||||||
|
tr {
|
||||||
|
td {
|
||||||
|
code { (camera.uuid) }
|
||||||
|
}
|
||||||
|
td { (camera.name) }
|
||||||
|
td {
|
||||||
|
@if let Some(desc) = &camera.desc {
|
||||||
|
(desc)
|
||||||
|
} @else {
|
||||||
|
em.text-muted { "No description" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
a href=(format!("/admin/cameras/{}/edit", camera.uuid)) class="secondary" style="margin-right: 0.5rem;" { "Edit" }
|
||||||
|
form method="POST" action="/admin/cameras/delete" style="display: inline;" {
|
||||||
|
input type="hidden" name="uuid" value=(camera.uuid);
|
||||||
|
input
|
||||||
|
type="submit"
|
||||||
|
value="Delete"
|
||||||
|
class="secondary"
|
||||||
|
onclick="return confirm('Are you sure you want to delete this camera? This will also remove all associated sightings.')";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
a href="/admin/cameras/add" { "Add New Camera" }
|
||||||
|
" | "
|
||||||
|
a href="/protected" { "← Back to Admin Dashboard" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
markup.into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_camera(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
private_cookies: PrivateCookieJar,
|
||||||
|
Form(form): Form<CameraForm>,
|
||||||
|
) -> Response {
|
||||||
|
// Check if admin is authenticated
|
||||||
|
if private_cookies.get("admin_session").is_none() {
|
||||||
|
return Redirect::to("/admin/login").into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse UUID
|
||||||
|
let uuid = match Uuid::parse_str(&form.uuid) {
|
||||||
|
Ok(uuid) => uuid,
|
||||||
|
Err(_) => return Redirect::to("/admin/cameras/add?error=invalid_uuid").into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if camera already exists
|
||||||
|
if state.backend.get_camera(&uuid).await.is_some() {
|
||||||
|
return Redirect::to("/admin/cameras/add?error=already_exists").into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the camera
|
||||||
|
let desc = if form
|
||||||
|
.desc
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.trim().is_empty())
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
form.desc.as_deref()
|
||||||
|
};
|
||||||
|
|
||||||
|
match state.backend.create_camera(&uuid, &form.name, desc).await {
|
||||||
|
Ok(_) => Redirect::to("/admin/cameras?camera_added=1").into_response(),
|
||||||
|
Err(_) => Redirect::to("/admin/cameras/add?error=creation_failed").into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn edit_camera_page(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
private_cookies: PrivateCookieJar,
|
||||||
|
cookies: CookieJar,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path(uuid_str): Path<String>,
|
||||||
|
) -> Response {
|
||||||
|
// Check if admin is authenticated
|
||||||
|
if private_cookies.get("admin_session").is_none() {
|
||||||
|
return Redirect::to("/admin/login").into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let lang = language(&cookies, &headers);
|
||||||
|
rust_i18n::set_locale(lang.to_locale());
|
||||||
|
|
||||||
|
// Parse UUID
|
||||||
|
let uuid = match Uuid::parse_str(&uuid_str) {
|
||||||
|
Ok(uuid) => uuid,
|
||||||
|
Err(_) => return Redirect::to("/admin/cameras?error=invalid_uuid").into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get camera details
|
||||||
|
let Some(camera) = state.backend.get_camera(&uuid).await else {
|
||||||
|
return Redirect::to("/admin/cameras?error=not_found").into_response();
|
||||||
|
};
|
||||||
|
|
||||||
|
let markup = Page::new(lang).content(html! {
|
||||||
|
h1 { "Edit Camera" }
|
||||||
|
p.text-muted { "UUID: " code { (camera.uuid) } }
|
||||||
|
|
||||||
|
form method="POST" action=(format!("/admin/cameras/{}/edit", camera.uuid)) {
|
||||||
|
fieldset {
|
||||||
|
label for="name" { "Camera Name:" }
|
||||||
|
input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
id="name"
|
||||||
|
value=(camera.name)
|
||||||
|
required;
|
||||||
|
|
||||||
|
label for="desc" { "Description (optional):" }
|
||||||
|
textarea
|
||||||
|
name="desc"
|
||||||
|
id="desc"
|
||||||
|
placeholder="e.g., Camera monitoring the main entrance" {
|
||||||
|
@if let Some(desc) = &camera.desc {
|
||||||
|
(desc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input type="submit" value="Update Camera";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
a href="/admin/cameras" { "← Back to Camera List" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
markup.into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_camera(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
private_cookies: PrivateCookieJar,
|
||||||
|
Path(uuid_str): Path<String>,
|
||||||
|
Form(form): Form<EditCameraForm>,
|
||||||
|
) -> Response {
|
||||||
|
// Check if admin is authenticated
|
||||||
|
if private_cookies.get("admin_session").is_none() {
|
||||||
|
return Redirect::to("/admin/login").into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse UUID
|
||||||
|
let uuid = match Uuid::parse_str(&uuid_str) {
|
||||||
|
Ok(uuid) => uuid,
|
||||||
|
Err(_) => return Redirect::to("/admin/cameras?error=invalid_uuid").into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process description
|
||||||
|
let desc = if form
|
||||||
|
.desc
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.trim().is_empty())
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
form.desc.as_deref()
|
||||||
|
};
|
||||||
|
|
||||||
|
match state.backend.update_camera(&uuid, &form.name, desc).await {
|
||||||
|
Ok(true) => Redirect::to("/admin/cameras?camera_updated=1").into_response(),
|
||||||
|
Ok(false) => Redirect::to("/admin/cameras?error=not_found").into_response(),
|
||||||
|
Err(_) => Redirect::to("/admin/cameras?error=update_failed").into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_camera(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
private_cookies: PrivateCookieJar,
|
||||||
|
Form(form): Form<DeleteCameraForm>,
|
||||||
|
) -> Response {
|
||||||
|
// Check if admin is authenticated
|
||||||
|
if private_cookies.get("admin_session").is_none() {
|
||||||
|
return Redirect::to("/admin/login").into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse UUID
|
||||||
|
let uuid = match Uuid::parse_str(&form.uuid) {
|
||||||
|
Ok(uuid) => uuid,
|
||||||
|
Err(_) => return Redirect::to("/admin/cameras?error=invalid_uuid").into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match state.backend.delete_camera(&uuid).await {
|
||||||
|
Ok(true) => Redirect::to("/admin/cameras?camera_deleted=1").into_response(),
|
||||||
|
Ok(false) => Redirect::to("/admin/cameras?error=not_found").into_response(),
|
||||||
|
Err(_) => Redirect::to("/admin/cameras?error=deletion_failed").into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/admin/login", get(login_page))
|
||||||
|
.route("/admin/login", post(login))
|
||||||
|
.route("/admin/logout", post(logout))
|
||||||
|
.route("/protected", get(protected_page))
|
||||||
|
.route("/admin/cameras", get(manage_cameras_page))
|
||||||
|
.route("/admin/cameras/add", get(add_camera_page))
|
||||||
|
.route("/admin/cameras/add", post(add_camera))
|
||||||
|
.route("/admin/cameras/{uuid}/edit", get(edit_camera_page))
|
||||||
|
.route("/admin/cameras/{uuid}/edit", post(update_camera))
|
||||||
|
.route("/admin/cameras/delete", post(delete_camera))
|
||||||
|
}
|
195
src/game.rs
195
src/game.rs
@@ -1,44 +1,64 @@
|
|||||||
use crate::{Backend, NameUpdateError, language::language, page::Page};
|
use crate::{
|
||||||
|
language::language,
|
||||||
|
page::{MyMessage, Page},
|
||||||
|
AppState, NameUpdateError,
|
||||||
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
Form, Router,
|
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
http::HeaderMap,
|
http::HeaderMap,
|
||||||
response::{IntoResponse, Redirect, Response},
|
response::{IntoResponse, Response},
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
|
Form, Router,
|
||||||
};
|
};
|
||||||
use axum_extra::extract::CookieJar;
|
use axum_extra::extract::{CookieJar, PrivateCookieJar};
|
||||||
use axum_messages::Messages;
|
use maud::{html, Markup, PreEscaped};
|
||||||
use maud::{Markup, PreEscaped, html};
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::sync::Arc;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
async fn index(
|
async fn index(
|
||||||
State(backend): State<Arc<Backend>>,
|
State(state): State<AppState>,
|
||||||
cookies: CookieJar,
|
cookies: PrivateCookieJar,
|
||||||
messages: Messages,
|
lang_cookies: CookieJar,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let (cookies, req) = backend.client_full(cookies, &headers).await;
|
retu(state, cookies, lang_cookies, headers, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn retu(
|
||||||
|
state: AppState,
|
||||||
|
cookies: PrivateCookieJar,
|
||||||
|
lang_cookies: CookieJar,
|
||||||
|
headers: HeaderMap,
|
||||||
|
message: Option<MyMessage>,
|
||||||
|
) -> Response {
|
||||||
|
let backend = &state.backend;
|
||||||
|
let (cookies, req) = backend.client_full(cookies, &lang_cookies, &headers).await;
|
||||||
|
|
||||||
|
// Check if user is admin
|
||||||
|
let is_admin = cookies.get("admin_session").is_some();
|
||||||
let client = req.client;
|
let client = req.client;
|
||||||
|
rust_i18n::set_locale(&req.lang.to_string());
|
||||||
|
|
||||||
let sightings = backend.sightings_for_client(&client).await;
|
let sightings = backend.sightings_for_client(&client).await;
|
||||||
let amount_total_cameras = backend.amount_total_cameras().await;
|
let amount_total_cameras = backend.amount_total_cameras().await;
|
||||||
let highscore = backend.highscore().await;
|
let highscore = backend.highscore(&client).await;
|
||||||
|
let amount_participants = backend.amount_participants().await;
|
||||||
|
|
||||||
let mut page = Page::new(req.lang);
|
let mut page = Page::new(req.lang);
|
||||||
page.messages(messages);
|
if let Some(message) = message {
|
||||||
|
page.set_message(message);
|
||||||
|
}
|
||||||
let markup = page.content(html! {
|
let markup = page.content(html! {
|
||||||
hgroup {
|
hgroup {
|
||||||
h1 { (t!("game_title")) }
|
h1 { (t!("game_title")) }
|
||||||
}
|
}
|
||||||
p {
|
p {
|
||||||
mark { (t!("game_explanation_todo")) }
|
(t!("game_explanation_todo"))
|
||||||
}
|
}
|
||||||
|
|
||||||
div.mb-sm { (t!("ask_to_change_name", name = client.get_display_name())) }
|
div.mb-sm { (t!("ask_to_change_name", name = client.get_display_name())) }
|
||||||
|
|
||||||
form action="/name" method="post" {
|
form action="/game" method="post" {
|
||||||
fieldset role="group" {
|
fieldset role="group" {
|
||||||
input
|
input
|
||||||
name="name"
|
name="name"
|
||||||
@@ -69,12 +89,25 @@ async fn index(
|
|||||||
h2 { (t!("highscore_title")) }
|
h2 { (t!("highscore_title")) }
|
||||||
ul.iterated {
|
ul.iterated {
|
||||||
@for rank in highscore {
|
@for rank in highscore {
|
||||||
|
@if rank.show_dots_above {
|
||||||
|
li.no-border {
|
||||||
|
span {
|
||||||
|
"⋮"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
li.card {
|
li.card {
|
||||||
span {
|
span {
|
||||||
span.font-headline.rank.text-muted { (rank.rank) "." }
|
span.font-headline.rank.text-muted { (rank.rank) "." }
|
||||||
@if rank.client == client { (PreEscaped("<mark id='ranking'>")) }
|
@if rank.client == client { (PreEscaped("<mark id='ranking'>")) }
|
||||||
(rank.client.get_display_name())
|
(rank.client.get_display_name())
|
||||||
@if rank.client == client { (PreEscaped("</mark>")) }
|
@if rank.client == client { (PreEscaped("</mark>")) }
|
||||||
|
@if is_admin && rank.client.name.is_some() && rank.client.name.as_ref().unwrap() != "***" {
|
||||||
|
form method="POST" action="/game/ban-name" style="display: inline; margin-left: 0.5rem;" {
|
||||||
|
input type="hidden" name="name" value=(rank.client.name.as_ref().unwrap());
|
||||||
|
input type="submit" value="Block" class="secondary" style="font-size: 0.8rem; padding: 0.25rem 0.5rem;";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
span.font-headline.font-lg {
|
span.font-headline.font-lg {
|
||||||
(rank.amount)
|
(rank.amount)
|
||||||
@@ -84,6 +117,9 @@ async fn index(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
span.small {
|
||||||
|
(t!("amount_participants", amount = amount_participants))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,34 +127,42 @@ async fn index(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn game(
|
async fn game(
|
||||||
State(backend): State<Arc<Backend>>,
|
State(state): State<AppState>,
|
||||||
cookies: CookieJar,
|
cookies: PrivateCookieJar,
|
||||||
|
lang_cookies: CookieJar,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
messages: Messages,
|
|
||||||
Path(uuid): Path<String>,
|
Path(uuid): Path<String>,
|
||||||
) -> Result<Redirect, Response> {
|
) -> Response {
|
||||||
let (cookies, client) = backend.client(cookies).await;
|
let backend = &state.backend;
|
||||||
|
let (cookies, req) = backend.client_full(cookies, &lang_cookies, &headers).await;
|
||||||
|
let client = req.client;
|
||||||
|
rust_i18n::set_locale(req.lang.to_locale());
|
||||||
|
|
||||||
let Ok(uuid) = Uuid::parse_str(&uuid) else {
|
let Ok(uuid) = Uuid::parse_str(&uuid) else {
|
||||||
return Err(not_found(cookies, headers).await.into_response());
|
return not_found(lang_cookies, headers).await.into_response();
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(camera) = backend.get_camera(&uuid).await else {
|
let Some(camera) = backend.get_camera(&uuid).await else {
|
||||||
return Err(not_found(cookies, headers).await.into_response());
|
// Check if user is admin
|
||||||
|
if cookies.get("admin_session").is_some() {
|
||||||
|
// Redirect to camera add form with pre-filled UUID
|
||||||
|
return axum::response::Redirect::to(&format!("/admin/cameras/add?uuid={}", uuid))
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
return not_found(lang_cookies, headers).await.into_response();
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Ok(number) = backend.client_found_camera(&client, &camera).await {
|
let message = if let Ok(number) = backend.client_found_camera(&client, &camera).await {
|
||||||
messages.info(format!("found-cam|{}|{number}", camera.name));
|
MyMessage::FoundCam(camera.name, number)
|
||||||
} else {
|
} else {
|
||||||
messages.info(format!(
|
MyMessage::Error(
|
||||||
"err|{}|{}|{}",
|
t!("error_already_found_title").into(),
|
||||||
t!("error_already_found_title"),
|
t!("error_already_found_body").into(),
|
||||||
t!("error_already_found_body"),
|
t!("error_already_found_footer").into(),
|
||||||
t!("error_already_found_footer")
|
)
|
||||||
));
|
};
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Redirect::to("/game"))
|
retu(state, cookies, lang_cookies, headers, Some(message)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn not_found(cookies: CookieJar, headers: HeaderMap) -> Markup {
|
async fn not_found(cookies: CookieJar, headers: HeaderMap) -> Markup {
|
||||||
@@ -133,45 +177,72 @@ struct NameForm {
|
|||||||
name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct BanNameForm {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
async fn set_name(
|
async fn set_name(
|
||||||
State(backend): State<Arc<Backend>>,
|
State(state): State<AppState>,
|
||||||
cookies: CookieJar,
|
cookies: PrivateCookieJar,
|
||||||
messages: Messages,
|
lang_cookies: CookieJar,
|
||||||
|
headers: HeaderMap,
|
||||||
Form(form): Form<NameForm>,
|
Form(form): Form<NameForm>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let (cookies, client) = backend.client(cookies).await;
|
let backend = &state.backend;
|
||||||
|
let (cookies, req) = backend.client_full(cookies, &lang_cookies, &headers).await;
|
||||||
|
let client = req.client;
|
||||||
|
rust_i18n::set_locale(req.lang.to_locale());
|
||||||
|
|
||||||
match backend.set_client_name(&client, &form.name).await {
|
let message = match backend.set_client_name(&client, &form.name).await {
|
||||||
Ok(()) => messages.info("set-name-succ"),
|
Ok(()) => MyMessage::NameChanged,
|
||||||
Err(NameUpdateError::TooShort(expected, actual)) => messages.info(format!(
|
Err(NameUpdateError::TooShort(expected, actual)) => MyMessage::Error(
|
||||||
"err|{}|{}|{}: {}",
|
t!("error_name_too_short_title").into(),
|
||||||
t!("error_name_too_short_title"),
|
t!("error_name_too_short_body", expected = expected).into(),
|
||||||
t!("error_name_too_short_body", expected = expected),
|
format!("{}: {actual}", t!("received_characters")),
|
||||||
t!("received_characters"),
|
),
|
||||||
actual
|
Err(NameUpdateError::TooLong(expected, actual)) => MyMessage::Error(
|
||||||
)),
|
t!("error_name_too_long_title").into(),
|
||||||
Err(NameUpdateError::TooLong(expected, actual)) => messages.info(format!(
|
t!("error_name_too_long_body", expected = expected).into(),
|
||||||
"err|{}|{}|{}: {}",
|
format!("{}: {actual}", t!("received_characters")),
|
||||||
t!("error_name_too_long_title"),
|
),
|
||||||
t!("error_name_too_long_body", expected = expected),
|
Err(NameUpdateError::ContainsBadWord) => MyMessage::Error(
|
||||||
t!("received_characters"),
|
t!("error_bad_word_title").into(),
|
||||||
actual
|
t!("error_bad_word_body").into(),
|
||||||
)),
|
t!("error_bad_word_footer").into(),
|
||||||
Err(NameUpdateError::ContainsBadWord) => messages.info(format!(
|
),
|
||||||
"err|{}|{}|{}",
|
|
||||||
t!("error_bad_word_title"),
|
|
||||||
t!("error_bad_word_body"),
|
|
||||||
t!("error_bad_word_footer")
|
|
||||||
)),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Redirect back to the game page
|
retu(state, cookies, lang_cookies, headers, Some(message)).await
|
||||||
(cookies, Redirect::to("/game")).into_response()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn routes() -> Router<Arc<Backend>> {
|
async fn ban_name(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
private_cookies: PrivateCookieJar,
|
||||||
|
_lang_cookies: CookieJar,
|
||||||
|
_headers: HeaderMap,
|
||||||
|
Form(form): Form<BanNameForm>,
|
||||||
|
) -> Response {
|
||||||
|
// Check if user is admin
|
||||||
|
if private_cookies.get("admin_session").is_none() {
|
||||||
|
return axum::response::Redirect::to("/game").into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let backend = &state.backend;
|
||||||
|
|
||||||
|
// Ban the name
|
||||||
|
let _ = backend.ban_name(&form.name).await;
|
||||||
|
|
||||||
|
// Replace existing instances with asterisks
|
||||||
|
let _ = backend.replace_banned_names_with_asterisks().await;
|
||||||
|
|
||||||
|
axum::response::Redirect::to("/game").into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn routes() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/game", get(index))
|
.route("/game", get(index))
|
||||||
.route("/name", post(set_name))
|
.route("/game", post(set_name))
|
||||||
|
.route("/game/ban-name", post(ban_name))
|
||||||
.route("/{*uuid}", get(game))
|
.route("/{*uuid}", get(game))
|
||||||
}
|
}
|
||||||
|
298
src/index.rs
298
src/index.rs
@@ -1,11 +1,14 @@
|
|||||||
use crate::{language::language, page::Page};
|
use crate::{
|
||||||
use axum::http::HeaderMap;
|
language::language,
|
||||||
|
page::{MyMessage, Page},
|
||||||
|
};
|
||||||
|
use axum::{extract::Query, http::HeaderMap};
|
||||||
use axum_extra::extract::CookieJar;
|
use axum_extra::extract::CookieJar;
|
||||||
use maud::{Markup, PreEscaped, html};
|
use maud::{html, Markup, PreEscaped};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
pub(super) async fn index(cookies: CookieJar, headers: HeaderMap) -> Markup {
|
pub(super) async fn index(cookies: CookieJar, headers: HeaderMap) -> Markup {
|
||||||
let lang = language(&cookies, &headers);
|
let lang = language(&cookies, &headers);
|
||||||
|
|
||||||
rust_i18n::set_locale(lang.to_locale());
|
rust_i18n::set_locale(lang.to_locale());
|
||||||
|
|
||||||
let page = Page::new(lang);
|
let page = Page::new(lang);
|
||||||
@@ -27,29 +30,290 @@ pub(super) async fn index(cookies: CookieJar, headers: HeaderMap) -> Markup {
|
|||||||
}
|
}
|
||||||
p { (t!("project_description")) }
|
p { (t!("project_description")) }
|
||||||
|
|
||||||
h2 { (t!("what_to_do_title")) }
|
div.grid.gap-lg.grid-cols-2 {
|
||||||
|
|
||||||
div.grid.gap-lg {
|
|
||||||
article {
|
article {
|
||||||
header { (t!("visit_booth_title")) }
|
header { (t!("what_to_do_title")) }
|
||||||
|
div class="text-center" {
|
||||||
(t!("visit_booth_description"))
|
(PreEscaped(t!("new_header")))
|
||||||
|
}
|
||||||
a
|
a
|
||||||
href="https://www.jku.at/ars-electronica-2025-panic-yes-no/digital-shadows/"
|
href="https://ars.electronica.art/panic/de/view/digital-shadows-22a38ddb450c81d08bc4f50a818b0319/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
title=(t!("jku_link_title")) { (t!("find_out_more")) }
|
class="btn mt-1 block"
|
||||||
|
role="button" {
|
||||||
footer { (t!("location_postcity")) }
|
(t!("more_infos"))
|
||||||
|
(PreEscaped(" ↗️"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
article {
|
article {
|
||||||
header { (t!("play_game_title")) }
|
header { (t!("play_game_title")) }
|
||||||
|
|
||||||
(t!("play_game_description"))
|
(t!("play_game_description"))
|
||||||
|
|
||||||
a href="/game" title=(t!("game_link_title")) { (t!("find_out_more")) }
|
a
|
||||||
|
href="/game"
|
||||||
|
class="btn mt-1 block"
|
||||||
|
title=(t!("game_link_title"))
|
||||||
|
role="button" { (t!("find_out_more")) }
|
||||||
|
}
|
||||||
|
article.col-span-2 {
|
||||||
|
header { (PreEscaped(t!("tour_aef_title"))) }
|
||||||
|
|
||||||
footer { (t!("location_linz")) }
|
(PreEscaped(t!("tour_aef_description")))
|
||||||
|
|
||||||
|
a
|
||||||
|
href="https://reglist24.com/digital-shadows-tour/form"
|
||||||
|
target="_blank"
|
||||||
|
class="btn mt-1 block"
|
||||||
|
role="button"
|
||||||
|
title=(t!("tour_register_title")) {
|
||||||
|
(t!("tour_register"))
|
||||||
|
(PreEscaped(" ↗️"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
a
|
||||||
|
href="https://digidow.eu"
|
||||||
|
target="_blank"
|
||||||
|
class="btn mt-1 block"
|
||||||
|
role="button" { (t!("scientific_background"))(PreEscaped(" ↗️")) }
|
||||||
|
|
||||||
|
|
||||||
|
h2 { (t!("calendar")) }
|
||||||
|
div #calendar-options .calendar-options {
|
||||||
|
p { (t!("cal_options")) }
|
||||||
|
|
||||||
|
a href="https://calendar.google.com/calendar/embed?height=600&wkst=2&ctz=Europe%2FVienna&showPrint=0&showTz=0&showCalendars=0&showTabs=0&showDate=0&showNav=0&showTitle=0&mode=AGENDA&hl=en&src=YjA4NTZlZDUyZDAwNmY3ZTczNTgxYTk2NjI4MjFjMDZhYWJkZjUzMDBkMjZmMDZiOWFiYzg2YWE3YzBhNzFlNkBncm91cC5jYWxlbmRhci5nb29nbGUuY29t&color=%23d50000"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="calendar-link" {
|
||||||
|
(t!("cal_new_tab"))
|
||||||
|
(PreEscaped(" ↗️"))
|
||||||
|
}
|
||||||
|
|
||||||
|
button #embed-calendar .embed-button {
|
||||||
|
(PreEscaped(t!("cal_show_here")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe #google-calendar
|
||||||
|
data-src="https://calendar.google.com/calendar/embed?height=600&wkst=2&ctz=Europe%2FVienna&showPrint=0&showTz=0&showCalendars=0&showTabs=0&showDate=0&showNav=0&showTitle=0&mode=AGENDA&hl=en&src=YjA4NTZlZDUyZDAwNmY3ZTczNTgxYTk2NjI4MjFjMDZhYWJkZjUzMDBkMjZmMDZiOWFiYzg2YWE3YzBhNzFlNkBncm91cC5jYWxlbmRhci5nb29nbGUuY29t&color=%23d50000"
|
||||||
|
width="100%"
|
||||||
|
height="600"
|
||||||
|
frameborder="0"
|
||||||
|
scrolling="no"
|
||||||
|
class="hidden" {}
|
||||||
|
|
||||||
|
script {
|
||||||
|
(PreEscaped(r#"
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const calendarOptions = document.getElementById('calendar-options');
|
||||||
|
const embedButton = document.getElementById('embed-calendar');
|
||||||
|
const iframe = document.getElementById('google-calendar');
|
||||||
|
|
||||||
|
embedButton.addEventListener('click', function() {
|
||||||
|
// Hide the options
|
||||||
|
calendarOptions.classList.add('hidden');
|
||||||
|
|
||||||
|
// Load the iframe by setting src from data-src
|
||||||
|
const src = iframe.getAttribute('data-src');
|
||||||
|
iframe.setAttribute('src', src);
|
||||||
|
|
||||||
|
// Show the iframe
|
||||||
|
iframe.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
"#))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn cam(cookies: CookieJar, headers: HeaderMap) -> Markup {
|
||||||
|
let lang = language(&cookies, &headers);
|
||||||
|
rust_i18n::set_locale(lang.to_locale());
|
||||||
|
|
||||||
|
let page = Page::new(lang);
|
||||||
|
page.content(html! {
|
||||||
|
hgroup {
|
||||||
|
h1 { (PreEscaped(t!("cam_title"))) }
|
||||||
|
p { (t!("cam_title2")) }
|
||||||
|
}
|
||||||
|
hgroup {
|
||||||
|
h2 { (t!("cam_subtitle")) }
|
||||||
|
p { (t!("cam_description")) }
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
(t!("cam_project_by"))
|
||||||
|
span.highlight { (t!("cam_institute")) }
|
||||||
|
}
|
||||||
|
blockquote {
|
||||||
|
(t!("cam_mission_quote"))
|
||||||
|
footer {
|
||||||
|
cite { (t!("cam_mission_attribution")) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p { (t!("cam_project_description")) }
|
||||||
|
|
||||||
|
h2 { (t!("cam_how_it_works")) }
|
||||||
|
|
||||||
|
div.grid.gap-lg {
|
||||||
|
article {
|
||||||
|
header { (t!("cam_tech_setup_title")) }
|
||||||
|
|
||||||
|
p { (t!("cam_tech_setup_p1")) }
|
||||||
|
|
||||||
|
p { (t!("cam_tech_setup_p2")) }
|
||||||
|
}
|
||||||
|
article {
|
||||||
|
header { (PreEscaped(t!("cam_data_processing_title"))) }
|
||||||
|
|
||||||
|
p { (PreEscaped(t!("cam_data_processing_p1"))) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 { (t!("cam_festival_details")) }
|
||||||
|
|
||||||
|
div.info-box {
|
||||||
|
h3 { (t!("cam_when_where_title")) }
|
||||||
|
p {
|
||||||
|
(t!("cam_festival_info")) br;
|
||||||
|
(t!("cam_festival_dates")) br;
|
||||||
|
(t!("cam_festival_location"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 { (t!("cam_legal_compliance")) }
|
||||||
|
|
||||||
|
p { (t!("cam_legal_description")) }
|
||||||
|
|
||||||
|
div.legal-docs {
|
||||||
|
a href="/static/dsb-request.pdf" target="_blank" title=(t!("cam_legal_request_title")) { (t!("cam_legal_request"))(PreEscaped(" ↗️"))}
|
||||||
|
" | "
|
||||||
|
a href="/static/dsb-accept.pdf" target="_blank" title=(t!("cam_legal_decision_title")) { (t!("cam_legal_decision"))(PreEscaped(" ↗️")) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub(super) struct PrivacyQuery {
|
||||||
|
deleted: Option<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn data(
|
||||||
|
cookies: CookieJar,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Query(query): Query<PrivacyQuery>,
|
||||||
|
) -> Markup {
|
||||||
|
let lang = language(&cookies, &headers);
|
||||||
|
rust_i18n::set_locale(lang.to_locale());
|
||||||
|
|
||||||
|
let mut page = Page::new(lang);
|
||||||
|
|
||||||
|
// Show success message if data was deleted
|
||||||
|
if query.deleted == Some(1) {
|
||||||
|
page.set_message(MyMessage::DataDeleted);
|
||||||
|
}
|
||||||
|
page.content(html! {
|
||||||
|
h1 { (PreEscaped(t!("privacy_policy_title"))) }
|
||||||
|
h2 { (t!("data_controller")) }
|
||||||
|
p {
|
||||||
|
(PreEscaped(t!("data_controller_info")))
|
||||||
|
}
|
||||||
|
h2 { (t!("overview")) }
|
||||||
|
p {
|
||||||
|
(PreEscaped(t!("privacy_overview")))
|
||||||
|
}
|
||||||
|
h2 { (t!("data_we_collect")) }
|
||||||
|
h3 { (t!("cookies")) }
|
||||||
|
p {
|
||||||
|
(t!("cookies_description"))
|
||||||
|
ol {
|
||||||
|
li {
|
||||||
|
kbd { "client_id" }
|
||||||
|
" "
|
||||||
|
(t!("cookie_client_id"))
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
kbd { "lang" }
|
||||||
|
" "
|
||||||
|
(t!("cookie_lang"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h3 { (t!("game_data")) }
|
||||||
|
p {
|
||||||
|
(t!("game_data_description"))
|
||||||
|
ul {
|
||||||
|
li { (t!("chosen_name")) }
|
||||||
|
li { (t!("game_progress")) }
|
||||||
|
li {
|
||||||
|
(PreEscaped(t!("random_client_id")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
h2 { (t!("purpose_legal_basis")) }
|
||||||
|
ul {
|
||||||
|
li { (t!("game_functionality")) }
|
||||||
|
li { (t!("language_preference")) }
|
||||||
|
li { (t!("statistical_analysis")) }
|
||||||
|
}
|
||||||
|
h2 { (t!("data_retention")) }
|
||||||
|
p {
|
||||||
|
(t!("data_retention_description"))
|
||||||
|
}
|
||||||
|
h2 { (t!("data_sharing")) }
|
||||||
|
p {
|
||||||
|
(t!("data_sharing_description"))
|
||||||
|
}
|
||||||
|
h2 { (t!("server_logfiles")) }
|
||||||
|
p {
|
||||||
|
(t!("server_logfiles_description"))
|
||||||
|
}
|
||||||
|
h2 { (t!("data_security")) }
|
||||||
|
p {
|
||||||
|
(t!("data_security_description"))
|
||||||
|
}
|
||||||
|
h2 { (t!("minors")) }
|
||||||
|
p {
|
||||||
|
(t!("minors_description"))
|
||||||
|
}
|
||||||
|
h2 { (t!("data_protection_officer")) }
|
||||||
|
p {
|
||||||
|
(PreEscaped(t!("data_protection_officer_contact_full")))
|
||||||
|
}
|
||||||
|
h2 { (t!("data_collection_timing")) }
|
||||||
|
p {
|
||||||
|
(t!("data_collection_timing_description"))
|
||||||
|
}
|
||||||
|
h2 { (t!("your_rights_gdpr")) }
|
||||||
|
p {
|
||||||
|
(t!("rights_description"))
|
||||||
|
ul {
|
||||||
|
li { (t!("right_access")) }
|
||||||
|
li { (t!("right_rectification")) }
|
||||||
|
li { (t!("right_erasure")) }
|
||||||
|
li { (t!("right_restriction")) }
|
||||||
|
li { (t!("right_portability")) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h3 { (t!("how_to_exercise_rights")) }
|
||||||
|
ul {
|
||||||
|
li { (t!("clear_cookies")) }
|
||||||
|
li {
|
||||||
|
(PreEscaped(t!("contact_us")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h3 { (t!("delete_personal_data")) }
|
||||||
|
p {
|
||||||
|
(t!("delete_data_description"))
|
||||||
|
}
|
||||||
|
form method="POST" action="/delete-data" onsubmit={"return confirm('" (t!("delete_confirmation")) "');"} {
|
||||||
|
button type="submit" class="secondary" {
|
||||||
|
(t!("delete_my_data"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
303
src/main.rs
303
src/main.rs
@@ -1,16 +1,27 @@
|
|||||||
use crate::model::client::Client;
|
use crate::model::client::Client;
|
||||||
use axum::{Router, http::HeaderMap, routing::get};
|
use axum::{
|
||||||
use axum_extra::extract::{CookieJar, cookie::Cookie};
|
http::HeaderMap,
|
||||||
use axum_messages::MessagesManagerLayer;
|
response::Redirect,
|
||||||
use sqlx::{SqlitePool, pool::PoolOptions, sqlite::SqliteConnectOptions};
|
routing::{get, post},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use axum_extra::extract::{
|
||||||
|
cookie::{Cookie, Expiration, Key},
|
||||||
|
CookieJar, PrivateCookieJar,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, SqlitePool};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
|
fs,
|
||||||
|
path::Path,
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
sync::{Arc, LazyLock},
|
sync::{Arc, LazyLock},
|
||||||
};
|
};
|
||||||
|
use time::{Date, Month, OffsetDateTime, Time};
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use tower_sessions::{MemoryStore, SessionManagerLayer};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
@@ -18,6 +29,7 @@ extern crate rust_i18n;
|
|||||||
|
|
||||||
i18n!("locales", fallback = "en");
|
i18n!("locales", fallback = "en");
|
||||||
|
|
||||||
|
mod admin;
|
||||||
mod game;
|
mod game;
|
||||||
mod index;
|
mod index;
|
||||||
pub(crate) mod language;
|
pub(crate) mod language;
|
||||||
@@ -70,6 +82,7 @@ impl From<String> for Language {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
struct Req {
|
struct Req {
|
||||||
client: Client,
|
client: Client,
|
||||||
lang: Language,
|
lang: Language,
|
||||||
@@ -82,36 +95,65 @@ pub(crate) enum NameUpdateError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static BAD_WORDS: LazyLock<HashSet<String>> = LazyLock::new(|| {
|
static BAD_WORDS: LazyLock<HashSet<String>> = LazyLock::new(|| {
|
||||||
const BAD_WORDS_FILE: &str = include_str!("../bad/merged_output.txt");
|
const BAD_WORDS_FILE: &str = include_str!("../bad/bad-list.txt");
|
||||||
|
|
||||||
BAD_WORDS_FILE
|
BAD_WORDS_FILE
|
||||||
.lines()
|
.lines()
|
||||||
.map(|line| line.trim())
|
.map(|line| line.trim())
|
||||||
.filter(|line| !line.is_empty() && !line.starts_with('#')) // Skip empty lines and comments
|
.map(|word| word.to_lowercase())
|
||||||
.map(|word| {
|
|
||||||
word.to_lowercase()
|
|
||||||
.chars()
|
|
||||||
.filter(|c| c.is_alphabetic())
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.filter(|word: &String| !word.is_empty())
|
|
||||||
.collect()
|
.collect()
|
||||||
});
|
});
|
||||||
|
|
||||||
fn contains_bad_word(text: &str) -> bool {
|
fn contains_bad_word(text: &str) -> bool {
|
||||||
|
// check parts of string, e.g. ABC_DEF checks both ABC and DEF
|
||||||
|
let cleaned_text = text.to_lowercase();
|
||||||
|
if cleaned_text
|
||||||
|
.split(|c: char| !c.is_alphabetic())
|
||||||
|
.any(|part| BAD_WORDS.iter().any(|bad| part == bad))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check string as a whole, e.g. ABC_DEF checks for ABCDEF
|
||||||
let cleaned_text: String = text
|
let cleaned_text: String = text
|
||||||
.to_lowercase()
|
.to_lowercase()
|
||||||
.chars()
|
.chars()
|
||||||
.filter(|c| c.is_alphabetic())
|
.filter(|c| c.is_alphabetic())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
BAD_WORDS
|
BAD_WORDS.iter().any(|bad_word| &cleaned_text == bad_word)
|
||||||
.iter()
|
}
|
||||||
.any(|bad_word| cleaned_text.contains(bad_word))
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::contains_bad_word;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_whitelist_words_are_not_flagged() {
|
||||||
|
let whitelist_content =
|
||||||
|
fs::read_to_string("bad/test-common-names.txt").expect("Failed to read file");
|
||||||
|
|
||||||
|
for (line_number, line) in whitelist_content.lines().enumerate() {
|
||||||
|
let word = line.trim();
|
||||||
|
|
||||||
|
// Skip empty lines
|
||||||
|
if word.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!contains_bad_word(word),
|
||||||
|
"Word '{}' on line {} should not be flagged as bad but was detected",
|
||||||
|
word,
|
||||||
|
line_number + 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Backend {
|
impl Backend {
|
||||||
async fn client(&self, cookies: CookieJar) -> (CookieJar, Client) {
|
async fn client(&self, cookies: PrivateCookieJar) -> (PrivateCookieJar, Client) {
|
||||||
let existing_uuid = cookies
|
let existing_uuid = cookies
|
||||||
.get("client_id")
|
.get("client_id")
|
||||||
.and_then(|cookie| Uuid::parse_str(cookie.value()).ok());
|
.and_then(|cookie| Uuid::parse_str(cookie.value()).ok());
|
||||||
@@ -120,16 +162,30 @@ impl Backend {
|
|||||||
Some(uuid) => (cookies, self.get_client(&uuid).await),
|
Some(uuid) => (cookies, self.get_client(&uuid).await),
|
||||||
None => {
|
None => {
|
||||||
let new_id = Uuid::new_v4();
|
let new_id = Uuid::new_v4();
|
||||||
let updated_cookies = cookies.add(Cookie::new("client_id", new_id.to_string()));
|
let expiration_date = OffsetDateTime::new_utc(
|
||||||
|
Date::from_calendar_date(2025, Month::September, 7).unwrap(),
|
||||||
|
Time::from_hms(20, 0, 0).unwrap(),
|
||||||
|
);
|
||||||
|
let mut cookie = Cookie::new("client_id", new_id.to_string());
|
||||||
|
cookie.set_expires(Expiration::DateTime(expiration_date));
|
||||||
|
cookie.set_http_only(true);
|
||||||
|
cookie.set_secure(true);
|
||||||
|
|
||||||
|
let updated_cookies = cookies.add(cookie);
|
||||||
(updated_cookies, self.get_client(&new_id).await)
|
(updated_cookies, self.get_client(&new_id).await)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combined method for getting both client and language
|
// Combined method for getting both client and language
|
||||||
async fn client_full(&self, cookies: CookieJar, headers: &HeaderMap) -> (CookieJar, Req) {
|
async fn client_full(
|
||||||
|
&self,
|
||||||
|
cookies: PrivateCookieJar,
|
||||||
|
lang_cookies: &CookieJar,
|
||||||
|
headers: &HeaderMap,
|
||||||
|
) -> (PrivateCookieJar, Req) {
|
||||||
let (cookies, client) = self.client(cookies).await;
|
let (cookies, client) = self.client(cookies).await;
|
||||||
let lang = language::language(&cookies, headers);
|
let lang = language::language(lang_cookies, headers);
|
||||||
(cookies, Req { client, lang })
|
(cookies, Req { client, lang })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +196,7 @@ impl Backend {
|
|||||||
if name.len() < 3 {
|
if name.len() < 3 {
|
||||||
return Err(NameUpdateError::TooShort(3, name.len()));
|
return Err(NameUpdateError::TooShort(3, name.len()));
|
||||||
}
|
}
|
||||||
if contains_bad_word(name) {
|
if contains_bad_word(name) || self.is_name_banned(name).await {
|
||||||
return Err(NameUpdateError::ContainsBadWord);
|
return Err(NameUpdateError::ContainsBadWord);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,12 +215,192 @@ impl Backend {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn is_name_banned(&self, name: &str) -> bool {
|
||||||
|
match self {
|
||||||
|
Backend::Sqlite(db) => {
|
||||||
|
let result = sqlx::query!("SELECT name FROM banned_names WHERE name = ?", name)
|
||||||
|
.fetch_optional(db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
result.is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ban_name(&self, name: &str) -> Result<(), sqlx::Error> {
|
||||||
|
match self {
|
||||||
|
Backend::Sqlite(db) => {
|
||||||
|
sqlx::query!("INSERT OR IGNORE INTO banned_names (name) VALUES (?)", name)
|
||||||
|
.execute(db)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn unban_name(&self, name: &str) -> Result<(), sqlx::Error> {
|
||||||
|
match self {
|
||||||
|
Backend::Sqlite(db) => {
|
||||||
|
sqlx::query!("DELETE FROM banned_names WHERE name = ?", name)
|
||||||
|
.execute(db)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_banned_names(&self) -> Vec<String> {
|
||||||
|
match self {
|
||||||
|
Backend::Sqlite(db) => {
|
||||||
|
let rows = sqlx::query!("SELECT name FROM banned_names ORDER BY banned_at DESC")
|
||||||
|
.fetch_all(db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
rows.into_iter().map(|row| row.name).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn replace_banned_names_with_asterisks(&self) -> Result<(), sqlx::Error> {
|
||||||
|
match self {
|
||||||
|
Backend::Sqlite(db) => {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE client SET name = '***' WHERE name IN (SELECT name FROM banned_names)"
|
||||||
|
)
|
||||||
|
.execute(db)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub(crate) backend: Arc<Backend>,
|
||||||
|
pub key: Key,
|
||||||
|
pub admin_password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl axum::extract::FromRef<AppState> for Key {
|
||||||
|
fn from_ref(state: &AppState) -> Self {
|
||||||
|
state.key.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl axum::extract::FromRef<AppState> for Arc<Backend> {
|
||||||
|
fn from_ref(state: &AppState) -> Self {
|
||||||
|
state.backend.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct Config {
|
||||||
|
key: Vec<u8>,
|
||||||
|
admin_password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
fn generate() -> Self {
|
||||||
|
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||||
|
let admin_password: String = thread_rng()
|
||||||
|
.sample_iter(&Alphanumeric)
|
||||||
|
.take(15)
|
||||||
|
.map(char::from)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
key: Key::generate().master().to_vec(),
|
||||||
|
admin_password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_or_create_config() -> Result<(Key, Config), Box<dyn std::error::Error>> {
|
||||||
|
let config_path = "config.toml";
|
||||||
|
|
||||||
|
// Try to read existing config
|
||||||
|
if Path::new(config_path).exists() {
|
||||||
|
let content = fs::read_to_string(config_path)?;
|
||||||
|
|
||||||
|
// Try to parse as complete config first
|
||||||
|
if let Ok(config) = toml::from_str::<Config>(&content) {
|
||||||
|
let key = Key::from(&config.key);
|
||||||
|
return Ok((key, config));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If that fails, try to parse just the key and generate new admin password
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct PartialConfig {
|
||||||
|
key: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(partial_config) = toml::from_str::<PartialConfig>(&content) {
|
||||||
|
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||||
|
let admin_password: String = thread_rng()
|
||||||
|
.sample_iter(&Alphanumeric)
|
||||||
|
.take(15)
|
||||||
|
.map(char::from)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let config = Config {
|
||||||
|
key: partial_config.key,
|
||||||
|
admin_password,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write the updated config back
|
||||||
|
let toml_string = toml::to_string(&config)?;
|
||||||
|
fs::write(config_path, toml_string)?;
|
||||||
|
|
||||||
|
let key = Key::from(&config.key);
|
||||||
|
return Ok((key, config));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new config if file doesn't exist or parsing failed
|
||||||
|
let config = Config::generate();
|
||||||
|
let toml_string = toml::to_string(&config)?;
|
||||||
|
fs::write(config_path, toml_string)?;
|
||||||
|
let key = Key::from(&config.key);
|
||||||
|
|
||||||
|
Ok((key, config))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_personal_data(
|
||||||
|
axum::extract::State(state): axum::extract::State<AppState>,
|
||||||
|
cookies: PrivateCookieJar,
|
||||||
|
) -> (PrivateCookieJar, Redirect) {
|
||||||
|
let backend = &state.backend;
|
||||||
|
// Get the client from cookies
|
||||||
|
if let Some(client_cookie) = cookies.get("client_id") {
|
||||||
|
if let Ok(uuid) = Uuid::parse_str(client_cookie.value()) {
|
||||||
|
// Delete all client data from database
|
||||||
|
let _ = backend.delete_client_data(&uuid).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the client_id cookie by setting an expired cookie
|
||||||
|
let expired_cookie = Cookie::build(("client_id", ""))
|
||||||
|
.expires(Expiration::DateTime(
|
||||||
|
OffsetDateTime::now_utc() - time::Duration::days(1),
|
||||||
|
))
|
||||||
|
.http_only(true)
|
||||||
|
.secure(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let updated_cookies = cookies.add(expired_cookie);
|
||||||
|
|
||||||
|
// Redirect back to privacy page with success message
|
||||||
|
(updated_cookies, Redirect::to("/privacy?deleted=1"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let session_store = MemoryStore::default();
|
tracing_subscriber::registry()
|
||||||
let session_layer = SessionManagerLayer::new(session_store).with_secure(false);
|
.with(tracing_subscriber::fmt::layer())
|
||||||
|
.with(EnvFilter::from_default_env())
|
||||||
|
.init();
|
||||||
|
|
||||||
let connection_options = SqliteConnectOptions::from_str("sqlite://db.sqlite").unwrap();
|
let connection_options = SqliteConnectOptions::from_str("sqlite://db.sqlite").unwrap();
|
||||||
let db: SqlitePool = PoolOptions::new()
|
let db: SqlitePool = PoolOptions::new()
|
||||||
@@ -172,13 +408,26 @@ async fn main() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let (key, config) = load_or_create_config().unwrap();
|
||||||
|
|
||||||
|
// Print admin password for convenience
|
||||||
|
tracing::info!("Admin password: {}", config.admin_password);
|
||||||
|
|
||||||
|
let state = AppState {
|
||||||
|
backend: Arc::new(Backend::Sqlite(db)),
|
||||||
|
key,
|
||||||
|
admin_password: config.admin_password,
|
||||||
|
};
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(index::index))
|
.route("/", get(index::index))
|
||||||
|
.route("/privacy", get(index::data))
|
||||||
|
.route("/cam", get(index::cam))
|
||||||
|
.route("/delete-data", post(delete_personal_data))
|
||||||
.nest_service("/static", ServeDir::new("./static/serve"))
|
.nest_service("/static", ServeDir::new("./static/serve"))
|
||||||
.merge(game::routes())
|
.merge(game::routes())
|
||||||
.with_state(Arc::new(Backend::Sqlite(db)))
|
.merge(admin::routes())
|
||||||
.layer(MessagesManagerLayer)
|
.with_state(state);
|
||||||
.layer(session_layer);
|
|
||||||
|
|
||||||
// run our app with hyper, listening globally on port 3000
|
// run our app with hyper, listening globally on port 3000
|
||||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||||
|
@@ -25,6 +25,69 @@ impl Backend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn create_camera(&self, uuid: &Uuid, name: &str, desc: Option<&str>) -> Result<(), sqlx::Error> {
|
||||||
|
let uuid_str = uuid.to_string();
|
||||||
|
match self {
|
||||||
|
Backend::Sqlite(db) => {
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO camera (uuid, name, desc) VALUES (?, ?, ?)",
|
||||||
|
uuid_str,
|
||||||
|
name,
|
||||||
|
desc
|
||||||
|
)
|
||||||
|
.execute(db)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn update_camera(&self, uuid: &Uuid, name: &str, desc: Option<&str>) -> Result<bool, sqlx::Error> {
|
||||||
|
let uuid_str = uuid.to_string();
|
||||||
|
match self {
|
||||||
|
Backend::Sqlite(db) => {
|
||||||
|
let result = sqlx::query!(
|
||||||
|
"UPDATE camera SET name = ?, desc = ? WHERE uuid = ?",
|
||||||
|
name,
|
||||||
|
desc,
|
||||||
|
uuid_str
|
||||||
|
)
|
||||||
|
.execute(db)
|
||||||
|
.await?;
|
||||||
|
Ok(result.rows_affected() > 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn delete_camera(&self, uuid: &Uuid) -> Result<bool, sqlx::Error> {
|
||||||
|
let uuid_str = uuid.to_string();
|
||||||
|
match self {
|
||||||
|
Backend::Sqlite(db) => {
|
||||||
|
let result = sqlx::query!(
|
||||||
|
"DELETE FROM camera WHERE uuid = ?",
|
||||||
|
uuid_str
|
||||||
|
)
|
||||||
|
.execute(db)
|
||||||
|
.await?;
|
||||||
|
Ok(result.rows_affected() > 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_all_cameras(&self) -> Vec<Camera> {
|
||||||
|
match self {
|
||||||
|
Backend::Sqlite(db) => {
|
||||||
|
sqlx::query_as!(
|
||||||
|
Camera,
|
||||||
|
"SELECT uuid, desc, name FROM camera ORDER BY name"
|
||||||
|
)
|
||||||
|
.fetch_all(db)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn amount_total_cameras(&self) -> i64 {
|
pub(crate) async fn amount_total_cameras(&self) -> i64 {
|
||||||
match self {
|
match self {
|
||||||
Backend::Sqlite(db) => {
|
Backend::Sqlite(db) => {
|
||||||
|
@@ -41,4 +41,28 @@ impl Backend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn delete_client_data(&self, uuid: &Uuid) -> Result<(), sqlx::Error> {
|
||||||
|
let uuid_str = uuid.to_string();
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Backend::Sqlite(db) => {
|
||||||
|
// Start a transaction to ensure data consistency
|
||||||
|
let mut tx = db.begin().await?;
|
||||||
|
|
||||||
|
// Delete sightings first (foreign key constraint)
|
||||||
|
sqlx::query!("DELETE FROM sightings WHERE client_uuid = ?", uuid_str)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Delete client record
|
||||||
|
sqlx::query!("DELETE FROM client WHERE uuid = ?", uuid_str)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,40 +1,101 @@
|
|||||||
use crate::{Backend, model::client::Client};
|
use crate::{model::client::Client, Backend};
|
||||||
|
|
||||||
pub(crate) struct Rank {
|
pub(crate) struct Rank {
|
||||||
pub(crate) rank: i64,
|
pub(crate) rank: i64,
|
||||||
pub(crate) client: Client,
|
pub(crate) client: Client,
|
||||||
pub(crate) amount: i64,
|
pub(crate) amount: i64,
|
||||||
|
pub(crate) show_dots_above: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Backend {
|
impl Backend {
|
||||||
pub(crate) async fn highscore(&self) -> Vec<Rank> {
|
pub(crate) async fn amount_participants(&self) -> i64 {
|
||||||
|
match self {
|
||||||
|
Backend::Sqlite(db) => {
|
||||||
|
let row =
|
||||||
|
sqlx::query!("SELECT COUNT(DISTINCT client_uuid) AS count FROM sightings; ")
|
||||||
|
.fetch_one(db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
row.count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn highscore(&self, client: &Client) -> Vec<Rank> {
|
||||||
match self {
|
match self {
|
||||||
Backend::Sqlite(db) => {
|
Backend::Sqlite(db) => {
|
||||||
let rows = sqlx::query!(
|
let rows = sqlx::query!(
|
||||||
"SELECT
|
"WITH ranked_clients AS (
|
||||||
RANK() OVER (ORDER BY COUNT(s.client_uuid) DESC) as rank,
|
SELECT
|
||||||
c.name,
|
DENSE_RANK() OVER (ORDER BY COUNT(s.client_uuid) DESC) as rank,
|
||||||
c.uuid,
|
c.name,
|
||||||
COUNT(s.client_uuid) as amount
|
c.uuid,
|
||||||
FROM client c
|
COUNT(s.client_uuid) as amount
|
||||||
LEFT JOIN sightings s ON c.uuid = s.client_uuid
|
FROM client c
|
||||||
GROUP BY c.uuid, c.name
|
LEFT JOIN sightings s ON c.uuid = s.client_uuid
|
||||||
ORDER BY amount DESC"
|
GROUP BY c.uuid, c.name
|
||||||
|
)
|
||||||
|
SELECT rank, name, uuid, amount
|
||||||
|
FROM ranked_clients
|
||||||
|
WHERE rank <= COALESCE(
|
||||||
|
(SELECT rank FROM ranked_clients ORDER BY rank LIMIT 1 OFFSET 9),
|
||||||
|
(SELECT MAX(rank) FROM ranked_clients)
|
||||||
|
) AND amount > 0
|
||||||
|
ORDER BY rank, name"
|
||||||
)
|
)
|
||||||
.fetch_all(db)
|
.fetch_all(db)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
rows.into_iter()
|
let mut ret: Vec<Rank> = rows
|
||||||
|
.into_iter()
|
||||||
.map(|row| Rank {
|
.map(|row| Rank {
|
||||||
|
rank: row.rank.unwrap(),
|
||||||
|
client: Client {
|
||||||
|
uuid: row.uuid.unwrap(),
|
||||||
|
name: row.name,
|
||||||
|
},
|
||||||
|
amount: row.amount.unwrap(),
|
||||||
|
show_dots_above: false,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let user_is_in_top = ret.iter().any(|x| &x.client == client);
|
||||||
|
if !user_is_in_top {
|
||||||
|
let row = sqlx::query!(
|
||||||
|
"WITH ranked_clients AS (
|
||||||
|
SELECT
|
||||||
|
DENSE_RANK() OVER (ORDER BY COUNT(s.client_uuid) DESC) as rank,
|
||||||
|
c.name,
|
||||||
|
c.uuid,
|
||||||
|
COUNT(s.client_uuid) as amount
|
||||||
|
FROM client c
|
||||||
|
LEFT JOIN sightings s ON c.uuid = s.client_uuid
|
||||||
|
GROUP BY c.uuid, c.name
|
||||||
|
)
|
||||||
|
SELECT rank, name, uuid, amount
|
||||||
|
FROM ranked_clients
|
||||||
|
WHERE uuid = ?
|
||||||
|
ORDER BY rank, name",
|
||||||
|
client.uuid
|
||||||
|
)
|
||||||
|
.fetch_one(db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
ret.push(Rank {
|
||||||
rank: row.rank,
|
rank: row.rank,
|
||||||
client: Client {
|
client: Client {
|
||||||
uuid: row.uuid,
|
uuid: row.uuid,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
},
|
},
|
||||||
amount: row.amount,
|
amount: row.amount,
|
||||||
|
show_dots_above: true,
|
||||||
})
|
})
|
||||||
.collect()
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
113
src/page.rs
113
src/page.rs
@@ -1,52 +1,28 @@
|
|||||||
use crate::Language;
|
use crate::Language;
|
||||||
use axum_messages::Messages;
|
use maud::{html, Markup, DOCTYPE};
|
||||||
use maud::{DOCTYPE, Markup, html};
|
|
||||||
|
|
||||||
pub(crate) struct Page {
|
pub(crate) struct Page {
|
||||||
lang: Language,
|
lang: Language,
|
||||||
found_camera: Option<(String, i64)>,
|
message: Option<MyMessage>,
|
||||||
new_name: bool,
|
}
|
||||||
err: Option<(String, String, String)>,
|
|
||||||
|
pub(crate) enum MyMessage {
|
||||||
|
NameChanged,
|
||||||
|
FoundCam(String, i64),
|
||||||
|
Error(String, String, String),
|
||||||
|
DataDeleted,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Page {
|
impl Page {
|
||||||
pub fn new(lang: Language) -> Self {
|
pub fn new(lang: Language) -> Self {
|
||||||
Self {
|
Self {
|
||||||
lang,
|
lang,
|
||||||
found_camera: None,
|
message: None,
|
||||||
new_name: false,
|
|
||||||
err: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn messages(&mut self, messages: Messages) {
|
pub(crate) fn set_message(&mut self, message: MyMessage) {
|
||||||
for message in messages {
|
self.message = Some(message);
|
||||||
let text = &message.to_string()[..];
|
|
||||||
match (message.level, text) {
|
|
||||||
(_, "set-name-succ") => {
|
|
||||||
self.new_name = true;
|
|
||||||
}
|
|
||||||
(_, msg) if msg.starts_with("found-cam|") => {
|
|
||||||
let mut parts = msg.splitn(3, '|');
|
|
||||||
let _ = parts.next().expect("just checked |");
|
|
||||||
if let (Some(name), Some(amount)) = (parts.next(), parts.next()) {
|
|
||||||
if let Ok(amount) = amount.parse::<i64>() {
|
|
||||||
self.found_camera = Some((name.into(), amount));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(_, msg) if msg.starts_with("err|") => {
|
|
||||||
let mut parts = msg.splitn(4, '|');
|
|
||||||
let _ = parts.next().expect("just checked |");
|
|
||||||
if let (Some(title), Some(body), Some(footer)) =
|
|
||||||
(parts.next(), parts.next(), parts.next())
|
|
||||||
{
|
|
||||||
self.err = Some((title.into(), body.into(), footer.into()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(_, _) => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn content(self, content: Markup) -> Markup {
|
pub fn content(self, content: Markup) -> Markup {
|
||||||
@@ -92,43 +68,52 @@ impl Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main.container {
|
main.container {
|
||||||
@if let Some(found_camera) = &self.found_camera {
|
@if let Some(message) = &self.message {
|
||||||
div.flex {
|
@match message {
|
||||||
article class="succ msg" {
|
MyMessage::FoundCam(name, amount) => {
|
||||||
header { (t!("found_camera_title", name = found_camera.0)) }
|
div.flex {
|
||||||
(t!("found_camera_body", amount = found_camera.1))
|
article class="succ msg" {
|
||||||
footer {
|
header { (t!("found_camera_title", name = name)) }
|
||||||
a href="#ranking" { (t!("see_ranking")) }
|
(t!("found_camera_body", amount = amount))
|
||||||
|
footer {
|
||||||
|
a href="#ranking" { (t!("see_ranking")) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MyMessage::NameChanged => {
|
||||||
|
div.flex {
|
||||||
|
article class="name msg" {
|
||||||
|
header { (t!("new_name_title")) }
|
||||||
|
(t!("new_name_message"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MyMessage::Error(header, body, footer) => {
|
||||||
|
div.flex {
|
||||||
|
article class="error msg" {
|
||||||
|
header { (header) }
|
||||||
|
(body)
|
||||||
|
footer { (footer) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MyMessage::DataDeleted => {
|
||||||
|
div.flex {
|
||||||
|
article class="succ msg" {
|
||||||
|
header { (t!("data_deletion_success_title")) }
|
||||||
|
(t!("data_deletion_success_body"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@if self.new_name {
|
|
||||||
div.flex {
|
|
||||||
article class="name msg" {
|
|
||||||
header { (t!("new_name_title")) }
|
|
||||||
(t!("new_name_message"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@if let Some(err) = &self.err {
|
|
||||||
div.flex {
|
|
||||||
article class="error msg" {
|
|
||||||
header { (err.0) }
|
|
||||||
(err.1)
|
|
||||||
footer { (err.2) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section { (content) }
|
section { (content) }
|
||||||
}
|
}
|
||||||
|
|
||||||
footer.container {
|
footer.container {
|
||||||
small {
|
small {
|
||||||
(t!("footer_text"))
|
a href="/privacy" { (t!("privacy_policy")) }
|
||||||
mark { (t!("footer_todo")) }
|
|
||||||
a href="#" { (t!("footer_links")) }
|
|
||||||
" • "
|
" • "
|
||||||
a target="_blank" href="https://www.digidow.eu/impressum/" {
|
a target="_blank" href="https://www.digidow.eu/impressum/" {
|
||||||
(t!("impressum"))
|
(t!("impressum"))
|
||||||
|
BIN
static/serve/dsb-accept.pdf
Normal file
BIN
static/serve/dsb-accept.pdf
Normal file
Binary file not shown.
BIN
static/serve/dsb-request.pdf
Normal file
BIN
static/serve/dsb-request.pdf
Normal file
Binary file not shown.
@@ -19,6 +19,48 @@
|
|||||||
--custom-box-shadow: #015886;
|
--custom-box-shadow: #015886;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
hyphens: auto;
|
||||||
|
}
|
||||||
|
.calendar-options {
|
||||||
|
border: 2px solid #ccc;
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: gray;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-link {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #4285f4;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 24px;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-button {
|
||||||
|
background-color: #34a853;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/** Headline Styles */
|
/** Headline Styles */
|
||||||
h1 {
|
h1 {
|
||||||
font-family: 'Rubik Doodle Shadow', sans-serif;
|
font-family: 'Rubik Doodle Shadow', sans-serif;
|
||||||
@@ -40,7 +82,8 @@ h2 {
|
|||||||
|
|
||||||
/** Button Styles */
|
/** Button Styles */
|
||||||
input[type="submit"],
|
input[type="submit"],
|
||||||
button:not([class="secondary"]) {
|
button:not([class="secondary"]),
|
||||||
|
.btn {
|
||||||
transition: box-shadow 0.3s ease;
|
transition: box-shadow 0.3s ease;
|
||||||
|
|
||||||
background: var(--pico-primary-background);
|
background: var(--pico-primary-background);
|
||||||
@@ -94,11 +137,19 @@ input[type="submit"]:active {
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mt-1 {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.flex {
|
.flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.block {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
.msg {
|
.msg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 40rem;
|
max-width: 40rem;
|
||||||
@@ -114,6 +165,14 @@ input[type="submit"]:active {
|
|||||||
.gap-lg {
|
.gap-lg {
|
||||||
grid-gap: 5rem;
|
grid-gap: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grid-cols-2 {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-span-2 {
|
||||||
|
grid-column: span 2 / span 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** custom ol style */
|
/** custom ol style */
|
||||||
@@ -139,19 +198,20 @@ ul.iterated > li {
|
|||||||
border-bottom: 2px solid var(--pico-color);
|
border-bottom: 2px solid var(--pico-color);
|
||||||
border-radius: 2% 6% 5% 4% / 1% 1% 2% 4%;
|
border-radius: 2% 6% 5% 4% / 1% 1% 2% 4%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
&::before {
|
ul.iterated > li > * {
|
||||||
content: '';
|
position: relative;
|
||||||
border-bottom: 1px solid var(--pico-color);
|
z-index: 1; /* Bring content forward */
|
||||||
display: block;
|
}
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
position: absolute;
|
ul.iterated > li.no-border {
|
||||||
top: 50%;
|
border-bottom: 0;
|
||||||
left: 50%;
|
}
|
||||||
transform: translate3d(-50%, -50%, 0) scale(1.015) rotate(0.5deg);
|
|
||||||
border-radius: 1% 1% 2% 4% / 2% 6% 5% 4%;
|
ul.iterated > li.no-border::before {
|
||||||
}
|
content: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header */
|
/* Header */
|
||||||
|
@@ -13,41 +13,64 @@ function setLanguageCookie() {
|
|||||||
// set lang, if lang attribute doesn't exit set default en
|
// set lang, if lang attribute doesn't exit set default en
|
||||||
let lang = langToggle.getAttribute('lang') ? langToggle.getAttribute('lang') : 'en';
|
let lang = langToggle.getAttribute('lang') ? langToggle.getAttribute('lang') : 'en';
|
||||||
document.cookie = "language=" + lang;
|
document.cookie = "language=" + lang;
|
||||||
window.location.reload();
|
window.location.assign(window.location.href);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** [ph] */
|
/** [ph] */
|
||||||
function switchTheme() {
|
function switchTheme() {
|
||||||
let isLight = true;
|
// Define the SVG icons first
|
||||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches){
|
|
||||||
isLight = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const html = document.documentElement
|
|
||||||
const switchTheme = document.getElementById('theme_switcher')
|
|
||||||
const os_default = '<svg viewBox="0 0 16 16"><path fill="currentColor" d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></svg>'
|
const os_default = '<svg viewBox="0 0 16 16"><path fill="currentColor" d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></svg>'
|
||||||
const sun = '<svg viewBox="0 0 16 16"><path fill="currentColor" d="M8 11a3 3 0 1 1 0-6a3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8a4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"/></svg>'
|
const sun = '<svg viewBox="0 0 16 16"><path fill="currentColor" d="M8 11a3 3 0 1 1 0-6a3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8a4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"/></svg>'
|
||||||
const moon = '<svg viewBox="0 0 16 16"><g fill="currentColor"><path d="M6 .278a.768.768 0 0 1 .08.858a7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277c.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316a.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71C0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278zM4.858 1.311A7.269 7.269 0 0 0 1.025 7.71c0 4.02 3.279 7.276 7.319 7.276a7.316 7.316 0 0 0 5.205-2.162c-.337.042-.68.063-1.029.063c-4.61 0-8.343-3.714-8.343-8.29c0-1.167.242-2.278.681-3.286z"/><path d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z"/></g></svg>'
|
const moon = '<svg viewBox="0 0 16 16"><g fill="currentColor"><path d="M6 .278a.768.768 0 0 1 .08.858a7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277c.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316a.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71C0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278zM4.858 1.311A7.269 7.269 0 0 0 1.025 7.71c0 4.02 3.279 7.276 7.319 7.276a7.316 7.316 0 0 0 5.205-2.162c-.337.042-.68.063-1.029.063c-4.61 0-8.343-3.714-8.343-8.29c0-1.167.242-2.278.681-3.286z"/><path d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z"/></g></svg>'
|
||||||
const removeTooltip = (timeInt = 1750) => {
|
|
||||||
setTimeout(()=>{
|
// Check for saved theme preference first
|
||||||
switchTheme.blur()
|
let savedTheme = localStorage.getItem('theme-preference');
|
||||||
},timeInt)
|
let isLight = true;
|
||||||
|
|
||||||
|
if (savedTheme) {
|
||||||
|
// Use saved preference
|
||||||
|
isLight = savedTheme === 'light';
|
||||||
|
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
// Fall back to OS preference only if no saved theme
|
||||||
|
isLight = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
switchTheme.innerHTML = os_default
|
const html = document.documentElement;
|
||||||
//html.setAttribute('data-theme', 'auto')
|
const switchTheme = document.getElementById('theme_switcher');
|
||||||
switchTheme.setAttribute('data-tooltip', 'os theme')
|
|
||||||
switchTheme.focus()
|
|
||||||
removeTooltip(3000)
|
|
||||||
|
|
||||||
switchTheme.addEventListener('click', (e)=> {
|
const removeTooltip = (timeInt = 1750) => {
|
||||||
e.preventDefault()
|
setTimeout(() => {
|
||||||
isLight = !isLight
|
switchTheme.blur();
|
||||||
html.setAttribute('data-theme', isLight? 'light':'dark')
|
}, timeInt);
|
||||||
switchTheme.innerHTML = isLight? sun : moon
|
};
|
||||||
switchTheme.setAttribute('data-tooltip', `theme ${isLight?'light':'dark'}`)
|
|
||||||
removeTooltip()
|
// Apply the theme immediately if we have a saved preference
|
||||||
})
|
if (savedTheme && savedTheme !== 'auto') {
|
||||||
|
html.setAttribute('data-theme', savedTheme);
|
||||||
|
switchTheme.innerHTML = isLight ? sun : moon;
|
||||||
|
switchTheme.setAttribute('data-tooltip', `theme ${savedTheme}`);
|
||||||
|
} else {
|
||||||
|
// Default OS theme
|
||||||
|
switchTheme.innerHTML = os_default;
|
||||||
|
switchTheme.setAttribute('data-tooltip', 'os theme');
|
||||||
|
}
|
||||||
|
|
||||||
|
switchTheme.focus();
|
||||||
|
removeTooltip(3000);
|
||||||
|
|
||||||
|
switchTheme.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
isLight = !isLight;
|
||||||
|
const newTheme = isLight ? 'light' : 'dark';
|
||||||
|
|
||||||
|
// Save preference
|
||||||
|
localStorage.setItem('theme-preference', newTheme);
|
||||||
|
|
||||||
|
html.setAttribute('data-theme', newTheme);
|
||||||
|
switchTheme.innerHTML = isLight ? sun : moon;
|
||||||
|
switchTheme.setAttribute('data-tooltip', `theme ${newTheme}`);
|
||||||
|
removeTooltip();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user