summaryrefslogtreecommitdiff
path: root/internal/gui/dialogs.go
blob: 072557e042b2b056148d13fda5ff553f09d61b58 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package gui

import (
	"fmt"
	"path/filepath"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/dialog"
	"fyne.io/fyne/v2/widget"

	"codeberg.org/snonux/totalrecall/internal/cli"
)

// showGalleryVideoDialog presents a multi-checkbox list of gallery PNG paths
// so the user can choose which pages to animate into MP4 clips using Veo.
// All items are selected by default. On confirmation the selected paths are
// passed to cli.GenerateSelectedVideos which runs in a background goroutine
// while a progress dialog keeps the user informed.
//
// galleryPNGs contains the absolute paths of candidate PNG files (typically
// story page images). outputDir is informational — videos are always written
// next to their source PNGs by the generator.
// apiKey is the Google/Gemini API key required by the Veo model.
// The function must be called on the Fyne main goroutine.
func (a *Application) showGalleryVideoDialog(galleryPNGs []string, outputDir, apiKey string) {
	if len(galleryPNGs) == 0 {
		dialog.ShowInformation("No Gallery Images",
			"No gallery PNG images were found to generate videos from.", a.window)
		return
	}

	// Build checkbox list — all ticked by default.
	selected := buildSelectionMap(galleryPNGs)
	checkboxes := buildCheckboxList(galleryPNGs, selected)

	content := buildGalleryDialogContent(outputDir, checkboxes)

	// Show a custom confirm dialog. On "Generate" the user-selected subset is
	// collected and passed to the video generator in a goroutine.
	customDialog := dialog.NewCustomConfirm(
		"Generate Videos",
		"Generate",
		"Cancel",
		content,
		func(confirmed bool) {
			if !confirmed {
				return
			}
			paths := collectSelectedPaths(galleryPNGs, selected)
			a.runVideoGeneration(paths, apiKey)
		},
		a.window,
	)

	customDialog.Resize(fyne.NewSize(520, 400))
	customDialog.Show()
}

// buildSelectionMap creates a map of path -> *bool with all entries set to true
// so every gallery image is selected by default.
func buildSelectionMap(galleryPNGs []string) map[string]*bool {
	selected := make(map[string]*bool, len(galleryPNGs))
	for _, p := range galleryPNGs {
		v := true
		selected[p] = &v
	}
	return selected
}

// buildCheckboxList creates one labelled checkbox per gallery PNG. Each checkbox
// mutates the corresponding *bool in selected so the confirm callback can read
// the final state without iterating widgets again.
func buildCheckboxList(galleryPNGs []string, selected map[string]*bool) []fyne.CanvasObject {
	checkboxes := make([]fyne.CanvasObject, 0, len(galleryPNGs))
	for _, p := range galleryPNGs {
		p := p // capture loop variable
		label := filepath.Base(p)
		check := widget.NewCheck(label, func(checked bool) {
			*selected[p] = checked
		})
		check.SetChecked(true)
		checkboxes = append(checkboxes, check)
	}
	return checkboxes
}

// buildGalleryDialogContent assembles the scrollable VBox shown inside the
// gallery video dialog. outputDir is displayed as a hint so the user knows
// where the output will land.
func buildGalleryDialogContent(outputDir string, checkboxes []fyne.CanvasObject) fyne.CanvasObject {
	hint := widget.NewLabel(fmt.Sprintf("Output directory: %s\nSelect which pages to animate:", outputDir))
	hint.Wrapping = fyne.TextWrapWord

	checkList := container.NewVBox(checkboxes...)
	scroll := container.NewVScroll(checkList)
	scroll.SetMinSize(fyne.NewSize(480, 260))

	return container.NewVBox(hint, widget.NewSeparator(), scroll)
}

// collectSelectedPaths returns the subset of galleryPNGs for which the user
// left the checkbox ticked.
func collectSelectedPaths(galleryPNGs []string, selected map[string]*bool) []string {
	paths := make([]string, 0, len(galleryPNGs))
	for _, p := range galleryPNGs {
		if v, ok := selected[p]; ok && *v {
			paths = append(paths, p)
		}
	}
	return paths
}

// runVideoGeneration shows an infinite-progress dialog and calls
// cli.GenerateSelectedVideos in a background goroutine. The progress dialog is
// dismissed and the result is shown to the user once generation completes.
// Must be called on the Fyne main goroutine.
func (a *Application) runVideoGeneration(paths []string, apiKey string) {
	if len(paths) == 0 {
		dialog.ShowInformation("Nothing Selected",
			"No images were selected for video generation.", a.window)
		return
	}

	// Infinite progress bar to indicate background work.
	progressDialog := dialog.NewCustom(
		"Generating Videos",
		"Please wait…",
		buildProgressContent(len(paths)),
		a.window,
	)
	progressDialog.Show()

	// Run generation in a background goroutine; update the UI when done.
	go func() {
		err := cli.GenerateSelectedVideos(apiKey, paths)
		fyne.Do(func() {
			progressDialog.Hide()
			if err != nil {
				dialog.ShowError(fmt.Errorf("video generation failed: %w", err), a.window)
				return
			}
			dialog.ShowInformation("Videos Generated",
				fmt.Sprintf("Successfully generated %d video(s).\nVideos are saved next to their source images.", len(paths)),
				a.window,
			)
		})
	}()
}

// buildProgressContent returns the widget shown inside the progress dialog:
// a spinning activity indicator together with a short explanatory label.
func buildProgressContent(count int) fyne.CanvasObject {
	bar := widget.NewProgressBarInfinite()
	label := widget.NewLabel(fmt.Sprintf("Generating %d video(s) with Veo — this may take several minutes…", count))
	label.Wrapping = fyne.TextWrapWord
	return container.NewVBox(label, bar)
}