tune up player and stations, meet backend with frontend

This commit is contained in:
L-Nafaryus 2024-09-26 19:27:38 +05:00
parent 47168d1cbc
commit 4f36c097ac
Signed by: L-Nafaryus
GPG Key ID: 553C97999B363D38
31 changed files with 820 additions and 266 deletions

370
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -4,5 +4,6 @@ node_modules/
*.tsbuildinfo
*.mjs
*.log
/dist
openapi.json

View File

@ -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"

View File

@ -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>

View 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

View File

@ -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%];
}
}

View File

@ -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',
}); };

View 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>

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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;
};

View 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()
);
}

View File

@ -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');

View File

@ -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);
}
};

View File

@ -1 +0,0 @@
export * from "@/plugins/directives";

View File

@ -1,5 +1,5 @@
import { createRouter, createWebHistory, useRoute } from "vue-router";
import { store, api, schemas } from "@";
import { store, api } from "@";
export const router = createRouter({

View File

@ -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 };
});

View File

@ -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;
};

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"
}
}
},

View File

@ -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));
}

View File

@ -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,

View File

@ -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()
}
}