diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-29 20:19:34 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-29 20:19:34 +0300 |
| commit | 4a70c0ea083306cb79d0fa325d331ffa4333154a (patch) | |
| tree | 84df7e2f781974ef45ac8c34f1a59e80fdd5c41d | |
| parent | cf30414c2a0696cc75c615f4da66ea85d0522c42 (diff) | |
feat: create web PWA frontend
| -rw-r--r-- | ... | 129 | ||||
| -rw-r--r-- | web/bootstrap.html | 25 | ||||
| -rw-r--r-- | web/css/components.css | 134 | ||||
| -rw-r--r-- | web/css/layout.css | 276 | ||||
| -rw-r--r-- | web/css/login.css | 27 | ||||
| -rw-r--r-- | web/css/player.css | 132 | ||||
| -rw-r--r-- | web/css/theme.css | 105 | ||||
| -rw-r--r-- | web/favicon.svg | 1 | ||||
| -rw-r--r-- | web/index.html | 121 | ||||
| -rw-r--r-- | web/js/admin.js | 101 | ||||
| -rw-r--r-- | web/js/api.js | 66 | ||||
| -rw-r--r-- | web/js/app.js | 311 | ||||
| -rw-r--r-- | web/js/keyboard.js | 28 | ||||
| -rw-r--r-- | web/js/notes.js | 45 | ||||
| -rw-r--r-- | web/js/player.js | 223 | ||||
| -rw-r--r-- | web/js/pwa.js | 7 | ||||
| -rw-r--r-- | web/js/search.js | 35 | ||||
| -rw-r--r-- | web/js/selection.js | 43 | ||||
| -rw-r--r-- | web/js/shuffle.js | 24 | ||||
| -rw-r--r-- | web/js/state.js | 11 | ||||
| -rw-r--r-- | web/js/themes.js | 20 | ||||
| -rw-r--r-- | web/login.html | 21 | ||||
| -rw-r--r-- | web/manifest.json | 11 | ||||
| -rw-r--r-- | web/sw.js | 41 |
24 files changed, 1932 insertions, 5 deletions
@@ -0,0 +1,129 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<title>KISS Media Player</title> +<link rel="manifest" href="/manifest.json"> +<link rel="stylesheet" href="/css/theme.css"> +<link rel="stylesheet" href="/css/layout.css"> +<link rel="stylesheet" href="/css/components.css"> +<link rel="stylesheet" href="/css/player.css"> +<script type="module" src="/js/app.js"></script> +</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> + <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" style="font-size:0.85rem;"></span> + <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> + +<nav id="sidebar" class="sidebar" aria-label="Sets"> + <h2>Sets</h2> + <div id="set-list"></div> +</nav> + +<div class="page" role="main"> + <div class="main-content"> + <div class="toolbar" style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.75rem;"> + <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> + <div class="spacer"></div> + <span id="result-count" class="text-muted" style="font-size:0.8rem;"></span> + </div> + <div id="media-grid" class="media-grid" tabindex="0" aria-label="Media grid"></div> + </div> +</div> + +<aside id="player" class="player open" aria-label="Player"> + <div class="stage"> + <video id="media-video" playsinline controlslist="nodownload"></video> + <audio id="media-audio" style="display:none;"></audio> + <div id="big-play" class="big-play" role="button" aria-label="Play" tabindex="0"> + <div class="play-icon">▶</div> + </div> + </div> + <div class="controls"> + <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"> + <div id="progress-fill" class="progress-fill"></div> + <div id="progress-thumb" class="progress-thumb" aria-hidden="true"></div> + </div> + <div class="time" id="time-total">0:00</div> + <button id="btn-prev" class="icon-btn" aria-label="Previous">⏮</button> + <button id="btn-next" class="icon-btn" aria-label="Next">⏭</button> + <div class="volume-control"> + <button id="btn-mute" class="icon-btn" aria-label="Mute">🔊</button> + <input id="volume-slider" type="range" min="0" max="1" step="0.05" value="1" class="volume-slider" aria-label="Volume"> + </div> + <button id="btn-fullscreen" class="icon-btn" aria-label="Fullscreen (f)">⛶</button> + </div> +</aside> + +<footer class="toast" id="toast" role="status" aria-live="polite"></footer> + +<!-- Notes Modal --> +<div id="notes-modal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="notes-title"> + <div class="modal"> + <div class="modal-header"> + <h3 id="notes-title">Notes</h3> + <div class="spacer"></div> + <button id="notes-close" class="icon-btn" aria-label="Close">✕</button> + </div> + <textarea id="notes-textarea" rows="6" placeholder="Add your notes…"></textarea> + <div style="display:flex;justify-content:flex-end;gap:0.5rem;margin-top:0.5rem;"> + <button id="notes-delete" class="btn btn-danger btn-sm">Delete</button> + <button id="notes-save" class="btn btn-primary btn-sm">Save</button> + </div> + </div> +</div> + +<!-- Admin Modal --> +<div id="admin-modal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="admin-title"> + <div class="modal" style="width:min(56rem,96vw);"> + <div class="modal-header"> + <h3 id="admin-title">Admin</h3> + <div class="spacer"></div> + <button id="admin-close" class="icon-btn" aria-label="Close">✕</button> + </div> + <div style="display:flex;gap:1rem;flex-wrap:wrap;"> + <section style="flex:1 1 18rem;"> + <h4 style="margin:0.25rem 0;font-size:0.9rem;">Users</h4> + <div id="admin-users"></div> + <form id="admin-user-form" style="margin-top:0.5rem;"> + <input name="username" placeholder="Username" required autocomplete="off"> + <input type="password" name="password" placeholder="Password" required autocomplete="new-password"> + <label style="display:flex;align-items:center;gap:0.35rem;font-size:0.8rem;"> + <input type="checkbox" name="is_admin"> Admin + </label> + <button type="submit" class="btn btn-primary btn-sm" style="margin-top:0.25rem;">Add user</button> + </form> + </section> + <section style="flex:1 1 18rem;"> + <h4 style="margin:0.25rem 0;font-size:0.9rem;">Permissions</h4> + <div id="admin-permissions"></div> + </section> + </div> + <div style="display:flex;gap:0.5rem;margin-top:0.75rem;"> + <button id="admin-rescan" class="btn btn-primary btn-sm">Trigger rescan</button> + <button id="admin-trash" class="btn btn-danger btn-sm">View trash</button> + </div> + </div> +</div> +</body> +</html> diff --git a/web/bootstrap.html b/web/bootstrap.html index 378b595..440a55f 100644 --- a/web/bootstrap.html +++ b/web/bootstrap.html @@ -2,9 +2,30 @@ <html lang="en"> <head> <meta charset="UTF-8"> -<title>Bootstrap - KISS Media Player</title> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<title>Bootstrap — KISS Media 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> -<h1>First Admin Setup</h1> +<div class="login-container" role="main"> + <h1>Create Admin Account</h1> + <p class="subtitle">One-time setup. This form is available only while no users exist.</p> + <form id="bootstrap-form" class="login-form" novalidate> + <label for="username">Username</label> + <input type="text" id="username" name="username" autocomplete="username" required autofocus> + + <label for="password">Password</label> + <input type="password" id="password" name="password" autocomplete="new-password" required minlength="6"> + + <label for="password-confirm">Confirm Password</label> + <input type="password" id="password-confirm" name="password-confirm" autocomplete="new-password" required minlength="6"> + + <button type="submit" class="btn btn-primary">Create Admin</button> + <p id="bootstrap-error" class="error-message" role="alert" aria-live="polite"></p> + </form> +</div> +<script type="module" src="/js/app.js"></script> </body> </html> diff --git a/web/css/components.css b/web/css/components.css new file mode 100644 index 0000000..6d2f074 --- /dev/null +++ b/web/css/components.css @@ -0,0 +1,134 @@ +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.35rem; + padding: 0.5rem 0.85rem; + border-radius: var(--radius-sm); + border: 1px solid transparent; + font-size: 0.85rem; + font-weight: 600; + line-height: 1.2; + cursor: pointer; + background: var(--bg-elevated); + color: var(--text-primary); + transition: background var(--transition-fast), border-color var(--transition-fast), transform 80ms ease; +} +.btn:hover { background: var(--bg-surface-hover); } +.btn:active { transform: translateY(1px); } +.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } + +.btn-primary { + background: var(--accent); + color: #fff; + border-color: var(--accent-active); +} +.btn-primary:hover { background: var(--accent-hover); } +.btn-danger { + background: var(--danger); + color: #fff; + border-color: #b91c1c; +} +.btn-danger:hover { background: #e04e4e; } +.btn-ghost { + background: transparent; + color: var(--text-secondary); + border-color: var(--border); +} +.btn-ghost:hover { color: var(--text-primary); background: var(--bg-surface-hover); } +.btn-sm { padding: 0.3rem 0.55rem; font-size: 0.78rem; } +.btn-lg { padding: 0.65rem 1.1rem; font-size: 0.95rem; } + +/* Icon buttons */ +.icon-btn { + width: 2.25rem; + height: 2.25rem; + border-radius: var(--radius-sm); + background: transparent; + border: 1px solid transparent; + color: var(--text-secondary); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 1.05rem; + line-height: 1; + padding: 0; + transition: background var(--transition-fast), color var(--transition-fast); +} +.icon-btn:hover { background: var(--bg-surface-hover); color: var(--text-primary); } +.icon-btn.active { color: var(--accent); background: var(--accent-soft); } + +/* Forms */ +input, textarea, select { + font: inherit; + color: var(--text-primary); + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 0.5rem 0.65rem; + width: 100%; +} +input:focus, textarea:focus, select:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft); +} +label { + display: block; + font-size: 0.8rem; + font-weight: 600; + margin: 0.35rem 0 0.2rem; + color: var(--text-secondary); +} + +/* Error message */ +.error-message { + color: var(--danger); + font-size: 0.85rem; + min-height: 1.25rem; + margin: 0.35rem 0 0; +} + +/* Search bar */ +.search-bar { + display: flex; + align-items: center; + gap: 0.4rem; + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 0.25rem 0.6rem; + max-width: 22rem; + width: 100%; +} +.search-bar input { + background: transparent; + border: none; + padding: 0.35rem 0.2rem; + flex: 1 1 auto; +} +.search-bar input:focus { box-shadow: none; } + +/* Toast */ +.toast { + position: fixed; + bottom: 1rem; + right: 1rem; + background: var(--bg-surface); + color: var(--text-primary); + border: 1px solid var(--border-strong); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + padding: 0.65rem 0.9rem; + z-index: 110; + font-size: 0.85rem; + transform: translateY(120%); + opacity: 0; + transition: transform var(--transition-base), opacity var(--transition-base); +} +.toast.show { transform: translateY(0); opacity: 1; } +.toast.info { border-left: 4px solid var(--accent); } +.toast.success { border-left: 4px solid var(--success); } +.toast.error { border-left: 4px solid var(--danger); } diff --git a/web/css/layout.css b/web/css/layout.css new file mode 100644 index 0000000..9f51008 --- /dev/null +++ b/web/css/layout.css @@ -0,0 +1,276 @@ +/* Reset & base */ +*, *::before, *::after { box-sizing: border-box; } +html, body { + height: 100%; +} +body { + margin: 0; + font-family: var(--font-sans); + background: var(--bg-body); + color: var(--text-primary); + line-height: 1.45; +} + +/* Header */ +.site-header { + position: fixed; + inset: 0 0 auto 0; + height: var(--header-height); + background: var(--bg-surface); + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + gap: var(--gap); + padding: 0 1rem; + z-index: 50; +} +.site-header .logo { + font-weight: 700; + letter-spacing: 0.04em; + color: var(--text-primary); + text-decoration: none; +} +.site-header .spacer { flex: 1 1 auto; } + +/* Sidebar */ +.sidebar { + position: fixed; + inset: var(--header-height) auto 0 0; + width: var(--sidebar-width); + background: var(--bg-surface); + border-right: 1px solid var(--border); + overflow: auto; + padding: 0.75rem 0.5rem; + display: flex; + flex-direction: column; + gap: 0.25rem; + z-index: 40; +} +.sidebar h2 { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); + margin: 0.5rem 0.5rem 0.25rem; + padding-bottom: 0.25rem; + border-bottom: 1px solid var(--border); +} +.sidebar .set-item { + display: block; + padding: 0.4rem 0.6rem; + border-radius: var(--radius-sm); + color: var(--text-secondary); + text-decoration: none; + cursor: pointer; + user-select: none; +} +.sidebar .set-item:hover, +.sidebar .set-item:focus-visible { + background: var(--bg-surface-hover); + color: var(--text-primary); + outline: none; +} +.sidebar .set-item.active { + background: var(--accent-soft); + color: var(--accent); + font-weight: 600; +} + +/* Main layout */ +.page { + padding-top: var(--header-height); + padding-left: var(--sidebar-width); + min-height: 100vh; +} +.main-content { + padding: var(--gap); + max-width: 100%; +} + +/* Media grid */ +.media-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); + gap: var(--gap); +} +.media-card { + position: relative; + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius-md); + overflow: hidden; + cursor: pointer; + transition: transform var(--transition-fast), box-shadow var(--transition-fast), border-color var(--transition-fast); +} +.media-card:hover { + box-shadow: var(--shadow-md); + border-color: var(--border-strong); +} +.media-card.selected { + border-color: var(--selected-border); + background: var(--selected-bg); +} +.media-card.playing { + border-color: var(--playing-border); + background: var(--playing-bg); +} +.media-card .thumb-wrap { + position: relative; + aspect-ratio: 16 / 9; + background: var(--bg-elevated); + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} +.media-card .thumb-wrap img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} +.media-card .thumb-wrap .placeholder { + color: var(--text-muted); + font-size: 0.85rem; +} +.media-card .meta { + padding: 0.55rem 0.65rem; + display: flex; + flex-direction: column; + gap: 0.15rem; + min-height: 3.5rem; +} +.media-card .title { + font-size: 0.9rem; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.media-card .subtitle { + font-size: 0.78rem; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.media-card .badge { + position: absolute; + top: 0.35rem; + right: 0.35rem; + font-size: 0.65rem; + padding: 0.15rem 0.4rem; + border-radius: var(--radius-sm); + background: var(--bg-overlay); + color: var(--text-primary); +} + +/* Audio row variant */ +.media-row { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.5rem 0.75rem; + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius-md); + cursor: pointer; + transition: border-color var(--transition-fast), background var(--transition-fast); +} +.media-row:hover { + background: var(--bg-surface-hover); + border-color: var(--border-strong); +} +.media-row.selected { + border-color: var(--selected-border); + background: var(--selected-bg); +} +.media-row.playing { + border-color: var(--playing-border); + background: var(--playing-bg); +} +.media-row .row-icon { + color: var(--text-muted); + font-size: 1.1rem; +} +.media-row .row-body { + min-width: 0; + flex: 1 1 auto; +} +.media-row .row-title { + font-size: 0.9rem; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.media-row .row-meta { + font-size: 0.78rem; + color: var(--text-secondary); +} +.media-row .row-duration { + font-size: 0.78rem; + color: var(--text-muted); + white-space: nowrap; +} + +/* Modal */ +.modal-overlay { + position: fixed; + inset: 0; + background: var(--bg-overlay); + display: grid; + place-items: center; + z-index: 100; + opacity: 0; + visibility: hidden; + transition: opacity var(--transition-base), visibility var(--transition-base); +} +.modal-overlay.open { + opacity: 1; + visibility: visible; +} +.modal { + background: var(--bg-surface); + border: 1px solid var(--border-strong); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + width: min(40rem, 92vw); + max-height: 86vh; + overflow: auto; + padding: 1.25rem; +} +.modal-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.75rem; +} +.modal-header h3 { margin: 0; font-size: 1.1rem; } +.modal-header .spacer { flex: 1 1 auto; } + +/* Responsive */ +@media (max-width: 860px) { + .sidebar { + position: fixed; + inset: var(--header-height) auto auto 0; + width: 100%; + height: auto; + max-height: 70vh; + border-right: none; + border-bottom: 1px solid var(--border); + display: none; + } + .sidebar.open { display: flex; } + .page { padding-left: 0; } + .media-grid { grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); } +} + +@media (max-width: 520px) { + .media-grid { grid-template-columns: repeat(2, 1fr); } +} + +/* Utilities */ +.hidden { display: none !important; } +.overflow-hidden { overflow: hidden; } + diff --git a/web/css/login.css b/web/css/login.css new file mode 100644 index 0000000..a32ebd8 --- /dev/null +++ b/web/css/login.css @@ -0,0 +1,27 @@ +/* Login / bootstrap page layout */ +.login-container { + min-height: 100vh; + display: grid; + place-items: center; + padding: 2rem 1rem; + background: var(--bg-body); +} +.login-container h1 { + margin: 0 0 0.25rem; + font-size: 1.5rem; +} +.login-container .subtitle { + margin: 0 0 1rem; + color: var(--text-secondary); + font-size: 0.85rem; +} +.login-form { + width: 100%; + max-width: 22rem; + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-md); + padding: 1.25rem 1.25rem 1rem; +} +.login-form .btn { width: 100%; margin-top: 0.5rem; } diff --git a/web/css/player.css b/web/css/player.css new file mode 100644 index 0000000..e1c13b4 --- /dev/null +++ b/web/css/player.css @@ -0,0 +1,132 @@ +/* Player */ +.player { + position: fixed; + inset: auto 0 0 var(--sidebar-width); + background: var(--player-bg); + color: var(--text-primary); + border-top: 1px solid var(--border); + z-index: 60; + transition: inset var(--transition-base); +} +.player.is-fullscreen { + inset: 0; + background: #000; + display: flex; + flex-direction: column; + justify-content: center; +} + +.player .stage { + position: relative; + background: #000; +} +.player:not(.is-fullscreen) .stage { + height: 0; + overflow: hidden; + transition: height var(--transition-base); +} +.player.open .stage { + height: clamp(10rem, 28vh, 26rem); +} +.player.is-fullscreen .stage { + flex: 1 1 auto; + height: auto; + display: flex; + align-items: center; + justify-content: center; +} + +.player video, .player audio { + width: 100%; + height: 100%; + display: block; + object-fit: contain; +} +.player.is-fullscreen video, .player.is-fullscreen audio { + max-height: 100%; +} + +/* Overlay controls */ +.player .controls { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; +} +.player .controls .spacer { flex: 1 1 auto; } + +.player .time { + font-family: var(--font-mono); + font-size: 0.78rem; + color: var(--text-secondary); + white-space: nowrap; +} + +/* Progress bar */ +.progress-track { + position: relative; + flex: 1 1 auto; + height: 0.35rem; + background: var(--player-progress-bg); + border-radius: 999px; + cursor: pointer; + overflow: hidden; +} +.progress-track:hover { height: 0.5rem; } +.progress-fill { + position: absolute; + inset: 0 auto 0 0; + width: 0%; + background: var(--player-progress-fill); + border-radius: 999px; + transition: width 120ms linear; +} +.progress-thumb { + position: absolute; + top: 50%; + width: 0.9rem; + height: 0.9rem; + background: var(--player-thumb); + border-radius: 50%; + transform: translate(-50%, -50%) scale(0); + transition: transform var(--transition-fast); + pointer-events: none; +} +.progress-track:hover .progress-thumb, +.progress-track:focus-within .progress-thumb { + transform: translate(-50%, -50%) scale(1); +} + +/* Volume */ +.volume-control { + display: flex; + align-items: center; + gap: 0.25rem; +} +.volume-slider { + width: 5rem; + height: 0.25rem; + cursor: pointer; + accent-color: var(--accent); +} + +/* Big play button */ +.big-play { + position: absolute; + inset: 0; + display: grid; + place-items: center; + background: rgba(0,0,0,0.35); + cursor: pointer; + z-index: 5; +} +.big-play .play-icon { + width: 3.5rem; + height: 3.5rem; + border-radius: 50%; + background: rgba(255,255,255,0.9); + color: #000; + display: grid; + place-items: center; + font-size: 1.25rem; +} diff --git a/web/css/theme.css b/web/css/theme.css new file mode 100644 index 0000000..927b3b0 --- /dev/null +++ b/web/css/theme.css @@ -0,0 +1,105 @@ +:root { + /* Core palette */ + --bg-body: #0f1117; + --bg-surface: #161920; + --bg-surface-hover: #1b1f27; + --bg-elevated: #1e222c; + --bg-overlay: rgba(0,0,0,0.65); + + /* Text */ + --text-primary: #e6e8ef; + --text-secondary: #a3a8b8; + --text-muted: #6b7280; + --text-inverse: #0b0d12; + + /* Accent */ + --accent: #5e9eff; + --accent-hover: #7cb2ff; + --accent-active: #3d8bf7; + --accent-soft: rgba(94,158,255,0.18); + + /* Borders */ + --border: #252a36; + --border-strong: #2e3546; + + /* Feedback */ + --danger: #f25c5c; + --danger-soft: rgba(242,92,92,0.18); + --success: #3ddc84; + --success-soft: rgba(61,220,132,0.18); + --warn: #ffb300; + + /* Player */ + --player-bg: #0b0d12; + --player-progress-bg: #2e3546; + --player-progress-fill: #5e9eff; + --player-thumb: #7cb2ff; + + /* Selection */ + --selected-bg: rgba(94,158,255,0.25); + --selected-border: #5e9eff; + --playing-bg: rgba(61,220,132,0.18); + --playing-border: #3ddc84; + + /* Layout */ + --header-height: 3.25rem; + --sidebar-width: 16rem; + --gap: 0.75rem; + --radius-sm: 0.375rem; + --radius-md: 0.625rem; + --radius-lg: 0.875rem; + + /* Typography */ + --font-sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0,0,0,0.35); + --shadow-md: 0 4px 12px rgba(0,0,0,0.45); + --shadow-lg: 0 10px 28px rgba(0,0,0,0.55); + + /* Transitions */ + --transition-fast: 120ms ease; + --transition-base: 200ms ease; +} + +/* Light theme overrides */ +[data-theme="light"] { + --bg-body: #f4f5f8; + --bg-surface: #ffffff; + --bg-surface-hover: #f0f2f7; + --bg-elevated: #ffffff; + --bg-overlay: rgba(0,0,0,0.35); + + --text-primary: #12131a; + --text-secondary: #4a4f5e; + --text-muted: #8b90a0; + --text-inverse: #ffffff; + + --accent: #2b6cb0; + --accent-hover: #1a5490; + --accent-active: #17447a; + --accent-soft: rgba(43,108,176,0.14); + + --border: #d6dae4; + --border-strong: #c3c9d6; + + --danger: #c53030; + --danger-soft: rgba(197,48,48,0.14); + --success: #258855; + --success-soft: rgba(37,136,85,0.14); + + --player-bg: #0b0d12; + --player-progress-bg: #d6dae4; + --player-progress-fill: #2b6cb0; + --player-thumb: #1a5490; + + --selected-bg: rgba(43,108,176,0.18); + --selected-border: #2b6cb0; + --playing-bg: rgba(37,136,85,0.14); + --playing-border: #258855; + + --shadow-sm: 0 1px 2px rgba(0,0,0,0.08); + --shadow-md: 0 4px 12px rgba(0,0,0,0.10); + --shadow-lg: 0 10px 28px rgba(0,0,0,0.12); +} diff --git a/web/favicon.svg b/web/favicon.svg new file mode 100644 index 0000000..a77ff7a --- /dev/null +++ b/web/favicon.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" rx="20" fill="#5e9eff"/><polygon points="40,30 70,50 40,70" fill="#fff"/></svg>
\ No newline at end of file diff --git a/web/index.html b/web/index.html index 630ee64..f16c0de 100644 --- a/web/index.html +++ b/web/index.html @@ -2,9 +2,128 @@ <html lang="en"> <head> <meta charset="UTF-8"> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>KISS Media Player</title> +<link rel="manifest" href="/manifest.json"> +<link rel="stylesheet" href="/css/theme.css"> +<link rel="stylesheet" href="/css/layout.css"> +<link rel="stylesheet" href="/css/components.css"> +<link rel="stylesheet" href="/css/player.css"> +<script type="module" src="/js/app.js"></script> </head> <body> -<h1>Hello KISS</h1> +<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> + <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" style="font-size:0.85rem;"></span> + <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> + +<nav id="sidebar" class="sidebar" aria-label="Sets"> + <h2>Sets</h2> + <div id="set-list"></div> +</nav> + +<div class="page" role="main"> + <div class="main-content"> + <div class="toolbar" style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.75rem;"> + <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> + <div class="spacer"></div> + <span id="result-count" class="text-muted" style="font-size:0.8rem;"></span> + </div> + <div id="media-grid" class="media-grid" tabindex="0" aria-label="Media grid"></div> + </div> +</div> + +<aside id="player" class="player open" aria-label="Player"> + <div class="stage"> + <video id="media-video" playsinline controlslist="nodownload"></video> + <audio id="media-audio" style="display:none;"></audio> + <div id="big-play" class="big-play" role="button" aria-label="Play" tabindex="0"> + <div class="play-icon">▶</div> + </div> + </div> + <div class="controls"> + <button id="btn-play" class="icon-btn" aria-label="Play/Pause (Space/p)">▶</button> + |
