package main import ( "context" "ffmpeg-gui/internal/config" "ffmpeg-gui/internal/ffmpeg" "ffmpeg-gui/internal/gpu" "ffmpeg-gui/internal/hwaccel" "ffmpeg-gui/internal/media" "ffmpeg-gui/internal/platform" "ffmpeg-gui/internal/task" "fmt" "os" "strings" "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 cfg *config.Config 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 a.cfg = config.Load() runtime.LogInfo(ctx, "[app] config loaded") // Detect ffmpeg/ffprobe (system → saved → bundled) bins, err := detectBins(a.cfg) if err != nil { // System/PATH not found — extract embedded zip if bundled zipData, zipErr := bundledDir.ReadFile("bundled/ffmpeg.zip") runtime.LogInfo(ctx, fmt.Sprintf("ffmpeg not in PATH, bundled zip: err=%v size=%d", zipErr, len(zipData))) if zipErr == nil { ffmpegPath, ffprobePath := a.cfg.ExtractZip(zipData) if ffmpegPath != "" { a.cfg.FFmpegPath = ffmpegPath } if ffprobePath != "" { a.cfg.FFprobePath = ffprobePath } if a.cfg.FFmpegPath != "" || a.cfg.FFprobePath != "" { a.cfg.Save() runtime.LogInfo(ctx, fmt.Sprintf("config saved with ffmpeg=%s ffprobe=%s", a.cfg.FFmpegPath, a.cfg.FFprobePath)) } } // Retry with extracted binaries bins, err = detectBins(a.cfg) } 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) // Restore saved preferred accelerator if a.cfg.PreferredAccel != "" { a.taskMgr.SetHWAccel(a.cfg.PreferredAccel) } 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() } // GetGPUInfo returns detected GPU models and their encoder capabilities. func (a *App) GetGPUInfo() []gpu.GPUInfo { return gpu.DetectGPUs(a.exec) } // detectBins finds ffmpeg/ffprobe, checking config paths first. func detectBins(cfg *config.Config) (ffmpeg.BinPaths, error) { // Try config-saved paths first if cfg.FFmpegPath != "" && cfg.FFprobePath != "" { if fileExists(cfg.FFmpegPath) && fileExists(cfg.FFprobePath) { return ffmpeg.BinPaths{FFmpeg: cfg.FFmpegPath, FFprobe: cfg.FFprobePath}, nil } } // Fall back to default detection (bundled, then PATH) return ffmpeg.Detect() } func fileExists(path string) bool { _, err := os.Stat(path) return err == nil } // ---- Config API ---- // GetConfig returns the current app configuration. func (a *App) GetConfig() map[string]string { return map[string]string{ "theme": a.cfg.Theme, "preferredAccel": a.cfg.PreferredAccel, "outputDir": a.cfg.OutputDir, "namingRule": a.cfg.NamingRule, } } // SaveConfig saves a configuration value. func (a *App) SaveConfig(key, value string) { switch key { case "theme": a.cfg.Theme = value case "preferredAccel": a.cfg.PreferredAccel = value case "outputDir": a.cfg.OutputDir = value case "namingRule": a.cfg.NamingRule = value } a.cfg.Save() } // ---- Hardware Detection ---- // CheckFFmpeg returns ffmpeg and ffprobe paths and version info. func (a *App) CheckFFmpeg() map[string]string { result := map[string]string{ "ffmpegPath": "", "ffprobePath": "", "ffmpegVer": "", "ffprobeVer": "", } if a.exec == nil { // Try config paths as fallback bins, err := detectBins(a.cfg) if err != nil { return result } result["ffmpegPath"] = bins.FFmpeg result["ffprobePath"] = bins.FFprobe // Can't get version without executor, but at least show paths return result } // Use saved config paths if available if a.cfg.FFmpegPath != "" && fileExists(a.cfg.FFmpegPath) { result["ffmpegPath"] = a.cfg.FFmpegPath } else { bins, _ := ffmpeg.Detect() result["ffmpegPath"] = bins.FFmpeg } if a.cfg.FFprobePath != "" && fileExists(a.cfg.FFprobePath) { result["ffprobePath"] = a.cfg.FFprobePath } else { bins, _ := ffmpeg.Detect() result["ffprobePath"] = bins.FFprobe } out, err := a.exec.RunSync("-version") if err == nil { lines := strings.Split(out, "\n") if len(lines) > 0 { result["ffmpegVer"] = strings.TrimSpace(lines[0]) } } out, err = a.exec.Probe("-version") if err == nil { lines := strings.Split(out, "\n") if len(lines) > 0 { result["ffprobeVer"] = strings.TrimSpace(lines[0]) } } return result } // 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{ Title: "选择输出文件", DefaultFilename: defaultName, Filters: []runtime.FileFilter{ {DisplayName: "MP4 (*.mp4)", Pattern: "*.mp4"}, {DisplayName: "MKV (*.mkv)", Pattern: "*.mkv"}, {DisplayName: "MOV (*.mov)", Pattern: "*.mov"}, {DisplayName: "所有文件 (*.*)", Pattern: "*.*"}, }, }) } // SelectOutputDir opens a directory dialog for selecting a default output folder. func (a *App) SelectOutputDir() (string, error) { return runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{ Title: "选择默认输出目录", }) } // 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: "*.*"}, }, }) }