Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab1f673689 | ||
|
|
4b42e1ee11 | ||
|
|
e142ad9326 | ||
|
|
46fcafd228 |
No files matched your search
@@ -2,8 +2,6 @@ name: Build Windows Installer
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -74,8 +72,17 @@ jobs:
|
|||||||
-ldflags="-H windowsgui -s -w -X main.Version=${{ steps.ver.outputs.version }}" \
|
-ldflags="-H windowsgui -s -w -X main.Version=${{ steps.ver.outputs.version }}" \
|
||||||
-o dist/convert.exe .
|
-o dist/convert.exe .
|
||||||
|
|
||||||
|
- name: Cache static FFmpeg binaries
|
||||||
|
id: cache-ffmpeg
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: dist/bin
|
||||||
|
key: ffmpeg-win64-master-static
|
||||||
|
|
||||||
- name: Download static FFmpeg & FFprobe for Windows
|
- name: Download static FFmpeg & FFprobe for Windows
|
||||||
|
if: steps.cache-ffmpeg.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
|
mkdir -p dist/bin
|
||||||
wget -q -O ffmpeg.zip "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip"
|
wget -q -O ffmpeg.zip "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip"
|
||||||
unzip -j -q ffmpeg.zip "*/bin/ffmpeg.exe" "*/bin/ffprobe.exe" -d dist/bin/
|
unzip -j -q ffmpeg.zip "*/bin/ffmpeg.exe" "*/bin/ffprobe.exe" -d dist/bin/
|
||||||
rm -f ffmpeg.zip
|
rm -f ffmpeg.zip
|
||||||
|
|||||||
+82
-4
@@ -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>
|
||||||
|
|
||||||
@@ -414,6 +420,26 @@ function crfQualityHint(crf: number): string {
|
|||||||
return tr.crfHintSmall;
|
return tr.crfHintSmall;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let saveCfgTimer = 0;
|
||||||
|
function persistVideoSettings() {
|
||||||
|
const api = appApi();
|
||||||
|
if (!api || !state.appInfo) return;
|
||||||
|
state.appInfo.config.defaultEncoder = state.encoder;
|
||||||
|
state.appInfo.config.defaultHwDecode = state.hwDecode;
|
||||||
|
state.appInfo.config.defaultPreset = state.preset;
|
||||||
|
state.appInfo.config.defaultCrf = state.crf;
|
||||||
|
state.appInfo.config.defaultFps = state.fps;
|
||||||
|
state.appInfo.config.defaultTune = state.tune;
|
||||||
|
state.appInfo.config.defaultMergeAudio = state.audioMode === 'merge';
|
||||||
|
|
||||||
|
window.clearTimeout(saveCfgTimer);
|
||||||
|
saveCfgTimer = window.setTimeout(() => {
|
||||||
|
if (state.appInfo) {
|
||||||
|
void api.SaveConfig(state.appInfo.config);
|
||||||
|
}
|
||||||
|
}, 350);
|
||||||
|
}
|
||||||
|
|
||||||
function syncControlsFromState() {
|
function syncControlsFromState() {
|
||||||
el<'select'>('encoderSelect').value = state.encoder;
|
el<'select'>('encoderSelect').value = state.encoder;
|
||||||
el<'select'>('hwDecodeSelect').value = state.hwDecode;
|
el<'select'>('hwDecodeSelect').value = state.hwDecode;
|
||||||
@@ -567,6 +593,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 +736,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 +784,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 +798,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 +1117,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)
|
||||||
@@ -1117,26 +1188,32 @@ function bindEvents() {
|
|||||||
el<'select'>('encoderSelect').addEventListener('change', (e) => {
|
el<'select'>('encoderSelect').addEventListener('change', (e) => {
|
||||||
state.encoder = (e.target as HTMLSelectElement).value;
|
state.encoder = (e.target as HTMLSelectElement).value;
|
||||||
syncControlsFromState();
|
syncControlsFromState();
|
||||||
|
persistVideoSettings();
|
||||||
});
|
});
|
||||||
el<'select'>('hwDecodeSelect').addEventListener('change', (e) => {
|
el<'select'>('hwDecodeSelect').addEventListener('change', (e) => {
|
||||||
state.hwDecode = (e.target as HTMLSelectElement).value;
|
state.hwDecode = (e.target as HTMLSelectElement).value;
|
||||||
|
persistVideoSettings();
|
||||||
void refreshCLI();
|
void refreshCLI();
|
||||||
});
|
});
|
||||||
el<'input'>('crfRange').addEventListener('input', (e) => {
|
el<'input'>('crfRange').addEventListener('input', (e) => {
|
||||||
state.crf = Number((e.target as HTMLInputElement).value);
|
state.crf = Number((e.target as HTMLInputElement).value);
|
||||||
el<'span'>('crfLabel').textContent = `${state.crf} · ${crfQualityHint(state.crf)}`;
|
el<'span'>('crfLabel').textContent = `${state.crf} · ${crfQualityHint(state.crf)}`;
|
||||||
|
persistVideoSettings();
|
||||||
void refreshCLI();
|
void refreshCLI();
|
||||||
});
|
});
|
||||||
el<'select'>('presetSelect').addEventListener('change', (e) => {
|
el<'select'>('presetSelect').addEventListener('change', (e) => {
|
||||||
state.preset = (e.target as HTMLSelectElement).value;
|
state.preset = (e.target as HTMLSelectElement).value;
|
||||||
|
persistVideoSettings();
|
||||||
void refreshCLI();
|
void refreshCLI();
|
||||||
});
|
});
|
||||||
el<'select'>('tuneSelect').addEventListener('change', (e) => {
|
el<'select'>('tuneSelect').addEventListener('change', (e) => {
|
||||||
state.tune = (e.target as HTMLSelectElement).value;
|
state.tune = (e.target as HTMLSelectElement).value;
|
||||||
|
persistVideoSettings();
|
||||||
void refreshCLI();
|
void refreshCLI();
|
||||||
});
|
});
|
||||||
el<'select'>('fpsSelect').addEventListener('change', (e) => {
|
el<'select'>('fpsSelect').addEventListener('change', (e) => {
|
||||||
state.fps = Number((e.target as HTMLSelectElement).value);
|
state.fps = Number((e.target as HTMLSelectElement).value);
|
||||||
|
persistVideoSettings();
|
||||||
void refreshCLI();
|
void refreshCLI();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1155,6 +1232,7 @@ function bindEvents() {
|
|||||||
b.addEventListener('click', () => {
|
b.addEventListener('click', () => {
|
||||||
state.audioMode = b.dataset.amode as 'merge' | 'track' | 'mute';
|
state.audioMode = b.dataset.amode as 'merge' | 'track' | 'mute';
|
||||||
syncControlsFromState();
|
syncControlsFromState();
|
||||||
|
persistVideoSettings();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ Unicode True
|
|||||||
!include "MUI2.nsh"
|
!include "MUI2.nsh"
|
||||||
|
|
||||||
!ifndef VERSION
|
!ifndef VERSION
|
||||||
!define VERSION "2.1.0"
|
!define VERSION "2.1.1"
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
!define APP_NAME "ConvertGUI"
|
!define APP_NAME "ConvertGUI"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Version is injected at build time via -ldflags "-X main.Version=..."
|
// Version is injected at build time via -ldflags "-X main.Version=..."
|
||||||
var Version = "2.1.0-dev"
|
var Version = "2.1.1-dev"
|
||||||
|
|
||||||
//go:embed all:frontend/dist
|
//go:embed all:frontend/dist
|
||||||
var assets embed.FS
|
var assets embed.FS
|
||||||
|
|||||||
Reference in new issue
Block a user