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);