diff --git a/src/main.rs b/src/main.rs index eb9b862..e4b72fa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use agahnim_web_v2::{ domain::AppState, - templates::{index::home, miniplayer::miniplayer, music::music, notfound::notfound}, + templates::{home::home, miniplayer::miniplayer, music::music, notfound::notfound}, }; use axum::{Router, routing::get}; use tower_http::services::ServeDir; diff --git a/src/templates/index.rs b/src/templates/home.rs similarity index 53% rename from src/templates/index.rs rename to src/templates/home.rs index 1f27c1c..8cf79ee 100644 --- a/src/templates/index.rs +++ b/src/templates/home.rs @@ -5,17 +5,17 @@ use axum::{ }; #[derive(Template)] -#[template(path = "index.html")] -struct IndexTemplate; +#[template(path = "home.html")] +struct HomeTemplate; #[derive(Template)] -#[template(path = "partials/index.html")] -struct IndexPartialTemplate; +#[template(path = "partials/home.html")] +struct HomePartialTemplate; pub async fn home(headers: HeaderMap) -> impl IntoResponse { if headers.contains_key("hx-request") { - Html(IndexPartialTemplate.render().unwrap()) + Html(HomePartialTemplate.render().unwrap()) } else { - Html(IndexTemplate.render().unwrap()) + Html(HomeTemplate.render().unwrap()) } } diff --git a/src/templates/mod.rs b/src/templates/mod.rs index 8b35472..4f7ff4f 100644 --- a/src/templates/mod.rs +++ b/src/templates/mod.rs @@ -1,4 +1,4 @@ -pub mod index; +pub mod home; pub mod miniplayer; pub mod music; pub mod notfound; diff --git a/static/miniplayer.js b/static/miniplayer.js new file mode 100644 index 0000000..de586d6 --- /dev/null +++ b/static/miniplayer.js @@ -0,0 +1,114 @@ +function initMiniPlayer() { + const player = document.querySelector("mini-player"); + if (!player) return; + + const audio = player.querySelector("audio"); + const playBtn = player.querySelector('button[data-action="play"]'); + const prevBtn = player.querySelector('button[data-action="prev"]'); + const nextBtn = player.querySelector('button[data-action="next"]'); + const playIcon = playBtn.querySelector(".play-icon"); + const pauseIcon = playBtn.querySelector(".pause-icon"); + const progressInput = player.querySelector(".progress-input"); + const volumeInput = player.querySelector(".volume-input"); + const currentTimeEl = player.querySelector("current-time"); + const durationTimeEl = player.querySelector("duration-time"); + const trackTitleEl = player.querySelector("track-title"); + const trackArtistEl = player.querySelector("track-artist"); + + let currentTrackIndex = 0; + let isPlaying = false; + const sources = audio.querySelectorAll("source"); + const tracks = Array.from(sources).map((s) => ({ + src: s.getAttribute("src"), + title: s.getAttribute("data-title"), + artist: s.getAttribute("data-artist"), + })); + + if (!tracks.length) return; + + audio.src = tracks[0].src; + audio.volume = parseFloat(volumeInput.value) || 0.7; + + const formatTime = (time) => { + if (isNaN(time)) return "00:00"; + const m = Math.floor(time / 60); + const s = Math.floor(time % 60); + return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; + }; + + const updatePlayState = () => { + if (isPlaying) { + playIcon.style.display = "none"; + pauseIcon.style.display = ""; + } else { + playIcon.style.display = ""; + pauseIcon.style.display = "none"; + } + }; + + const loadTrack = (index) => { + currentTrackIndex = index; + audio.src = tracks[index].src; + audio.currentTime = 0; + progressInput.value = 0; + currentTimeEl.textContent = "00:00"; + if (trackTitleEl) trackTitleEl.textContent = tracks[index].title; + if (trackArtistEl) trackArtistEl.textContent = tracks[index].artist; + if (isPlaying) audio.play(); + }; + + const handlePlayPause = () => { + if (isPlaying) { + audio.pause(); + } else { + audio.play(); + } + isPlaying = !isPlaying; + updatePlayState(); + }; + + const handlePrev = () => { + const newIndex = currentTrackIndex === 0 ? tracks.length - 1 : currentTrackIndex - 1; + loadTrack(newIndex); + }; + + const handleNext = () => { + const newIndex = currentTrackIndex === tracks.length - 1 ? 0 : currentTrackIndex + 1; + loadTrack(newIndex); + }; + + playBtn.addEventListener("click", handlePlayPause); + prevBtn.addEventListener("click", handlePrev); + nextBtn.addEventListener("click", handleNext); + + audio.addEventListener("loadeddata", () => { + progressInput.max = audio.duration; + durationTimeEl.textContent = formatTime(audio.duration); + }); + + audio.addEventListener("timeupdate", () => { + progressInput.value = audio.currentTime; + currentTimeEl.textContent = formatTime(audio.currentTime); + }); + + audio.addEventListener("ended", handleNext); + + progressInput.addEventListener("input", (e) => { + audio.currentTime = parseFloat(e.target.value); + }); + + volumeInput.addEventListener("input", (e) => { + audio.volume = parseFloat(e.target.value); + }); + + document.addEventListener("keydown", (e) => { + if (e.target.tagName === "INPUT") return; + if (e.code === "Space") { + e.preventDefault(); + handlePlayPause(); + } + }); +} + +initMiniPlayer(); +document.addEventListener("htmx:afterSwap", initMiniPlayer); diff --git a/static/style.css b/static/style.css index 6783349..2cf1b39 100644 --- a/static/style.css +++ b/static/style.css @@ -84,8 +84,7 @@ navbar { label { cursor: pointer; padding: 4px 8px; - border: 2px solid; - border-color: #ffffff #808080 #808080 #ffffff; + border: 2px solid black; background: var(--win-bg-grey); &:has(input:checked) { @@ -150,44 +149,180 @@ katcenkat { /* Mini player */ -miniplayer { +mini-player { position: fixed; top: 3.5rem; left: 0; z-index: 100; + width: min(240px, 100svw); background-color: var(--win-bg-grey); - border: 2px solid var(--grey-500); + border: 2px solid; + border-color: #ffffff #808080 #808080 #ffffff; + box-shadow: 2px 2px 0 #000; + + + transition: opacity 0.3s, + translate 0.3s; ominous-message { + display: block; + padding: 0.5rem; font-style: italic; + font-size: 0.75rem; } - controls { - + title-bar { display: flex; - flex-direction: row; - gap: 5rem; + justify-content: space-between; + align-items: center; + border-bottom: 2px solid; + border-color: #808080 #ffffff #ffffff #808080; + padding: 0.25rem 0.5rem; - state { + track-info { display: flex; - align-items: center; - gap: 5px; + flex-direction: column; + gap: 0.125rem; + max-width: 60%; + overflow: hidden; + + track-title { + font-weight: bold; + font-size: 0.75rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + track-artist { + font-size: 0.625rem; + color: #555; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } } - volume { - display: flex; - align-items: center; - gap: 5px; + time-display { + font-size: 0.625rem; + color: #333; + } + } + + progress-bar-container { + padding: 0.25rem 0.5rem; + display: flex; + align-items: center; + justify-content: center; + flex-wrap: nowrap; + + .progress-input { + display: block; + width: 90%; + height: 0.2rem; + appearance: none; + background: #fff; + border: 1px solid #808080; + outline: none; + cursor: pointer; - input { - height: 4px; - width: 5rem; + &::-webkit-slider-thumb { appearance: none; - background-color: white; - border: 1px solid var(--grey-500); + width: 10px; + height: 10px; + background: var(--win-bg-grey); + border: 2px solid; + border-color: #ffffff #808080 #808080 #ffffff; + cursor: pointer; + } + + &::-moz-range-thumb { + width: 10px; + height: 10px; + background: var(--win-bg-grey); + border: 2px solid; + border-color: #ffffff #808080 #808080 #ffffff; + cursor: pointer; } } } + controls-bar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.25rem 0.5rem; + + transport-controls { + display: flex; + gap: 1px; + + button { + display: flex; + align-items: center; + justify-content: center; + padding: 0.25rem; + background: var(--win-bg-grey); + border: 2px solid; + border-color: #ffffff #808080 #808080 #ffffff; + cursor: pointer; + + &:hover { + background: #0002; + } + + &:active { + border-color: #808080 #ffffff #ffffff #808080; + } + + &:focus-visible { + outline: 1px solid #000; + } + + img { + pointer-events: none; + } + } + } + + volume-controls { + display: flex; + align-items: center; + gap: 0.25rem; + + .volume-input { + width: 4rem; + height: 4px; + appearance: none; + background: #fff; + border: 1px solid #808080; + outline: none; + cursor: pointer; + + &::-webkit-slider-thumb { + appearance: none; + width: 8px; + height: 8px; + background: var(--win-bg-grey); + border: 2px solid; + border-color: #ffffff #808080 #808080 #ffffff; + cursor: pointer; + } + + &::-moz-range-thumb { + width: 8px; + height: 8px; + background: var(--win-bg-grey); + border: 2px solid; + border-color: #ffffff #808080 #808080 #ffffff; + cursor: pointer; + } + } + } + } + + audio { + display: none; + } } \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 535ad46..fdd3916 100644 --- a/templates/base.html +++ b/templates/base.html @@ -3,6 +3,7 @@ + Agahnim proto @@ -26,6 +27,7 @@ {% block content %}{% endblock %} +