6 Commits
5 changed files with 166 additions and 8 deletions

No files matched your search

+9 -2
View File
@@ -2,8 +2,6 @@ name: Build Windows Installer
on:
push:
branches:
- main
tags:
- 'v*'
workflow_dispatch:
@@ -74,8 +72,17 @@ jobs:
-ldflags="-H windowsgui -s -w -X main.Version=${{ steps.ver.outputs.version }}" \
-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
if: steps.cache-ffmpeg.outputs.cache-hit != 'true'
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"
unzip -j -q ffmpeg.zip "*/bin/ffmpeg.exe" "*/bin/ffprobe.exe" -d dist/bin/
rm -f ffmpeg.zip
+82 -4
View File
@@ -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() {
<button id="setEndBtn" disabled>${tr.setEnd} ⏭</button>
<button id="loopBtn" class="pill-active">🔁 ${tr.loopTrim}</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>
@@ -414,6 +420,26 @@ function crfQualityHint(crf: number): string {
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() {
el<'select'>('encoderSelect').value = state.encoder;
el<'select'>('hwDecodeSelect').value = state.hwDecode;
@@ -567,6 +593,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 +736,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 +784,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 +798,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 +1117,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)
@@ -1117,26 +1188,32 @@ function bindEvents() {
el<'select'>('encoderSelect').addEventListener('change', (e) => {
state.encoder = (e.target as HTMLSelectElement).value;
syncControlsFromState();
persistVideoSettings();
});
el<'select'>('hwDecodeSelect').addEventListener('change', (e) => {
state.hwDecode = (e.target as HTMLSelectElement).value;
persistVideoSettings();
void refreshCLI();
});
el<'input'>('crfRange').addEventListener('input', (e) => {
state.crf = Number((e.target as HTMLInputElement).value);
el<'span'>('crfLabel').textContent = `${state.crf} · ${crfQualityHint(state.crf)}`;
persistVideoSettings();
void refreshCLI();
});
el<'select'>('presetSelect').addEventListener('change', (e) => {
state.preset = (e.target as HTMLSelectElement).value;
persistVideoSettings();
void refreshCLI();
});
el<'select'>('tuneSelect').addEventListener('change', (e) => {
state.tune = (e.target as HTMLSelectElement).value;
persistVideoSettings();
void refreshCLI();
});
el<'select'>('fpsSelect').addEventListener('change', (e) => {
state.fps = Number((e.target as HTMLSelectElement).value);
persistVideoSettings();
void refreshCLI();
});
@@ -1155,6 +1232,7 @@ function bindEvents() {
b.addEventListener('click', () => {
state.audioMode = b.dataset.amode as 'merge' | 'track' | 'mute';
syncControlsFromState();
persistVideoSettings();
});
});
+42
View File
@@ -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);
+32 -1
View File
@@ -2,7 +2,7 @@ Unicode True
!include "MUI2.nsh"
!ifndef VERSION
!define VERSION "2.1.0"
!define VERSION "2.1.2"
!endif
!define APP_NAME "ConvertGUI"
@@ -23,6 +23,7 @@ SetOverwrite on
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_COMPONENTS
!define MUI_PAGE_CUSTOMFUNCTION_LEAVE DirectoryLeave
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!define MUI_FINISHPAGE_RUN "$INSTDIR\${EXE_NAME}"
@@ -107,6 +108,36 @@ LangString DESC_DESKTOP ${LANG_ENGLISH} "Create a shortcut on the Desktop."
LangString DESC_DESKTOP ${LANG_RUSSIAN} "Создать ярлык на Рабочем столе."
LangString MSG_RUNNING ${LANG_ENGLISH} "${APP_NAME} is currently running.$\r$\n$\r$\nClick OK to close it and proceed with the update, or Cancel to abort."
LangString MSG_RUNNING ${LANG_RUSSIAN} "Приложение ${APP_NAME} сейчас запущено.$\r$\n$\r$\nНажмите ОК, чтобы закрыть его и продолжить обновление, или Отмена для выхода."
LangString MSG_ADMIN_REQ ${LANG_ENGLISH} "Administrator privileges are required to install into:$\r$\n$INSTDIR$\r$\n$\r$\nRestart installer with administrator privileges?"
LangString MSG_ADMIN_REQ ${LANG_RUSSIAN} "Для установки в выбранную папку требуются права администратора:$\r$\n$INSTDIR$\r$\n$\r$\nПерезапустить установку от имени администратора?"
Function DirectoryLeave
ClearErrors
UserInfo::GetAccountType
Pop $0
StrCmp $0 "Admin" can_write 0
CreateDirectory "$INSTDIR"
FileOpen $0 "$INSTDIR\.write_test" w
IfErrors need_admin
FileClose $0
Delete "$INSTDIR\.write_test"
Goto can_write
need_admin:
ClearErrors
MessageBox MB_YESNO|MB_ICONQUESTION $(MSG_ADMIN_REQ) IDYES do_elevate IDNO stay_here
do_elevate:
ExecShell "runas" "$EXEPATH" '/D=$INSTDIR'
Quit
stay_here:
Abort
can_write:
FunctionEnd
Function .onInit
check_running:
+1 -1
View File
@@ -12,7 +12,7 @@ import (
)
// Version is injected at build time via -ldflags "-X main.Version=..."
var Version = "2.1.0-dev"
var Version = "2.1.2-dev"
//go:embed all:frontend/dist
var assets embed.FS