summaryrefslogtreecommitdiff
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/index.html8
-rw-r--r--web/js/app.js18
-rw-r--r--web/js/keyboard.js8
-rw-r--r--web/js/player.js38
-rw-r--r--web/js/state.js2
-rw-r--r--web/share.html143
6 files changed, 143 insertions, 74 deletions
diff --git a/web/index.html b/web/index.html
index 626ee25..3ac9db5 100644
--- a/web/index.html
+++ b/web/index.html
@@ -46,10 +46,8 @@
</div>
<div id="filter-advanced" class="toolbar hidden">
<input id="filter-tags" type="text" placeholder="Tags (comma)" aria-label="Filter by tags" class="input-sm">
- <input id="filter-min-duration" type="number" min="0" placeholder="Min s" aria-label="Min duration seconds" class="input-sm">
- <input id="filter-max-duration" type="number" min="0" placeholder="Max s" aria-label="Max duration seconds" class="input-sm">
- <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">
+ <input id="filter-min-duration" type="number" min="0" placeholder="Min min" aria-label="Min duration minutes" class="input-sm">
+ <input id="filter-max-duration" type="number" min="0" placeholder="Max min" aria-label="Max duration minutes" class="input-sm">
</div>
<!-- Search bar: hidden by default, shown on '/' key -->
@@ -126,6 +124,8 @@
<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>Focus min duration filter</td></tr>
+ <tr><td><kbd>.</kbd></td><td>Focus max duration filter</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>
diff --git a/web/js/app.js b/web/js/app.js
index cd5ded4..2d0e15d 100644
--- a/web/js/app.js
+++ b/web/js/app.js
@@ -112,6 +112,8 @@ async function initApp() {
toolbar: toggleToolbar,
sidebar: toggleSidebar,
upload: () => showUpload(),
+ focusMinDuration: () => focusFilter('filter-min-duration'),
+ focusMaxDuration: () => focusFilter('filter-max-duration'),
});
initNotes(() => toast('Note saved'));
initAdmin();
@@ -129,8 +131,6 @@ async function initApp() {
document.getElementById('filter-tags')?.addEventListener('change', (e) => { state.filters.tags = e.target.value; loadMedia(); });
document.getElementById('filter-min-duration')?.addEventListener('change', (e) => { state.filters.minDuration = e.target.value; loadMedia(); });
document.getElementById('filter-max-duration')?.addEventListener('change', (e) => { state.filters.maxDuration = e.target.value; loadMedia(); });
- document.getElementById('filter-min-filesize')?.addEventListener('change', (e) => { state.filters.minFilesizeMB = e.target.value; loadMedia(); });
- document.getElementById('filter-max-filesize')?.addEventListener('change', (e) => { state.filters.maxFilesizeMB = e.target.value; loadMedia(); });
document.getElementById('filter-toggle')?.addEventListener('click', () => {
document.getElementById('filter-advanced')?.classList.toggle('hidden');
});
@@ -265,10 +265,8 @@ async function loadMedia() {
search: state.filters.search,
favorites: state.filters.favorites ? 'true' : '',
tags: state.filters.tags || '',
- min_duration: state.filters.minDuration || '',
- max_duration: state.filters.maxDuration || '',
- filesize_min: state.filters.minFilesizeMB ? String(parseInt(state.filters.minFilesizeMB, 10) << 20) : '',
- filesize_max: state.filters.maxFilesizeMB ? String(parseInt(state.filters.maxFilesizeMB, 10) << 20) : '',
+ min_duration: state.filters.minDuration ? String(parseFloat(state.filters.minDuration) * 60) : '',
+ max_duration: state.filters.maxDuration ? String(parseFloat(state.filters.maxDuration) * 60) : '',
sort: isShuffle() ? 'random' : 'name',
limit: '200',
};
@@ -539,6 +537,14 @@ function toggleSidebar() {
page?.classList.toggle('has-sidebar', open);
}
+function focusFilter(id) {
+ const el = document.getElementById(id);
+ if (!el) return;
+ document.getElementById('filter-advanced')?.classList.remove('hidden');
+ el.focus();
+ el.select();
+}
+
function showSearch() {
const bar = document.getElementById('search-bar');
bar?.classList.remove('hidden');
diff --git a/web/js/keyboard.js b/web/js/keyboard.js
index cad695c..2a9c78e 100644
--- a/web/js/keyboard.js
+++ b/web/js/keyboard.js
@@ -91,6 +91,14 @@ export function initKeyboard(handlers) {
e.preventDefault();
handlers.help?.(e);
break;
+ case ',':
+ e.preventDefault();
+ handlers.focusMinDuration?.(e);
+ break;
+ case '.':
+ e.preventDefault();
+ handlers.focusMaxDuration?.(e);
+ break;
}
});
}
diff --git a/web/js/player.js b/web/js/player.js
index 88ef404..cb3ccab 100644
--- a/web/js/player.js
+++ b/web/js/player.js
@@ -139,6 +139,44 @@ export function selectAndPlay(media, index, resumeFrom = 0) {
highlightPlayingCard();
}
+export function loadMediaDirect(media, streamUrl, thumbnailUrl, resumeFrom = 0) {
+ const e = els();
+ const isVideo = media.type === 'video';
+ const src = streamUrl;
+ 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.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';
+ e.audio.src = src;
+ e.audio.currentTime = resumeFrom;
+ if (e.coverArt) {
+ if (thumbnailUrl) {
+ e.coverArt.src = thumbnailUrl;
+ e.coverArt.classList.remove('hidden');
+ } else {
+ e.coverArt.classList.add('hidden');
+ e.coverArt.src = '';
+ }
+ }
+ }
+ e.player?.classList.add('open');
+ e.btnPlay.textContent = '⏸';
+ e.bigPlay?.classList.add('hidden');
+ e.timeTotal.textContent = fmt(media.duration ?? 0);
+ e.fill.style.width = '0%';
+ e.thumb.style.left = '0%';
+}
+
function loadMedia(media, resumeFrom = 0) {
const e = els();
const isVideo = media.type === 'video';
diff --git a/web/js/state.js b/web/js/state.js
index a0e715b..6597c00 100644
--- a/web/js/state.js
+++ b/web/js/state.js
@@ -4,7 +4,7 @@ export const state = {
selectedSetId: null,
selectedSetIds: [], // multi-selection
media: [],
- filters: { type: '', search: '', favorites: false, tags: '', sort: 'name', minDuration: '', maxDuration: '', minFilesizeMB: '', maxFilesizeMB: '' },
+ filters: { type: '', search: '', favorites: false, tags: '', sort: 'name', minDuration: '', maxDuration: '' },
isAdmin: false,
};
diff --git a/web/share.html b/web/share.html
index 065e839..8ca053a 100644
--- a/web/share.html
+++ b/web/share.html
@@ -5,17 +5,23 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shared Media</title>
<link rel="stylesheet" href="/css/theme.css">
+<link rel="stylesheet" href="/css/player.css">
<style>
body {
margin: 0;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
min-height: 100vh;
background: var(--bg-body);
color: var(--text-primary);
font-family: var(--font-sans);
+ display: flex;
+ flex-direction: column;
+ }
+ .page-wrap {
+ flex: 1 1 auto;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
padding: 1rem;
box-sizing: border-box;
}
@@ -29,77 +35,88 @@
font-size: 1.25rem;
color: var(--text-primary);
}
- .player-wrapper {
- position: relative;
- background: var(--player-bg);
- border-radius: var(--radius-md);
- overflow: hidden;
- box-shadow: var(--shadow-md);
- }
- video, audio {
- width: 100%;
- display: block;
- }
- .play-overlay {
- position: absolute;
- inset: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- background: var(--bg-overlay);
- cursor: pointer;
- transition: opacity var(--transition-base);
- }
- .play-overlay.hidden {
- opacity: 0;
- pointer-events: none;
- }
- .play-icon {
- font-size: 3rem;
- color: var(--text-primary);
- background: var(--accent-soft);
- border-radius: 50%;
- width: 4rem;
- height: 4rem;
- display: flex;
- align-items: center;
- justify-content: center;
- line-height: 1;
- }
.back-link {
margin-top: 1.5rem;
color: var(--accent);
text-decoration: none;
font-size: 0.9rem;
}
- .back-link:hover {
- color: var(--accent-hover);
+ .back-link:hover { color: var(--accent-hover); }
+
+ /* Player overrides for standalone share page */
+ .player {
+ display: block;
+ position: static;
+ border-top: none;
+ border-radius: var(--radius-md);
+ overflow: hidden;
+ box-shadow: var(--shadow-md);
+ }
+ .player .stage {
+ height: clamp(14rem, 35vh, 28rem);
+ background: var(--player-stage-bg);
+ }
+ .player.is-fullscreen {
+ position: fixed;
+ inset: 0;
+ z-index: 100;
+ border-radius: 0;
}
</style>
</head>
<body>
-<div class="container">
- <h1>Shared Media</h1>
- <div class="player-wrapper">
- <video id="player" playsinline preload="metadata" src="./stream"></video>
- <div id="overlay" class="play-overlay" role="button" aria-label="Play">
- <div class="play-icon">▶</div>
- </div>
+<div class="page-wrap">
+ <div class="container">
+ <h1>Shared Media</h1>
+ <aside id="player" class="player open" aria-label="Player">
+ <div class="stage">
+ <video id="media-video" playsinline controlslist="nodownload"></video>
+ <audio id="media-audio" class="hidden"></audio>
+ <img id="cover-art" class="cover-art hidden" alt="Cover" loading="lazy">
+ <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-toggle-stage" class="icon-btn" aria-label="Toggle stage">⛶</button>
+ <button id="btn-play" class="icon-btn" aria-label="Play/Pause">▶</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>
+ <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">⛶</button>
+ </div>
+ </aside>
+ <a class="back-link" href="/">Back to Home</a>
</div>
- <a class="back-link" href="/">Back to Home</a>
</div>
-<script>
- const player = document.getElementById('player');
- const overlay = document.getElementById('overlay');
- overlay.addEventListener('click', () => {
- player.play();
- });
- player.addEventListener('play', () => {
- overlay.classList.add('hidden');
- });
- player.addEventListener('pause', () => {
- overlay.classList.remove('hidden');
- });
+
+<script type="module">
+ import { initPlayer, loadMediaDirect } from '/js/player.js';
+
+ initPlayer();
+
+ // Parse injected metadata
+ const metaEl = document.getElementById('share-meta');
+ let data = null;
+ try {
+ data = JSON.parse(metaEl.textContent);
+ } catch {}
+
+ if (data && data.media) {
+ loadMediaDirect(data.media, data.stream_url, data.thumb_url, 0);
+ // Auto-play on first click via the big play overlay handled by player.js,
+ // but start paused so the controls are visible. The overlay click already
+ // wires to togglePlay.
+ }
</script>
+
+<script id="share-meta" type="application/json"><!--SHARE_MEDIA--></script>
</body>
</html>