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 }