package main import ( "context" "fmt" "os/exec" "path/filepath" goruntime "runtime" "strings" "sync" "convert/internal/config" "convert/internal/convert" "convert/internal/media" "convert/internal/preview" "github.com/wailsapp/wails/v2/pkg/runtime" ) type AppInfo struct { Version string `json:"version"` Binaries media.Binaries `json:"binaries"` Config config.AppConfig `json:"config"` InitialFile string `json:"initialFile"` } type LoadedVideo struct { Info *media.VideoInfo `json:"info"` VideoID string `json:"videoId"` PreviewURL string `json:"previewUrl"` ProxyURL string `json:"proxyUrl"` ProxyMergedURL string `json:"proxyMergedUrl"` ThumbBaseURL string `json:"thumbBaseUrl"` DefaultOutputPath string `json:"defaultOutputPath"` } type App struct { ctx context.Context initialFile string cfg config.AppConfig bins media.Binaries previewSrv *preview.Server runner *convert.Runner mu sync.RWMutex currentInfo *media.VideoInfo } func NewApp(initialFile string) *App { cfg := config.Load() bins := media.FindBinaries(cfg.FFmpegPath, cfg.FFprobePath) return &App{ initialFile: initialFile, cfg: cfg, bins: bins, runner: convert.NewRunner(), } } func (a *App) startup(ctx context.Context) { a.ctx = ctx srv, err := preview.Start(a.bins.FFmpeg) if err == nil { a.previewSrv = srv } } func (a *App) shutdown(_ context.Context) { if a.runner != nil { a.runner.Cancel() } if a.previewSrv != nil { a.previewSrv.Close() } } func (a *App) GetAppInfo() AppInfo { a.mu.RLock() defer a.mu.RUnlock() return AppInfo{ Version: Version, Binaries: a.bins, Config: a.cfg, InitialFile: a.initialFile, } } func (a *App) RefreshBinaries() media.Binaries { a.mu.Lock() defer a.mu.Unlock() a.bins = media.FindBinaries(a.cfg.FFmpegPath, a.cfg.FFprobePath) if a.previewSrv != nil { a.previewSrv.SetFFmpeg(a.bins.FFmpeg) } return a.bins } func (a *App) SaveConfig(cfg config.AppConfig) (AppInfo, error) { if err := config.Save(cfg); err != nil { return AppInfo{}, err } a.mu.Lock() a.cfg = cfg a.bins = media.FindBinaries(cfg.FFmpegPath, cfg.FFprobePath) if a.previewSrv != nil { a.previewSrv.SetFFmpeg(a.bins.FFmpeg) } a.mu.Unlock() return a.GetAppInfo(), nil } func (a *App) OpenVideoDialog() (string, error) { return runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ Title: "Select Video File", Filters: []runtime.FileFilter{ { DisplayName: "Video Files (*.mp4, *.mkv, *.mov, *.avi, *.webm, *.m4v, *.ts, *.flv)", Pattern: "*.mp4;*.mkv;*.mov;*.avi;*.webm;*.m4v;*.ts;*.flv;*.wmv", }, { DisplayName: "All Files (*.*)", Pattern: "*.*", }, }, }) } func (a *App) SaveVideoDialog(defaultPath string) (string, error) { dir := filepath.Dir(defaultPath) file := filepath.Base(defaultPath) return runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{ Title: "Save Output Video", DefaultDirectory: dir, DefaultFilename: file, Filters: []runtime.FileFilter{ { DisplayName: "MP4 Video (*.mp4)", Pattern: "*.mp4", }, }, }) } func (a *App) SelectFFmpegDialog() (AppInfo, error) { selected, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ Title: "Locate ffmpeg.exe", Filters: []runtime.FileFilter{ {DisplayName: "ffmpeg executable", Pattern: "ffmpeg.exe;ffmpeg"}, {DisplayName: "All Files", Pattern: "*.*"}, }, }) if err != nil || selected == "" { return a.GetAppInfo(), err } a.mu.Lock() a.cfg.FFmpegPath = selected dir := filepath.Dir(selected) probeName := "ffprobe" if goruntime.GOOS == "windows" { probeName = "ffprobe.exe" } a.cfg.FFprobePath = filepath.Join(dir, probeName) _ = config.Save(a.cfg) a.bins = media.FindBinaries(a.cfg.FFmpegPath, a.cfg.FFprobePath) if a.previewSrv != nil { a.previewSrv.SetFFmpeg(a.bins.FFmpeg) } a.mu.Unlock() return a.GetAppInfo(), nil } func (a *App) LoadVideo(path string) (*LoadedVideo, error) { a.mu.RLock() ffprobeBin := a.bins.FFprobe suffix := a.cfg.OutputSuffix a.mu.RUnlock() info, err := media.Probe(ffprobeBin, path) if err != nil { return nil, err } a.mu.Lock() a.currentInfo = info a.mu.Unlock() if a.previewSrv == nil { return nil, fmt.Errorf("preview server is not running") } id := a.previewSrv.Register(info) base := a.previewSrv.BaseURL() return &LoadedVideo{ Info: info, VideoID: id, PreviewURL: fmt.Sprintf("%s/video?id=%s", base, id), ProxyURL: fmt.Sprintf("%s/proxy?id=%s&merge=0", base, id), ProxyMergedURL: fmt.Sprintf("%s/proxy?id=%s&merge=1", base, id), ThumbBaseURL: fmt.Sprintf("%s/thumb?id=%s", base, id), DefaultOutputPath: convert.DefaultOutputPath(path, suffix), }, nil } func (a *App) PreviewCommand(req convert.Request) (string, error) { a.mu.RLock() info := a.currentInfo a.mu.RUnlock() if info == nil { return "", nil } built, err := convert.BuildCommand(req, info) if err != nil { return "", err } parts := make([]string, 0, len(built.Args)+1) parts = append(parts, "ffmpeg") for _, arg := range built.Args { if strings.ContainsAny(arg, " \t") { parts = append(parts, `"`+arg+`"`) } else { parts = append(parts, arg) } } return strings.Join(parts, " "), nil } func (a *App) StartConvert(req convert.Request) error { a.mu.RLock() info := a.currentInfo ffmpegBin := a.bins.FFmpeg a.mu.RUnlock() if info == nil { return fmt.Errorf("no video loaded") } if ffmpegBin == "" { return fmt.Errorf("ffmpeg binary not found") } if a.runner.IsRunning() { return fmt.Errorf("conversion already running") } built, err := convert.BuildCommand(req, info) if err != nil { return err } go func() { err := a.runner.Run(a.ctx, ffmpegBin, built, func(p convert.Progress) { runtime.EventsEmit(a.ctx, "convert:progress", p) }) if err != nil { runtime.EventsEmit(a.ctx, "convert:error", err.Error()) return } runtime.EventsEmit(a.ctx, "convert:done", built.OutputPath) }() return nil } func (a *App) CancelConvert() bool { return a.runner.Cancel() } func (a *App) OpenInExplorer(path string) error { if path == "" { return nil } if goruntime.GOOS == "windows" { cmd := exec.Command("explorer", "/select,", filepath.Clean(path)) return cmd.Start() } return exec.Command("xdg-open", filepath.Dir(path)).Start() } func (a *App) OpenOutputFile(path string) error { if path == "" { return nil } if goruntime.GOOS == "windows" { cmd := exec.Command("cmd", "/c", "start", "", filepath.Clean(path)) media.HideCmdWindow(cmd) return cmd.Start() } return exec.Command("xdg-open", path).Start() } func (a *App) WindowMinimise() { if a.ctx != nil { runtime.WindowMinimise(a.ctx) } } func (a *App) WindowToggleMaximise() { if a.ctx != nil { runtime.WindowToggleMaximise(a.ctx) } } func (a *App) WindowClose() { if a.ctx != nil { runtime.Quit(a.ctx) } } func (a *App) WindowIsMaximised() bool { if a.ctx != nil { return runtime.WindowIsMaximised(a.ctx) } return false }