summaryrefslogtreecommitdiff
path: root/web
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-02 22:42:35 +0300
committerPaul Buetow <paul@buetow.org>2026-05-02 22:42:35 +0300
commitdaccc2fd2c43545fe2a904cccb1f866e38817a10 (patch)
treedca78b9da806adbf61d4a259c576f93027e57ea9 /web
parente80e81f4a47a201074f12c6bcd7cf1591a6722e3 (diff)
add player minimize toggle
Diffstat (limited to 'web')
-rw-r--r--web/css/player.css38
-rw-r--r--web/index.html5
-rw-r--r--web/js/app.js3
-rw-r--r--web/js/keyboard.js6
-rw-r--r--web/js/player.js31
5 files changed, 78 insertions, 5 deletions
diff --git a/web/css/player.css b/web/css/player.css
index 45dc948..f660134 100644
--- a/web/css/player.css
+++ b/web/css/player.css
@@ -46,6 +46,44 @@
height: 2rem;
font-size: 0.8rem;
}
+.player.minimized {
+ inset: auto auto 1rem 1rem;
+ width: min(20rem, calc(100vw - 2rem));
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ box-shadow: 0 8px 32px rgba(0,0,0,0.4);
+ overflow: hidden;
+}
+.player.minimized .stage,
+.player.minimized .controls {
+ display: none;
+}
+.player .player-restore {
+ display: none;
+}
+.player.minimized .player-restore {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ min-height: 2.25rem;
+ padding: 0.45rem 0.65rem;
+ border: 0;
+ background: var(--player-bg);
+ color: var(--text-secondary);
+ cursor: pointer;
+ font: inherit;
+ font-size: 0.8rem;
+ text-align: left;
+}
+.player.minimized .player-restore:hover {
+ color: var(--text-primary);
+ background: var(--bg-surface-hover);
+}
+.player.minimized .player-restore span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
.player.is-fullscreen {
inset: 0;
background: var(--player-stage-bg);
diff --git a/web/index.html b/web/index.html
index 7ea78b8..c22ce7f 100644
--- a/web/index.html
+++ b/web/index.html
@@ -87,6 +87,7 @@
</div>
<div class="controls">
<button id="btn-toggle-stage" class="icon-btn" aria-label="Toggle stage (c)">⛶</button>
+ <button id="btn-minimize" class="icon-btn" aria-label="Minimize player (Shift+C)">▾</button>
<button id="btn-play" class="icon-btn" aria-label="Play/Pause (Space/p)">▶</button>
<div class="time" id="time-elapsed">0:00</div>
<div id="progress-track" class="progress-track" tabindex="0" aria-label="Seek">
@@ -103,6 +104,9 @@
</div>
<button id="btn-fullscreen" class="icon-btn" aria-label="Fullscreen (f)">⛶</button>
</div>
+ <button id="btn-restore-player" class="player-restore" type="button" aria-label="Restore player">
+ <span id="player-restore-title">Player minimized</span>
+ </button>
</aside>
<!-- Detached player placeholder bar -->
@@ -130,6 +134,7 @@
<tr><td><kbd>Space</kbd> <kbd>p</kbd></td><td>Play / pause</td></tr>
<tr><td><kbd>f</kbd></td><td>Toggle fullscreen</td></tr>
<tr><td><kbd>c</kbd></td><td>Collapse / expand player stage</td></tr>
+ <tr><td><kbd>C</kbd></td><td>Minimize / restore player</td></tr>
<tr><td><kbd>d</kbd></td><td>Detach / attach player popup</td></tr>
<tr><td><kbd>D</kbd></td><td>Download selected media</td></tr>
<tr><td><kbd>Esc</kbd></td><td>Exit fullscreen, deselect item</td></tr>
diff --git a/web/js/app.js b/web/js/app.js
index a0e8893..b2e79e9 100644
--- a/web/js/app.js
+++ b/web/js/app.js
@@ -1,7 +1,7 @@
import { API } from './api.js';
import { initKeyboard } from './keyboard.js';
import { initSelection, select, selectByElement, next, prev, currentIndex, currentElement, navUp, navDown, navLeft, navRight } from './selection.js';
-import { initPlayer, togglePlay, toggleFullscreen, toggleStage, toggleDetach, exitFullscreenIfNeeded, currentMediaId, selectAndPlay, playPrevious, playNext } from './player.js';
+import { initPlayer, togglePlay, toggleFullscreen, toggleStage, toggleMinimize, toggleDetach, exitFullscreenIfNeeded, currentMediaId, selectAndPlay, playPrevious, playNext } from './player.js';
import { initSearch, focusSearch, trigger as triggerSearch } from './search.js';
import { initShuffle, toggle as toggleShuffle, isOn as isShuffle } from './shuffle.js';
import { initThemes } from './themes.js';
@@ -109,6 +109,7 @@ async function initApp() {
playPause: () => togglePlay(),
fullscreen: () => toggleFullscreen(),
toggleStage: () => toggleStage(),
+ toggleMinimize: () => toggleMinimize(),
escape: () => { exitFullscreenIfNeeded(); const el = currentElement(); if (el) el.classList.remove('selected'); closeAllModals(); },
shuffle: () => { toggleShuffle(); loadMedia(); },
share: () => shareSelected(),
diff --git a/web/js/keyboard.js b/web/js/keyboard.js
index a293d3f..db0d4db 100644
--- a/web/js/keyboard.js
+++ b/web/js/keyboard.js
@@ -101,10 +101,8 @@ export function initKeyboard(handlers) {
break;
case 'Enter': handlers.enter?.(e); break;
case 'p': handlers.playPause?.(e); break;
- case 'c':
- case 'C':
- handlers.toggleStage?.(e);
- break;
+ case 'c': handlers.toggleStage?.(e); break;
+ case 'C': handlers.toggleMinimize?.(e); break;
case 'f': handlers.fullscreen?.(e); break;
case 'Escape': handlers.escape?.(e); break;
case 'Backspace': handlers.backspace?.(e); break;
diff --git a/web/js/player.js b/web/js/player.js
index 253548c..46e6246 100644
--- a/web/js/player.js
+++ b/web/js/player.js
@@ -21,6 +21,9 @@ const els = () => ({
btnMute: document.getElementById('btn-mute'),
btnFs: document.getElementById('btn-fullscreen'),
btnToggleStage: document.getElementById('btn-toggle-stage'),
+ btnMinimize: document.getElementById('btn-minimize'),
+ btnRestore: document.getElementById('btn-restore-player'),
+ restoreTitle: document.getElementById('player-restore-title'),
bigPlay: document.getElementById('big-play'),
coverArt: document.getElementById('cover-art'),
track: document.getElementById('progress-track'),
@@ -42,6 +45,8 @@ export function initPlayer() {
e.btnMute?.addEventListener('click', toggleMute);
e.btnFs?.addEventListener('click', toggleFullscreen);
e.btnToggleStage?.addEventListener('click', toggleStage);
+ e.btnMinimize?.addEventListener('click', minimizePlayer);
+ e.btnRestore?.addEventListener('click', toggleMinimize);
e.bigPlay?.addEventListener('click', togglePlay);
e.bigPlay?.addEventListener('keydown', (ev) => { if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); togglePlay(); } });
@@ -251,6 +256,7 @@ export function loadMediaDirect(media, streamUrl, thumbnailUrl, resumeFrom = 0)
e.buffered && (e.buffered.style.background = 'transparent');
e.fill.style.width = '0%';
e.thumb.style.left = '0%';
+ updateMinimizedTitle();
updateCollapsedSize();
}
@@ -292,6 +298,7 @@ function loadMedia(media, resumeFrom = 0) {
e.buffered && (e.buffered.style.background = 'transparent');
e.fill.style.width = '0%';
e.thumb.style.left = '0%';
+ updateMinimizedTitle();
updateCollapsedSize();
}
@@ -349,10 +356,34 @@ export function toggleFullscreen() {
export function toggleStage() {
const e = els();
+ e.player?.classList.remove('minimized');
e.player?.classList.toggle('collapsed');
console.log('toggleStage: collapsed =', e.player?.classList.contains('collapsed'));
}
+export function toggleMinimize() {
+ const e = els();
+ if (!e.player || !e.player.classList.contains('open')) return;
+ const willMinimize = !e.player.classList.contains('minimized');
+ e.player.classList.toggle('minimized');
+ if (willMinimize) exitFullscreenIfNeeded();
+ updateMinimizedTitle();
+}
+
+function minimizePlayer() {
+ const e = els();
+ if (!e.player || !e.player.classList.contains('open')) return;
+ e.player.classList.add('minimized');
+ exitFullscreenIfNeeded();
+ updateMinimizedTitle();
+}
+
+function updateMinimizedTitle() {
+ const e = els();
+ if (!e.restoreTitle) return;
+ e.restoreTitle.textContent = currentMedia?.file_name ? `Restore: ${currentMedia.file_name}` : 'Restore player';
+}
+
export function exitFullscreenIfNeeded() {
if (document.fullscreenElement) {
document.exitFullscreen().catch(() => {});