feat: FFmpeg GUI desktop application

Wails v2 + Vue3 + Go project with:
- Encode/Remux/Subtitle burn with hardware acceleration
- Real-time progress with ffmpeg stderr parsing (\r delimiter handling)
- Task queue with cancel support
- Per-task log viewer with color-coded output
- Custom frameless window with resize support
- Dark/light theme toggle
- Hardware encoder detection (NVENC/QSV/AMF)
This commit is contained in:
sansen
2026-07-29 02:17:57 +08:00
parent 51b768a2ba
commit 79f5fb8ac3
36 changed files with 5216 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
<template>
<div class="app" :data-theme="theme">
<Header
:currentView="currentView"
:theme="theme"
@navigate="navigate"
@toggleTheme="toggleTheme"
/>
<div class="main-area">
<Sidebar :currentView="currentView" @navigate="navigate" />
<div class="workspace-column">
<main class="workspace">
<EncodePage v-if="currentView === 'encode'" @taskAdded="onTaskAdded" />
<RemuxPage v-if="currentView === 'remux'" @taskAdded="onTaskAdded" />
<BurnPage v-if="currentView === 'burn'" @taskAdded="onTaskAdded" />
<SettingsPage v-if="currentView === 'settings'" />
<LogPage v-show="currentView === 'logs'" ref="logPageRef" />
</main>
<TaskDrawer :tasks="tasks" @cancel="handleCancel" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import Header from './components/Header.vue'
import Sidebar from './components/Sidebar.vue'
import TaskDrawer from './components/TaskDrawer.vue'
import EncodePage from './views/EncodePage.vue'
import RemuxPage from './views/RemuxPage.vue'
import BurnPage from './views/BurnPage.vue'
import SettingsPage from './views/SettingsPage.vue'
import LogPage from './views/LogPage.vue'
import { api, onTaskUpdated, onTaskProgress, onTaskLog } from './api/wails'
import type { Task, Progress } from './types'
const currentView = ref('encode')
const theme = ref<'dark'|'light'>('light')
const tasks = ref<Task[]>([])
const logPageRef = ref<InstanceType<typeof LogPage> | null>(null)
onMounted(() => {
document.documentElement.setAttribute('data-theme', theme.value)
loadTasks()
onTaskUpdated((task: Task) => {
const found = tasks.value.find(t => t.id === task.id)
if (found) {
Object.assign(found, task)
} else {
tasks.value.push(task)
}
logPageRef.value?.upsertTask({
id: task.id,
inputFile: task.inputFile,
type: task.type,
status: task.status,
args: task.args,
logs: task.logs,
})
})
onTaskProgress((data: { taskId: string; progress: Progress }) => {
const found = tasks.value.find(t => t.id === data.taskId)
if (found) {
Object.assign(found.progress, data.progress)
}
})
onTaskLog((data: { taskId: string; line: string }) => {
logPageRef.value?.appendLog(data.taskId, data.line)
})
})
function navigate(view: string) {
currentView.value = view
}
function toggleTheme() {
theme.value = theme.value === 'dark' ? 'light' : 'dark'
document.documentElement.setAttribute('data-theme', theme.value)
}
async function loadTasks() {
try { tasks.value = await api.getTasks() } catch { /* not connected */ }
}
function onTaskAdded() { loadTasks() }
async function handleCancel(id: string) {
try { await api.cancelTask(id); await loadTasks() } catch {}
}
</script>
<style scoped>
.app {
height: 100vh;
display: flex;
flex-direction: column;
}
.main-area {
flex: 1;
display: flex;
overflow: hidden;
}
.workspace-column {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.workspace {
flex: 1;
overflow-y: auto;
padding: 28px 36px;
}
</style>