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 | |
| 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
| -rw-r--r-- | AGENTS.md | 8 | ||||
| -rw-r--r-- | Magefile.go | 4 | ||||
| -rw-r--r-- | cmd/mediaplayer/main.go | 2 | ||||
| -rw-r--r-- | internal/api/middleware.go | 15 | ||||
| -rw-r--r-- | internal/scanner/scanner.go | 14 | ||||
| -rw-r--r-- | internal/scanner/scanner_test.go | 13 | ||||
| -rw-r--r-- | internal/service/admin.go | 3 | ||||
| -rw-r--r-- | k8s/deployment.yaml | 16 | ||||
| -rw-r--r-- | k8s/pvc-db.yaml | 4 | ||||
| -rw-r--r-- | k8s/pvc-media.yaml | 4 | ||||
| -rw-r--r-- | k8s/secret.yaml | 4 | ||||
| -rw-r--r-- | k8s/service.yaml | 6 | ||||
| -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 |
20 files changed, 271 insertions, 67 deletions
@@ -1,6 +1,6 @@ -# KISS Media Player β Agent Documentation +# Player β Agent Documentation -This file is written for coding agents working on the `kiss-media-player` project. +This file is written for coding agents working on the `player` project. --- @@ -81,8 +81,8 @@ Available targets (from `Magefile.go`): | `mage test` | Run `go test ./...` | | `mage install` | Build and copy `play` to `$GOPATH/bin` (or `~/go/bin`) | | `mage clean` | Remove the `play` binary | -| `mage docker-build` | Build container image as `kiss-media-player:latest` | -| `mage docker-push` | Push `kiss-media-player:latest` to registry | +| `mage docker-build` | Build container image as `player:latest` | +| `mage docker-push` | Push `player:latest` to registry | --- diff --git a/Magefile.go b/Magefile.go index 5303b20..7895e48 100644 --- a/Magefile.go +++ b/Magefile.go @@ -67,10 +67,10 @@ func Clean() error { // DockerBuild builds the container image. func DockerBuild() error { - return sh.RunV("docker", "build", "-t", "kiss-media-player:latest", ".") + return sh.RunV("docker", "build", "-t", "player:latest", ".") } // DockerPush pushes the container image to the registry. func DockerPush() error { - return sh.RunV("docker", "push", "kiss-media-player:latest") + return sh.RunV("docker", "push", "player:latest") } diff --git a/cmd/mediaplayer/main.go b/cmd/mediaplayer/main.go index 7e5df5c..33927c0 100644 --- a/cmd/mediaplayer/main.go +++ b/cmd/mediaplayer/main.go @@ -95,7 +95,7 @@ func run(args []string) error { gs := api.NewGracefulServer(server, cfg) - log.Printf("kiss-media-player %s starting on %s", internal.Version, gs.Server.Addr) + log.Printf("player %s starting on %s", internal.Version, gs.Server.Addr) go func() { if err := gs.Server.ListenAndServe(); err != nil && err != http.ErrServerClosed { diff --git a/internal/api/middleware.go b/internal/api/middleware.go index ee1d6c9..fd779b5 100644 --- a/internal/api/middleware.go +++ b/internal/api/middleware.go @@ -29,15 +29,24 @@ func NewMiddleware(store repository.Store, sm *auth.SessionManager) *Middleware } // RequireSession validates the session cookie and injects the session into request context. +// For HTML page requests (Accept: text/html), redirects to /login.html instead of returning 401. func (mw *Middleware) RequireSession(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("session") if err != nil { + if wantsHTML(r) { + http.Redirect(w, r, "/login.html", http.StatusTemporaryRedirect) + return + } http.Error(w, "unauthorized", http.StatusUnauthorized) return } sess, err := mw.sm.ValidateSession(r.Context(), cookie.Value) if err != nil || sess == nil { + if wantsHTML(r) { + http.Redirect(w, r, "/login.html", http.StatusTemporaryRedirect) + return + } http.Error(w, "unauthorized", http.StatusUnauthorized) return } @@ -46,6 +55,12 @@ func (mw *Middleware) RequireSession(next http.Handler) http.Handler { }) } +// wantsHTML returns true if the request appears to be from a browser expecting an HTML page. +func wantsHTML(r *http.Request) bool { + accept := r.Header.Get("Accept") + return strings.Contains(accept, "text/html") +} + // RequireAdmin ensures the authenticated user is an admin. func (mw *Middleware) RequireAdmin(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index a1effd7..2e82fbe 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -132,7 +132,9 @@ func (s *FSScanner) scanSet(ctx context.Context, root, setPath string) error { meta, err := s.prober.Probe(ctx, path) if err != nil { - return fmt.Errorf("probe %q: %w", path, err) + // Skip unprobeable/corrupt files instead of aborting the whole scan. + fmt.Printf("[scanner] skipping unprobeable file %q: %v\n", path, err) + return nil } meta.FileSizeBytes = info.Size() @@ -146,7 +148,10 @@ func (s *FSScanner) scanSet(ctx context.Context, root, setPath string) error { thumbName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) + ".jpg" thumbnailPath = filepath.Join(thumbDir, thumbName) if err := s.thumbGen.Generate(ctx, path, thumbnailPath, meta.Duration); err != nil { - return fmt.Errorf("thumbnail %q: %w", path, err) + // Skip thumbnail generation errors (e.g., corrupt or audio-only video files) + // and continue scanning without a thumbnail. + fmt.Printf("[scanner] skipping thumbnail for %q: %v\n", path, err) + thumbnailPath = "" } } @@ -182,6 +187,11 @@ var mediaExtensions = map[string]struct{}{ } func isMediaFile(path string) bool { + base := filepath.Base(path) + // Skip macOS resource fork files (._*) + if strings.HasPrefix(base, "._") { + return false + } ext := strings.ToLower(filepath.Ext(path)) _, ok := mediaExtensions[ext] return ok diff --git a/internal/scanner/scanner_test.go b/internal/scanner/scanner_test.go index 8da32a4..f6299dd 100644 --- a/internal/scanner/scanner_test.go +++ b/internal/scanner/scanner_test.go @@ -345,9 +345,13 @@ func TestFSScanner_Scan(t *testing.T) { } s := newTestScanner(store, prober, &thumb.MockGenerator{}, clk, mfs) + // Unprobeable files are skipped with a log instead of failing the whole scan. err := s.Scan(ctx, "/media") - if err == nil { - t.Fatal("expected error for probe failure") + if err != nil { + t.Fatalf("unexpected error for probe failure; expected skip, got: %v", err) + } + if store.MediaRepo.CreateMediaFunc != nil { + // no media should have been created for the bad file } }) @@ -381,9 +385,10 @@ func TestFSScanner_Scan(t *testing.T) { } s := newTestScanner(store, prober, gen, clk, mfs) + // Thumbnail generation errors are skipped so the scan continues. err := s.Scan(ctx, "/media") - if err == nil { - t.Fatal("expected error for thumbnail failure") + if err != nil { + t.Fatalf("unexpected error for thumbnail failure; expected skip, got: %v", err) } }) diff --git a/internal/service/admin.go b/internal/service/admin.go index afccc78..f16037a 100644 --- a/internal/service/admin.go +++ b/internal/service/admin.go @@ -39,7 +39,8 @@ func (s *adminService) TriggerRescan(ctx context.Context) error { if s.scanner == nil { return fmt.Errorf("scanner not configured") } - if err := s.scanner.Scan(ctx, s.mediaRoot); err != nil { + // Use a background context so the scan isn't canceled when the HTTP request finishes. + if err := s.scanner.Scan(context.Background(), s.mediaRoot); err != nil { return fmt.Errorf("scan failed: %w", err) } return nil diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index c4c605e..56bc11b 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -1,25 +1,25 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: kiss-media-player + name: player labels: - app: kiss-media-player + app: player spec: replicas: 1 selector: matchLabels: - app: kiss-media-player + app: player template: metadata: labels: - app: kiss-media-player + app: player spec: securityContext: runAsNonRoot: true fsGroup: 65534 containers: - name: app - image: kiss-media-player:latest + image: player:latest imagePullPolicy: IfNotPresent securityContext: runAsUser: 65534 @@ -31,7 +31,7 @@ spec: name: http envFrom: - secretRef: - name: kiss-media-player-secret + name: player-secret optional: true env: - name: PORT @@ -67,7 +67,7 @@ spec: volumes: - name: data persistentVolumeClaim: - claimName: kiss-media-player-db + claimName: player-db - name: media persistentVolumeClaim: - claimName: kiss-media-player-media + claimName: player-media diff --git a/k8s/pvc-db.yaml b/k8s/pvc-db.yaml index 87fad81..3328172 100644 --- a/k8s/pvc-db.yaml +++ b/k8s/pvc-db.yaml @@ -1,9 +1,9 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: - name: kiss-media-player-db + name: player-db labels: - app: kiss-media-player + app: player spec: accessModes: - ReadWriteOnce diff --git a/k8s/pvc-media.yaml b/k8s/pvc-media.yaml index 76e2d41..f75e5df 100644 --- a/k8s/pvc-media.yaml +++ b/k8s/pvc-media.yaml @@ -1,9 +1,9 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: - name: kiss-media-player-media + name: player-media labels: - app: kiss-media-player + app: player spec: accessModes: - ReadWriteMany diff --git a/k8s/secret.yaml b/k8s/secret.yaml index 68b21fc..bbde6dd 100644 --- a/k8s/secret.yaml +++ b/k8s/secret.yaml @@ -1,9 +1,9 @@ apiVersion: v1 kind: Secret metadata: - name: kiss-media-player-secret + name: player-secret labels: - app: kiss-media-player + app: player type: Opaque stringData: # Set this to the desired admin password before applying. diff --git a/k8s/service.yaml b/k8s/service.yaml index c3da918..bf0cbd9 100644 --- a/k8s/service.yaml +++ b/k8s/service.yaml @@ -1,13 +1,13 @@ apiVersion: v1 kind: Service metadata: - name: kiss-media-player + name: player labels: - app: kiss-media-player + app: player spec: type: ClusterIP selector: - app: kiss-media-player + app: player ports: - port: 8080 targetPort: http 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", |
