- 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.
353 lines
9.5 KiB
Go
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
|
|
}
|