Compare commits

...

4 Commits

3 changed files with 92 additions and 52 deletions

View File

@ -12,6 +12,7 @@ import (
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
"git.leaktechnologies.dev/stu/VideoTools/internal/queue"
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
)
// BuildQueueView creates the queue viewer UI
@ -114,10 +115,13 @@ func buildJobItem(
statusRect.SetMinSize(fyne.NewSize(6, 0))
// Title and description
titleLabel := widget.NewLabel(job.Title)
titleText := utils.ShortenMiddle(job.Title, 60)
descText := utils.ShortenMiddle(job.Description, 90)
titleLabel := widget.NewLabel(titleText)
titleLabel.TextStyle = fyne.TextStyle{Bold: true}
descLabel := widget.NewLabel(job.Description)
descLabel := widget.NewLabel(descText)
descLabel.TextStyle = fyne.TextStyle{Italic: true}
descLabel.Wrapping = fyne.TextWrapWord

133
main.go
View File

@ -432,6 +432,54 @@ func (c convertConfig) CoverLabel() string {
return filepath.Base(c.CoverArtPath)
}
// defaultConvertConfigPath returns the path to the persisted convert config.
func defaultConvertConfigPath() string {
configDir, err := os.UserConfigDir()
if err != nil || configDir == "" {
home := os.Getenv("HOME")
if home != "" {
configDir = filepath.Join(home, ".config")
}
}
if configDir == "" {
return "convert.json"
}
return filepath.Join(configDir, "VideoTools", "convert.json")
}
// loadPersistedConvertConfig loads the saved convert configuration from disk.
func loadPersistedConvertConfig() (convertConfig, error) {
var cfg convertConfig
path := defaultConvertConfigPath()
data, err := os.ReadFile(path)
if err != nil {
return cfg, err
}
if err := json.Unmarshal(data, &cfg); err != nil {
return cfg, err
}
if cfg.OutputAspect == "" {
cfg.OutputAspect = "Source"
cfg.AspectUserSet = false
} else if !strings.EqualFold(cfg.OutputAspect, "Source") {
cfg.AspectUserSet = true
}
return cfg, nil
}
// savePersistedConvertConfig writes the convert configuration to disk.
func savePersistedConvertConfig(cfg convertConfig) error {
path := defaultConvertConfigPath()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0o644)
}
type appState struct {
window fyne.Window
active string
@ -474,6 +522,12 @@ type appState struct {
autoCompare bool // Auto-load Compare module after conversion
}
func (s *appState) persistConvertConfig() {
if err := savePersistedConvertConfig(s.convert); err != nil {
logging.Debug(logging.CatSystem, "failed to persist convert config: %v", err)
}
}
func (s *appState) stopPreview() {
if s.anim != nil {
s.anim.Stop()
@ -1405,10 +1459,6 @@ func (s *appState) jobExecutor(ctx context.Context, job *queue.Job, progressCall
// executeConvertJob executes a conversion job from the queue
func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progressCallback func(float64)) error {
return s.executeConvertJobWithFallback(ctx, job, progressCallback, false)
}
func (s *appState) executeConvertJobWithFallback(ctx context.Context, job *queue.Job, progressCallback func(float64), hwFallbackTried bool) error {
cfg := job.Config
inputPath := cfg["inputPath"].(string)
outputPath := cfg["outputPath"].(string)
@ -2211,6 +2261,8 @@ func (s *appState) executeSnippetJob(ctx context.Context, job *queue.Job, progre
}
func (s *appState) shutdown() {
s.persistConvertConfig()
// Stop queue without saving - we want a clean slate each session
if s.jobQueue != nil {
s.jobQueue.Stop()
@ -2359,6 +2411,12 @@ func runGUI() {
playerPaused: true,
}
if cfg, err := loadPersistedConvertConfig(); err == nil {
state.convert = cfg
} else if !errors.Is(err, os.ErrNotExist) {
logging.Debug(logging.CatSystem, "failed to load persisted convert config: %v", err)
}
// Initialize conversion stats bar
state.statsBar = ui.NewConversionStatsBar(func() {
// Clicking the stats bar opens the queue view
@ -2827,7 +2885,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
transformHint := widget.NewLabel("Apply flips and rotation to correct video orientation")
transformHint.Wrapping = fyne.TextWrapWord
aspectTargets := []string{"Source", "16:9", "4:3", "1:1", "9:16", "21:9"}
aspectTargets := []string{"Source", "16:9", "4:3", "5:4", "1:1", "9:16", "21:9"}
targetAspectSelect := widget.NewSelect(aspectTargets, func(value string) {
logging.Debug(logging.CatUI, "target aspect set to %s", value)
state.convert.OutputAspect = value
@ -3719,6 +3777,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
if updateQualityVisibility != nil {
updateQualityVisibility()
}
state.persistConvertConfig()
logging.Debug(logging.CatUI, "convert settings reset to defaults")
})
statusLabel := widget.NewLabel("")
@ -3746,6 +3805,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
// Add to Queue button
addQueueBtn := widget.NewButton("Add to Queue", func() {
state.persistConvertConfig()
if err := state.addConvertToQueue(); err != nil {
dialog.ShowError(err, state.window)
} else {
@ -3762,6 +3822,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
}
convertBtn = widget.NewButton("CONVERT NOW", func() {
state.persistConvertConfig()
// Add job to queue and start immediately
if err := state.addConvertToQueue(); err != nil {
dialog.ShowError(err, state.window)
@ -3843,49 +3904,24 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
// Load/Save config buttons
loadCfgBtn := widget.NewButton("Load Config", func() {
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
if err != nil || reader == nil {
return
cfg, err := loadPersistedConvertConfig()
if err != nil {
if errors.Is(err, os.ErrNotExist) {
dialog.ShowInformation("No Config", "No saved config found yet. It will save automatically after your first change.", state.window)
} else {
dialog.ShowError(fmt.Errorf("failed to load config: %w", err), state.window)
}
path := reader.URI().Path()
reader.Close()
data, err := os.ReadFile(path)
if err != nil {
dialog.ShowError(fmt.Errorf("failed to read config: %w", err), state.window)
return
}
var cfg convertConfig
if err := json.Unmarshal(data, &cfg); err != nil {
dialog.ShowError(fmt.Errorf("failed to parse config: %w", err), state.window)
return
}
if cfg.OutputAspect == "" {
cfg.OutputAspect = "Source"
cfg.AspectUserSet = false
} else if !strings.EqualFold(cfg.OutputAspect, "Source") {
cfg.AspectUserSet = true
}
state.convert = cfg
state.showConvertView(state.source)
}, state.window)
return
}
state.convert = cfg
state.showConvertView(state.source)
})
saveCfgBtn := widget.NewButton("Save Config", func() {
dialog.ShowFileSave(func(writer fyne.URIWriteCloser, err error) {
if err != nil || writer == nil {
return
}
path := writer.URI().Path()
defer writer.Close()
data, err := json.MarshalIndent(state.convert, "", " ")
if err != nil {
dialog.ShowError(fmt.Errorf("failed to serialize config: %w", err), state.window)
return
}
if err := os.WriteFile(path, data, 0o644); err != nil {
dialog.ShowError(fmt.Errorf("failed to save config: %w", err), state.window)
return
}
}, state.window)
if err := savePersistedConvertConfig(state.convert); err != nil {
dialog.ShowError(fmt.Errorf("failed to save config: %w", err), state.window)
return
}
dialog.ShowInformation("Config Saved", fmt.Sprintf("Saved to %s", defaultConvertConfigPath()), state.window)
})
leftControls := container.NewHBox(resetBtn, loadCfgBtn, saveCfgBtn, autoCompareCheck)
@ -6223,16 +6259,13 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
strings.Contains(stderrOutput, "vaapi") ||
strings.Contains(stderrOutput, "videotoolbox"))
if isHardwareFailure && !hwFallbackTried && resolvedAccel != "none" && resolvedAccel != "" {
if isHardwareFailure && !strings.EqualFold(s.convert.HardwareAccel, "none") && resolvedAccel != "none" && resolvedAccel != "" {
s.convert.HardwareAccel = "none"
if logFile != nil {
fmt.Fprintf(logFile, "\nAuto-fallback: retrying with software encoder at %s\n", time.Now().Format(time.RFC3339))
fmt.Fprintf(logFile, "\nAuto-fallback: hardware encoder failed; switched to software for next attempt at %s\n", time.Now().Format(time.RFC3339))
_ = logFile.Close()
}
s.convertCancel = nil
if err := s.executeConvertJobWithFallback(ctx, job, progressCallback, true); err == nil {
return
}
}
fyne.CurrentApp().Driver().DoFromGoroutine(func() {

View File

@ -45,6 +45,9 @@ echo ""
echo "🔨 Building VideoTools..."
# Fyne needs cgo for GLFW/OpenGL bindings; build with CGO enabled.
export CGO_ENABLED=1
export GOCACHE="$PROJECT_ROOT/.cache/go-build"
export GOMODCACHE="$PROJECT_ROOT/.cache/go-mod"
mkdir -p "$GOCACHE" "$GOMODCACHE"
if go build -o "$BUILD_OUTPUT" .; then
echo "✓ Build successful! (VideoTools $APP_VERSION)"
echo ""