summaryrefslogtreecommitdiff
path: root/web/js
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-01 16:45:55 +0300
committerPaul Buetow <paul@buetow.org>2026-05-01 16:45:55 +0300
commit048d16b8aa253c93d4ae0cd1c313bf8d8b12f368 (patch)
treeab7b288399e5e0b9ed9ebdfcc157b061cf5bbc7a /web/js
parent65ee2e4d7f2be8036457e3cdca0e26e09eb56e0b (diff)
Add player stage collapse, multi-set selection, grid nav, m4b support, content-type fixes, thumbnail refresh, and verbose logging
Diffstat (limited to 'web/js')
-rw-r--r--web/js/app.js11
-rw-r--r--web/js/keyboard.js7
-rw-r--r--web/js/player.js33
3 files changed, 47 insertions, 4 deletions
diff --git a/web/js/app.js b/web/js/app.js
index 8f8ca84..cd5ded4 100644
--- a/web/js/app.js
+++ b/web/js/app.js
@@ -462,6 +462,17 @@ async function regenThumb(mediaId) {
try {
await API.regenThumbnail(mediaId);
toast('Thumbnail regenerated');
+ // Force refresh any thumbnail images for this media in the grid
+ document.querySelectorAll(`#media-grid [data-id="${mediaId}"] img`).forEach((img) => {
+ const base = img.src.split('?')[0];
+ img.src = `${base}?t=${Date.now()}`;
+ });
+ // If the same media is currently loaded in the player, refresh cover art too
+ const e = document.getElementById('cover-art');
+ if (e && !e.classList.contains('hidden')) {
+ const base = e.src.split('?')[0];
+ e.src = `${base}?t=${Date.now()}`;
+ }
} catch (err) { toast(err.message || 'Thumbnail failed', 'error'); }
}
diff --git a/web/js/keyboard.js b/web/js/keyboard.js
index de67208..cad695c 100644
--- a/web/js/keyboard.js
+++ b/web/js/keyboard.js
@@ -35,6 +35,8 @@ export function initKeyboard(handlers) {
return;
}
+ if (e.ctrlKey || e.metaKey || e.altKey) return; // don't intercept browser shortcuts
+
switch (e.key) {
// Navigation
case 'ArrowUp':
@@ -68,7 +70,10 @@ export function initKeyboard(handlers) {
break;
case 'Enter': handlers.enter?.(e); break;
case 'p': handlers.playPause?.(e); break;
- case 'c': handlers.toggleStage?.(e); break;
+ case 'c':
+ case 'C':
+ handlers.toggleStage?.(e);
+ break;
case 'f': handlers.fullscreen?.(e); break;
case 'Escape': handlers.escape?.(e); break;
case 'r': handlers.shuffle?.(e); break;
diff --git a/web/js/player.js b/web/js/player.js
index 002a781..88ef404 100644
--- a/web/js/player.js
+++ b/web/js/player.js
@@ -70,6 +70,23 @@ export function initPlayer() {
m.addEventListener('pause', () => { updateUI(false); isPlaying = false; stopProgressTimer(); });
});
+ function setupVideoDebug(m) {
+ const events = ['loadstart','loadeddata','loadedmetadata','canplay','canplaythrough','playing','waiting','stalled','suspend','error','abort','emptied','ended'];
+ events.forEach(event => {
+ m.addEventListener(event, () => {
+ console.log('[video-debug]', event,
+ 'readyState=', m.readyState,
+ 'networkState=', m.networkState,
+ 'paused=', m.paused,
+ 'src=', m.src?.slice(-40),
+ 'error=', m.error?.code || 'none',
+ 'errorMsg=', m.error?.message || '');
+ });
+ });
+ }
+ setupVideoDebug(e.video);
+ setupVideoDebug(e.audio);
+
let seeking = false;
const seekToFraction = (frac) => {
const m = currentMediaElement();
@@ -112,7 +129,13 @@ export function selectAndPlay(media, index, resumeFrom = 0) {
currentMediaIndex = index ?? -1;
loadMedia(media, resumeFrom);
isPlaying = true;
- currentMediaElement()?.play().catch(() => {});
+ const m = currentMediaElement();
+ if (m) {
+ console.log('selectAndPlay: type=', media.type, 'src=', m.src, 'readyState=', m.readyState);
+ m.play().catch((err) => { console.error('play() failed:', err); });
+ } else {
+ console.error('selectAndPlay: no media element found for type', media.type);
+ }
highlightPlayingCard();
}
@@ -121,16 +144,19 @@ function loadMedia(media, resumeFrom = 0) {
const isVideo = media.type === 'video';
const src = `/api/media/${media.id}/stream`;
if (isVideo) {
+ e.video.pause();
+ e.audio.pause(); e.audio.src = '';
e.video.style.display = '';
e.audio.style.display = 'none';
e.coverArt?.classList.add('hidden');
- e.audio.pause(); e.audio.src = '';
e.video.src = src;
+ e.video.load();
e.video.currentTime = resumeFrom;
} else {
+ e.audio.pause();
+ e.video.pause(); e.video.src = '';
e.video.style.display = 'none';
e.audio.style.display = 'none'; /* keep playing but invisible so cover-art fills stage */
- e.video.pause(); e.video.src = '';
e.audio.src = src;
e.audio.currentTime = resumeFrom;
if (e.coverArt) {
@@ -195,6 +221,7 @@ export function toggleFullscreen() {
export function toggleStage() {
const e = els();
e.player?.classList.toggle('collapsed');
+ console.log('toggleStage: collapsed =', e.player?.classList.contains('collapsed'));
}
export function exitFullscreenIfNeeded() {