diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-01 12:40:30 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-01 12:40:30 +0300 |
| commit | 27dbaf2b2d5895a091d04aa1c852abb91a619e2d (patch) | |
| tree | 846ba964ee6f0fba52ccb2b1750d44b21c454efb /web | |
| parent | badc2543eaa319657ecc97453fbdca58679ecb87 (diff) | |
Minimalist UI: hide all elements until activated; add help modal and keyboard shortcuts
- Redesign UI to be invisible by default: only header + '?' button shown
- Press m to show sidebar, t for toolbar, / for search, ? for help
- Add help modal with all keyboard shortcuts
- Fix scanner to skip corrupt/unprobeable files instead of aborting
- Fix scanner to skip thumbnail errors instead of aborting
- Fix rescan to use background context so it completes after HTTP response
- Rename project from KISS Media Player to Player
Diffstat (limited to 'web')
| -rw-r--r-- | web/bootstrap.html | 2 | ||||
| -rw-r--r-- | web/css/layout.css | 71 | ||||
| -rw-r--r-- | web/css/player.css | 9 | ||||
| -rw-r--r-- | web/index.html | 80 | ||||
| -rw-r--r-- | web/js/app.js | 70 | ||||
| -rw-r--r-- | web/js/keyboard.js | 5 | ||||
| -rw-r--r-- | web/login.html | 4 | ||||
| -rw-r--r-- | web/manifest.json | 4 |
8 files changed, 209 insertions, 36 deletions
diff --git a/web/bootstrap.html b/web/bootstrap.html index 440a55f..3f028d2 100644 --- a/web/bootstrap.html +++ b/web/bootstrap.html @@ -3,7 +3,7 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> -<title>Bootstrap β KISS Media Player</title> +<title>Bootstrap β Player</title> <link rel="stylesheet" href="/css/theme.css"> <link rel="stylesheet" href="/css/components.css"> <link rel="stylesheet" href="/css/login.css"> diff --git a/web/css/layout.css b/web/css/layout.css index 7dd70a4..ca5acd8 100644 --- a/web/css/layout.css +++ b/web/css/layout.css @@ -41,11 +41,14 @@ body { border-right: 1px solid var(--border); overflow: auto; padding: 0.75rem 0.5rem; - display: flex; + display: none; /* hidden by default */ flex-direction: column; gap: 0.25rem; z-index: 40; } +.sidebar.open { + display: flex; +} .sidebar h2 { font-size: 0.75rem; text-transform: uppercase; @@ -75,13 +78,20 @@ body { color: var(--accent); font-weight: 600; } +.sidebar-header { + display: flex; + align-items: center; + justify-content: space-between; +} /* Main layout */ .page { padding-top: var(--header-height); - padding-left: var(--sidebar-width); min-height: 100vh; } +.page.has-sidebar { + padding-left: var(--sidebar-width); +} .main-content { padding: var(--gap); max-width: 100%; @@ -255,6 +265,35 @@ body { .modal-header h3 { margin: 0; font-size: 1.1rem; } .modal-header .spacer { flex: 1 1 auto; } +/* Help modal */ +.help-content { + overflow: auto; +} +.help-table { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; +} +.help-table td { + padding: 0.4rem 0.5rem; + border-bottom: 1px solid var(--border); +} +.help-table td:first-child { + white-space: nowrap; + width: 1%; + font-family: var(--font-mono); +} +.help-table kbd { + display: inline-block; + padding: 0.15rem 0.4rem; + border-radius: var(--radius-sm); + background: var(--bg-elevated); + border: 1px solid var(--border-strong); + font-size: 0.8rem; + min-width: 1.6rem; + text-align: center; +} + /* Responsive */ @media (max-width: 860px) { :root { @@ -282,6 +321,7 @@ body { } .sidebar.open { display: flex; } .page { padding-left: 0; } + .page.has-sidebar { padding-left: 0; } .player { inset: auto 0 0 0 !important; } .toolbar { flex-wrap: wrap; @@ -361,6 +401,32 @@ body { margin-bottom: 0.75rem; } +/* Empty state hint */ +.empty-hint { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 50vh; + gap: 0.5rem; + text-align: center; +} +.empty-hint p { + margin: 0; + font-size: 1.1rem; + color: var(--text-secondary); +} +.empty-hint kbd { + display: inline-block; + padding: 0.15rem 0.4rem; + border-radius: var(--radius-sm); + background: var(--bg-elevated); + border: 1px solid var(--border-strong); + font-family: var(--font-mono); + font-size: 0.85rem; +} +.empty-hint.hidden { display: none !important; } + /* Modal sizes */ .modal-wide { width: min(56rem, 96vw); } @@ -383,4 +449,3 @@ body { .admin-list li.is-admin { background: var(--accent-soft); } - diff --git a/web/css/player.css b/web/css/player.css index 706667d..29baf30 100644 --- a/web/css/player.css +++ b/web/css/player.css @@ -1,13 +1,20 @@ /* Player */ .player { position: fixed; - inset: auto 0 0 var(--sidebar-width); + inset: auto 0 0 0; background: var(--player-bg); color: var(--text-primary); border-top: 1px solid var(--border); z-index: 60; + display: none; /* hidden by default until something plays */ transition: inset var(--transition-base); } +.player.open { + display: block; +} +.page.has-sidebar .player { + inset: auto 0 0 var(--sidebar-width); +} .player.is-fullscreen { inset: 0; background: var(--player-stage-bg); diff --git a/web/index.html b/web/index.html index 1fc583e..33478ad 100644 --- a/web/index.html +++ b/web/index.html @@ -3,7 +3,7 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> -<title>KISS Media Player</title> +<title>Player</title> <link rel="manifest" href="/manifest.json"> <link rel="stylesheet" href="/css/theme.css"> <link rel="stylesheet" href="/css/layout.css"> @@ -13,36 +13,33 @@ </head> <body> <header class="site-header" role="banner"> - <a href="/" class="logo">KISS Media</a> - <div class="search-bar" role="search"> - <span aria-hidden="true">π</span> - <input id="search-input" type="search" placeholder="Searchβ¦ (press /)" autocomplete="off" aria-label="Search media"> - <button id="search-clear" class="icon-btn btn-sm" title="Clear">β</button> - </div> + <a href="/" class="logo">Player</a> <div class="spacer"></div> - <button id="shuffle-toggle" class="icon-btn" title="Shuffle (r)">π</button> - <button id="admin-toggle" class="icon-btn hidden" title="Admin panel (a)">β</button> - <button id="theme-toggle" class="icon-btn" title="Toggle theme">β</button> - <span id="user-name" class="text-secondary text-xs"></span> - <button id="upload-toggle" class="icon-btn" title="Upload">β¬</button> + <button id="help-toggle" class="icon-btn" title="Help (?)">?</button> + <button id="admin-toggle" class="icon-btn hidden" title="Admin panel">β</button> <button id="logout-btn" class="btn btn-ghost btn-sm">Log out</button> - <button id="menu-btn" class="icon-btn" title="Menu" aria-label="Open sets">β°</button> </header> +<!-- Sidebar: hidden by default, toggled via keyboard or menu --> <nav id="sidebar" class="sidebar" aria-label="Sets"> - <h2>Sets</h2> + <div class="sidebar-header"> + <h2>Sets</h2> + <button id="menu-close" class="icon-btn btn-sm" title="Close">β</button> + </div> <div id="set-list"></div> </nav> +<!-- Main page area --> <div class="page" role="main"> <div class="main-content"> - <div class="toolbar"> + <!-- Toolbar: hidden by default, shown on 't' key or when searching --> + <div id="toolbar" class="toolbar hidden"> <select id="filter-type" aria-label="Filter by type"> <option value="">All</option> <option value="video">Video</option> <option value="audio">Audio</option> </select> - <button id="filter-favorites" class="btn btn-ghost btn-sm">β€ Favorites</button> + <button id="filter-favorites" class="btn btn-ghost btn-sm">β₯ Favorites</button> <div class="spacer"></div> <button id="filter-toggle" class="btn btn-ghost btn-sm" title="More filters">Filters βΎ</button> <span id="result-count" class="text-muted text-xs"></span> @@ -54,11 +51,27 @@ <input id="filter-min-filesize" type="number" min="0" placeholder="Min MB" aria-label="Min file size MB" class="input-sm"> <input id="filter-max-filesize" type="number" min="0" placeholder="Max MB" aria-label="Max file size MB" class="input-sm"> </div> - <div id="media-grid" class="media-grid" tabindex="0" aria-label="Media grid"></div> + + <!-- Search bar: hidden by default, shown on '/' key --> + <div id="search-bar" class="search-bar hidden" role="search"> + <span aria-hidden="true">π</span> + <input id="search-input" type="search" placeholder="Searchβ¦ (press /)" autocomplete="off" aria-label="Search media"> + <button id="search-clear" class="icon-btn btn-sm" title="Clear">β</button> + </div> + + <!-- Empty state hint: shown when no set is selected --> + <div id="empty-hint" class="empty-hint"> + <p>Welcome to Player</p> + <p class="text-muted text-sm">Press <kbd>m</kbd> to browse sets, <kbd>/</kbd> to search, or <kbd>?</kbd> for help.</p> + </div> + + <!-- Media grid: hidden until a set is loaded or search is active --> + <div id="media-grid" class="media-grid hidden" tabindex="0" aria-label="Media grid"></div> </div> </div> -<aside id="player" class="player open" aria-label="Player"> +<!-- Player: hidden until something plays --> +<aside id="player" class="player" aria-label="Player"> <div class="stage"> <video id="media-video" playsinline controlslist="nodownload"></video> <audio id="media-audio" class="hidden"></audio> @@ -86,6 +99,37 @@ <footer class="toast" id="toast" role="status" aria-live="polite"></footer> +<!-- Help Modal --> +<div id="help-modal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="help-title"> + <div class="modal"> + <div class="modal-header"> + <h3 id="help-title">Keyboard Shortcuts</h3> + <div class="spacer"></div> + <button id="help-close" class="icon-btn" aria-label="Close">β</button> + </div> + <div class="help-content"> + <table class="help-table"> + <tr><td><kbd>β</kbd> <kbd>β</kbd></td><td>Navigate media list</td></tr> + <tr><td><kbd>k</kbd> <kbd>j</kbd></td><td>Navigate media list (vim-style)</td></tr> + <tr><td><kbd>β</kbd> <kbd>β</kbd></td><td>Switch sets / pages</td></tr> + <tr><td><kbd>h</kbd> <kbd>l</kbd></td><td>Switch sets / pages (vim-style)</td></tr> + <tr><td><kbd>Enter</kbd></td><td>Open selected media</td></tr> + <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>Esc</kbd></td><td>Exit fullscreen, deselect item</td></tr> + <tr><td><kbd>r</kbd></td><td>Toggle shuffle</td></tr> + <tr><td><kbd>s</kbd></td><td>Share selected media</td></tr> + <tr><td><kbd>/</kbd></td><td>Focus search bar</td></tr> + <tr><td><kbd>n</kbd></td><td>Open notes for selected media</td></tr> + <tr><td><kbd>t</kbd></td><td>Toggle toolbar / filters</td></tr> + <tr><td><kbd>m</kbd></td><td>Toggle sets sidebar</td></tr> + <tr><td><kbd>?</kbd></td><td>Show / hide this help</td></tr> + </table> + <p class="text-muted text-xs mt-2">Click the <strong>?</strong> button or press <kbd>?</kbd> anytime to show or hide this help.</p> + </div> + </div> +</div> + <!-- Notes Modal --> <div id="notes-modal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="notes-title"> <div class="modal"> diff --git a/web/js/app.js b/web/js/app.js index e72b58e..884cdcf 100644 --- a/web/js/app.js +++ b/web/js/app.js @@ -92,18 +92,23 @@ async function initApp() { }, playPause: () => togglePlay(), fullscreen: () => toggleFullscreen(), - escape: () => { exitFullscreenIfNeeded(); const el = currentElement(); if (el) el.classList.remove('selected'); }, + escape: () => { exitFullscreenIfNeeded(); const el = currentElement(); if (el) el.classList.remove('selected'); closeAllModals(); }, shuffle: () => { toggleShuffle(); loadMedia(); }, share: () => shareSelected(), - search: focusSearch, + search: () => showSearch(), notes: openNotesForSelected, tags: openTagsForSelected, download: downloadSelected, + help: toggleHelp, + toolbar: toggleToolbar, + sidebar: toggleSidebar, + upload: () => showUpload(), }); initNotes(() => toast('Note saved')); initAdmin(); initPWA(); initUpload(); + initHelp(); // Filters document.getElementById('filter-type')?.addEventListener('change', (e) => { state.filters.type = e.target.value; loadMedia(); }); @@ -121,10 +126,12 @@ async function initApp() { document.getElementById('filter-advanced')?.classList.toggle('hidden'); }); - // Menu toggle - document.getElementById('menu-btn')?.addEventListener('click', () => { - document.getElementById('sidebar')?.classList.toggle('open'); + // Sidebar close + document.getElementById('menu-close')?.addEventListener('click', () => { + document.getElementById('sidebar')?.classList.remove('open'); + document.querySelector('.page')?.classList.remove('has-sidebar'); }); + document.getElementById('logout-btn')?.addEventListener('click', async () => { try { await API.logout(); location.href = '/login.html'; } catch {} }); @@ -135,7 +142,7 @@ async function initApp() { // Admin check via admin users endpoint API.users().then(() => { state.isAdmin = true; showAdmin(); }).catch(() => {}); renderSets(); - if (state.sets.length) { state.selectedSetId = state.sets[0].id; setActiveSet(state.sets[0].id); loadMedia(); } + // Do NOT auto-load a set by default; keep the page blank until the user chooses. } catch (err) { toast(err.message || 'Error loading sets', 'error'); } @@ -196,7 +203,10 @@ function setSetByDelta(dx) { async function loadMedia() { const grid = document.getElementById('media-grid'); + const hint = document.getElementById('empty-hint'); if (!grid) return; + grid.classList.remove('hidden'); + hint?.classList.add('hidden'); grid.innerHTML = '<p class="text-muted text-sm grid-full">Loading...</p>'; try { const params = { @@ -409,8 +419,6 @@ function initUpload() { const form = document.getElementById('upload-form'); const fileInput = document.getElementById('upload-file'); - // Open via an icon button in header (already added in HTML) - document.getElementById('upload-toggle')?.addEventListener('click', () => modal?.classList.add('open')); closeBtn?.addEventListener('click', () => modal?.classList.remove('open')); modal?.addEventListener('click', (e) => { if (e.target === modal) modal.classList.remove('open'); }); @@ -431,6 +439,52 @@ function initUpload() { }); } +function showUpload() { + const modal = document.getElementById('upload-modal'); + modal?.classList.add('open'); +} + +// Help modal +function initHelp() { + const modal = document.getElementById('help-modal'); + const closeBtn = document.getElementById('help-close'); + const toggleBtn = document.getElementById('help-toggle'); + + toggleBtn?.addEventListener('click', toggleHelp); + closeBtn?.addEventListener('click', () => modal?.classList.remove('open')); + modal?.addEventListener('click', (e) => { if (e.target === modal) modal.classList.remove('open'); }); +} + +function toggleHelp() { + const modal = document.getElementById('help-modal'); + if (!modal) return; + modal.classList.toggle('open'); +} + +// Toolbar / sidebar / search visibility toggles +function toggleToolbar() { + const toolbar = document.getElementById('toolbar'); + toolbar?.classList.toggle('hidden'); +} + +function toggleSidebar() { + const sidebar = document.getElementById('sidebar'); + const page = document.querySelector('.page'); + if (!sidebar) return; + const open = sidebar.classList.toggle('open'); + page?.classList.toggle('has-sidebar', open); +} + +function showSearch() { + const bar = document.getElementById('search-bar'); + bar?.classList.remove('hidden'); + focusSearch(); +} + +function closeAllModals() { + document.querySelectorAll('.modal-overlay.open').forEach((m) => m.classList.remove('open')); +} + function showAdmin() { const btn = document.getElementById('admin-toggle'); if (btn) btn.classList.remove('hidden'); diff --git a/web/js/keyboard.js b/web/js/keyboard.js index dbf1ac0..7d4dd77 100644 --- a/web/js/keyboard.js +++ b/web/js/keyboard.js @@ -23,8 +23,11 @@ export function initKeyboard(handlers) { case 's': handlers.share?.(e); break; case '/': e.preventDefault(); handlers.search?.(e); break; case 'n': handlers.notes?.(e); break; - case 't': handlers.tags?.(e); break; + case 't': handlers.toolbar?.(e); break; + case 'm': handlers.sidebar?.(e); break; case 'd': handlers.download?.(e); break; + case 'u': handlers.upload?.(e); break; + case '?': e.preventDefault(); handlers.help?.(e); break; } }); } diff --git a/web/login.html b/web/login.html index 6925e6f..970a004 100644 --- a/web/login.html +++ b/web/login.html @@ -3,14 +3,14 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> -<title>Login β KISS Media Player</title> +<title>Login β Player</title> <link rel="stylesheet" href="/css/theme.css"> <link rel="stylesheet" href="/css/components.css"> <link rel="stylesheet" href="/css/login.css"> </head> <body> <div class="login-container" role="main"> - <h1>KISS Media Player</h1> + <h1>Player</h1> <form id="login-form" class="login-form" novalidate> <label for="username">Username</label> <input type="text" id="username" name="username" autocomplete="username" required autofocus> diff --git a/web/manifest.json b/web/manifest.json index d3ddb9b..8d3cfd3 100644 --- a/web/manifest.json +++ b/web/manifest.json @@ -1,6 +1,6 @@ { - "name": "KISS Media Player", - "short_name": "KISS Media", + "name": "Player", + "short_name": "Player", "start_url": "/", "display": "standalone", "background_color": "#0f1117", |
