VideoTools/internal/ui/command_editor.go
Stu Leak 2332f2e9ca fix: update main menu version display to dev22
- Update appVersion constant from dev21 to dev22
- Ensures main menu footer and About dialog show correct version
- Completes dev22 release preparation

All build fixes applied and version correctly displayed.
2026-01-03 13:58:22 -05:00

353 lines
9.5 KiB
Go

package ui
import (
"fmt"
"strings"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"git.leaktechnologies.dev/stu/VideoTools/internal/queue"
)
// CommandEditor provides UI for editing FFmpeg commands
type CommandEditor struct {
window fyne.Window
editManager queue.EditJobManager
jobID string
// UI components
jsonEntry *widget.Entry
validateBtn *widget.Button
applyBtn *widget.Button
resetBtn *widget.Button
cancelBtn *widget.Button
statusLabel *widget.Label
historyList *widget.List
// Data
editableJob *queue.EditableJob
editHistory []queue.EditHistoryEntry
}
// CommandEditorConfig holds configuration for the command editor
type CommandEditorConfig struct {
Window fyne.Window
EditManager queue.EditJobManager
JobID string
Title string
}
// NewCommandEditor creates a new command editor dialog
func NewCommandEditor(config CommandEditorConfig) *CommandEditor {
editor := &CommandEditor{
window: config.Window,
editManager: config.EditManager,
jobID: config.JobID,
}
// Load editable job
editableJob, err := editor.editManager.GetEditableJob(config.JobID)
if err != nil {
dialog.ShowError(fmt.Errorf("Failed to load job: %w", err), config.Window)
return nil
}
editor.editableJob = editableJob
// Load edit history
history, err := editor.editManager.GetEditHistory(config.JobID)
if err == nil {
editor.editHistory = history
}
editor.buildUI(config.Title)
return editor
}
// buildUI creates the command editor interface
func (e *CommandEditor) buildUI(title string) {
// JSON editor with syntax highlighting
e.jsonEntry = widget.NewMultiLineEntry()
e.jsonEntry.SetPlaceHolder("FFmpeg command JSON will appear here...")
e.jsonEntry.TextStyle = fyne.TextStyle{Monospace: true}
// Load current command
if e.editableJob.CurrentCommand != nil {
e.jsonEntry.SetText(e.editableJob.CurrentCommand.ToJSON())
}
// Command validation status
e.statusLabel = widget.NewLabel("Ready")
e.statusLabel.Importance = widget.MediumImportance
// Action buttons
e.validateBtn = widget.NewButtonWithIcon("Validate", theme.ConfirmIcon(), e.validateCommand)
e.validateBtn.Importance = widget.MediumImportance
e.applyBtn = widget.NewButtonWithIcon("Apply Changes", theme.ConfirmIcon(), e.applyChanges)
e.applyBtn.Importance = widget.HighImportance
e.applyBtn.Disable()
e.resetBtn = widget.NewButtonWithIcon("Reset to Original", theme.ViewRefreshIcon(), e.resetToOriginal)
e.resetBtn.Importance = widget.MediumImportance
e.cancelBtn = widget.NewButtonWithIcon("Cancel", theme.CancelIcon(), func() {
e.close()
})
// Edit history list
e.historyList = widget.NewList(
func() int { return len(e.editHistory) },
func() fyne.CanvasObject {
return container.NewVBox(
widget.NewLabel("Timestamp"),
widget.NewLabel("Change Reason"),
widget.NewSeparator(),
)
},
func(id widget.ListItemID, obj fyne.CanvasObject) {
if id >= len(e.editHistory) {
return
}
entry := e.editHistory[id]
vbox := obj.(*fyne.Container)
timestamp := vbox.Objects[0].(*widget.Label)
reason := vbox.Objects[1].(*widget.Label)
timestamp.SetText(entry.Timestamp.Format(time.RFC822))
reason.SetText(entry.ChangeReason)
if entry.Applied {
timestamp.Importance = widget.SuccessImportance
}
},
)
// Layout
content := container.NewHSplit(
container.NewVBox(
widget.NewCard("Command Editor", "",
container.NewVBox(
widget.NewLabel("Edit FFmpeg command in JSON format:"),
container.NewScroll(e.jsonEntry),
e.statusLabel,
container.NewHBox(
e.validateBtn,
e.applyBtn,
e.resetBtn,
layout.NewSpacer(),
e.cancelBtn,
),
),
),
),
container.NewVBox(
widget.NewCard("Edit History", "", e.historyList),
e.buildCommandPreview(),
),
)
content.Resize(fyne.NewSize(900, 600))
// Dialog
dlg := dialog.NewCustom(title, "", content, e.window)
dlg.Resize(fyne.NewSize(950, 650))
dlg.Show()
// Auto-validation on text change
e.jsonEntry.OnChanged = func(text string) {
e.applyBtn.Disable()
e.statusLabel.SetText("Unsaved changes")
e.statusLabel.Importance = widget.MediumImportance
}
}
// validateCommand validates the current command
func (e *CommandEditor) validateCommand() {
jsonText := e.jsonEntry.Text
cmd, err := queue.FFmpegCommandFromJSON(jsonText)
if err != nil {
e.statusLabel.SetText(fmt.Sprintf("Invalid JSON: %v", err))
e.statusLabel.Importance = widget.DangerImportance
e.applyBtn.Disable()
return
}
if err := e.editManager.ValidateCommand(cmd); err != nil {
e.statusLabel.SetText(fmt.Sprintf("Invalid command: %v", err))
e.statusLabel.Importance = widget.DangerImportance
e.applyBtn.Disable()
return
}
if err := queue.ValidateCommandStructure(cmd); err != nil {
e.statusLabel.SetText(fmt.Sprintf("Command structure error: %v", err))
e.statusLabel.Importance = widget.DangerImportance
e.applyBtn.Disable()
return
}
e.statusLabel.SetText("Valid command")
e.statusLabel.Importance = widget.SuccessImportance
e.applyBtn.Enable()
}
// applyChanges applies the edited command
func (e *CommandEditor) applyChanges() {
jsonText := e.jsonEntry.Text
cmd, err := queue.FFmpegCommandFromJSON(jsonText)
if err != nil {
dialog.ShowError(fmt.Errorf("Invalid JSON: %w", err), e.window)
return
}
// Show reason dialog
reasonEntry := widget.NewEntry()
reasonEntry.SetPlaceHolder("Enter reason for change...")
content := container.NewVBox(
widget.NewLabel("Please enter a reason for this change:"),
reasonEntry,
)
buttons := container.NewHBox(
widget.NewButton("Cancel", func() {}),
widget.NewButton("Apply", func() {
reason := reasonEntry.Text
if reason == "" {
reason = "Manual edit via command editor"
}
if err := e.editManager.UpdateJobCommand(e.jobID, cmd, reason); err != nil {
dialog.ShowError(fmt.Errorf("Failed to update job: %w", err), e.window)
return
}
if err := e.editManager.ApplyEdit(e.jobID); err != nil {
dialog.ShowError(fmt.Errorf("Failed to apply edit: %w", err), e.window)
return
}
dialog.ShowInformation("Success", "Command updated successfully", e.window)
e.refreshData()
e.close()
}),
)
reasonDlg := dialog.NewCustom("Apply Changes", "OK", content, e.window)
reasonDlg.SetOnClosed(func() {
// Handle button clicks manually
})
// Create a custom dialog layout
dialogContent := container.NewVBox(content, buttons)
customDlg := dialog.NewCustomWithoutButtons("Apply Changes", dialogContent, e.window)
customDlg.Show()
reasonDlg.Show()
}
// resetToOriginal resets the command to original
func (e *CommandEditor) resetToOriginal() {
if e.editableJob.OriginalCommand == nil {
dialog.ShowInformation("Info", "No original command available", e.window)
return
}
confirmDlg := dialog.NewConfirm("Reset Command",
"Are you sure you want to reset to the original command? This will discard all current changes.",
func(confirmed bool) {
if confirmed {
e.jsonEntry.SetText(e.editableJob.OriginalCommand.ToJSON())
e.statusLabel.SetText("Reset to original")
e.statusLabel.Importance = widget.MediumImportance
e.applyBtn.Disable()
}
}, e.window)
confirmDlg.Show()
}
// buildCommandPreview creates a preview of the command
func (e *CommandEditor) buildCommandPreview() fyne.CanvasObject {
previewLabel := widget.NewLabel("")
previewLabel.TextStyle = fyne.TextStyle{Monospace: true}
previewLabel.Wrapping = fyne.TextWrapBreak
refreshPreview := func() {
jsonText := e.jsonEntry.Text
cmd, err := queue.FFmpegCommandFromJSON(jsonText)
if err != nil {
previewLabel.SetText("Invalid command")
return
}
previewLabel.SetText(cmd.ToFullCommand())
}
// Initial preview
refreshPreview()
// Update preview on text change
e.jsonEntry.OnChanged = func(text string) {
refreshPreview()
e.applyBtn.Disable()
e.statusLabel.SetText("Unsaved changes")
e.statusLabel.Importance = widget.MediumImportance
}
return widget.NewCard("Command Preview", "",
container.NewScroll(previewLabel))
}
// refreshData refreshes the editor data
func (e *CommandEditor) refreshData() {
// Reload editable job
editableJob, err := e.editManager.GetEditableJob(e.jobID)
if err == nil {
e.editableJob = editableJob
}
// Reload history
history, err := e.editManager.GetEditHistory(e.jobID)
if err == nil {
e.editHistory = history
e.historyList.Refresh()
}
}
// close closes the editor
func (e *CommandEditor) close() {
// Close dialog by finding parent dialog
// This is a workaround since Fyne doesn't expose direct dialog closing
for _, win := range fyne.CurrentApp().Driver().AllWindows() {
if win.Title() == "Command Editor" || strings.Contains(win.Title(), "Edit Job") {
win.Close()
break
}
}
}
// ShowCommandEditorDialog shows a command editor for a specific job
func ShowCommandEditorDialog(window fyne.Window, editManager queue.EditJobManager, jobID, jobTitle string) {
config := CommandEditorConfig{
Window: window,
EditManager: editManager,
JobID: jobID,
Title: fmt.Sprintf("Edit Job: %s", jobTitle),
}
NewCommandEditor(config)
}
// CreateCommandEditorButton creates a button that opens the command editor
func CreateCommandEditorButton(window fyne.Window, editManager queue.EditJobManager, jobID, jobTitle string) *widget.Button {
btn := widget.NewButtonWithIcon("Edit Command", theme.DocumentCreateIcon(), func() {
ShowCommandEditorDialog(window, editManager, jobID, jobTitle)
})
btn.Importance = widget.MediumImportance
return btn
}