2026-07-29 02:17:57 +08:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"ffmpeg-gui/internal/ffmpeg"
|
2026-07-29 10:46:19 +08:00
|
|
|
"ffmpeg-gui/internal/gpu"
|
2026-07-29 02:17:57 +08:00
|
|
|
"ffmpeg-gui/internal/hwaccel"
|
|
|
|
|
"ffmpeg-gui/internal/media"
|
|
|
|
|
"ffmpeg-gui/internal/platform"
|
|
|
|
|
"ffmpeg-gui/internal/task"
|
|
|
|
|
"fmt"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// App is the main application struct. Its exported methods are bound to the frontend.
|
|
|
|
|
type App struct {
|
|
|
|
|
ctx context.Context
|
|
|
|
|
exec *ffmpeg.Executor
|
|
|
|
|
taskMgr *task.Manager
|
|
|
|
|
hwDetect *hwaccel.Detector
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewApp creates a new App instance.
|
|
|
|
|
func NewApp() *App {
|
|
|
|
|
return &App{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// startup is called when the app starts.
|
|
|
|
|
func (a *App) startup(ctx context.Context) {
|
|
|
|
|
a.ctx = ctx
|
|
|
|
|
|
|
|
|
|
// Detect ffmpeg/ffprobe binaries
|
|
|
|
|
bins, err := ffmpeg.Detect()
|
|
|
|
|
if err != nil {
|
|
|
|
|
runtime.LogError(ctx, fmt.Sprintf("ffmpeg detect failed: %v", err))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
runtime.LogInfo(ctx, fmt.Sprintf("ffmpeg: %s, ffprobe: %s", bins.FFmpeg, bins.FFprobe))
|
|
|
|
|
|
|
|
|
|
a.exec = ffmpeg.NewExecutor(bins)
|
|
|
|
|
a.hwDetect = hwaccel.NewDetector(a.exec)
|
|
|
|
|
a.taskMgr = task.NewManager(a.exec)
|
|
|
|
|
|
|
|
|
|
a.taskMgr.SetEventCallback(func(eventType string, data any) {
|
|
|
|
|
runtime.EventsEmit(ctx, eventType, data)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Enable window resize borders on frameless windows (Windows only)
|
|
|
|
|
go func() {
|
|
|
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
|
platform.EnableResizeBorder()
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// shutdown is called when the app is closing.
|
|
|
|
|
func (a *App) shutdown(ctx context.Context) {
|
|
|
|
|
if a.taskMgr != nil {
|
|
|
|
|
a.taskMgr.Stop()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Window Controls ----
|
|
|
|
|
|
|
|
|
|
// MinimizeWindow minimizes the application window.
|
|
|
|
|
func (a *App) MinimizeWindow() {
|
|
|
|
|
runtime.WindowMinimise(a.ctx)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MaximizeWindow toggles the window between maximized and normal.
|
|
|
|
|
func (a *App) MaximizeWindow() {
|
|
|
|
|
if runtime.WindowIsMaximised(a.ctx) {
|
|
|
|
|
runtime.WindowUnmaximise(a.ctx)
|
|
|
|
|
} else {
|
|
|
|
|
runtime.WindowMaximise(a.ctx)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CloseWindow closes the application.
|
|
|
|
|
func (a *App) CloseWindow() {
|
|
|
|
|
runtime.Quit(a.ctx)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IsMaximised returns whether the window is currently maximized.
|
|
|
|
|
func (a *App) IsMaximised() bool {
|
|
|
|
|
return runtime.WindowIsMaximised(a.ctx)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Media Info ----
|
|
|
|
|
|
|
|
|
|
// GetMediaInfo retrieves media file information via ffprobe.
|
|
|
|
|
func (a *App) GetMediaInfo(inputFile string) (*media.MediaInfo, error) {
|
|
|
|
|
if a.exec == nil {
|
|
|
|
|
return nil, fmt.Errorf("ffmpeg not initialized")
|
|
|
|
|
}
|
|
|
|
|
return media.GetInfo(a.exec, inputFile)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Hardware Detection ----
|
|
|
|
|
|
|
|
|
|
// GetHardwareEncoders returns detected hardware encoders.
|
|
|
|
|
func (a *App) GetHardwareEncoders() ([]hwaccel.HWEncoder, error) {
|
|
|
|
|
if a.hwDetect == nil {
|
|
|
|
|
return nil, fmt.Errorf("hw detector not initialized")
|
|
|
|
|
}
|
|
|
|
|
return a.hwDetect.DetectEncoders()
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-29 10:46:19 +08:00
|
|
|
// GetGPUInfo returns detected GPU models and their encoder capabilities.
|
|
|
|
|
func (a *App) GetGPUInfo() []gpu.GPUInfo {
|
|
|
|
|
if a.exec == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return gpu.DetectGPUs(a.exec)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-29 02:17:57 +08:00
|
|
|
// GetAccelerators returns detected hardware acceleration methods.
|
|
|
|
|
func (a *App) GetAccelerators() ([]hwaccel.Accelerator, error) {
|
|
|
|
|
if a.hwDetect == nil {
|
|
|
|
|
return nil, fmt.Errorf("hw detector not initialized")
|
|
|
|
|
}
|
|
|
|
|
return a.hwDetect.DetectAccelerators()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Task Management ----
|
|
|
|
|
|
|
|
|
|
// AddTask adds a new task to the queue and returns the task ID.
|
|
|
|
|
func (a *App) AddTask(t *task.Task) (string, error) {
|
|
|
|
|
if a.taskMgr == nil {
|
|
|
|
|
return "", fmt.Errorf("task manager not initialized")
|
|
|
|
|
}
|
|
|
|
|
id := a.taskMgr.Add(t)
|
|
|
|
|
return id, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// StartTask starts a specific task by ID.
|
|
|
|
|
func (a *App) StartTask(taskID string) error {
|
|
|
|
|
if a.taskMgr == nil {
|
|
|
|
|
return fmt.Errorf("task manager not initialized")
|
|
|
|
|
}
|
|
|
|
|
return a.taskMgr.Start(taskID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// StartAllTasks starts all pending tasks sequentially.
|
|
|
|
|
func (a *App) StartAllTasks() {
|
|
|
|
|
if a.taskMgr != nil {
|
|
|
|
|
a.taskMgr.StartAll()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CancelTask cancels a running or pending task.
|
|
|
|
|
func (a *App) CancelTask(taskID string) error {
|
|
|
|
|
if a.taskMgr == nil {
|
|
|
|
|
return fmt.Errorf("task manager not initialized")
|
|
|
|
|
}
|
|
|
|
|
return a.taskMgr.Cancel(taskID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RemoveTask removes a completed/failed/canceled task.
|
|
|
|
|
func (a *App) RemoveTask(taskID string) error {
|
|
|
|
|
if a.taskMgr == nil {
|
|
|
|
|
return fmt.Errorf("task manager not initialized")
|
|
|
|
|
}
|
|
|
|
|
return a.taskMgr.Remove(taskID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetTasks returns all tasks in the queue.
|
|
|
|
|
func (a *App) GetTasks() []*task.Task {
|
|
|
|
|
if a.taskMgr == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return a.taskMgr.List()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetTaskLogs returns the stderr logs for a specific task.
|
|
|
|
|
func (a *App) GetTaskLogs(taskID string) []string {
|
|
|
|
|
if a.taskMgr == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
for _, t := range a.taskMgr.List() {
|
|
|
|
|
if t.ID == taskID {
|
|
|
|
|
return t.Logs
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SetHWAccel sets the hardware acceleration method for encoding.
|
|
|
|
|
func (a *App) SetHWAccel(accel string) {
|
|
|
|
|
if a.taskMgr != nil {
|
|
|
|
|
a.taskMgr.SetHWAccel(accel)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- File Dialogs ----
|
|
|
|
|
|
|
|
|
|
// SelectInputFile opens a file dialog for selecting an input media file.
|
|
|
|
|
func (a *App) SelectInputFile() (string, error) {
|
|
|
|
|
return runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
|
|
|
|
|
Title: "选择输入文件",
|
|
|
|
|
Filters: []runtime.FileFilter{
|
|
|
|
|
{DisplayName: "视频文件 (*.mp4;*.mkv;*.mov;*.ts;*.avi;*.webm;*.flv)", Pattern: "*.mp4;*.mkv;*.mov;*.ts;*.avi;*.webm;*.flv"},
|
|
|
|
|
{DisplayName: "所有文件 (*.*)", Pattern: "*.*"},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SelectOutputFile opens a save file dialog.
|
|
|
|
|
func (a *App) SelectOutputFile(defaultName string) (string, error) {
|
|
|
|
|
return runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
2026-07-29 10:46:19 +08:00
|
|
|
Title: "选择输出文件",
|
2026-07-29 02:17:57 +08:00
|
|
|
DefaultFilename: defaultName,
|
|
|
|
|
Filters: []runtime.FileFilter{
|
|
|
|
|
{DisplayName: "MP4 (*.mp4)", Pattern: "*.mp4"},
|
|
|
|
|
{DisplayName: "MKV (*.mkv)", Pattern: "*.mkv"},
|
|
|
|
|
{DisplayName: "MOV (*.mov)", Pattern: "*.mov"},
|
|
|
|
|
{DisplayName: "所有文件 (*.*)", Pattern: "*.*"},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SelectSubtitleFile opens a file dialog for selecting an external subtitle file.
|
|
|
|
|
func (a *App) SelectSubtitleFile() (string, error) {
|
|
|
|
|
return runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
|
|
|
|
|
Title: "选择字幕文件",
|
|
|
|
|
Filters: []runtime.FileFilter{
|
|
|
|
|
{DisplayName: "字幕文件 (*.srt;*.ass;*.ssa;*.vtt;*.sub)", Pattern: "*.srt;*.ass;*.ssa;*.vtt;*.sub"},
|
|
|
|
|
{DisplayName: "所有文件 (*.*)", Pattern: "*.*"},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|