tune up player and stations, meet backend with frontend
This commit is contained in:
parent
47168d1cbc
commit
4f36c097ac
370
Cargo.lock
generated
370
Cargo.lock
generated
@ -26,6 +26,61 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28"
|
||||
dependencies = [
|
||||
"askama_derive",
|
||||
"askama_escape",
|
||||
"humansize",
|
||||
"num-traits",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_axum"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a41603f7cdbf5ac4af60760f17253eb6adf6ec5b6f14a7ed830cf687d375f163"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"axum-core",
|
||||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_derive"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83"
|
||||
dependencies = [
|
||||
"askama_parser",
|
||||
"basic-toml",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_escape"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
|
||||
|
||||
[[package]]
|
||||
name = "askama_parser"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.12"
|
||||
@ -144,12 +199,40 @@ dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "basic-toml"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
|
||||
[[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 = "bstr"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bufstream"
|
||||
version = "0.1.4"
|
||||
@ -168,6 +251,15 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.2"
|
||||
@ -177,6 +269,41 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
||||
|
||||
[[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 = "deranged"
|
||||
version = "0.3.11"
|
||||
@ -186,11 +313,44 @@ dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
|
||||
dependencies = [
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[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 = "elnafo-radio"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"elnafo-radio-frontend",
|
||||
"mime_guess",
|
||||
"mpd",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -202,6 +362,18 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "elnafo-radio-frontend"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"askama_axum",
|
||||
"derive_more",
|
||||
"ignore",
|
||||
"npm_rs",
|
||||
"rust-embed",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
@ -272,12 +444,35 @@ dependencies = [
|
||||
"pin-utils",
|
||||
]
|
||||
|
||||
[[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 = "gimli"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64"
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata 0.4.7",
|
||||
"regex-syntax 0.8.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.6"
|
||||
@ -355,6 +550,15 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "humansize"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
|
||||
dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.4.1"
|
||||
@ -392,6 +596,22 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata 0.4.7",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.5.0"
|
||||
@ -420,6 +640,12 @@ version = "0.2.158"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.22"
|
||||
@ -453,6 +679,22 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.0"
|
||||
@ -483,6 +725,25 @@ dependencies = [
|
||||
"bufstream",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "npm_rs"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1454347ca3c562570eff8af4a09445783dc4b7ccd00853390a7f88f76037b55"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
@ -499,6 +760,15 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.4"
|
||||
@ -626,6 +896,40 @@ version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "8.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "8.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d"
|
||||
dependencies = [
|
||||
"sha2",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
@ -644,6 +948,15 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.210"
|
||||
@ -707,6 +1020,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
@ -993,18 +1317,55 @@ dependencies = [
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
|
||||
dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
@ -1027,6 +1388,15 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
|
@ -10,7 +10,8 @@ repository = "https://vcs.elnafo.ru/L-Nafaryus/elnafo-radio"
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.7.6", features = ["http2", "macros"] }
|
||||
#elnafo-radio-frontend = { version = "0.1.0", path = "crates/frontend" }
|
||||
elnafo-radio-frontend = { version = "0.1.0", path = "crates/frontend" }
|
||||
mime_guess = "2.0.5"
|
||||
mpd = "0.1.0"
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
serde_json = "1.0.128"
|
||||
@ -22,6 +23,6 @@ tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
|
||||
|
||||
#[workspace]
|
||||
#members = ["crates/frontend"]
|
||||
#resolver = "2"
|
||||
[workspace]
|
||||
members = ["crates/frontend"]
|
||||
resolver = "2"
|
||||
|
1
crates/frontend/.gitignore
vendored
1
crates/frontend/.gitignore
vendored
@ -4,5 +4,6 @@ node_modules/
|
||||
*.tsbuildinfo
|
||||
*.mjs
|
||||
*.log
|
||||
/dist
|
||||
|
||||
openapi.json
|
||||
|
@ -11,4 +11,5 @@ npm_rs = "1.0.0"
|
||||
[dependencies]
|
||||
askama = { version = "0.12.1", features = ["with-axum"] }
|
||||
askama_axum = "0.4.0"
|
||||
derive_more = { version = "1.0.0", features = ["display"] }
|
||||
rust-embed = "8.3.0"
|
||||
|
@ -2,9 +2,9 @@
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/resources/assets/logo.svg">
|
||||
<link rel="icon" href="/assets/vinyl.svg" type="image/svg+xml" sizes="16x16 32x32">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Materia Dev</title>
|
||||
<title>Elnafo Radio Dev</title>
|
||||
</head>
|
||||
<body class="h-full text-zinc-200 font-sans ">
|
||||
<div id="app" class="flex flex-col h-full"></div>
|
||||
|
50
crates/frontend/src/assets/logo.svg
Normal file
50
crates/frontend/src/assets/logo.svg
Normal file
@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg fill="#000000" width="128" height="128" viewBox="0 0 187.62987 187.62987" version="1.1" id="svg1" xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs id="defs1" />
|
||||
<g id="layer1" transform="translate(-254,-327.14301)">
|
||||
<g id="g22" style="display:inline" transform="translate(-12.05508,278.08297)">
|
||||
<path id="path29"
|
||||
style="fill:#000000;fill-opacity:0.500678;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:0.250793"
|
||||
d="M 359.87002,49.060055 A 93.814941,93.814941 0 0 0 266.05508,142.875 93.814941,93.814941 0 0 0 359.87002,236.68995 93.814941,93.814941 0 0 0 453.68497,142.875 93.814941,93.814941 0 0 0 359.87002,49.060055 Z m 0,92.013505 a 1.801514,1.801514 0 0 1 1.80144,1.80144 1.801514,1.801514 0 0 1 -1.80144,1.80144 1.801514,1.801514 0 0 1 -1.80144,-1.80144 1.801514,1.801514 0 0 1 1.80144,-1.80144 z" />
|
||||
<path id="path31"
|
||||
style="fill:#000000;fill-opacity:0.500678;stroke:none;stroke-width:1.10062;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 359.87002,111.42486 a 31.450164,31.450164 0 0 0 -31.45014,31.45014 31.450164,31.450164 0 0 0 31.45014,31.45014 31.450164,31.450164 0 0 0 31.45014,-31.45014 31.450164,31.450164 0 0 0 -31.45014,-31.45014 z m 0,29.46744 a 1.9827772,1.9827772 0 0 1 1.9827,1.9827 1.9827772,1.9827772 0 0 1 -1.9827,1.9827 1.9827772,1.9827772 0 0 1 -1.9827,-1.9827 1.9827772,1.9827772 0 0 1 1.9827,-1.9827 z" />
|
||||
<g id="g21"
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#878787;stroke-width:2;stroke-opacity:0.304089">
|
||||
<path
|
||||
d="m 293.5304,142.70602 c -0.005,0.0563 -0.0101,0.11265 -0.015,0.16898 m 0,0 c 0,36.64663 29.70797,66.3546 66.3546,66.35461 m 0,0 c 0.0563,-0.005 0.11267,-0.01 0.16899,-0.015 m 0,-8.90592 c -0.0563,0.005 -0.11265,0.0101 -0.16899,0.015 M 302.42131,142.875 c 0.005,-0.0563 0.01,-0.11266 0.015,-0.16898"
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#878787;stroke-width:2;stroke-opacity:0.304089"
|
||||
id="path21" />
|
||||
<path
|
||||
d="m 359.87006,218.07661 c 0.0563,-0.004 0.11267,-0.009 0.16899,-0.0134 m 0,-8.84856 c -0.0563,0.005 -0.11265,0.0101 -0.16899,0.015 m -66.3546,-66.35461 c 0.005,-0.0563 0.01,-0.11266 0.015,-0.16898 m -8.84861,-4e-5 c -0.005,0.0563 -0.009,0.11265 -0.0134,0.16898 m 0,0 c 10e-6,41.5327 33.66891,75.2016 75.20161,75.20161"
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#878787;stroke-width:2;stroke-opacity:0.304089"
|
||||
id="path20" />
|
||||
<path
|
||||
d="m 284.66841,142.87497 c 0.004,-0.0563 0.009,-0.11266 0.0134,-0.16898 m -8.72505,0 c -0.005,0.0563 -0.009,0.11265 -0.0134,0.16898 m 0,0 c -4e-5,46.35143 37.57523,83.9267 83.92666,83.92666 m 0,0 c 0.0563,-0.004 0.11266,-0.009 0.16899,-0.0134 m 0,0 v -1e-5 m 0,-8.72504 c -0.0563,0.005 -0.11265,0.009 -0.16899,0.0134"
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#878787;stroke-width:2;stroke-opacity:0.304089"
|
||||
id="path19" />
|
||||
<path
|
||||
d="m 302.43632,142.70602 c -0.005,0.0563 -0.0101,0.11265 -0.015,0.16898 m 0,0 c 1.3e-4,31.72798 25.72071,57.44856 57.44869,57.44869 m 0,0 c 0.0563,-0.005 0.11267,-0.01 0.16899,-0.015 m 0,-9.79165 c -0.0563,0.006 -0.11265,0.0111 -0.16899,0.0165 m 0,0 c -26.32115,8e-5 -47.65867,-21.33744 -47.65859,-47.65859 m 0,0 c 0.005,-0.0563 0.0109,-0.11266 0.0165,-0.16898 m 0,0 5e-5,5e-5"
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#878787;stroke-width:2;stroke-opacity:0.304089"
|
||||
id="path12" />
|
||||
<path
|
||||
d="m 359.87006,85.426307 c -0.0563,0.0049 -0.11266,0.0099 -0.16898,0.01499 m 0,0 -4e-5,-1e-6 m 0,9.791651 c 0.0563,-0.0056 0.11264,-0.01113 0.16898,-0.01654 m 0,0 c 26.32115,-7.9e-5 47.65867,21.337443 47.65859,47.658593"
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#878787;stroke-width:2;stroke-opacity:0.304089"
|
||||
id="path8" />
|
||||
<path
|
||||
d="m 359.87002,76.52039 c -0.0563,0.0049 -0.11266,0.0099 -0.16898,0.01499 m 0,8.905916 c 0.0563,-0.0051 0.11265,-0.01008 0.16898,-0.01499 m 0,0 c 31.72819,-1.6e-4 57.44908,25.720504 57.44921,57.448694"
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#878787;stroke-width:2;stroke-opacity:0.304089"
|
||||
id="path6" />
|
||||
<path
|
||||
d="m 359.70104,76.53538 c 0.0563,-0.0051 0.11265,-0.01006 0.16898,-0.01499 m 0,0 c 36.64664,2e-6 66.35461,29.70797 66.35461,66.35461 m 0,0 c -0.005,0.0563 -0.01,0.11266 -0.015,0.16898 m 8.84856,0 c 0.005,-0.0563 0.009,-0.11265 0.0134,-0.16898 m 0,0 c -1e-5,-41.5327 -33.66891,-75.201604 -75.20161,-75.201615 m 0,0 c -0.0563,0.0044 -0.11266,0.0089 -0.16898,0.01344 m 0,0 h 4e-5"
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#878787;stroke-width:2;stroke-opacity:0.304089"
|
||||
id="path4" />
|
||||
<path
|
||||
d="m 435.07163,142.875 c -0.004,0.0563 -0.009,0.11265 -0.0134,0.16898 m 8.72505,0 c 0.005,-0.0563 0.009,-0.11265 0.0134,-0.16898 m 0,0 c 4e-5,-46.351434 -37.57523,-83.926703 -83.92666,-83.926664 m 0,0 c -0.0563,0.0044 -0.11266,0.0089 -0.16898,0.01344 m 0,8.725049 c 0.0563,-0.0045 0.11265,-0.009 0.16898,-0.01344"
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#979797;stroke-width:2;stroke-opacity:0.304089"
|
||||
id="path2" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 5.8 KiB |
@ -27,7 +27,7 @@
|
||||
}
|
||||
|
||||
.button {
|
||||
@apply pt-1 pb-1 pl-3 pr-3 sm:pt-2 sm:pb-2 sm:pl-5 sm:pr-5 rounded bg-ctp-peach border hover:bg-ctp-overlay0/20 text-ctp-blue cursor-pointer;
|
||||
@apply pt-1 pb-1 pl-3 pr-3 sm:pt-2 sm:pb-2 sm:pl-5 sm:pr-5 rounded-md bg-ctp-peach border hover:bg-ctp-surface0/20 text-ctp-blue cursor-pointer;
|
||||
}
|
||||
|
||||
.link-button {
|
||||
@ -51,19 +51,3 @@
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.bg-grid {
|
||||
|
||||
background:
|
||||
linear-gradient(180deg, rgba(0, 0, 0, 0) 0px, rgba(187, 65, 143, 1) 10%,
|
||||
rgba(187, 65, 143, 1) 2px, rgba(0, 0, 0, 0) 0px),
|
||||
linear-gradient(90deg, rgba(0, 0, 0, 0) 0px, rgba(187, 65, 143, 1) 10%,
|
||||
rgba(187, 65, 143, 1) 2px, rgba(0, 0, 0, 0) 0px);
|
||||
background-size: 2em 4em, 6em 2em;
|
||||
transform: perspective(500px) rotateX(60deg) scale(0.5);
|
||||
transform-origin: 50% 0%;
|
||||
z-index: -1;
|
||||
|
||||
@apply absolute w-[250%] -left-[75%] h-[200%];
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,8 @@ export type StationInfo = {
|
||||
export type SongInfo = {
|
||||
artist: string | null;
|
||||
title: string | null;
|
||||
duration: number | null;
|
||||
elapsed: number | null;
|
||||
tags: Array<[string, string]>;
|
||||
};
|
||||
|
||||
@ -38,7 +40,7 @@ export const stationsInfo = <ThrowOnError extends boolean = false>(options?: Opt
|
||||
url: '/api/stations'
|
||||
}); };
|
||||
|
||||
export const songStatus = <ThrowOnError extends boolean = false>(options?: Options<unknown, ThrowOnError>) => { return (options?.client ?? client).get<SongInfo | null, unknown, ThrowOnError>({
|
||||
export const songStatus = <ThrowOnError extends boolean = false>(options?: Options<unknown, ThrowOnError>) => { return (options?.client ?? client).post<SongInfo | null, unknown, ThrowOnError>({
|
||||
...options,
|
||||
url: '/api/status'
|
||||
url: '/api/status',
|
||||
}); };
|
||||
|
7
crates/frontend/src/components/DiscordIcon.vue
Normal file
7
crates/frontend/src/components/DiscordIcon.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg class="w-6 h-6" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" fill="currentColor" stroke="none">
|
||||
<path
|
||||
d="M20.992 20.163c-1.511-0.099-2.699-1.349-2.699-2.877 0-0.051 0.001-0.102 0.004-0.153l-0 0.007c-0.003-0.048-0.005-0.104-0.005-0.161 0-1.525 1.19-2.771 2.692-2.862l0.008-0c1.509 0.082 2.701 1.325 2.701 2.847 0 0.062-0.002 0.123-0.006 0.184l0-0.008c0.003 0.050 0.005 0.109 0.005 0.168 0 1.523-1.191 2.768-2.693 2.854l-0.008 0zM11.026 20.163c-1.511-0.099-2.699-1.349-2.699-2.877 0-0.051 0.001-0.102 0.004-0.153l-0 0.007c-0.003-0.048-0.005-0.104-0.005-0.161 0-1.525 1.19-2.771 2.692-2.862l0.008-0c1.509 0.082 2.701 1.325 2.701 2.847 0 0.062-0.002 0.123-0.006 0.184l0-0.008c0.003 0.048 0.005 0.104 0.005 0.161 0 1.525-1.19 2.771-2.692 2.862l-0.008 0zM26.393 6.465c-1.763-0.832-3.811-1.49-5.955-1.871l-0.149-0.022c-0.005-0.001-0.011-0.002-0.017-0.002-0.035 0-0.065 0.019-0.081 0.047l-0 0c-0.234 0.411-0.488 0.924-0.717 1.45l-0.043 0.111c-1.030-0.165-2.218-0.259-3.428-0.259s-2.398 0.094-3.557 0.275l0.129-0.017c-0.27-0.63-0.528-1.142-0.813-1.638l0.041 0.077c-0.017-0.029-0.048-0.047-0.083-0.047-0.005 0-0.011 0-0.016 0.001l0.001-0c-2.293 0.403-4.342 1.060-6.256 1.957l0.151-0.064c-0.017 0.007-0.031 0.019-0.040 0.034l-0 0c-2.854 4.041-4.562 9.069-4.562 14.496 0 0.907 0.048 1.802 0.141 2.684l-0.009-0.11c0.003 0.029 0.018 0.053 0.039 0.070l0 0c2.14 1.601 4.628 2.891 7.313 3.738l0.176 0.048c0.008 0.003 0.018 0.004 0.028 0.004 0.032 0 0.060-0.015 0.077-0.038l0-0c0.535-0.72 1.044-1.536 1.485-2.392l0.047-0.1c0.006-0.012 0.010-0.027 0.010-0.043 0-0.041-0.026-0.075-0.062-0.089l-0.001-0c-0.912-0.352-1.683-0.727-2.417-1.157l0.077 0.042c-0.029-0.017-0.048-0.048-0.048-0.083 0-0.031 0.015-0.059 0.038-0.076l0-0c0.157-0.118 0.315-0.24 0.465-0.364 0.016-0.013 0.037-0.021 0.059-0.021 0.014 0 0.027 0.003 0.038 0.008l-0.001-0c2.208 1.061 4.8 1.681 7.536 1.681s5.329-0.62 7.643-1.727l-0.107 0.046c0.012-0.006 0.025-0.009 0.040-0.009 0.022 0 0.043 0.008 0.059 0.021l-0-0c0.15 0.124 0.307 0.248 0.466 0.365 0.023 0.018 0.038 0.046 0.038 0.077 0 0.035-0.019 0.065-0.046 0.082l-0 0c-0.661 0.395-1.432 0.769-2.235 1.078l-0.105 0.036c-0.036 0.014-0.062 0.049-0.062 0.089 0 0.016 0.004 0.031 0.011 0.044l-0-0.001c0.501 0.96 1.009 1.775 1.571 2.548l-0.040-0.057c0.017 0.024 0.046 0.040 0.077 0.040 0.010 0 0.020-0.002 0.029-0.004l-0.001 0c2.865-0.892 5.358-2.182 7.566-3.832l-0.065 0.047c0.022-0.016 0.036-0.041 0.039-0.069l0-0c0.087-0.784 0.136-1.694 0.136-2.615 0-5.415-1.712-10.43-4.623-14.534l0.052 0.078c-0.008-0.016-0.022-0.029-0.038-0.036l-0-0z">
|
||||
</path>
|
||||
</svg>
|
||||
</template>
|
@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4">
|
||||
<path
|
||||
d="M4.61824 16.3959C5.01386 16 5.55038 15.7776 6.10981 15.7776C6.66932 15.7777 7.20584 16.0001 7.60137 16.396C7.99699 16.7919 8.21926 17.3289 8.21926 17.8888C8.21926 18.4487 7.99699 18.9857 7.60137 19.3816C7.20584 19.7776 6.66932 20 6.10981 20C5.55038 20 5.01386 19.7776 4.61824 19.3818C4.22236 18.9859 4 18.4489 4 17.8888C4 17.3288 4.22236 16.7917 4.61824 16.3959ZM20.4547 19.9436C20.0442 19.9436 19.6505 19.7803 19.3603 19.4897C19.07 19.1992 18.907 18.8051 18.9071 18.3943C18.9111 16.6164 18.5563 14.8561 17.8641 13.2187C16.8486 10.8089 15.1447 8.75272 12.9663 7.30797C10.7879 5.86328 8.23187 5.0945 5.61851 5.09759C5.20772 5.09819 4.81364 4.93521 4.52292 4.64468C4.23226 4.35423 4.06898 3.95992 4.06898 3.5488C4.06898 3.13768 4.23226 2.74346 4.52292 2.45292C4.81366 2.16238 5.20772 1.99948 5.61851 2.00001C7.77018 1.99785 9.901 2.42086 11.8889 3.24482C13.8767 4.06878 15.6824 5.27739 17.2025 6.80155C18.7253 8.32267 19.9332 10.1297 20.7564 12.1192C21.5796 14.1087 22.0022 16.241 22 18.3945C22.001 18.8051 21.8387 19.1994 21.5487 19.4901C21.2588 19.7808 20.8652 19.9439 20.4547 19.9436ZM13.6308 19.9436C13.2202 19.9436 12.8265 19.7803 12.5363 19.4897C12.246 19.1992 12.083 18.8051 12.0831 18.3943C12.0834 17.2585 11.7848 16.1426 11.2174 15.1593C10.6501 14.1756 9.83395 13.3589 8.851 12.7911C7.86813 12.2233 6.75325 11.9247 5.61835 11.925C5.06539 11.925 4.55444 11.6297 4.27792 11.1504C4.00148 10.6712 4.00148 10.0807 4.27792 9.60148C4.55444 9.12219 5.06539 8.82692 5.61835 8.82692C7.49753 8.82761 9.33491 9.38206 10.9011 10.4211C12.4674 11.4602 13.6932 12.938 14.4256 14.6698C14.9251 15.8477 15.1811 17.1147 15.178 18.3942C15.1781 18.8051 15.0152 19.1991 14.7249 19.4897C14.4347 19.7802 14.0413 19.9436 13.6308 19.9436Z"
|
||||
fill="currentColor"></path>
|
||||
<svg class="w-4 h-4" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
|
||||
<g>
|
||||
<path
|
||||
d="M9 .75A.75.75 0 019.75 0h4.5c.206 0 .393.083.529.218l.001.002.002.001A.748.748 0 0115 .75v4.5a.75.75 0 01-1.5 0V2.56L7.28 8.78a.75.75 0 01-1.06-1.06l6.22-6.22H9.75A.75.75 0 019 .75z" />
|
||||
<path
|
||||
d="M3.25 3.5a.75.75 0 00-.75.75v7.5c0 .414.336.75.75.75h7.5a.75.75 0 00.75-.75v-4a.75.75 0 011.5 0v4A2.25 2.25 0 0110.75 14h-7.5A2.25 2.25 0 011 11.75v-7.5A2.25 2.25 0 013.25 2h4a.75.75 0 010 1.5h-4z" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
7
crates/frontend/src/components/GitIcon.vue
Normal file
7
crates/frontend/src/components/GitIcon.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M30.428 14.663l-13.095-13.094c-0.35-0.349-0.833-0.565-1.367-0.565s-1.017 0.216-1.367 0.565l0-0-2.713 2.718 3.449 3.449c0.22-0.077 0.473-0.121 0.737-0.121 1.269 0 2.297 1.028 2.297 2.297 0 0.269-0.046 0.526-0.131 0.766l0.005-0.016 3.322 3.324c0.222-0.079 0.479-0.125 0.746-0.125 1.268 0 2.296 1.028 2.296 2.296s-1.028 2.296-2.296 2.296c-1.268 0-2.296-1.028-2.296-2.296 0-0.313 0.063-0.611 0.176-0.883l-0.006 0.015-3.11-3.094v8.154c0.764 0.385 1.279 1.163 1.279 2.061 0 1.27-1.030 2.3-2.3 2.3s-2.3-1.030-2.3-2.3c0-0.634 0.256-1.207 0.671-1.623l-0 0c0.211-0.211 0.462-0.382 0.741-0.502l0.015-0.006v-8.234c-0.842-0.354-1.422-1.173-1.422-2.126 0-0.32 0.065-0.624 0.183-0.901l-0.006 0.015-3.389-3.405-8.98 8.974c-0.348 0.351-0.562 0.834-0.562 1.368s0.215 1.017 0.563 1.368l13.096 13.092c0.349 0.35 0.832 0.566 1.366 0.566s1.016-0.216 1.366-0.566l13.034-13.034c0.35-0.349 0.567-0.833 0.567-1.366s-0.217-1.017-0.567-1.366l-0-0z">
|
||||
</path>
|
||||
</svg>
|
||||
</template>
|
@ -1,48 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { defineProps, defineEmits, ref } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
isOpen: Boolean,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close-modal"]);
|
||||
|
||||
const closeModal = (action) => {
|
||||
emit("close-modal", action);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isOpen" class="modal-mask">
|
||||
<div class="modal-wrapper">
|
||||
<div class="modal-container">
|
||||
<div class="flex mb-4">
|
||||
<slot name="header"></slot>
|
||||
<button class="mr-0 ml-auto button" @click="closeModal">x</button>
|
||||
</div>
|
||||
<div>
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
<div class="flex mt-4">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-mask {
|
||||
@apply fixed z-[666] top-0 left-0 w-full h-full;
|
||||
@apply bg-ctp-crust bg-opacity-50;
|
||||
}
|
||||
|
||||
.modal-wrapper {}
|
||||
|
||||
.modal-container {
|
||||
@apply flex-grow w-full lg:w-[calc(100%-100px)] max-w-[1000px] ml-auto mr-auto lg:mt-20 lg:mb-20 p-5;
|
||||
@apply bg-ctp-crust shadow-ctp-overlay0 border rounded border-ctp-surface1 text-ctp-text;
|
||||
@apply h-screen lg:h-full;
|
||||
}
|
||||
</style>
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="relative h-12">
|
||||
<nav class="absolute w-full h-full flex justify-between items-center m-0 pl-3 pr-3 bg-ctp-peach">
|
||||
<div class="h-12 mx-auto bg-ctp-peach rounded-b-xl">
|
||||
<nav class="w-full h-full max-w-[1200px] flex justify-between items-center mx-auto my-0 pl-3 pr-3">
|
||||
<div class="items-center m-0 flex">
|
||||
<slot name="left"></slot>
|
||||
</div>
|
||||
|
@ -6,16 +6,15 @@ import { api, store } from "@";
|
||||
import { ref, onUpdated, onMounted } from "vue";
|
||||
|
||||
const player = store.usePlayer();
|
||||
const audioRef = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
player.register(audioRef.value);
|
||||
player.create();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex sticky w-full mx-auto bottom-2 px-2">
|
||||
<div class="flex sticky w-full mx-auto bottom-2 px-2 max-w-[1200px]">
|
||||
<div class="flex rounded-md m-2 bg-ctp-base mx-auto w-full border-2 border-ctp-mantle">
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="h-20 flex rounded-l-md aspect-square items-center justify-center bg-ctp-overlay0">
|
||||
@ -23,11 +22,12 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex flex-col justify-center px-4 py-4">
|
||||
<span class="flex-grow font-bold">{{ player.station ? "ASD" : "Unknown" }}</span>
|
||||
<span class="text-ctp-subtext0">Location</span>
|
||||
<span class="flex-grow font-bold">{{ player.songInfo ? player.songInfo.artist : "Unknown" }}</span>
|
||||
<span class="text-ctp-subtext0">{{ player.songInfo ? player.songInfo.title : "Unknown" }}</span>
|
||||
</div>
|
||||
<div class="flex w-8 h-20 p-2">
|
||||
<input type="range" class="slider" />
|
||||
<input type="range" class="slider" min="0" max="100" value="100"
|
||||
@input="player.volume($event.target.value)" />
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<button @click="player.toggle()"
|
||||
@ -37,7 +37,6 @@ onMounted(() => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<audio class="hidden" ref="audioRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -59,10 +58,10 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.slider::-webkit-progress-value {
|
||||
@apply bg-ctp-peach/20 w-4 rounded-full;
|
||||
@apply bg-ctp-peach/30 w-4 rounded-full;
|
||||
}
|
||||
|
||||
.slider::-moz-range-progress {
|
||||
@apply bg-ctp-peach/20 w-4 rounded-full;
|
||||
@apply bg-ctp-peach/30 w-4 rounded-full;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { api } from "@";
|
||||
import PlayIcon from "@/components/PlayIcon.vue";
|
||||
import PauseIcon from "@/components/PauseIcon.vue";
|
||||
import ExternalIcon from "@/components/ExternalIcon.vue";
|
||||
import LocationIcon from "@/components/LocationIcon.vue"
|
||||
import TicketIcon from "@/components/TicketIcon.vue";
|
||||
@ -8,8 +9,10 @@ import VinylIcon from "@/components/VinylIcon.vue";
|
||||
import InfoIcon from "@/components/InfoIcon.vue";
|
||||
import Tooltip from "@/components/Tooltip.vue";
|
||||
import { store } from "@";
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
const player = store.usePlayer();
|
||||
const isPlayable = ref(null);
|
||||
|
||||
const { stationInfo } = defineProps({
|
||||
stationInfo: api.StationInfo,
|
||||
@ -17,19 +20,26 @@ const { stationInfo } = defineProps({
|
||||
|
||||
const status = () => {
|
||||
if (stationInfo.status === api.StationStatus.Receive) {
|
||||
return api.StationStatus.Online;
|
||||
isPlayable.value = api.StationStatus.Online;
|
||||
return;
|
||||
}
|
||||
if (stationInfo.playback === api.Playback.Stopped || stationInfo.playback === api.Playback.Paused) {
|
||||
return api.StationStatus.Offline;
|
||||
isPlayable.value = api.StationStatus.Offline;
|
||||
return;
|
||||
}
|
||||
return stationInfo.status;
|
||||
isPlayable.value = stationInfo.status;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
status();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex rounded-xl m-2 bg-ctp-base">
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="h-20 sm:h-36 flex rounded-l-xl aspect-square items-center justify-center bg-ctp-maroon/50">
|
||||
<div class="h-20 sm:h-36 flex rounded-l-xl aspect-square items-center justify-center bg-ctp-peach/50">
|
||||
<VinylIcon class="p-1" />
|
||||
</div>
|
||||
</div>
|
||||
@ -54,17 +64,19 @@ const status = () => {
|
||||
<div class="flex items-center justify-center space-x-2">
|
||||
<button
|
||||
class="p-1.5 sm:p-2.5 w-8 sm:w-10 inline-flex rounded-md items-center justify-center bg-ctp-mantle hover:bg-ctp-mantle/50">
|
||||
<Tooltip :text="status()">
|
||||
<Tooltip :text="stationInfo.status">
|
||||
<InfoIcon class="text-ctp-peach" />
|
||||
</Tooltip>
|
||||
</button>
|
||||
<button
|
||||
class="p-1.5 sm:p-2.5 w-8 sm:w-10 inline-flex rounded-md items-center justify-center bg-ctp-mantle hover:bg-ctp-mantle/50">
|
||||
<a :href="stationInfo.url"
|
||||
class="p-1.5 sm:p-2.5 w-8 sm:w-10 inline-flex rounded-md items-center justify-center bg-ctp-mantle hover:bg-ctp-mantle/50 cursor-pointer">
|
||||
<ExternalIcon class="text-ctp-peach" />
|
||||
</button>
|
||||
<button v-if="status()" @click="player.load(stationInfo, true)"
|
||||
</a>
|
||||
<button v-if="isPlayable === api.StationStatus.Online"
|
||||
@click="player.playing ? (player.station?.id === stationInfo.id ? player.pause() : player.play(stationInfo)) : player.play(stationInfo)"
|
||||
class="p-1.5 sm:p-2.5 w-8 sm:w-10 inline-flex rounded-md items-center justify-center bg-ctp-mantle hover:bg-ctp-mantle/50">
|
||||
<PlayIcon class="text-ctp-peach" />
|
||||
<PauseIcon v-if="player.station?.id === stationInfo.id && player.playing" class="text-ctp-peach" />
|
||||
<PlayIcon v-else class="text-ctp-peach" />
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
@ -10,31 +10,40 @@
|
||||
<path id="path31"
|
||||
style="fill:#000000;fill-opacity:0.500678;stroke:none;stroke-width:1.10062;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 359.87002,111.42486 a 31.450164,31.450164 0 0 0 -31.45014,31.45014 31.450164,31.450164 0 0 0 31.45014,31.45014 31.450164,31.450164 0 0 0 31.45014,-31.45014 31.450164,31.450164 0 0 0 -31.45014,-31.45014 z m 0,29.46744 a 1.9827772,1.9827772 0 0 1 1.9827,1.9827 1.9827772,1.9827772 0 0 1 -1.9827,1.9827 1.9827772,1.9827772 0 0 1 -1.9827,-1.9827 1.9827772,1.9827772 0 0 1 1.9827,-1.9827 z" />
|
||||
<g id="g21" style="fill:none;fill-opacity:0.500678;stroke:#676767;stroke-opacity:0.304089">
|
||||
<g id="g21"
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#878787;stroke-width:2;stroke-opacity:0.304089">
|
||||
<path
|
||||
d="m 293.5304,142.70602 c -0.005,0.0563 -0.0101,0.11265 -0.015,0.16898 m 0,0 c 0,36.64663 29.70797,66.3546 66.3546,66.35461 m 0,0 c 0.0563,-0.005 0.11267,-0.01 0.16899,-0.015 m 0,-8.90592 c -0.0563,0.005 -0.11265,0.0101 -0.16899,0.015 M 302.42131,142.875 c 0.005,-0.0563 0.01,-0.11266 0.015,-0.16898"
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#676767;stroke-opacity:0.304089" id="path21" />
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#878787;stroke-width:2;stroke-opacity:0.304089"
|
||||
id="path21" />
|
||||
<path
|
||||
d="m 359.87006,218.07661 c 0.0563,-0.004 0.11267,-0.009 0.16899,-0.0134 m 0,-8.84856 c -0.0563,0.005 -0.11265,0.0101 -0.16899,0.015 m -66.3546,-66.35461 c 0.005,-0.0563 0.01,-0.11266 0.015,-0.16898 m -8.84861,-4e-5 c -0.005,0.0563 -0.009,0.11265 -0.0134,0.16898 m 0,0 c 10e-6,41.5327 33.66891,75.2016 75.20161,75.20161"
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#676767;stroke-opacity:0.304089" id="path20" />
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#878787;stroke-width:2;stroke-opacity:0.304089"
|
||||
id="path20" />
|
||||
<path
|
||||
d="m 284.66841,142.87497 c 0.004,-0.0563 0.009,-0.11266 0.0134,-0.16898 m -8.72505,0 c -0.005,0.0563 -0.009,0.11265 -0.0134,0.16898 m 0,0 c -4e-5,46.35143 37.57523,83.9267 83.92666,83.92666 m 0,0 c 0.0563,-0.004 0.11266,-0.009 0.16899,-0.0134 m 0,0 v -1e-5 m 0,-8.72504 c -0.0563,0.005 -0.11265,0.009 -0.16899,0.0134"
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#676767;stroke-opacity:0.304089" id="path19" />
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#878787;stroke-width:2;stroke-opacity:0.304089"
|
||||
id="path19" />
|
||||
<path
|
||||
d="m 302.43632,142.70602 c -0.005,0.0563 -0.0101,0.11265 -0.015,0.16898 m 0,0 c 1.3e-4,31.72798 25.72071,57.44856 57.44869,57.44869 m 0,0 c 0.0563,-0.005 0.11267,-0.01 0.16899,-0.015 m 0,-9.79165 c -0.0563,0.006 -0.11265,0.0111 -0.16899,0.0165 m 0,0 c -26.32115,8e-5 -47.65867,-21.33744 -47.65859,-47.65859 m 0,0 c 0.005,-0.0563 0.0109,-0.11266 0.0165,-0.16898 m 0,0 5e-5,5e-5"
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#676767;stroke-opacity:0.304089" id="path12" />
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#878787;stroke-width:2;stroke-opacity:0.304089"
|
||||
id="path12" />
|
||||
<path
|
||||
d="m 359.87006,85.426307 c -0.0563,0.0049 -0.11266,0.0099 -0.16898,0.01499 m 0,0 -4e-5,-1e-6 m 0,9.791651 c 0.0563,-0.0056 0.11264,-0.01113 0.16898,-0.01654 m 0,0 c 26.32115,-7.9e-5 47.65867,21.337443 47.65859,47.658593"
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#676767;stroke-opacity:0.304089" id="path8" />
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#878787;stroke-width:2;stroke-opacity:0.304089"
|
||||
id="path8" />
|
||||
<path
|
||||
d="m 359.87002,76.52039 c -0.0563,0.0049 -0.11266,0.0099 -0.16898,0.01499 m 0,8.905916 c 0.0563,-0.0051 0.11265,-0.01008 0.16898,-0.01499 m 0,0 c 31.72819,-1.6e-4 57.44908,25.720504 57.44921,57.448694"
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#676767;stroke-opacity:0.304089" id="path6" />
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#878787;stroke-width:2;stroke-opacity:0.304089"
|
||||
id="path6" />
|
||||
<path
|
||||
d="m 359.70104,76.53538 c 0.0563,-0.0051 0.11265,-0.01006 0.16898,-0.01499 m 0,0 c 36.64664,2e-6 66.35461,29.70797 66.35461,66.35461 m 0,0 c -0.005,0.0563 -0.01,0.11266 -0.015,0.16898 m 8.84856,0 c 0.005,-0.0563 0.009,-0.11265 0.0134,-0.16898 m 0,0 c -1e-5,-41.5327 -33.66891,-75.201604 -75.20161,-75.201615 m 0,0 c -0.0563,0.0044 -0.11266,0.0089 -0.16898,0.01344 m 0,0 h 4e-5"
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#676767;stroke-opacity:0.304089" id="path4" />
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#878787;stroke-width:2;stroke-opacity:0.304089"
|
||||
id="path4" />
|
||||
<path
|
||||
d="m 435.07163,142.875 c -0.004,0.0563 -0.009,0.11265 -0.0134,0.16898 m 8.72505,0 c 0.005,-0.0563 0.009,-0.11265 0.0134,-0.16898 m 0,0 c 4e-5,-46.351434 -37.57523,-83.926703 -83.92666,-83.926664 m 0,0 c -0.0563,0.0044 -0.11266,0.0089 -0.16898,0.01344 m 0,8.725049 c 0.0563,-0.0045 0.11265,-0.009 0.16898,-0.01344"
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#676767;stroke-opacity:0.304089" id="path2" />
|
||||
style="fill:none;fill-opacity:0.500678;stroke:#979797;stroke-width:2;stroke-opacity:0.304089"
|
||||
id="path2" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
@ -1,9 +1,24 @@
|
||||
export * as plugins from "@/plugins";
|
||||
export { router } from "@/router";
|
||||
export { client } from "@/client.ts";
|
||||
export * as api from "@/client.ts";
|
||||
export * as schemas from "@/client/schemas.gen.ts";
|
||||
export * as api_types from "@/client/types.gen.ts";
|
||||
export * as store from "@/store";
|
||||
export * as style from "@/assets/style.css";
|
||||
export * as types from "@/types";
|
||||
|
||||
export * from "@/assets/style.css";
|
||||
export * from "@/assets/logo.svg";
|
||||
|
||||
|
||||
// Used for debug
|
||||
export const devel = import.meta.hot;
|
||||
|
||||
// Retrieve content from meta tags
|
||||
export const headMeta = (metaName: string): string | null => {
|
||||
let meta = document.getElementsByTagName('meta');
|
||||
|
||||
for (let item of meta) {
|
||||
if (item.getAttribute("name") === metaName) {
|
||||
return item.getAttribute("content");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
40
crates/frontend/src/lib.rs
Normal file
40
crates/frontend/src/lib.rs
Normal file
@ -0,0 +1,40 @@
|
||||
extern crate derive_more;
|
||||
use askama_axum::Template;
|
||||
use derive_more::Display;
|
||||
use rust_embed::RustEmbed;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "dist/assets/"]
|
||||
pub struct Assets;
|
||||
|
||||
#[derive(Display, Clone)]
|
||||
#[display("({}, {})", name, content)]
|
||||
pub struct Meta {
|
||||
pub name: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "base.html")]
|
||||
pub struct BaseTemplate {
|
||||
pub view: String,
|
||||
pub title: String,
|
||||
pub meta: Option<Vec<Meta>>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render() {
|
||||
println!(
|
||||
"{}",
|
||||
BaseTemplate {
|
||||
view: String::from("home"),
|
||||
title: String::from("test"),
|
||||
meta: Some(vec![Meta {
|
||||
name: String::from("github"),
|
||||
content: String::from("https://github.com")
|
||||
}])
|
||||
}
|
||||
.render()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
@ -2,20 +2,16 @@ import App from "@/App.vue";
|
||||
|
||||
import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import { plugins, router, client, style } from "@";
|
||||
import { devel, router, client } from "@";
|
||||
|
||||
|
||||
const debug = import.meta.hot;
|
||||
|
||||
client.setConfig({
|
||||
baseURL: debug ? "http://localhost:54605" : "/",
|
||||
baseURL: devel ? "http://localhost:54605" : "/",
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
createApp(App)
|
||||
.use(createPinia())
|
||||
.use(router)
|
||||
.directive("click-outside", plugins.clickOutside)
|
||||
.directive("tooltip", plugins.tooltip)
|
||||
.mount('#app');
|
||||
|
||||
|
@ -1,34 +0,0 @@
|
||||
export const clickOutside = {
|
||||
beforeMount: function(element: any, binding: any) {
|
||||
element.clickOutsideEvent = function(event: any) {
|
||||
if (!(element == event.target || element.contains(event.target))) {
|
||||
binding.value(event);
|
||||
}
|
||||
};
|
||||
|
||||
document.body.addEventListener("click", element.clickOutsideEvent);
|
||||
document.body.addEventListener("contextmenu", element.clickOutsideEvent);
|
||||
},
|
||||
unmounted: function(element: any) {
|
||||
document.body.removeEventListener("click", element.clickOutsideEvent);
|
||||
document.body.removeEventListener("contextmenu", element.clickOutsideEvent);
|
||||
}
|
||||
};
|
||||
|
||||
export const tooltip = {
|
||||
beforeMount: function (element: any, binding: any) {
|
||||
element.tooltip = function (event) {
|
||||
let target = event.target;
|
||||
if (target.offsetWidth < target.scrollWidth) {
|
||||
target.setAttribute('title', binding.value?.text ? binding.value.text : event.target.textContent);
|
||||
} else {
|
||||
target.hasAttribute('title') && target.removeAttribute('title');
|
||||
}
|
||||
};
|
||||
|
||||
document.body.addEventListener('mouseover', element.tooltip);
|
||||
},
|
||||
unmounted: function(element: any) {
|
||||
document.body.removeEventListener("mouseover", element.tooltip);
|
||||
}
|
||||
};
|
@ -1 +0,0 @@
|
||||
export * from "@/plugins/directives";
|
@ -1,5 +1,5 @@
|
||||
import { createRouter, createWebHistory, useRoute } from "vue-router";
|
||||
import { store, api, schemas } from "@";
|
||||
import { store, api } from "@";
|
||||
|
||||
|
||||
export const router = createRouter({
|
||||
|
@ -4,36 +4,57 @@ import { useRoute } from "vue-router";
|
||||
import axios, { CancelToken } from "axios";
|
||||
import { api } from "@";
|
||||
|
||||
type TimerId = number;
|
||||
|
||||
export const usePlayer = defineStore("player", () => {
|
||||
const station = ref(null);
|
||||
const playing = ref(false);
|
||||
const instance = ref(null);
|
||||
const stations: Ref<api.StationInfo[]> = ref(null);
|
||||
const station: Ref<api.StationInfo> = ref(null);
|
||||
const playing: Ref<boolean> = ref(false);
|
||||
const instance: Ref<HTMLAudioElement> = ref(null);
|
||||
const songInfo: Ref<api.SongInfo> = ref(null);
|
||||
const updateTimer: Ref<TimerId> = ref(null);
|
||||
|
||||
const register = (_instance) => {
|
||||
instance.value = _instance;
|
||||
const create = () => {
|
||||
let audio: HTMLAudioElement = new Audio();
|
||||
audio.id = "audioPlayer";
|
||||
audio.className = "hidden";
|
||||
audio.preload = "auto";
|
||||
audio.onplay = async () => {
|
||||
playing.value = true;
|
||||
await update();
|
||||
};
|
||||
audio.onpause = () => {
|
||||
playing.value = false;
|
||||
clearTimeout(updateTimer.value);
|
||||
};
|
||||
|
||||
document.body.appendChild(audio);
|
||||
instance.value = audio;
|
||||
};
|
||||
|
||||
const load = (station: api.StationInfo, start: bool = false) => {
|
||||
station.value = station;
|
||||
instance.value.src = station.value.url;
|
||||
instance.value.load();
|
||||
if (start) {
|
||||
play();
|
||||
const update = async () => {
|
||||
await refreshSongInfo();
|
||||
clearTimeout(updateTimer.value);
|
||||
updateTimer.value = setTimeout(update, (songInfo.value.duration - songInfo.value.elapsed) * 1000);
|
||||
};
|
||||
|
||||
const play = (_station: api.StationInfo | null = null) => {
|
||||
if (_station) {
|
||||
station.value = _station;
|
||||
instance.value.src = _station.url;
|
||||
}
|
||||
};
|
||||
|
||||
const play = () => {
|
||||
if (!instance.value.src)
|
||||
if (!instance.value.src){
|
||||
return;
|
||||
}
|
||||
instance.value.load();
|
||||
instance.value.play();
|
||||
playing.value = true;
|
||||
};
|
||||
|
||||
const pause = () => {
|
||||
if (!instance.value.src)
|
||||
if (!instance.value.src) {
|
||||
return;
|
||||
}
|
||||
instance.value.pause();
|
||||
playing.value = false;
|
||||
};
|
||||
|
||||
const toggle = () => {
|
||||
@ -44,6 +65,39 @@ export const usePlayer = defineStore("player", () => {
|
||||
}
|
||||
};
|
||||
|
||||
const volume = (value: number) => {
|
||||
instance.value.volume = Math.min(Math.max(value * 0.01, 0), 1);
|
||||
};
|
||||
|
||||
return { station, playing, load, instance, register, play, pause, toggle };
|
||||
const volumeEvent = (event: Event) => {
|
||||
volume(event.target.value);
|
||||
};
|
||||
|
||||
const refreshStations = async (error: Ref<object | null> | null = null) => {
|
||||
await api.stationsInfo({ throwOnError: true })
|
||||
.then(async res => {
|
||||
stations.value = res.data;
|
||||
})
|
||||
.catch(err => {
|
||||
if (error) {
|
||||
error.value = "Failed to retrieve stations";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const refreshSongInfo = async (error: Ref<object | null> | null = null) => {
|
||||
await api.songStatus ({ body: { id: station.value.id }, throwOnError: true })
|
||||
.then(async res => {
|
||||
songInfo.value = res.data;
|
||||
})
|
||||
.catch(err => {
|
||||
songInfo.value = null;
|
||||
if (error) {
|
||||
error.value = "Failed to retrieve song info";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
return { stations, station, playing, create, play, pause, toggle, volume, volumeEvent, songInfo, refreshStations, refreshSongInfo };
|
||||
});
|
||||
|
@ -1,30 +0,0 @@
|
||||
import { api_types } from "@";
|
||||
import { CancelToken } from "axios";
|
||||
|
||||
export type Error = object;
|
||||
|
||||
export type RepositoryItemMeta = {
|
||||
selected: bool;
|
||||
clickCount: number;
|
||||
clickTimer: (object | null);
|
||||
};
|
||||
|
||||
export type RepositoryItemType = "directory" | "file";
|
||||
|
||||
export type RepositoryItemInfo = {
|
||||
info: (api_types.DirectoryInfo | api_types.FileInfo);
|
||||
meta: RepositoryItemMeta;
|
||||
type: RepositoryItemType;
|
||||
};
|
||||
|
||||
export type RepositoryContent = RepositoryItemInfo[];
|
||||
|
||||
export type UploadFileStatus = "success" | "fail" | "transfer" | "idle";
|
||||
|
||||
export type UploadFile = {
|
||||
content: File;
|
||||
status: UploadFileStatus;
|
||||
progress: number;
|
||||
cancel: CancelToken | null;
|
||||
error: string | null;
|
||||
};
|
@ -1,22 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { router, api, store } from "@";
|
||||
import { headMeta, devel } from "@";
|
||||
import NavBar from "@/components/NavBar.vue";
|
||||
import DocumentationIcon from "@/components/DocumentationIcon.vue";
|
||||
import DiscordIcon from "@/components/DiscordIcon.vue";
|
||||
import GitIcon from "@/components/GitIcon.vue";
|
||||
import VinylIcon from "@/components/VinylIcon.vue";
|
||||
import Player from "@/components/Player.vue";
|
||||
import Error from "@/components/Error.vue";
|
||||
|
||||
|
||||
const title = ref(null);
|
||||
const author = ref(null);
|
||||
const discord = ref(null);
|
||||
const git = ref(null);
|
||||
const documentation = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
title.value = document.getElementsByTagName("title")[0].text;
|
||||
author.value = devel ? "L-Nafaryus" : headMeta("author");
|
||||
discord.value = devel ? "http://example.com" : headMeta("discord");
|
||||
git.value = devel ? "http://example.com" : headMeta("git");
|
||||
documentation.value = devel ? "http://example.com" : headMeta("documentation");
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VinylIcon class="absolute h-full mx-auto w-full -z-10 opacity-15" />
|
||||
<div class="flex-grow">
|
||||
<NavBar>
|
||||
<template #left>
|
||||
<RouterLink class="link-button" to="/">Home</RouterLink>
|
||||
<RouterLink class="link-button font-bold" to="/">{{ title }}</RouterLink>
|
||||
</template>
|
||||
|
||||
<template #right>
|
||||
<RouterLink class="link-button" to="/auth/signin">Sign In</RouterLink>
|
||||
<a v-if="discord" :href="discord"
|
||||
class="flex justify-center items-center space-x-1 text-ctp-surface0 link-button font-bold">
|
||||
<DiscordIcon />
|
||||
<span class="hidden sm:inline">Discord</span>
|
||||
</a>
|
||||
</template>
|
||||
</NavBar>
|
||||
|
||||
@ -27,14 +50,22 @@ import Error from "@/components/Error.vue";
|
||||
|
||||
<Player />
|
||||
|
||||
<footer class="flex justify-between pb-2 pt-2 pl-5 pr-5 bg-ctp-mantle">
|
||||
<a href="https://vcs.elnafo.ru/L-Nafaryus" class="text-ctp-peach">Made by L-Nafaryus, 2024</a>
|
||||
<div>
|
||||
<footer class="pb-2 pt-2 pl-5 pr-5 bg-ctp-mantle">
|
||||
<div class="flex justify-between max-w-[1200px] mx-auto">
|
||||
<span v-if="author" class="text-ctp-peach font-bold items-center flex">Made by {{ author }}</span>
|
||||
<div class="flex space-x-1">
|
||||
<a v-if="documentation" :href="documentation"
|
||||
class="flex justify-center items-center space-x-1 text-ctp-peach link-button font-bold bg-ctp-mantle">
|
||||
<DocumentationIcon />
|
||||
<span class="hidden sm:inline">Docs</span>
|
||||
</a>
|
||||
<a v-if="git" :href="git"
|
||||
class="flex justify-center items-center space-x-1 text-ctp-peach link-button font-bold bg-ctp-mantle">
|
||||
<GitIcon />
|
||||
<span class="hidden sm:inline">Git</span>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<a href="/api/docs" class="flex justify-center items-center space-x-1 text-ctp-peach">
|
||||
<DocumentationIcon />
|
||||
<span>API</span>
|
||||
</a>
|
||||
</footer>
|
||||
</template>
|
||||
|
@ -5,41 +5,20 @@ import ExternalIcon from "@/components/ExternalIcon.vue";
|
||||
import LocationIcon from "@/components/LocationIcon.vue";
|
||||
import Error from "@/components/Error.vue";
|
||||
import Station from "@/components/Station.vue";
|
||||
import { api } from "@";
|
||||
import { api, store } from "@";
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
|
||||
const player = store.usePlayer();
|
||||
const error = ref(null);
|
||||
const stations = ref(null);
|
||||
const update = ref(null);
|
||||
|
||||
const stationsInfo = async () => {
|
||||
error.value = null;
|
||||
|
||||
await api.stationsInfo({ throwOnError: true })
|
||||
.then(async stationsInfo => {
|
||||
stations.value = stationsInfo.data;
|
||||
console.log(stations.value);
|
||||
})
|
||||
.catch(err => {
|
||||
stations.value = null;
|
||||
error.value = "Failed to get stations list";
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await stationsInfo();
|
||||
update.value = setInterval(stationsInfo, 10000);
|
||||
await player.refreshStations(error);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(update.value);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Base>
|
||||
<Error :value="error" />
|
||||
<Station :stationInfo="station" v-for="station in stations" />
|
||||
<Station :stationInfo="station" v-for="station in player.stations" />
|
||||
</Base>
|
||||
</template>
|
||||
|
@ -2,13 +2,16 @@
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/resources/assets/logo.svg">
|
||||
<link rel="icon" type="image/svg+xml" sizes="16x16 32x32" href="/assets/logo.svg">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Materia</title>
|
||||
<script type="module" crossorigin src="/resources/assets/index.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/resources/assets/index.css">
|
||||
<title>{{ title }}</title>
|
||||
{% for item in meta.as_deref().unwrap_or([]) %}
|
||||
<meta name="{{ item.name }}" content="{{ item.content }}">
|
||||
{% endfor %}
|
||||
<script type="module" crossorigin src="/assets/index.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index.css">
|
||||
</head>
|
||||
<body class="h-full bg-zinc-900 text-zinc-200 font-sans">
|
||||
<body class="h-full text-zinc-200 font-sans">
|
||||
<div id="{{ view }}" class="flex flex-col h-full"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -13,9 +13,9 @@ export default defineConfig({
|
||||
//outDir: path.resolve(__dirname, "./frontend"),
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: "resources/assets/[name].js",
|
||||
assetFileNames: "resources/assets/[name][extname]",
|
||||
chunkFileNames: "resources/assets/[name].js"
|
||||
entryFileNames: "assets/[name].js",
|
||||
assetFileNames: "assets/[name][extname]",
|
||||
chunkFileNames: "assets/[name].js"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
35
src/api.rs
35
src/api.rs
@ -1,8 +1,11 @@
|
||||
use axum::{
|
||||
extract::DefaultBodyLimit,
|
||||
extract::State,
|
||||
http::{header::*, Method, StatusCode},
|
||||
response::IntoResponse,
|
||||
http::{
|
||||
header::{self, *},
|
||||
Method, StatusCode, Uri,
|
||||
},
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
@ -31,7 +34,7 @@ pub fn routes(state: Arc<Context>) -> Router {
|
||||
Router::new()
|
||||
.route("/healthcheck", get(healthcheck))
|
||||
.route("/stations", get(stations))
|
||||
.route("/status", get(status))
|
||||
.route("/status", post(status))
|
||||
.layer(cors)
|
||||
.fallback(fallback)
|
||||
.with_state(state)
|
||||
@ -48,6 +51,8 @@ pub enum Playback {
|
||||
pub struct SongInfo {
|
||||
pub artist: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub duration: Option<u64>,
|
||||
pub elapsed: Option<u64>,
|
||||
pub tags: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
@ -145,14 +150,22 @@ pub async fn status(
|
||||
|
||||
if let Ok(mut client) = connection {
|
||||
if let Ok(Some(current)) = client.currentsong() {
|
||||
return (
|
||||
StatusCode::OK,
|
||||
Json(Some(SongInfo {
|
||||
artist: current.artist,
|
||||
title: current.title,
|
||||
tags: current.tags,
|
||||
})),
|
||||
);
|
||||
let mut info = SongInfo {
|
||||
artist: current.artist,
|
||||
title: current.title,
|
||||
duration: None,
|
||||
elapsed: None,
|
||||
tags: current.tags,
|
||||
};
|
||||
|
||||
if let Ok(status) = client.status() {
|
||||
if let Some(time) = status.time {
|
||||
info.duration = Some(time.1.as_secs());
|
||||
info.elapsed = Some(time.0.as_secs());
|
||||
}
|
||||
}
|
||||
|
||||
return (StatusCode::OK, Json(Some(info)));
|
||||
} else {
|
||||
return (StatusCode::OK, Json(None));
|
||||
}
|
||||
|
@ -29,8 +29,15 @@ pub struct Server {
|
||||
pub port: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Base {
|
||||
pub title: String,
|
||||
pub meta: Option<Vec<(String, String)>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub base: Base,
|
||||
pub server: Server,
|
||||
pub stations: Option<Vec<Station>>,
|
||||
}
|
||||
@ -65,6 +72,10 @@ impl Config {
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Config {
|
||||
base: Base {
|
||||
title: String::from("Elnafo Radio"),
|
||||
meta: None,
|
||||
},
|
||||
server: Server {
|
||||
address: String::from("127.0.0.1"),
|
||||
port: 54605,
|
||||
|
97
src/main.rs
97
src/main.rs
@ -1,7 +1,16 @@
|
||||
pub mod api;
|
||||
pub mod config;
|
||||
|
||||
use axum::{http::Uri, response::IntoResponse, routing::get, Router};
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::{
|
||||
header::{self, *},
|
||||
Method, StatusCode, Uri,
|
||||
},
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tower_http::trace::{self, TraceLayer};
|
||||
@ -35,20 +44,90 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
config: config.clone(),
|
||||
});
|
||||
|
||||
let app = Router::new().nest("/api", api::routes(state)).layer(
|
||||
TraceLayer::new_for_http()
|
||||
.make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
|
||||
.on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
|
||||
);
|
||||
let app = Router::new()
|
||||
.route("/", get(frontend_handler))
|
||||
.with_state(state.clone())
|
||||
.nest("/api", api::routes(state))
|
||||
.route("/assets/*file", get(assets))
|
||||
.layer(
|
||||
TraceLayer::new_for_http()
|
||||
.make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
|
||||
.on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
|
||||
);
|
||||
|
||||
let address: SocketAddr =
|
||||
format!("{}:{}", config.server.address, config.server.port).parse()?;
|
||||
|
||||
let lister = tokio::net::TcpListener::bind(&address).await?;
|
||||
let listener = tokio::net::TcpListener::bind(&address).await?;
|
||||
|
||||
println!("Listening on {}", address);
|
||||
println!("Listening on http://{}", address);
|
||||
|
||||
axum::serve(lister, app.into_make_service()).await?;
|
||||
axum::serve(listener, app.into_make_service()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn assets(uri: Uri) -> Result<impl IntoResponse, ResourceError> {
|
||||
let path = uri.path().trim_start_matches("/assets/").to_string();
|
||||
|
||||
match elnafo_radio_frontend::Assets::get(&path) {
|
||||
Some(content) => {
|
||||
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
||||
Ok(([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response())
|
||||
}
|
||||
None => Err(ResourceError::NotFound),
|
||||
}
|
||||
}
|
||||
|
||||
async fn frontend_handler(State(state): State<Arc<Context>>, _: Uri) -> impl IntoResponse {
|
||||
use elnafo_radio_frontend::Meta;
|
||||
|
||||
elnafo_radio_frontend::BaseTemplate {
|
||||
view: String::from("app"),
|
||||
title: state.config.base.title.clone(),
|
||||
meta: if let Some(meta) = &state.config.base.meta {
|
||||
Some(
|
||||
meta.iter()
|
||||
.map(|kv| Meta {
|
||||
name: kv.0.clone(),
|
||||
content: kv.1.clone(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ResourceError {
|
||||
NotFound,
|
||||
NotExists,
|
||||
BadFormat,
|
||||
BadContent,
|
||||
}
|
||||
|
||||
impl std::error::Error for ResourceError {}
|
||||
|
||||
impl std::fmt::Display for ResourceError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NotFound | Self::NotExists => write!(f, "Resource was not found"),
|
||||
Self::BadFormat => write!(f, "Cannot determine file format"),
|
||||
Self::BadContent => write!(f, "Failed to read a file content"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for ResourceError {
|
||||
fn into_response(self) -> Response {
|
||||
let status = match self {
|
||||
Self::NotFound => StatusCode::NOT_FOUND,
|
||||
Self::NotExists => StatusCode::NO_CONTENT,
|
||||
Self::BadFormat | Self::BadContent => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
(status, format!("{}", self)).into_response()
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user