React UI
Some checks reported errors
continuous-integration/drone/push Build is passing
continuous-integration/drone Build was killed

This commit is contained in:
Andrey Tkachenko 2023-07-12 00:44:22 +04:00
parent 7f189b49b2
commit 6df0244cc5
83 changed files with 3660 additions and 2361 deletions

View File

@ -1,53 +1,29 @@
[package]
name = "rphotos"
version = "0.12.2"
authors = ["Rasmus Kaj <kaj@kth.se>"]
name = "album"
version = "0.1.0"
authors = ["Andrey Tkachenko<andrey@aidev.ru>"]
edition = "2021"
build = "src/build.rs"
[workspace]
[build-dependencies]
ructe = { version = "0.16.0", features = ["sass", "warp03"] }
members = [
"crates/album-api",
# "crates/album-db",
# "crates/album-core"
]
[dependencies]
album-api = { path = "./crates/album-api" }
async-trait = "0.1.64"
async-walkdir = "0.2.0"
brotli = "3.3.0"
chrono = "0.4.19" # Must match version used by diesel
chrono = "0.4.19"
clap = { version = "4.0.18", features = ["derive", "wrap_help", "env"] }
diesel-async = { version = "0.2.0", features = ["deadpool", "postgres"] }
diesel-async = { version = "0.3.1", features = ["deadpool", "postgres"] }
dotenv = "0.15"
flate2 = "1.0.14"
futures-lite = "1.12.0"
image = "0.24.0"
kamadak-exif = "0.5.0"
lazy-regex = "2.2.2"
libc = "0.2.68"
medallion = "2.3.1"
mime = "0.3.0"
r2d2-memcache = "0.6"
rand = "0.8"
reqwest = { version = "0.11.0", features = ["json"] }
serde = { version = "1.0.0", features = ["derive"] }
serde_json = "1.0"
slug = "0.1"
tokio = { version = "1.0.2", features = ["macros", "rt-multi-thread"] }
tracing = "0.1.35"
tracing-subscriber = { version = "0.3.14", features = ["env-filter"] }
[dependencies.djangohashers]
default-features = false
features = ["with_pbkdf2"]
version = "1.1.1"
[dependencies.diesel]
default-features = false
features = ["chrono", "postgres"]
version = "2.0.0"
[dependencies.warp]
default-features = false
version = "0.3.0"
[dev-dependencies]
serde_urlencoded = "0.7.1" # Same as warp depends on
version = "2.1.0"

View File

@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "Inflector"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]]
name = "addr2line"
version = "0.20.0"
@ -21,7 +27,48 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
name = "album-api"
version = "0.1.0"
dependencies = [
"async-stream",
"axum",
"rspc",
"tokio",
"tower-http",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "async-stream"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
dependencies = [
"async-stream-impl",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-stream-impl"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
]
[[package]]
@ -32,7 +79,21 @@ checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.23",
]
[[package]]
name = "async-tungstenite"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e6acf7e4a267eecbb127ed696bb2d50572c22ba7f586a646321e1798d8336a1"
dependencies = [
"futures-io",
"futures-util",
"log",
"pin-project-lite",
"tokio",
"tungstenite",
]
[[package]]
@ -49,7 +110,7 @@ checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39"
dependencies = [
"async-trait",
"axum-core",
"bitflags",
"bitflags 1.3.2",
"bytes",
"futures-util",
"http",
@ -107,12 +168,51 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.4.0"
@ -131,6 +231,79 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"winapi",
]
[[package]]
name = "cookie"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "cpufeatures"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "document-features"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e493c573fce17f00dcab13b6ac057994f3ce17d1af4dc39bfd482b83c6eb6157"
dependencies = [
"litrs",
]
[[package]]
name = "either"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
[[package]]
name = "fnv"
version = "1.0.7"
@ -146,6 +319,21 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "futures"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.28"
@ -153,6 +341,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@ -161,6 +350,34 @@ version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
[[package]]
name = "futures-executor"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
[[package]]
name = "futures-macro"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
]
[[package]]
name = "futures-sink"
version = "0.3.28"
@ -179,10 +396,37 @@ version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
@ -238,6 +482,12 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "http-range-header"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29"
[[package]]
name = "httparse"
version = "1.8.0"
@ -250,6 +500,25 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "httpz"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6769d2cb10425c442c41a787d1a3719744770a188cfa7885cd4d8126fa13f37a"
dependencies = [
"async-tungstenite",
"axum",
"base64 0.20.0",
"cookie",
"form_urlencoded",
"futures",
"http",
"hyper",
"sha1",
"thiserror",
"tokio",
]
[[package]]
name = "hyper"
version = "0.14.27"
@ -274,6 +543,39 @@ dependencies = [
"want",
]
[[package]]
name = "iana-time-zone"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "idna"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "1.9.3"
@ -284,18 +586,48 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "indoc"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306"
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a"
[[package]]
name = "js-sys"
version = "0.3.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "litrs"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9275e0933cf8bb20f008924c0cb07a0692fe54d8064996520bf998de9eb79aa"
[[package]]
name = "log"
version = "0.4.19"
@ -340,6 +672,15 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]]
name = "object"
version = "0.31.1"
@ -355,6 +696,12 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "paste"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35"
[[package]]
name = "percent-encoding"
version = "2.3.0"
@ -378,7 +725,7 @@ checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.23",
]
[[package]]
@ -393,6 +740,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "1.0.63"
@ -411,6 +764,51 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "rspc"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c68477fb8c11280503aeab140c41b7a0eed57156a73185062ffb4782c18a918f"
dependencies = [
"futures",
"httpz",
"serde",
"serde_json",
"specta",
"thiserror",
"tokio",
]
[[package]]
name = "rustc-demangle"
version = "0.1.23"
@ -434,6 +832,20 @@ name = "serde"
version = "1.0.167"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7daf513456463b42aa1d94cff7e0c24d682b429f020b9afa4f5ba5c40a22b237"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.167"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b69b106b68bc8054f0e974e70d19984040f8a5cf9215ca82626ea4853f82c4b9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
]
[[package]]
name = "serde_json"
@ -468,6 +880,17 @@ dependencies = [
"serde",
]
[[package]]
name = "sha1"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "slab"
version = "0.4.8"
@ -487,6 +910,48 @@ dependencies = [
"winapi",
]
[[package]]
name = "specta"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7230ab99f7b726fa49e350886816dbe5b5b151a5f4158470f92850246d5cae5e"
dependencies = [
"chrono",
"document-features",
"indoc",
"once_cell",
"paste",
"serde",
"serde_json",
"specta-macros",
"thiserror",
]
[[package]]
name = "specta-macros"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6252ed50d005e1dc2b77ed140f1c230299e0566492470eba40ba34e18cbcfa10"
dependencies = [
"Inflector",
"itertools",
"proc-macro2",
"quote",
"syn 1.0.109",
"termcolor",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.23"
@ -504,6 +969,77 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "termcolor"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
dependencies = [
"winapi-util",
]
[[package]]
name = "thiserror"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
]
[[package]]
name = "time"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446"
dependencies = [
"itoa",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
[[package]]
name = "time-macros"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4"
dependencies = [
"time-core",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.29.1"
@ -517,9 +1053,21 @@ dependencies = [
"mio",
"pin-project-lite",
"socket2",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
]
[[package]]
name = "tokio-util"
version = "0.7.8"
@ -550,6 +1098,24 @@ dependencies = [
"tracing",
]
[[package]]
name = "tower-http"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8bd22a874a2d0b70452d5597b12c537331d49060824a95f49f108994f94aa4c"
dependencies = [
"bitflags 2.3.3",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-range-header",
"pin-project-lite",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.2"
@ -589,12 +1155,75 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]]
name = "tungstenite"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788"
dependencies = [
"base64 0.13.1",
"byteorder",
"bytes",
"http",
"httparse",
"log",
"rand",
"sha1",
"thiserror",
"url",
"utf-8",
]
[[package]]
name = "typenum"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "unicode-bidi"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
[[package]]
name = "unicode-ident"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73"
[[package]]
name = "unicode-normalization"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
dependencies = [
"tinyvec",
]
[[package]]
name = "url"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "want"
version = "0.3.1"
@ -610,6 +1239,60 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.23",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
[[package]]
name = "winapi"
version = "0.3.9"
@ -626,12 +1309,30 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[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]]
name = "windows"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.48.0"

View File

@ -6,4 +6,9 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-stream = "0.3.5"
axum = { version = "0.6.18", features = ["tracing", "http2"] }
rspc = { version = "0.1.3", features = ["axum", "chrono"] }
tokio = "1.29.1"
tower = { version = "0.4.13", features = ["tokio", "tokio-stream"] }
tower-http = { version = "0.4.1", features = ["cors", "fs"] }

View File

@ -1,13 +1,128 @@
pub async fn start_server() {
// build our application with a route
let app = Router::new()
// `GET /` goes to `root`
.route("/", get(root))
// `POST /users` goes to `create_user`
.route("/users", post(create_user));
use std::{path::PathBuf, time::Duration};
// run our app with hyper, listening globally on port 3000
let listener =
tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
use async_stream::stream;
use axum::routing::get;
use rspc::{Config, ErrorCode, MiddlewareContext, Router};
use tokio::time::sleep;
use tower_http::cors::{Any, CorsLayer};
#[derive(Debug, Clone)]
pub struct UnauthenticatedContext {
pub session_id: Option<String>,
}
#[derive(Debug, Clone)]
#[allow(unused)]
pub struct User {
name: String,
}
async fn db_get_user_from_session(_session_id: &str) -> User {
User {
name: "Monty Beaumont".to_string(),
}
}
#[derive(Debug, Clone)]
#[allow(unused)]
pub struct AuthenticatedCtx {
user: User,
}
pub async fn start_server(ui_root: PathBuf) {
let router = Router::<UnauthenticatedContext>::new()
.config(Config::new().export_ts_bindings(ui_root))
// Logger middleware
.middleware(|mw| {
mw.middleware(|mw| async move {
let state = (mw.req.clone(), mw.ctx.clone(), mw.input.clone());
Ok(mw.with_state(state))
})
.resp(|state, result| async move {
println!(
"[LOG] req='{:?}' ctx='{:?}' input='{:?}' result='{:?}'",
state.0, state.1, state.2, result
);
Ok(result)
})
})
.query("version", |t| {
t(|_ctx, _: ()| {
println!("ANOTHER QUERY");
env!("CARGO_PKG_VERSION")
})
})
// Auth middleware
.middleware(|mw| {
mw.middleware(|mw| async move {
match mw.ctx.session_id {
Some(ref session_id) => {
let user = db_get_user_from_session(session_id).await;
Ok(mw.with_ctx(AuthenticatedCtx { user }))
}
None => Err(rspc::Error::new(
ErrorCode::Unauthorized,
"Unauthorized".into(),
)),
}
})
})
.query("another", |t| {
t(|_, _: ()| {
println!("ANOTHER QUERY");
"Another Result!"
})
})
.subscription("subscriptions.pings", |t| {
t(|_ctx, _args: ()| {
stream! {
println!("Client subscribed to 'pings'");
for i in 0..5 {
println!("Sending ping {}", i);
yield "ping".to_string();
sleep(Duration::from_secs(1)).await;
}
}
})
})
// Reject all middleware
.middleware(|mw| {
mw.middleware(|_mw| async move {
Err(rspc::Error::new(
ErrorCode::Unauthorized,
"Unauthorized".into(),
)) as Result<MiddlewareContext<_>, _>
})
})
// Plugin middleware // TODO: Coming soon!
// .middleware(|mw| mw.openapi(OpenAPIConfig {}))
.build()
.arced();
let app = axum::Router::new()
.route("/", get(|| async { "Hello 'rspc'!" }))
// Attach the rspc router to your axum router. The closure is used to generate the request context for each request.
.nest(
"/rspc",
router
.endpoint(|| UnauthenticatedContext {
session_id: Some("abc".into()), // Change this line to control whether you are authenticated and can access the "another" query.
})
.axum(),
)
// We disable CORS because this is just an example. DON'T DO THIS IN PRODUCTION!
.layer(
CorsLayer::new()
.allow_methods(Any)
.allow_headers(Any)
.allow_origin(Any),
);
let addr = "[::]:4000".parse::<std::net::SocketAddr>().unwrap(); // This listens on IPv6 and IPv4
println!("listening on http://{}/rspc/version", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}

View File

@ -0,0 +1,10 @@
// This file was generated by [rspc](https://github.com/oscartbeaumont/rspc). Do not edit this file manually.
export type Procedures = {
queries:
{ key: "another", input: never, result: string } |
{ key: "version", input: never, result: string },
mutations: never,
subscriptions:
{ key: "subscriptions.pings", input: never, result: string }
};

View File

@ -1,5 +0,0 @@
# For documentation on how to configure this file,
# see diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"

View File

View File

@ -1 +0,0 @@
DROP TABLE photos

View File

@ -1,11 +0,0 @@
CREATE TABLE photos (
id SERIAL PRIMARY KEY,
path VARCHAR UNIQUE NOT NULL,
date TIMESTAMP,
grade SMALLINT,
rotation SMALLINT NOT NULL
);
CREATE UNIQUE INDEX photos_path_idx ON photos (path);
CREATE INDEX photos_date_idx ON photos (date DESC NULLS LAST);
CREATE INDEX photos_grade_idx ON photos (grade DESC NULLS LAST);

View File

@ -1,2 +0,0 @@
DROP TABLE photo_tags;
DROP TABLE tags;

View File

@ -1,14 +0,0 @@
CREATE TABLE tags (
id SERIAL PRIMARY KEY,
slug VARCHAR UNIQUE NOT NULL,
tag_name VARCHAR UNIQUE NOT NULL
);
CREATE UNIQUE INDEX tags_slug_idx ON tags (slug);
CREATE UNIQUE INDEX tags_name_idx ON tags (tag_name);
CREATE TABLE photo_tags (
id SERIAL PRIMARY KEY,
photo_id INTEGER NOT NULL REFERENCES photos (id),
tag_id INTEGER NOT NULL REFERENCES tags (id)
);

View File

@ -1,2 +0,0 @@
DROP TABLE photo_people;
DROP TABLE people;

View File

@ -1,14 +0,0 @@
CREATE TABLE people (
id SERIAL PRIMARY KEY,
slug VARCHAR UNIQUE NOT NULL,
person_name VARCHAR UNIQUE NOT NULL
);
CREATE UNIQUE INDEX people_slug_idx ON people (slug);
CREATE UNIQUE INDEX people_name_idx ON people (person_name);
CREATE TABLE photo_people (
id SERIAL PRIMARY KEY,
photo_id INTEGER NOT NULL REFERENCES photos (id),
person_id INTEGER NOT NULL REFERENCES people (id)
);

View File

@ -1,2 +0,0 @@
DROP TABLE photo_places;
DROP TABLE places;

View File

@ -1,14 +0,0 @@
CREATE TABLE places (
id SERIAL PRIMARY KEY,
slug VARCHAR UNIQUE NOT NULL,
place_name VARCHAR UNIQUE NOT NULL
);
CREATE UNIQUE INDEX places_slug_idx ON places (slug);
CREATE UNIQUE INDEX places_name_idx ON places (place_name);
CREATE TABLE photo_places (
id SERIAL PRIMARY KEY,
photo_id INTEGER NOT NULL REFERENCES photos (id),
place_id INTEGER NOT NULL REFERENCES places (id)
);

View File

@ -1 +0,0 @@
DROP TABLE users;

View File

@ -1,7 +0,0 @@
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR UNIQUE NOT NULL,
password VARCHAR UNIQUE NOT NULL
);
CREATE UNIQUE INDEX users_username_idx ON users (username);

View File

@ -1 +0,0 @@
DROP TABLE positions;

View File

@ -1,12 +0,0 @@
-- Rather than using floating points or DECIMAL(8,5) or something like
-- that, lat and long are stored as signed microdegrees integer values.
CREATE TABLE positions (
id SERIAL PRIMARY KEY,
photo_id INTEGER NOT NULL REFERENCES photos (id),
latitude INTEGER NOT NULL,
longitude INTEGER NOT NULL
);
CREATE INDEX positions_photo_idx ON positions (photo_id);
CREATE INDEX positions_lat_idx ON positions (latitude);
CREATE INDEX positions_long_idx ON positions (longitude);

View File

@ -1 +0,0 @@
ALTER TABLE photos DROP COLUMN is_public;

View File

@ -1 +0,0 @@
ALTER TABLE photos ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT FALSE;

View File

@ -1,2 +0,0 @@
ALTER TABLE photos DROP COLUMN camera_id;
DROP TABLE cameras;

View File

@ -1,9 +0,0 @@
CREATE TABLE cameras (
id SERIAL PRIMARY KEY,
manufacturer VARCHAR NOT NULL,
model VARCHAR NOT NULL
);
CREATE UNIQUE INDEX cameras_idx ON cameras (manufacturer, model);
ALTER TABLE photos ADD COLUMN camera_id INTEGER REFERENCES cameras (id);

View File

@ -1,2 +0,0 @@
ALTER TABLE photos DROP COLUMN attribution_id;
DROP TABLE attributions;

View File

@ -1,7 +0,0 @@
CREATE TABLE attributions (
id SERIAL PRIMARY KEY,
name VARCHAR UNIQUE NOT NULL
);
ALTER TABLE photos ADD COLUMN attribution_id
INTEGER REFERENCES attributions (id);

View File

@ -1,2 +0,0 @@
ALTER TABLE photos DROP COLUMN width;
ALTER TABLE photos DROP COLUMN height;

View File

@ -1,6 +0,0 @@
-- Add width and height to photos
-- Intially make them nullable, to make it possible to apply the
-- migration to an existing database. A NOT NULL constraint should
-- be added later, when all photos has sizes.
ALTER TABLE photos ADD COLUMN width INTEGER;
ALTER TABLE photos ADD COLUMN height INTEGER;

View File

@ -1,2 +0,0 @@
DROP INDEX positions_photo_idx;
CREATE INDEX positions_photo_idx ON positions (photo_id);

View File

@ -1,3 +0,0 @@
-- Each photo may only have one single position
DROP INDEX positions_photo_idx;
CREATE UNIQUE INDEX positions_photo_idx ON positions (photo_id);

View File

@ -1,4 +0,0 @@
ALTER TABLE places DROP COLUMN osm_id;
ALTER TABLE places DROP COLUMN osm_level;
CREATE UNIQUE INDEX places_name_idx ON places (place_name);

View File

@ -1,9 +0,0 @@
-- Add fields for OpenStreetMap-based locations to places table.
ALTER TABLE places ADD COLUMN osm_id BIGINT UNIQUE;
ALTER TABLE places ADD COLUMN osm_level SMALLINT;
CREATE INDEX places_osm_idx ON places (osm_id);
CREATE INDEX places_osml_idx ON places (osm_level);
DROP INDEX places_name_idx;
CREATE UNIQUE INDEX places_name_idx ON places (place_name, osm_level);

View File

@ -1,2 +0,0 @@
ALTER TABLE photos ALTER COLUMN width DROP NOT NULL;
ALTER TABLE photos ALTER COLUMN height DROP NOT NULL;

View File

@ -1,2 +0,0 @@
ALTER TABLE photos ALTER COLUMN width SET NOT NULL;
ALTER TABLE photos ALTER COLUMN height SET NOT NULL;

View File

@ -1,5 +0,0 @@
-- This file should undo anything in `up.sql`
drop function year_of_timestamp;
drop function month_of_timestamp;
drop function day_of_timestamp;

View File

@ -1,16 +0,0 @@
-- SQL functions for handling dates
create function year_of_timestamp(arg timestamp with time zone)
returns smallint
language sql immutable strict parallel safe
as $func$ select cast(date_part('year', arg at time zone 'UTC') as smallint); $func$;
create function month_of_timestamp(arg timestamp with time zone)
returns smallint
language sql immutable strict parallel safe
as $func$ select cast(date_part('month', arg at time zone 'UTC') as smallint); $func$;
create function day_of_timestamp(arg timestamp with time zone)
returns smallint
language sql immutable strict parallel safe
as $func$ select cast(date_part('day', arg at time zone 'UTC') as smallint); $func$;

View File

@ -1,6 +0,0 @@
alter table photo_tags drop constraint photo_tags_pkey;
alter table photo_tags add column id serial primary key;
alter table photo_people drop constraint photo_people_pkey;
alter table photo_people add column id serial primary key;
alter table photo_places drop constraint photo_places_pkey;
alter table photo_places add column id serial primary key;

View File

@ -1,8 +0,0 @@
alter table photo_tags drop column id;
alter table photo_tags add primary key (photo_id, tag_id);
alter table photo_people drop column id;
alter table photo_people add primary key (photo_id, person_id);
alter table photo_places drop column id;
alter table photo_places add primary key (photo_id, place_id);

View File

@ -1,351 +0,0 @@
// Admin functionality for rphotos
(function (d) {
var details = d.querySelector('main.details'), p;
if (!details) {
return; // Admin is for single image only
}
function rotate(event) {
var imgid = details.dataset.imgid;
var angle = event.target.dataset.angle;
var r = new XMLHttpRequest();
d.body.classList.add('busy');
r.open('POST', '/adm/rotate');
r.onload = function() {
if (r.status === 200) {
// Yay!
d.location.reload(true);
} else {
alert("Rotating failed: " + r.status);
}
d.body.classList.remove('busy');
}
r.onerror = function() {
alert("Rotating failed.");
d.body.classList.remove('busy');
}
r.ontimeout = function() {
alert("Rotating timed out");
d.body.classList.remove('busy');
}
r.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
r.send("angle=" + angle + "&image=" + imgid)
}
function makeform(category) {
let oldform = p.querySelector('form');
if (oldform) {
oldform.remove();
}
var f = d.createElement("form");
f.className = "admin " + category;
f.action = "/adm/" + category;
f.method = "post";
var i = d.createElement("input");
i.type="hidden";
i.name="image";
i.value = details.dataset.imgid;
f.appendChild(i);
return f;
}
function disable_one(one) {
p.querySelectorAll('button').forEach(function(b) {
b.disabled = (b === one);
})
}
function tag_form(event, category) {
disable_one(event.target);
var f = makeform(category);
var l = d.createElement("label");
l.innerHTML = event.target.title;
f.appendChild(l);
var i = d.createElement("input");
i.type = "text";
i.autocomplete="off";
i.tabindex="1";
i.name = category;
i.id = category+'name';
l.htmlFor = i.id;
var list = d.createElement("div");
list.className = "completions";
i.addEventListener('input', e => {
let i = e.target, v = i.value;
if (v.length > 0) {
let r = new XMLHttpRequest();
r.onload = function() {
let t = JSON.parse(this.responseText);
list.style.top = i.offsetTop + i.offsetHeight + "px"
list.style.left = i.offsetLeft + "px"
list.innerHTML = '';
t.map(x => {
let a = d.createElement('a');
a.innerHTML = x;
a.tabIndex = 2;
a.href = "#";
a.onclick = function(e) {
i.value = x;
list.innerHTML = '';
i.focus();
e.preventDefault();
e.stopPropagation();
return true;
}
list.appendChild(a)
})
};
r.open('GET', d.location.origin + '/ac/' + category + '?q=' + encodeURIComponent(v));
r.send(null);
} else {
list.innerHTML = '';
}
});
f.appendChild(i);
f.appendChild(list);
f.addEventListener('keydown', e => {
if (!list.innerHTML) {
if (e.code === 'Escape') {
if (i.value) {
i.value = '';
i.focus();
} else { // close form
e.target.closest('form').remove();
event.target.disabled = false;
event.target.focus();
}
e.preventDefault();
e.stopPropagation();
return false;
}
return;
}
let t = e.target;
switch(e.code) {
case 'ArrowUp':
(t.parentNode == list && t.previousSibling || list.querySelector('a:last-child')).focus();
break;
case 'ArrowDown':
(t.parentNode == list && t.nextSibling || list.querySelector('a:first-child')).focus();
break;
case 'Escape':
list.innerHTML = '';
i.focus();
break;
default:
return true;
};
e.preventDefault();
e.stopPropagation();
return false;
});
let s = d.createElement("button");
s.innerHTML = "Ok";
s.type = "submit";
f.appendChild(s);
let c = d.createElement("button");
c.innerHTML = "&#x1f5d9;";
c.className = 'close';
c.title = 'close';
c.onclick = e => {
e.target.closest('form').remove();
event.target.disabled = false; // The old event creating this form
event.target.focus();
};
f.appendChild(c);
p.append(f);
i.focus();
}
function grade_form(event) {
disable_one(event.target);
var grade = details.dataset.grade;
var f = makeform("grade");
var l = d.createElement("label");
l.innerHTML = event.target.title;
f.appendChild(l);
var i = d.createElement("input");
i.type="range";
i.name="grade";
if (grade) {
i.value=grade;
}
i.min=0;
i.max=100;
f.appendChild(i);
let s = d.createElement("button");
s.innerHTML = "Ok";
s.type = "submit";
f.appendChild(s);
let c = d.createElement("button");
c.innerHTML = "&#x1f5d9;";
c.className = 'close';
c.title = 'close';
c.onclick = e => {
e.target.closest('form').remove();
event.target.disabled = false; // The old event creating this form
event.target.focus();
};
f.appendChild(c);
f.addEventListener('keydown', e => {
switch(e.code) {
case 'Escape':
e.target.closest('form').remove();
event.target.disabled = false;
event.target.focus();
break;
case 'Enter':
f.submit();
break;
default:
return true;
};
e.preventDefault();
e.stopPropagation();
return false;
});
p.append(f);
i.focus();
}
function location_form(event) {
disable_one(event.target);
var position = details.dataset.position || localStorage.getItem('lastpos');
var f = makeform("locate");
var lat = d.createElement("input");
lat.type="hidden";
lat.name="lat";
f.appendChild(lat);
var lng = d.createElement("input");
lng.type="hidden";
lng.name="lng";
f.appendChild(lng);
let h = d.querySelector('head');
var csslink = d.createElement('link');
csslink.rel = 'stylesheet';
csslink.href = '/static/l140/leaflet.css';
h.append(csslink);
f.insertAdjacentHTML('beforeend', '<div id="amap"></div>');
var slink = d.createElement('script');
slink.type = 'text/javascript';
slink.src = '/static/l140/leaflet.js';
slink.async = 'async';
var marker;
slink.onload = () => {
var map = L.map('amap');
L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19
}).addTo(map);
if (position) {
position = JSON.parse(position);
map.setView(position, 16);
} else {
map.fitWorld();
position = [0, 0];
}
var me = d.getElementById('amap');
marker = L.marker(position, {
'draggable': true,
'autoPan': true,
'autoPanPadding': [me.clientWidth/4, me.clientHeight/4],
});
marker.addTo(map);
me.focus();
}
h.append(slink);
let b = d.createElement("button");
b.innerHTML = "Ok";
f.addEventListener('submit', presubmit);
f.appendChild(b);
function presubmit() {
let pos = marker.getLatLng();
lat.value = pos.lat;
lng.value = pos.lng;
localStorage.setItem('lastpos', `[${pos.lat},${pos.lng}]`)
}
function keyHandler(e) {
console.log("In keyhandler", e);
switch(e.code) {
case 'Escape':
e.target.closest('form').remove();
event.target.disabled = false;
event.target.focus();
break;
case 'Enter':
presubmit();
f.submit();
break;
default:
return true;
};
e.preventDefault();
e.stopPropagation();
return false;
}
let c = d.createElement("button");
c.innerHTML = "&#x1f5d9;";
c.className = 'close';
c.title = 'close';
c.onclick = e => {
e.target.closest('form').remove();
event.target.disabled = false; // The old event creating this form
event.target.focus();
};
f.appendChild(c);
f.addEventListener('keydown', keyHandler);
p.append(f);
}
p = d.createElement("div");
p.className = 'admbuttons';
r = d.createElement("button");
r.onclick = rotate;
r.innerHTML = "\u27f2";
r.dataset.angle = "-90";
r.title = "Rotate left";
p.appendChild(r);
p.appendChild(d.createTextNode(" "));
r = d.createElement("button");
r.onclick = rotate;
r.innerHTML = "\u27f3";
r.dataset.angle = "90";
r.title = "Rotate right";
p.appendChild(r);
p.appendChild(d.createTextNode(" "));
r = d.createElement("button");
r.onclick = e => tag_form(e, 'tag');
r.innerHTML = "&#x1f3f7;";
r.title = "Tag";
r.accessKey = "t";
p.appendChild(r);
p.appendChild(d.createTextNode(" "));
r = d.createElement("button");
r.onclick = e => tag_form(e, 'person');
r.innerHTML = "\u263a";
r.title = "Person";
r.accessKey = "p";
p.appendChild(r);
p.appendChild(d.createTextNode(" "));
r = d.createElement("button");
r.onclick = e => location_form(e);
r.innerHTML = "\u{1f5fa}";
r.title = "Location";
r.accessKey = "l";
p.appendChild(r);
p.appendChild(d.createTextNode(" "));
r = d.createElement("button");
r.onclick = e => grade_form(e);
r.innerHTML = "\u2606";
r.title = "Grade";
r.accessKey = "g";
p.appendChild(r);
details.appendChild(p);
})(document)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 618 B

View File

@ -1,635 +0,0 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg,
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-tile {
will-change: opacity;
}
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline: 0;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-container a.leaflet-active {
outline: 2px solid orange;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a,
.leaflet-bar a:hover {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path {
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.7);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover {
text-decoration: underline;
}
.leaflet-container .leaflet-control-attribution,
.leaflet-container .leaflet-control-scale {
font-size: 11px;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
font-size: 11px;
white-space: nowrap;
overflow: hidden;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: #fff;
background: rgba(255, 255, 255, 0.5);
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 19px;
line-height: 1.4;
}
.leaflet-popup-content p {
margin: 18px 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
padding: 4px 4px 0 0;
border: none;
text-align: center;
width: 18px;
height: 14px;
font: 16px/14px Tahoma, Verdana, sans-serif;
color: #c3c3c3;
text-decoration: none;
font-weight: bold;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover {
color: #999;
}
.leaflet-popup-scrolled {
overflow: auto;
border-bottom: 1px solid #ddd;
border-top: 1px solid #ddd;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-popup-tip-container {
margin-top: -1px;
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-clickable {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}

File diff suppressed because one or more lines are too long

View File

@ -1,60 +0,0 @@
.marker-cluster-small {
background-color: rgba(181, 226, 140, 0.6);
}
.marker-cluster-small div {
background-color: rgba(110, 204, 57, 0.6);
}
.marker-cluster-medium {
background-color: rgba(241, 211, 87, 0.6);
}
.marker-cluster-medium div {
background-color: rgba(240, 194, 12, 0.6);
}
.marker-cluster-large {
background-color: rgba(253, 156, 115, 0.6);
}
.marker-cluster-large div {
background-color: rgba(241, 128, 23, 0.6);
}
/* IE 6-8 fallback colors */
.leaflet-oldie .marker-cluster-small {
background-color: rgb(181, 226, 140);
}
.leaflet-oldie .marker-cluster-small div {
background-color: rgb(110, 204, 57);
}
.leaflet-oldie .marker-cluster-medium {
background-color: rgb(241, 211, 87);
}
.leaflet-oldie .marker-cluster-medium div {
background-color: rgb(240, 194, 12);
}
.leaflet-oldie .marker-cluster-large {
background-color: rgb(253, 156, 115);
}
.leaflet-oldie .marker-cluster-large div {
background-color: rgb(241, 128, 23);
}
.marker-cluster {
background-clip: padding-box;
border-radius: 20px;
}
.marker-cluster div {
width: 30px;
height: 30px;
margin-left: 5px;
margin-top: 5px;
text-align: center;
border-radius: 15px;
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
}
.marker-cluster span {
line-height: 30px;
}

View File

@ -1,14 +0,0 @@
.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
-webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;
-moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;
-o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;
transition: transform 0.3s ease-out, opacity 0.3s ease-in;
}
.leaflet-cluster-spider-leg {
/* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */
-webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in;
-moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in;
-o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in;
transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in;
}

File diff suppressed because one or more lines are too long

View File

@ -1,453 +0,0 @@
$border: 1px solid #333;
html, body {
height: 100%;
margin: 0;
padding: 0;
}
body {
background: #eee;
display: flex;
flex-direction: column;
font-family: serif;
justify-content: space-between;
line-height: 1.6;
}
h1 {
margin: .3em 0;
small.n_hits {
font-weight: normal;
font-size: 1ex;
}
}
p {
margin: 0;
}
p + p {
margin-top: 1ex;
}
header {
background-color: #eee;
box-shadow: 0 0 1ex #444;
position: sticky;
top: 0;
z-index: 9999;
}
footer {
background: #999;
color: white;
:link, :visited {
color: #edf;
}
p { margin: 0; }
}
header, footer {
display: flex;
& > span {
flex-grow: 1;
margin-right: 1em;
}
& > :last-child {
flex-grow: 5;
padding-left: 1em;
text-align: right;
}
a {
text-decoration: none;
&:focus, &:hover {
text-decoration: underline;
}
}
}
header {
.user {
flex-grow: 5;
text-align: right;
}
form.search {
flex-grow: 3;
&.hidden {
flex-grow: 0;
.refs { display: none; }
}
}
}
form.search {
border: 0;
display: flex;
flex-flow: row wrap;
padding: 0;
text-align: left;
position: relative;
label {
padding: 0 .3em 0 0;
}
.list {
left: 1.5em;
position: absolute;
top: 1.9em;
z-index: 10;
a.hit {
background: white;
border: solid 1px #666;
color: black;
display: block;
padding: 0 1ex;
text-align: left;
text-decoration: none;
&:focus {
background: #bbf;
}
&:nth-child(n + 2) {
border-top: 0;
}
}
}
.refs {
display: inline-flex;
flex-flow: row wrap;
background: white;
border: solid 1px #baa;
border-radius: .2em;
margin: 2px 0;
padding: 0;
flex-grow: 1;
label {
background: #bbf;
border-radius: 1ex;
line-height: 1.4;
margin: .1em;
padding: 0 1ex;
&.not {
background: #fbb;
text-decoration: line-through;
}
}
input {
background: transparent;
border: 0;
flex-basis: 2em;
flex-grow: 1;
margin: 2px;
}
}
.l:before {
content: '';
margin-left: .4em;
}
.p:before {
content: '';
margin-left: .3em;
}
.t:before {
content: '🏷 ';
margin-left: .1em;
}
.time {
margin-left: 1em;
}
}
main {
flex-grow: 1;
margin-bottom: 1em;
form.search {
width: -moz-available;
}
}
header, footer, main {
flex-wrap: wrap;
padding: 0 1ex;
align-items: center;
}
.item {
box-shadow: 0 .2em 1em -.2em #000;
background: #ccc;
border: solid 1px white;
}
div.group {
display: flex;
flex-flow: row-reverse wrap;
justify-content: space-between;
margin: 0 -.1em;
.item {
position: relative;
margin: .4em .1em;
flex-grow: .1;
&.portrait {
flex-grow: .3;
}
img {
display: block;
height: calc(5em + 10vw);
object-fit: cover;
object-position: 50% 33%;
width: -moz-available;
width: -webkit-fill-available;
width: available;
}
// TODO: Set first-line class by js.
&.first-line img {
height: calc(6em + 11vw);
}
h2, .lable {
text-shadow: 0 0 .1em white, 0 0 .4em white;
padding: 0 .2em;
}
h2 {
position: absolute;
margin: 0;
line-height: 1.2;
}
.lable {
position: absolute;
bottom: 0;
right: 0;
font-size: 80%;
background: rgba(white,0.2);
padding-left: 1em;
border-top-left-radius: 1.5em 100%;
}
&:hover {
h2 {
background: rgba(white,0.5);
width: -moz-available;
width: -webkit-fill-available;
width: available;
}
.lable {
background: rgba(white,0.8);
}
}
}
p.item {
justify-content: space-around;
}
}
main.details {
margin: 0;
padding: 1ex;
img.item {
height: auto;
width: -moz-available;
width: -webkit-fill-available;
width: available;
&:fullscreen {
object-fit: contain;
border: 0;
background: #222;
}
}
}
@media screen and (min-width: 56ch) {
main.details {
align-items: start;
display: grid;
flex: content 1 1;
grid-gap: 1ex;
grid-template-columns: 1fr clamp(24ch, 29%, 52ch);
grid-template-rows: min-content 1fr min-content 1fr;
max-height: -moz-available;
overflow: hidden;
min-height: 20em;
h1 {
grid-column: 2;
margin: 0;
}
img.item {
display: block;
grid-row: 1 / -1;
margin: 0 auto auto;
max-width: -moz-available;
max-height: -moz-available;
max-height: calc(100% - 2px);
max-width: calc(100% - 2px);
object-fit: scale-down;
width: auto;
height: auto;
}
.places a:nth-child(n+2) {
font-size: 80%;
}
.meta {
overflow: auto;
height: -moz-available;
height: 100%;
}
#map {
grid-row: -2;
height: calc(100% - 2px) !important;
margin: 0;
width: -moz-available;
}
.admbuttons {
flex-flow: row wrap;
margin: 0;
button {
margin: 0;
}
}
}
}
ul.alltags, ul.allpeople, ul.allplaces {
-moz-column-width: 13em;
column-width: 13em;
}
#map {
border: $border;
height: 10rem;
margin: 1ex auto;
max-height: 60vh;
}
div.admbuttons {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
margin: 1ex -.1em 0;
button {
flex: min-content .1 1;
margin: .1em;
}
}
form {
border: $border;
margin: auto;
padding: 1em;
width: -moz-fit-content;
width: fit-content;
p {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
}
label {
padding: .2em .6em .2em 0;
}
}
#help {
background: #fafafa;
border: solid 1px black;
bottom: 2em;
box-shadow: .3em .2em 1em;
display: none;
left: 2em;
padding: 1ex 1em;
position: fixed;
z-index: 1111;
&:target {
display: block;
}
h2 { margin: 0; }
a.close {
line-height: .8em;
padding: .1em;
position: absolute;
right: 0;
text-decoration: none;
top: 0;
}
}
// Relevant for admin forms only. Move to separate file?
form.admin {
display: flex;
margin: .3em .1em 0;
padding: 1.6em 1ex 1.2em;
position: relative;
width: -moz-available;
width: -webkit-fill-available;
width: available;
input[type="text"], input[type="range"] {
flex: min-content 1 1;
margin-right: 1ex;
}
button.close {
font-size: 50%;
margin: 0;
padding: 0;
position: absolute;
right: -1ex;
top: -1ex;
}
&.locate {
background: #eee;
box-shadow: .2em .4em 1em rgba(0,0,0,.7);
display: flex;
flex-flow: column;
height: calc(95vh - 6em);
left: 5vw;
padding-bottom: 1em;
position: fixed;
top: 3em;
width: calc(90vw - 1ex);
z-index: 10000;
#amap {
border: $border;
flex-grow: 1;
margin: 0 auto 1ex;
max-height: 95vh;
width: -moz-available;
width: -webkit-fill-available;
width: available;
}
button {
flex-grow: 0;
align-self: flex-end;
}
}
}
div.completions {
border-bottom: solid 1px #888;
box-shadow: .2em .1em .5em rgba(0, 0, 0, .7);
display: block;
margin-left: .5em;
position: absolute;
z-index: 800;
a {
background: rgba(255,255,255,0.95);
border-color: #888;
border-style: solid;
border-width: 1px 1px 0;
display: block;
padding: .2em .5em; // .2em 2.2em;
color: black;
text-decoration: none;
}
a:focus {
background-color: #aaaaff;
}
}
.leaflet-popup-content img {
max-width: 11em;
max-height: 11em;
}

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="#54009e" fill-rule="evenodd" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width=".5" d="M4.3 1.3h27.5v21.3H4.3z"/><path fill="#9728ff" fill-rule="evenodd" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width=".5" d="M-.1 5.7H27V27H-.1z" transform="rotate(-5.1) skewX(-.9)"/><path fill="#4ad8ff" fill-rule="evenodd" d="M2.2 9.2h27.1v21.3H2.2z"/><path fill="#17aa00" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-width=".5" d="M2 30.2c0-6.2.2-7.3 11.5-7.2 7.6-10.2 14.8.2 15.5 7.2"/><path fill="#17aa00" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-width=".5" d="M2 26c11.4-28.1 11-1.8 22.6.8"/><path fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width=".5" d="M2 8.9h27.1v21.3H2z"/></svg>

Before

Width:  |  Height:  |  Size: 874 B

196
res/ux.js
View File

@ -1,196 +0,0 @@
(function(d) {
if (d.querySelector('main form.search')) {
d.querySelector('header form.search').remove();
d.querySelector('main form.search input#s_q').focus();
}
let f = d.querySelector('footer');
f.insertAdjacentHTML(
'afterbegin',
'<p><a href="#help" title="Help" accesskey="?">?</a></p>')
f.querySelector('[href="#help"]').addEventListener('click', e => {
if (d.getElementById('help') == null) {
f.insertAdjacentHTML(
'beforebegin',
'<div id="help"><h2>Key bindings</h2><a href="#" class="close" title="close help">🗙</a>' +
[].map.call(
d.querySelectorAll('[accesskey]'),
e => e.accessKeyLabel + ": " + (e.title || e.innerText)).join('<br/>') +
'</div>');
}
return true;
});
var map;
function resize_map() {
var me = d.getElementById('map');
if (me) {
me.style.height = 4 * me.clientWidth / 5 + "px";
}
if (map) {
map.invalidateSize(false);
}
}
window.addEventListener('resize', resize_map);
let i = d.querySelector('.details .item');
if (i) {
i.addEventListener('click', e => {
if (!d.fullscreenElement) {
let full = d.querySelector('.meta a.full');
if (full && (i.src != full.href)) {
i.src = full.href;
}
i.requestFullscreen();
} else if (d.exitFullscreen) {
d.exitFullscreen();
}
resize_map();
});
}
function prepare_map(cb) {
let h = d.querySelector('head');
var csslink = d.createElement('link');
csslink.rel = 'stylesheet';
csslink.href = '/static/l140/leaflet.css';
h.append(csslink);
let m = d.querySelector('main');
m.insertAdjacentHTML('beforeend', '<div id="map"></div>');
var slink = d.createElement('script');
slink.type = 'text/javascript';
slink.src = '/static/l140/leaflet.js';
slink.async = 'async';
slink.onload = () => {
map = L.map('map', {'scrollWheelZoom': false, 'trackResize': false});
L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
resize_map();
cb(map);
}
h.append(slink);
}
let details = d.querySelector('.details');
let pos = details && details.dataset.position
if (pos) {
prepare_map((map) => {
let p = JSON.parse(pos);
map.setView(p, 16);
L.marker(p).addTo(map);
})
}
let group = d.querySelector('.group');
let poss = (details && details.dataset.positions) || (group && group.dataset.positions);
if (poss) {
prepare_map((map) => {
let h = d.querySelector('head');
h.insertAdjacentHTML(
'beforeend',
'<link rel="stylesheet" href="/static/lm141/lmc.css">' +
'<link rel="stylesheet" href="/static/lm141/lmc-default.css">'
)
var slink2 = d.createElement('script');
slink2.type = 'text/javascript';
slink2.src = '/static/lm141/lmc.js';
h.append(slink2);
slink2.onload = () => {
let pos = JSON.parse(poss);
var markers = L.markerClusterGroup({maxClusterRadius: 35});
map.fitBounds(L.polyline(pos).getBounds());
pos.forEach(p => {
let n = p.pop();
let m = L.marker(p, { title: n });
m.bindPopup(`<a href="/img/${n}"><img src="/img/${n}-s.jpg"></a>`);
markers.addLayer(m);
});
map.addLayer(markers);
};
})
}
(function(form) {
function prepareQtag(s) {
const i = s.querySelector('input');
i.addEventListener('change', function(e) {
if (!i.checked && !i.value.startsWith('!')) {
i.value = '!' + i.value;
i.checked = true;
s.classList.add('not');
} else if (i.value.startsWith('!')) {
i.value = i.value.substring(1);
s.classList.remove('not');
}
console.log("->", i.checked, i.value);
});
}
[].forEach.call(form.querySelectorAll('.refs label'), prepareQtag)
form.classList.add('hidden');
let sl = form.querySelector('label');
sl.addEventListener('click', e => form.classList.remove('hidden'))
let list = d.createElement('div');
list.className = 'list';
let tags = form.querySelector('div.refs');
form.insertBefore(list, tags);
let kindname = { 't': 'tag', 'p': 'person', 'l': 'place'}
let input = form.querySelector('input[name=q]');
input.autocomplete = "off";
input.addEventListener('keyup', e => {
let v = e.target.value;
if (new Set(['ArrowUp', 'ArrowDown', 'Escape']).has(e.code)) {
return;
}
if (v.length > 1) {
let r = new XMLHttpRequest();
r.onload = function() {
let t = JSON.parse(this.responseText);
list.innerHTML = '';
t.map(x => {
let a = d.createElement('a');
a.innerHTML = x.t + ' <small>(' + kindname[x.k] + ')</small>';
a.className='hit ' + x.k;
a.href = x.s;
a.onclick = function() {
let s = d.createElement('label');
s.innerHTML = x.t + ' <input type="checkbox" checked name="' + x.k +
'" value="' + x.s + '">';
s.className = x.k;
prepareQtag(s);
tags.insertBefore(s, input);
list.innerHTML = '';
input.value = '';
input.focus();
return false;
}
list.appendChild(a)
})
};
r.open('GET', d.location.origin + '/ac?q=' + encodeURIComponent(v));
r.send(null);
} else {
list.innerHTML = '';
}
})
form.addEventListener('keyup', e => {
let t = e.target;
switch(e.code) {
case 'ArrowUp':
(t.parentNode == list && t.previousSibling || list.querySelector('a:last-child')).focus();
break;
case 'ArrowDown':
(t.parentNode == list && t.nextSibling || list.querySelector('a:first-child')).focus();
break;
case 'Escape':
if (list.hasChildNodes()) {
input.focus();
list.innerHTML = '';
} else {
form.classList.add('hidden');
}
break;
default:
return true;
};
e.preventDefault();
e.stopPropagation();
return false;
});
//form.querySelector('.help .js').innerHTML = 'Du kan begränsa din sökning till de taggar som föreslås.';
})(d.querySelector('form.search'));
})(document)

119
src/lib1.rs Normal file
View File

@ -0,0 +1,119 @@
#![allow(proc_macro_derive_resolution_fallback)]
#![recursion_limit = "128"]
#[macro_use]
extern crate diesel;
mod adm;
mod dbopt;
mod fetch_places;
mod models;
mod myexif;
mod photosdir;
mod pidfiles;
mod schema;
mod server;
use crate::adm::result::Error;
use crate::adm::stats::show_stats;
use crate::adm::{findphotos, makepublic, precache, storestatics, users};
use crate::dbopt::DbOpt;
use clap::Parser;
use dotenv::dotenv;
use std::path::PathBuf;
use std::process::exit;
/// Command line interface for rphotos.
#[derive(Parser)]
enum RPhotos {
/// Make specific image(s) public.
///
/// The image path(s) are relative to the image root.
Makepublic(makepublic::Makepublic),
/// Get place tags for photos by looking up coordinates in OSM
Fetchplaces(fetch_places::Fetchplaces),
/// Find new photos in the photo directory
Findphotos(findphotos::Findphotos),
/// Make sure the photos has thumbnails stored in cache.
///
/// The time limit is checked after each stored image, so the
/// command will complete in slightly more than the max time and
/// one image will be processed even if the max time is zero.
Precache(precache::Args),
/// Show some statistics from the database
Stats(DbOpt),
/// Store statics as files for a web server
Storestatics {
/// Directory to store the files in
dir: String,
},
/// List existing users
Userlist {
#[clap(flatten)]
db: DbOpt,
},
/// Set password for a (new or existing) user
Userpass {
#[clap(flatten)]
db: DbOpt,
/// Username to set password for
// TODO: Use a special type that only accepts nice user names.
user: String,
},
/// Run the rphotos web server.
Runserver(server::Args),
}
#[derive(clap::Parser)]
struct CacheOpt {
/// How to connect to memcached.
#[clap(
long,
env = "MEMCACHED_SERVER",
default_value = "memcache://127.0.0.1:11211"
)]
memcached_url: String,
}
#[derive(clap::Parser)]
struct DirOpt {
/// Path to the root directory storing all actual photos.
#[clap(long, env = "RPHOTOS_DIR")]
photos_dir: PathBuf,
}
#[tokio::main]
async fn main() {
dotenv().ok();
tracing_subscriber::fmt()
.with_env_filter(
std::env::var("RUST_LOG").as_deref().unwrap_or("info"),
)
.init();
match run(&RPhotos::parse()).await {
Ok(()) => (),
Err(err) => {
println!("{err}");
exit(1);
}
}
}
async fn run(args: &RPhotos) -> Result<(), Error> {
match args {
RPhotos::Findphotos(cmd) => cmd.run().await,
RPhotos::Makepublic(cmd) => cmd.run().await,
RPhotos::Stats(db) => show_stats(&mut db.connect().await?).await,
RPhotos::Userlist { db } => {
users::list(&mut db.connect().await?).await
}
RPhotos::Userpass { db, user } => {
users::passwd(&mut db.connect().await?, user).await
}
RPhotos::Fetchplaces(cmd) => cmd.run().await,
RPhotos::Precache(cmd) => cmd.run().await,
RPhotos::Storestatics { dir } => storestatics::to_dir(dir),
RPhotos::Runserver(ra) => server::run(ra).await,
}
}
include!(concat!(env!("OUT_DIR"), "/templates.rs"));

View File

@ -1,119 +1,12 @@
#![allow(proc_macro_derive_resolution_fallback)]
#![recursion_limit = "128"]
#[macro_use]
extern crate diesel;
mod adm;
mod dbopt;
mod fetch_places;
mod models;
mod myexif;
mod photosdir;
mod pidfiles;
mod schema;
mod server;
use crate::adm::result::Error;
use crate::adm::stats::show_stats;
use crate::adm::{findphotos, makepublic, precache, storestatics, users};
use crate::dbopt::DbOpt;
use clap::Parser;
use dotenv::dotenv;
use std::path::PathBuf;
use std::process::exit;
/// Command line interface for rphotos.
#[derive(Parser)]
enum RPhotos {
/// Make specific image(s) public.
///
/// The image path(s) are relative to the image root.
Makepublic(makepublic::Makepublic),
/// Get place tags for photos by looking up coordinates in OSM
Fetchplaces(fetch_places::Fetchplaces),
/// Find new photos in the photo directory
Findphotos(findphotos::Findphotos),
/// Make sure the photos has thumbnails stored in cache.
///
/// The time limit is checked after each stored image, so the
/// command will complete in slightly more than the max time and
/// one image will be processed even if the max time is zero.
Precache(precache::Args),
/// Show some statistics from the database
Stats(DbOpt),
/// Store statics as files for a web server
Storestatics {
/// Directory to store the files in
dir: String,
},
/// List existing users
Userlist {
#[clap(flatten)]
db: DbOpt,
},
/// Set password for a (new or existing) user
Userpass {
#[clap(flatten)]
db: DbOpt,
/// Username to set password for
// TODO: Use a special type that only accepts nice user names.
user: String,
},
/// Run the rphotos web server.
Runserver(server::Args),
}
#[derive(clap::Parser)]
struct CacheOpt {
/// How to connect to memcached.
#[clap(
long,
env = "MEMCACHED_SERVER",
default_value = "memcache://127.0.0.1:11211"
)]
memcached_url: String,
}
#[derive(clap::Parser)]
struct DirOpt {
/// Path to the root directory storing all actual photos.
#[clap(long, env = "RPHOTOS_DIR")]
photos_dir: PathBuf,
}
use album_api::start_server;
#[tokio::main]
async fn main() {
dotenv().ok();
tracing_subscriber::fmt()
.with_env_filter(
std::env::var("RUST_LOG").as_deref().unwrap_or("info"),
)
.init();
match run(&RPhotos::parse()).await {
Ok(()) => (),
Err(err) => {
println!("{err}");
exit(1);
}
}
start_server(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("./ui/src/utils/bindings.ts"),
)
.await;
}
async fn run(args: &RPhotos) -> Result<(), Error> {
match args {
RPhotos::Findphotos(cmd) => cmd.run().await,
RPhotos::Makepublic(cmd) => cmd.run().await,
RPhotos::Stats(db) => show_stats(&mut db.connect().await?).await,
RPhotos::Userlist { db } => {
users::list(&mut db.connect().await?).await
}
RPhotos::Userpass { db, user } => {
users::passwd(&mut db.connect().await?, user).await
}
RPhotos::Fetchplaces(cmd) => cmd.run().await,
RPhotos::Precache(cmd) => cmd.run().await,
RPhotos::Storestatics { dir } => storestatics::to_dir(dir),
RPhotos::Runserver(ra) => server::run(ra).await,
}
}
include!(concat!(env!("OUT_DIR"), "/templates.rs"));

View File

@ -1,32 +0,0 @@
@use super::statics::{photos_css, admin_js, ux_js, rphotos_svg};
@use super::head_html;
@use crate::server::{Context, Link};
@(context: &Context, title: &str, lpath: &[Link], meta: Content, content: Content)
<!doctype html>
<html>
<head>
<title>@title</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" href="/static/@photos_css.name" type="text/css"/>
<link rel="icon" href="/static/@rphotos_svg.name"/>
@if context.is_authorized() {
<script src="/static/@admin_js.name" type="text/javascript" defer>
</script>
}
<script src="/static/@ux_js.name" type="text/javascript" defer>
</script>
@:meta()
</head>
<body>
@:head_html(context, lpath)
@:content()
<footer>
<p>Managed by
<a href="https://github.com/kaj/rphotos">rphotos
@env!("CARGO_PKG_VERSION")</a>.</p>
</footer>
</body>
</html>

View File

@ -1,5 +0,0 @@
@use crate::models::Coord;
@(coords: &[(Coord, i32)])
@if let Some(((c, p), rest)) = coords.split_first()
{ data-positions="[[@c.x,@c.y,@p]@for (c,p) in rest {,[@c.x,@c.y,@p]}]"}

View File

@ -1,34 +0,0 @@
@use super::base_html;
@use crate::models::{PhotoDetails, SizeTag};
@use crate::server::{Context, Link};
@(context: &Context, lpath: &[Link], photo: &PhotoDetails)
@:base_html(context, "Photo details", lpath, {
<meta property='og:title' content='Photo @if let Some(d) = photo.date {(@d.format("%F"))}'>
<meta property='og:type' content='image' />
<meta property='og:image' content='/img/@photo.id-m.jpg' />
<meta property='og:description' content='@for p in &photo.people {@p.person_name, }@for t in &photo.tags {#@t.tag_name, }@if let Some(p) = &photo.places.first() {@p.place_name}'>
}, {
<main class="details" data-imgid="@photo.id"@if let Some(g) = photo.grade { data-grade="@g"}@if let Some(ref p) = photo.pos { data-position="[@p.x, @p.y]"}>
<h1>Photo details</h1>
<img class="item" src="/img/@photo.id-m.jpg" width="@photo.get_size(SizeTag::Medium).0" height="@photo.get_size(SizeTag::Medium).1">
<div class="meta">
@if context.is_authorized() {
<p><a href="/img/@photo.id-l.jpg" class="full">@photo.path</a></p>
@if photo.is_public() {<p>This photo is public.</p>}
else {<p>This photo is not public.</p>}
}
@if let Some(g) = photo.grade {<p>Grade: @g</p>}
@if let Some(d) = photo.date {<p>Time: @d.format("%F %T")</p>}
@if !photo.people.is_empty() {
<p>People: @for p in &photo.people {<a href="/person/@p.slug">@p.person_name</a>, }</p>}
@if !photo.tags.is_empty() {
<p>Tags: @for t in &photo.tags {<a href="/tag/@t.slug">@t.tag_name</a>, }</p>}
@if !photo.places.is_empty() {
<p class="places">Places: @for p in &photo.places {<a href="/place/@p.slug">@p.place_name</a>, }</p>}
@if let Some(ref pos) = photo.pos {<p>Position: @pos.x @pos.y</p>}
@if let Some(ref a) = photo.attribution {<p>Av: @a</p>}
@if let Some(ref c) = photo.camera {<p>Camera: @c.model (@c.manufacturer)</p>}
</div>
</main>
})

View File

@ -1,38 +0,0 @@
@use super::statics::{photos_css, ux_js};
@use warp::http::StatusCode;
@(code: StatusCode, message: &str, detail: &str)
<!doctype html>
<html>
<head>
<title>Error @code.as_u16() @code.canonical_reason().unwrap_or("error")</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" href="/static/@photos_css.name" type="text/css"/>
<script src="/static/@ux_js.name" type="text/javascript" defer>
</script>
</head>
<body>
<header>
<span><a href="/" accesskey="h" title="Images from all years">Images</a>
</span>
<span>· <a href="/tag/">Tags</a></span>
<span>· <a href="/person/">People</a></span>
<span>· <a href="/place/">Places</a></span>
<span>· <a href="/thisday">On this day</a></span>
<span>· <a href="/random" accesskey="r">Random pic</a></span>
<span class="user"></span>
</header>
<main>
<h1>@code.canonical_reason().unwrap_or("error")</h1>
<p>@message (@code.as_u16())</p>
<p>@detail</p>
</main>
<footer>
<p>Managed by
<a href="https://github.com/kaj/rphotos">rphotos
@env!("CARGO_PKG_VERSION")</a>.</p>
</footer>
</body>
</html>

View File

@ -1,20 +0,0 @@
@use crate::server::{Context, Link};
@(context: &Context, lpath: &[Link])
<header>
<span><a href="/" accesskey="h" title="Images from all years">Images</a>
@for p in lpath { - @p}
</span>
<span>· <a href="/tag/">Tags</a></span>
<span>· <a href="/person/">People</a></span>
<span>· <a href="/place/">Places</a></span>
<span>· <a href="/thisday">On this day</a></span>
<span>· <a href="/random" accesskey="r">Random pic</a></span>
@if let Some(ref u) = context.authorized_user() {<span class="user">@u (<a href="/logout">log out</a>)</span>}
else {<span class="user">(<a href="/login?next=@context.path_without_query()">log in</a>)</span>}
<form class="search" action="/search/" method="get">
<label for="s_q" accesskey="s" title="Search">🔍</label>
<div class="refs"><input id="s_q" name="q" type="search"/></div>
</form>
</header>

View File

@ -1,15 +0,0 @@
@use super::{data_positions_html, page_base_html, photo_link_html};
@use crate::models::Coord;
@use crate::server::{Context, Link, PhotoLink};
@(context: &Context, title: &str, lpath: &[Link], photos: &[PhotoLink], coords: &[(Coord, i32)])
@:page_base_html(context, title, lpath, {
<meta property='og:title' content='@title'>
@for img in photos {
<meta property='og:image' content='/img/@img.id-m.jpg' />}
}, {
<div class="group"@:data_positions_html(coords)>
@for p in photos {@:photo_link_html(p)}
</div>
})

View File

@ -1,21 +0,0 @@
@use super::page_base_html;
@use crate::server::Context;
@(context: &Context, next: Option<&str>, message: Option<&str>)
@:page_base_html(context, "login", &[], {
<meta name="robots" content="noindex, nofollow">
}, {
<form action="/login" method="post">
@if let Some(message) = message {<p>@message</p>}
<p><label for="user">User:</label>
<input id="user" name="user"></p>
<p><label for="password">Password:</label>
<input id="password" name="password" type="password"></p>
<p><span>@if let Some(ref next) = next {
<input type="hidden" name="next" value="@next">
}</span>
<button>Log in</button>
</p>
</form>
})

View File

@ -1,13 +0,0 @@
@use super::page_base_html;
@use crate::server::Context;
@use warp::http::StatusCode;
@(context: &Context, code: StatusCode, message: &str)
@:page_base_html(context, code.canonical_reason().unwrap_or("error"), &[], {}, {
<p>@message (@code.as_u16())</p>
@if !context.is_authorized() {
<p>At least nothing publicly visible, you might try
<a href="/login?next=@context.path_without_query()">logging in</a>.</p>
}
})

View File

@ -1,10 +0,0 @@
@use super::base_html;
@use crate::server::{Context, Link};
@(context: &Context, title: &str, lpath: &[Link], meta: Content, content: Content)
@:base_html(context, title, lpath, meta, {
<main>
<h1>@title</h1>
@:content()
</main>
})

View File

@ -1,12 +0,0 @@
@use super::page_base_html;
@use crate::models::Person;
@use crate::server::Context;
@(context: &Context, people: &[Person])
@:page_base_html(context, "Photo people", &[], {}, {
<ul class="allpeople">
@for p in people {
<li><a href="/person/@p.slug">@p.person_name</a>
}</ul>
</div>
})

View File

@ -1,10 +0,0 @@
@use super::{data_positions_html, page_base_html, photo_link_html};
@use crate::models::{Coord, Person};
@use crate::server::{Context, PhotoLink};
@(context: &Context, photos: &[PhotoLink], coords: &[(Coord, i32)], person: &Person)
@:page_base_html(context, &format!("Photos with {}", person.person_name), &[], {}, {
<div class="group"@:data_positions_html(coords)>
@for p in photos {@:photo_link_html(p)}
</div>
})

View File

@ -1,7 +0,0 @@
@use crate::server::PhotoLink;
@(photo: &PhotoLink)
<div class="item@if photo.is_portrait() { portrait}">@if let Some(ref title) = photo.title {<h2>@title</h2>}
<a href="@photo.href"><img src="/img/@photo.id-s.jpg" width="@photo.size.0" height="@photo.size.1" alt="Photo @photo.id"></a>
@if let Some(ref d) = photo.lable {<span class="lable">@d</span>}
</div>

View File

@ -1,10 +0,0 @@
@use super::{data_positions_html, page_base_html, photo_link_html};
@use crate::models::{Coord, Place};
@use crate::server::{Context, PhotoLink};
@(context: &Context, photos: &[PhotoLink], coords: &[(Coord, i32)], place: &Place)
@:page_base_html(context, &format!("Photos from {}", place.place_name), &[], {}, {
<div class="group"@:data_positions_html(coords)>
@for p in photos {@:photo_link_html(p)}
</div>
})

View File

@ -1,12 +0,0 @@
@use super::page_base_html;
@use crate::models::Place;
@use crate::server::Context;
@(context: &Context, places: &[Place])
@:page_base_html(context, "Photo places", &[], {}, {
<ul class="allplaces">
@for p in places {
<li><a href="/place/@p.slug">@p.place_name</a>
}</ul>
})

View File

@ -1,44 +0,0 @@
@use super::{base_html, data_positions_html, photo_link_html};
@use crate::models::Coord;
@use crate::server::{Context, PhotoLink};
@use crate::server::search::SearchQuery;
@(context: &Context, query: &SearchQuery, n: usize, photos: &[PhotoLink], coords: &[(Coord, i32)])
@:base_html(context, "Search", &[], {}, {
<main>
<h1>Search@if n > 0 { <small class="n_hits">(@n hits)</small>}</h1>
<form class="search" action="/search/" method="get">
<label for="s_q" accesskey="s" title="Search">🔍</label>
<div class="refs">
@for (p, inc) in &query.p {
<label class="@if !inc {not }p">@p.person_name <input type="checkbox" name="p" value="@if !inc {!}@p.slug" checked/></label>
}
@for (t, inc) in &query.t {
<label class="@if !inc {not }t">@t.tag_name <input type="checkbox" name="t" value="@if !inc {!}@t.slug" checked/></label>
}
@for (l, inc) in &query.l {
<label class="@if !inc {not }l">@l.place_name <input type="checkbox" name="l" value="@if !inc {!}@l.slug" checked/></label>
}
@if let Some(pos) = &query.pos {
<label@if !pos { class="not"}>pos <input type="checkbox" name="pos" value="@if !pos {!}t" checked/></label>
}
<input id="s_q" name="q" type="search"/>
</div>
<div class="time">
<span><input type="date" name="since_date" value="@query.since.date_val()">
<input type="time" name="since_time" value="@query.since.time_val()" step="1"></span>
-
<span><input type="date" name="until_date" value="@query.until.date_val()">
<input type="time" name="until_time" value="@query.until.time_val()" step="1"></span>
</div>
</form>
@if !query.q.is_empty() {
<p>Sorry, no raw queries supported yet.
Try selection some suggestions
(javascript is needed for this, sorry again).</p>
}
<div class="group"@:data_positions_html(coords)>
@for p in photos {@:photo_link_html(p)}
</div>
</main>
})

View File

@ -1,11 +0,0 @@
@use super::{data_positions_html, page_base_html, photo_link_html};
@use crate::models::{Coord, Tag};
@use crate::server::{Context, PhotoLink};
@(context: &Context, photos: &[PhotoLink], coords: &[(Coord, i32)], tag: &Tag)
@:page_base_html(context, &format!("Photos tagged {}", tag.tag_name), &[], {}, {
<div class="group"@:data_positions_html(coords)>
@for p in photos {@:photo_link_html(p)}
</div>
})

View File

@ -1,11 +0,0 @@
@use super::page_base_html;
@use crate::models::Tag;
@use crate::server::Context;
@(context: &Context, tags: &[Tag])
@:page_base_html(context, "Photo tags", &[], {}, {
<ul class="alltags">
@for tag in tags {
<li><a href="/tag/@tag.slug">@tag.tag_name</a>
}</ul>
})

24
ui/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

13
ui/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

2517
ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
ui/package.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "album-web-ui",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview --host",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@rspc/client": "^0.1.2",
"@tanstack/react-query": "^4.10.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@rspc/react": "^0.1.2",
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^2.1.0",
"typescript": "^4.6.4",
"vite": "^3.1.0"
}
}

1
ui/public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

14
ui/src/App.tsx Normal file
View File

@ -0,0 +1,14 @@
import { rspc } from "./utils/rspc";
function App() {
const { data } = rspc.useQuery(["version"]);
return (
<div>
<h1>You are running v{data}</h1>
<h3>This data is from the rust side</h3>
</div>
);
}
export default App;

15
ui/src/main.tsx Normal file
View File

@ -0,0 +1,15 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { rspc, client, queryClient } from "./utils/rspc";
import { QueryClientProvider } from "@tanstack/react-query";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<rspc.Provider client={client} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</rspc.Provider>
</React.StrictMode>
);

10
ui/src/utils/bindings.ts Normal file
View File

@ -0,0 +1,10 @@
// This file was generated by [rspc](https://github.com/oscartbeaumont/rspc). Do not edit this file manually.
export type Procedures = {
queries:
{ key: "another", input: never, result: string } |
{ key: "version", input: never, result: string },
mutations: never,
subscriptions:
{ key: "subscriptions.pings", input: never, result: string }
};

14
ui/src/utils/rspc.ts Normal file
View File

@ -0,0 +1,14 @@
import { QueryClient } from "@tanstack/react-query";
import { FetchTransport, createClient } from "@rspc/client";
import { createReactQueryHooks } from "@rspc/react";
import type { Procedures } from "./bindings"; // These are generated by rspc in Rust for you.
const client = createClient<Procedures>({
transport: new FetchTransport("http://192.168.1.213:4000/rspc"),
});
const queryClient = new QueryClient();
const rspc = createReactQueryHooks<Procedures>();
export { rspc, client, queryClient };

1
ui/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

21
ui/tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

9
ui/tsconfig.node.json Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

11
ui/vite.config.ts Normal file
View File

@ -0,0 +1,11 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
port: 4000,
strictPort: true
}
})