From e142ad9326e6633c5ea09cba7eb6a168fa5da18e Mon Sep 17 00:00:00 2001 From: m4xx1m Date: Thu, 12 Nov 2026 23:25:57 +0300 Subject: [PATCH] feat(player): add volume slider and wheel control for video preview --- frontend/src/main.ts | 59 +++++++++++++++++++++++++++++++++++++++--- frontend/src/style.css | 42 ++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 4 deletions(-) diff --git a/frontend/src/main.ts b/frontend/src/main.ts index e4a4b97..77a0f22 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -18,6 +18,7 @@ interface State { loopTrim: boolean; previewMixAudio: boolean; mutedPreview: boolean; + previewVolume: number; usingProxy: boolean; encoder: string; hwDecode: string; @@ -53,6 +54,7 @@ const state: State = { loopTrim: true, previewMixAudio: false, mutedPreview: false, + previewVolume: Number(localStorage.getItem('convert_preview_volume') ?? 1), usingProxy: false, encoder: 'libx264', hwDecode: 'none', @@ -188,7 +190,11 @@ function renderShell() { - +
+ + + 100% +
@@ -567,6 +573,37 @@ function updatePlayhead(currentTime: number) { el<'span'>('timeReadout').textContent = `${formatTimecode(currentTime, true)} / ${formatTimecode(dur, true)}`; } +function updateVolumeUI() { + const video = el<'video'>('videoEl'); + const btn = el<'button'>('mutePreviewBtn'); + const rng = el<'input'>('volumeRange'); + const val = el<'span'>('volumeVal'); + if (!btn || !rng || !val) return; + + const vol = Math.max(0, Math.min(1, state.previewVolume)); + if (video) { + video.volume = vol; + video.muted = state.mutedPreview; + } + + rng.value = String(vol); + val.textContent = state.mutedPreview ? 'Mute' : `${Math.round(vol * 100)}%`; + + if (state.mutedPreview || vol === 0) { + btn.textContent = '๐Ÿ”‡'; + btn.title = 'ะ’ะบะปัŽั‡ะธั‚ัŒ ะทะฒัƒะบ'; + } else if (vol < 0.4) { + btn.textContent = '๐Ÿ”ˆ'; + btn.title = 'ะ’ั‹ะบะปัŽั‡ะธั‚ัŒ ะทะฒัƒะบ'; + } else if (vol < 0.8) { + btn.textContent = '๐Ÿ”‰'; + btn.title = 'ะ’ั‹ะบะปัŽั‡ะธั‚ัŒ ะทะฒัƒะบ'; + } else { + btn.textContent = '๐Ÿ”Š'; + btn.title = 'ะ’ั‹ะบะปัŽั‡ะธั‚ัŒ ะทะฒัƒะบ'; + } +} + function buildRequest(): ConvertRequest { return { inputPath: state.loaded?.info.path ?? '', @@ -679,7 +716,7 @@ function applyLoadedUI( const mixBtn = el<'button'>('previewMixBtn'); mixBtn.classList.toggle('hidden', res.info.audioStreams.length < 2); mixBtn.classList.toggle('pill-active', state.previewMixAudio); - el<'button'>('mutePreviewBtn').textContent = state.mutedPreview ? '๐Ÿ”‡' : '๐Ÿ”Š'; + updateVolumeUI(); el<'pre'>('cliBox').classList.toggle('hidden', !state.cliExpanded); el<'button'>('copyCliBtn').classList.toggle('hidden', !state.cliExpanded); @@ -727,6 +764,7 @@ function setPlayerSource( const rate = Math.max(0.25, Math.min(8, state.speed)); video.muted = state.mutedPreview; + video.volume = Math.max(0, Math.min(1, state.previewVolume)); video.defaultPlaybackRate = rate; video.playbackRate = rate; @@ -740,6 +778,7 @@ function setPlayerSource( video.defaultPlaybackRate = r; video.playbackRate = r; video.muted = state.mutedPreview; + video.volume = Math.max(0, Math.min(1, state.previewVolume)); fitVideoStage(); if (seekSec > 0 && Number.isFinite(seekSec)) { video.currentTime = Math.min(seekSec, res.info.duration || seekSec); @@ -1058,8 +1097,20 @@ function bindEvents() { }); el<'button'>('mutePreviewBtn').addEventListener('click', () => { state.mutedPreview = !state.mutedPreview; - video.muted = state.mutedPreview; - el<'button'>('mutePreviewBtn').textContent = state.mutedPreview ? '๐Ÿ”‡' : '๐Ÿ”Š'; + if (!state.mutedPreview && state.previewVolume === 0) { + state.previewVolume = 0.5; + localStorage.setItem('convert_preview_volume', '0.5'); + } + updateVolumeUI(); + }); + el<'input'>('volumeRange').addEventListener('input', (e) => { + const v = Number((e.target as HTMLInputElement).value); + state.previewVolume = v; + if (v > 0) { + state.mutedPreview = false; + } + localStorage.setItem('convert_preview_volume', String(v)); + updateVolumeUI(); }); // Timeline drag (handles + seek) diff --git a/frontend/src/style.css b/frontend/src/style.css index a29150c..2b72aa9 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -457,6 +457,48 @@ video { border: 1px solid var(--border); } +.volume-box { + display: inline-flex; + align-items: center; + gap: 5px; + background: #181818; + border: 1px solid var(--border); + padding: 2px 6px; + height: 25px; +} + +.volume-box button { + padding: 0; + width: 20px; + height: 100%; + border: none; + background: transparent; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + cursor: pointer; +} + +.volume-box button:hover { + background: rgba(255, 255, 255, 0.08); +} + +.volume-box input[type='range'] { + width: 65px; + height: 12px; + cursor: pointer; + accent-color: var(--accent); +} + +.volume-val { + font-size: 10px; + color: var(--muted); + min-width: 28px; + text-align: right; + user-select: none; +} + /* Timeline & Trim */ .timeline-card { background: var(--panel);