mini-player: finished implementation, now need to keep trakc of state

This commit is contained in:
Agahnim 2026-03-20 14:18:12 +01:00
parent 15da7cb7d2
commit d06922b584
9 changed files with 320 additions and 48 deletions

114
static/miniplayer.js Normal file
View file

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

View file

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