React UI
52
Cargo.toml
@ -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"
|
707
crates/album-api/Cargo.lock
generated
@ -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"
|
||||
|
@ -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"] }
|
||||
|
@ -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();
|
||||
}
|
||||
|
10
crates/album-api/ui/bindings.ts
Normal 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 }
|
||||
};
|
@ -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"
|
@ -1 +0,0 @@
|
||||
DROP TABLE photos
|
@ -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);
|
@ -1,2 +0,0 @@
|
||||
DROP TABLE photo_tags;
|
||||
DROP TABLE tags;
|
@ -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)
|
||||
);
|
@ -1,2 +0,0 @@
|
||||
DROP TABLE photo_people;
|
||||
DROP TABLE people;
|
@ -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)
|
||||
);
|
@ -1,2 +0,0 @@
|
||||
DROP TABLE photo_places;
|
||||
DROP TABLE places;
|
@ -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)
|
||||
);
|
@ -1 +0,0 @@
|
||||
DROP TABLE users;
|
@ -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);
|
@ -1 +0,0 @@
|
||||
DROP TABLE positions;
|
@ -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);
|
@ -1 +0,0 @@
|
||||
ALTER TABLE photos DROP COLUMN is_public;
|
@ -1 +0,0 @@
|
||||
ALTER TABLE photos ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT FALSE;
|
@ -1,2 +0,0 @@
|
||||
ALTER TABLE photos DROP COLUMN camera_id;
|
||||
DROP TABLE cameras;
|
@ -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);
|
@ -1,2 +0,0 @@
|
||||
ALTER TABLE photos DROP COLUMN attribution_id;
|
||||
DROP TABLE attributions;
|
@ -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);
|
@ -1,2 +0,0 @@
|
||||
ALTER TABLE photos DROP COLUMN width;
|
||||
ALTER TABLE photos DROP COLUMN height;
|
@ -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;
|
@ -1,2 +0,0 @@
|
||||
DROP INDEX positions_photo_idx;
|
||||
CREATE INDEX positions_photo_idx ON positions (photo_id);
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -1,2 +0,0 @@
|
||||
ALTER TABLE photos ALTER COLUMN width DROP NOT NULL;
|
||||
ALTER TABLE photos ALTER COLUMN height DROP NOT NULL;
|
@ -1,2 +0,0 @@
|
||||
ALTER TABLE photos ALTER COLUMN width SET NOT NULL;
|
||||
ALTER TABLE photos ALTER COLUMN height SET NOT NULL;
|
@ -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;
|
@ -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$;
|
@ -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;
|
@ -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);
|
351
res/admin.js
@ -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 = "🗙";
|
||||
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 = "🗙";
|
||||
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 = "🗙";
|
||||
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 = "🏷";
|
||||
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)
|
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 696 B |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 618 B |
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
453
res/photos.scss
@ -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;
|
||||
}
|
@ -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
@ -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
@ -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"));
|
117
src/main.rs
@ -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"),
|
||||
start_server(
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("./ui/src/utils/bindings.ts"),
|
||||
)
|
||||
.init();
|
||||
match run(&RPhotos::parse()).await {
|
||||
Ok(()) => (),
|
||||
Err(err) => {
|
||||
println!("{err}");
|
||||
exit(1);
|
||||
.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"));
|
||||
|
@ -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>
|
@ -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]}]"}
|
@ -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>
|
||||
})
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
})
|
@ -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>
|
||||
})
|
@ -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>
|
||||
}
|
||||
})
|
@ -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>
|
||||
})
|
@ -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>
|
||||
})
|
@ -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>
|
||||
})
|
@ -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>
|
@ -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>
|
||||
})
|
@ -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>
|
||||
})
|
@ -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>
|
||||
})
|
@ -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>
|
||||
})
|
@ -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
@ -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
@ -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
26
ui/package.json
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
21
ui/tsconfig.json
Normal 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
@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
11
ui/vite.config.ts
Normal 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
|
||||
}
|
||||
})
|