feat(player): add volume slider and wheel control for video preview

This commit is contained in:
m4xx1m committed 2026-11-12 23:25:57 +03:00
1 parent 46fcafd228
commit e142ad9326
2 files changed
+97 -4

No files matched your search

+55 -4
View File
@@ -18,6 +18,7 @@ interface State {
loopTrim: boolean; loopTrim: boolean;
previewMixAudio: boolean; previewMixAudio: boolean;
mutedPreview: boolean; mutedPreview: boolean;
previewVolume: number;
usingProxy: boolean; usingProxy: boolean;
encoder: string; encoder: string;
hwDecode: string; hwDecode: string;
@@ -53,6 +54,7 @@ const state: State = {
loopTrim: true, loopTrim: true,
previewMixAudio: false, previewMixAudio: false,
mutedPreview: false, mutedPreview: false,
previewVolume: Number(localStorage.getItem('convert_preview_volume') ?? 1),
usingProxy: false, usingProxy: false,
encoder: 'libx264', encoder: 'libx264',
hwDecode: 'none', hwDecode: 'none',
@@ -188,7 +190,11 @@ function renderShell() {
<button id="setEndBtn" disabled>${tr.setEnd} ⏭</button> <button id="setEndBtn" disabled>${tr.setEnd} ⏭</button>
<button id="loopBtn" class="pill-active">🔁 ${tr.loopTrim}</button> <button id="loopBtn" class="pill-active">🔁 ${tr.loopTrim}</button>
<button id="previewMixBtn" class="hidden">🎧 ${tr.previewMix}</button> <button id="previewMixBtn" class="hidden">🎧 ${tr.previewMix}</button>
<button id="mutePreviewBtn">🔊</button> <div class="volume-box">
<button id="mutePreviewBtn" title="Звук">🔊</button>
<input type="range" id="volumeRange" min="0" max="1" step="0.05" value="1" title="Громкость" />
<span id="volumeVal" class="mono volume-val">100%</span>
</div>
</div> </div>
</div> </div>
@@ -567,6 +573,37 @@ function updatePlayhead(currentTime: number) {
el<'span'>('timeReadout').textContent = `${formatTimecode(currentTime, true)} / ${formatTimecode(dur, true)}`; 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 { function buildRequest(): ConvertRequest {
return { return {
inputPath: state.loaded?.info.path ?? '', inputPath: state.loaded?.info.path ?? '',
@@ -679,7 +716,7 @@ function applyLoadedUI(
const mixBtn = el<'button'>('previewMixBtn'); const mixBtn = el<'button'>('previewMixBtn');
mixBtn.classList.toggle('hidden', res.info.audioStreams.length < 2); mixBtn.classList.toggle('hidden', res.info.audioStreams.length < 2);
mixBtn.classList.toggle('pill-active', state.previewMixAudio); mixBtn.classList.toggle('pill-active', state.previewMixAudio);
el<'button'>('mutePreviewBtn').textContent = state.mutedPreview ? '🔇' : '🔊'; updateVolumeUI();
el<'pre'>('cliBox').classList.toggle('hidden', !state.cliExpanded); el<'pre'>('cliBox').classList.toggle('hidden', !state.cliExpanded);
el<'button'>('copyCliBtn').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)); const rate = Math.max(0.25, Math.min(8, state.speed));
video.muted = state.mutedPreview; video.muted = state.mutedPreview;
video.volume = Math.max(0, Math.min(1, state.previewVolume));
video.defaultPlaybackRate = rate; video.defaultPlaybackRate = rate;
video.playbackRate = rate; video.playbackRate = rate;
@@ -740,6 +778,7 @@ function setPlayerSource(
video.defaultPlaybackRate = r; video.defaultPlaybackRate = r;
video.playbackRate = r; video.playbackRate = r;
video.muted = state.mutedPreview; video.muted = state.mutedPreview;
video.volume = Math.max(0, Math.min(1, state.previewVolume));
fitVideoStage(); fitVideoStage();
if (seekSec > 0 && Number.isFinite(seekSec)) { if (seekSec > 0 && Number.isFinite(seekSec)) {
video.currentTime = Math.min(seekSec, res.info.duration || seekSec); video.currentTime = Math.min(seekSec, res.info.duration || seekSec);
@@ -1058,8 +1097,20 @@ function bindEvents() {
}); });
el<'button'>('mutePreviewBtn').addEventListener('click', () => { el<'button'>('mutePreviewBtn').addEventListener('click', () => {
state.mutedPreview = !state.mutedPreview; state.mutedPreview = !state.mutedPreview;
video.muted = state.mutedPreview; if (!state.mutedPreview && state.previewVolume === 0) {
el<'button'>('mutePreviewBtn').textContent = state.mutedPreview ? '🔇' : '🔊'; 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) // Timeline drag (handles + seek)
+42
View File
@@ -457,6 +457,48 @@ video {
border: 1px solid var(--border); 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 & Trim */
.timeline-card { .timeline-card {
background: var(--panel); background: var(--panel);