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.
This commit is contained in:
parent
40b50b9274
commit
aabe61ca0e
362
internal/queue/edit.go
Normal file
362
internal/queue/edit.go
Normal file
|
|
@ -0,0 +1,362 @@
|
||||||
|
package queue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EditJobStatus represents the edit state of a job
|
||||||
|
type EditJobStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
EditJobStatusOriginal EditJobStatus = "original" // Original job state
|
||||||
|
EditJobStatusModified EditJobStatus = "modified" // Job has been modified
|
||||||
|
EditJobStatusValidated EditJobStatus = "validated" // Job has been validated
|
||||||
|
EditJobStatusApplied EditJobStatus = "applied" // Changes have been applied
|
||||||
|
)
|
||||||
|
|
||||||
|
// EditHistoryEntry tracks changes made to a job
|
||||||
|
type EditHistoryEntry struct {
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
OldCommand *FFmpegCommand `json:"old_command,omitempty"`
|
||||||
|
NewCommand *FFmpegCommand `json:"new_command"`
|
||||||
|
ChangeReason string `json:"change_reason"`
|
||||||
|
Applied bool `json:"applied"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FFmpegCommand represents a structured FFmpeg command
|
||||||
|
type FFmpegCommand struct {
|
||||||
|
Executable string `json:"executable"`
|
||||||
|
Args []string `json:"args"`
|
||||||
|
InputFile string `json:"input_file"`
|
||||||
|
OutputFile string `json:"output_file"`
|
||||||
|
Options map[string]string `json:"options,omitempty"`
|
||||||
|
Metadata map[string]string `json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditableJob extends Job with editing capabilities
|
||||||
|
type EditableJob struct {
|
||||||
|
*Job
|
||||||
|
EditStatus EditJobStatus `json:"edit_status"`
|
||||||
|
EditHistory []EditHistoryEntry `json:"edit_history"`
|
||||||
|
OriginalCommand *FFmpegCommand `json:"original_command"`
|
||||||
|
CurrentCommand *FFmpegCommand `json:"current_command"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditJobManager manages job editing operations
|
||||||
|
type EditJobManager interface {
|
||||||
|
// GetEditableJob returns an editable version of a job
|
||||||
|
GetEditableJob(id string) (*EditableJob, error)
|
||||||
|
|
||||||
|
// UpdateJobCommand updates a job's FFmpeg command
|
||||||
|
UpdateJobCommand(id string, newCommand *FFmpegCommand, reason string) error
|
||||||
|
|
||||||
|
// ValidateCommand validates an FFmpeg command
|
||||||
|
ValidateCommand(cmd *FFmpegCommand) error
|
||||||
|
|
||||||
|
// GetEditHistory returns the edit history for a job
|
||||||
|
GetEditHistory(id string) ([]EditHistoryEntry, error)
|
||||||
|
|
||||||
|
// ApplyEdit applies pending edits to a job
|
||||||
|
ApplyEdit(id string) error
|
||||||
|
|
||||||
|
// ResetToOriginal resets a job to its original command
|
||||||
|
ResetToOriginal(id string) error
|
||||||
|
|
||||||
|
// CreateEditableJob creates a new editable job
|
||||||
|
CreateEditableJob(job *Job, cmd *FFmpegCommand) (*EditableJob, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// editJobManager implements EditJobManager
|
||||||
|
type editJobManager struct {
|
||||||
|
queue *Queue
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEditJobManager creates a new edit job manager
|
||||||
|
func NewEditJobManager(queue *Queue) EditJobManager {
|
||||||
|
return &editJobManager{queue: queue}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEditableJob returns an editable version of a job
|
||||||
|
func (e *editJobManager) GetEditableJob(id string) (*EditableJob, error) {
|
||||||
|
job, err := e.queue.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
editable := &EditableJob{
|
||||||
|
Job: job,
|
||||||
|
EditStatus: EditJobStatusOriginal,
|
||||||
|
EditHistory: make([]EditHistoryEntry, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract current command from job config if available
|
||||||
|
if cmd, err := e.extractCommandFromJob(job); err == nil {
|
||||||
|
editable.OriginalCommand = cmd
|
||||||
|
editable.CurrentCommand = cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
return editable, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateJobCommand updates a job's FFmpeg command
|
||||||
|
func (e *editJobManager) UpdateJobCommand(id string, newCommand *FFmpegCommand, reason string) error {
|
||||||
|
job, err := e.queue.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the new command
|
||||||
|
if err := e.ValidateCommand(newCommand); err != nil {
|
||||||
|
return fmt.Errorf("invalid command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create history entry
|
||||||
|
oldCmd, _ := e.extractCommandFromJob(job)
|
||||||
|
history := EditHistoryEntry{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
OldCommand: oldCmd,
|
||||||
|
NewCommand: newCommand,
|
||||||
|
ChangeReason: reason,
|
||||||
|
Applied: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update job config with new command
|
||||||
|
if job.Config == nil {
|
||||||
|
job.Config = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
job.Config["ffmpeg_command"] = newCommand
|
||||||
|
|
||||||
|
// Update job metadata
|
||||||
|
job.Config["last_edited"] = time.Now().Format(time.RFC3339)
|
||||||
|
job.Config["edit_reason"] = reason
|
||||||
|
|
||||||
|
// Add to edit history
|
||||||
|
editHistory := []EditHistoryEntry{history}
|
||||||
|
if existingHistoryInterface, exists := job.Config["edit_history"]; exists {
|
||||||
|
if historyBytes, err := json.Marshal(existingHistoryInterface); err == nil {
|
||||||
|
var existingHistory []EditHistoryEntry
|
||||||
|
if err := json.Unmarshal(historyBytes, &existingHistory); err == nil {
|
||||||
|
editHistory = append(existingHistory, history)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
job.Config["edit_history"] = editHistory
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateCommand validates an FFmpeg command
|
||||||
|
func (e *editJobManager) ValidateCommand(cmd *FFmpegCommand) error {
|
||||||
|
if cmd == nil {
|
||||||
|
return fmt.Errorf("command cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Executable == "" {
|
||||||
|
return fmt.Errorf("executable cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cmd.Args) == 0 {
|
||||||
|
return fmt.Errorf("command arguments cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic validation for input/output files
|
||||||
|
if cmd.InputFile != "" && !strings.Contains(cmd.InputFile, "INPUT") {
|
||||||
|
// Check if input file path is valid (basic check)
|
||||||
|
if strings.HasPrefix(cmd.InputFile, "-") {
|
||||||
|
return fmt.Errorf("input file cannot start with '-'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.OutputFile != "" && !strings.Contains(cmd.OutputFile, "OUTPUT") {
|
||||||
|
// Check if output file path is valid (basic check)
|
||||||
|
if strings.HasPrefix(cmd.OutputFile, "-") {
|
||||||
|
return fmt.Errorf("output file cannot start with '-'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEditHistory returns the edit history for a job
|
||||||
|
func (e *editJobManager) GetEditHistory(id string) ([]EditHistoryEntry, error) {
|
||||||
|
job, err := e.queue.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract history from job config
|
||||||
|
if historyInterface, exists := job.Config["edit_history"]; exists {
|
||||||
|
if historyBytes, err := json.Marshal(historyInterface); err == nil {
|
||||||
|
var history []EditHistoryEntry
|
||||||
|
if err := json.Unmarshal(historyBytes, &history); err == nil {
|
||||||
|
return history, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return make([]EditHistoryEntry, 0), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyEdit applies pending edits to a job
|
||||||
|
func (e *editJobManager) ApplyEdit(id string) error {
|
||||||
|
job, err := e.queue.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark edit as applied
|
||||||
|
if job.Config == nil {
|
||||||
|
job.Config = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
job.Config["edit_applied"] = time.Now().Format(time.RFC3339)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetToOriginal resets a job to its original command
|
||||||
|
func (e *editJobManager) ResetToOriginal(id string) error {
|
||||||
|
job, err := e.queue.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get original command from job config
|
||||||
|
if originalInterface, exists := job.Config["original_command"]; exists {
|
||||||
|
if job.Config == nil {
|
||||||
|
job.Config = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
job.Config["ffmpeg_command"] = originalInterface
|
||||||
|
job.Config["reset_to_original"] = time.Now().Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateEditableJob creates a new editable job
|
||||||
|
func (e *editJobManager) CreateEditableJob(job *Job, cmd *FFmpegCommand) (*EditableJob, error) {
|
||||||
|
if err := e.ValidateCommand(cmd); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
editable := &EditableJob{
|
||||||
|
Job: job,
|
||||||
|
EditStatus: EditJobStatusOriginal,
|
||||||
|
EditHistory: make([]EditHistoryEntry, 0),
|
||||||
|
OriginalCommand: cmd,
|
||||||
|
CurrentCommand: cmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store command in job config
|
||||||
|
if job.Config == nil {
|
||||||
|
job.Config = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
job.Config["ffmpeg_command"] = cmd
|
||||||
|
job.Config["original_command"] = cmd
|
||||||
|
|
||||||
|
return editable, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractCommandFromJob extracts FFmpeg command from job config
|
||||||
|
func (e *editJobManager) extractCommandFromJob(job *Job) (*FFmpegCommand, error) {
|
||||||
|
if job.Config == nil {
|
||||||
|
return nil, fmt.Errorf("job has no config")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmdInterface, exists := job.Config["ffmpeg_command"]; exists {
|
||||||
|
if cmdBytes, err := json.Marshal(cmdInterface); err == nil {
|
||||||
|
var cmd FFmpegCommand
|
||||||
|
if err := json.Unmarshal(cmdBytes, &cmd); err == nil {
|
||||||
|
return &cmd, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("no ffmpeg command found in job config")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSON converts FFmpegCommand to JSON string
|
||||||
|
func (cmd *FFmpegCommand) ToJSON() string {
|
||||||
|
data, err := json.MarshalIndent(cmd, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return "{}"
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromJSON creates FFmpegCommand from JSON string
|
||||||
|
func FFmpegCommandFromJSON(jsonStr string) (*FFmpegCommand, error) {
|
||||||
|
var cmd FFmpegCommand
|
||||||
|
err := json.Unmarshal([]byte(jsonStr), &cmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid JSON: %w", err)
|
||||||
|
}
|
||||||
|
return &cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToFullCommand converts FFmpegCommand to full command string
|
||||||
|
func (cmd *FFmpegCommand) ToFullCommand() string {
|
||||||
|
if cmd == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{cmd.Executable}
|
||||||
|
args = append(args, cmd.Args...)
|
||||||
|
|
||||||
|
if cmd.InputFile != "" {
|
||||||
|
args = append(args, "-i", cmd.InputFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.OutputFile != "" {
|
||||||
|
args = append(args, cmd.OutputFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(args, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateCommandStructure performs deeper validation of command structure
|
||||||
|
func ValidateCommandStructure(cmd *FFmpegCommand) error {
|
||||||
|
if cmd == nil {
|
||||||
|
return fmt.Errorf("command cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for common FFmpeg patterns
|
||||||
|
hasInput := false
|
||||||
|
hasOutput := false
|
||||||
|
|
||||||
|
for _, arg := range cmd.Args {
|
||||||
|
if arg == "-i" && cmd.InputFile != "" {
|
||||||
|
hasInput = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.InputFile != "" {
|
||||||
|
hasInput = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.OutputFile != "" {
|
||||||
|
hasOutput = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasInput {
|
||||||
|
return fmt.Errorf("command must specify an input file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasOutput {
|
||||||
|
return fmt.Errorf("command must specify an output file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for conflicting options
|
||||||
|
if cmd.Options != nil {
|
||||||
|
if overwrite, exists := cmd.Options["overwrite"]; exists && overwrite == "false" {
|
||||||
|
if cmd.OutputFile != "" && !strings.Contains(cmd.OutputFile, "OUTPUT") {
|
||||||
|
// Real file path with overwrite disabled
|
||||||
|
return fmt.Errorf("cannot overwrite existing file with overwrite disabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
113
internal/queue/execute_edit_job.go.wip
Normal file
113
internal/queue/execute_edit_job.go.wip
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
package queue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"git.leaktechnologies.dev/stu/VideoTools/internal/logging"
|
||||||
|
"git.leaktechnologies.dev/stu/VideoTools/internal/ui/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExecuteEditJob executes an editable job with dynamic FFmpeg command
|
||||||
|
func ExecuteEditJob(ctx context.Context, job *Job, progressCallback func(float64), ffmpegPath string) error {
|
||||||
|
logging.Debug(logging.CatSystem, "executing edit job %s: %s", job.ID, job.Title)
|
||||||
|
|
||||||
|
// Get FFmpeg command from job config
|
||||||
|
if job.Config == nil {
|
||||||
|
return fmt.Errorf("edit job has no config")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdInterface, exists := job.Config["ffmpeg_command"]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("edit job has no ffmpeg_command in config")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to FFmpegCommand
|
||||||
|
var cmd queue.FFmpegCommand
|
||||||
|
if cmdBytes, err := json.Marshal(cmdInterface); err == nil {
|
||||||
|
if err := json.Unmarshal(cmdBytes, &cmd); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse FFmpeg command: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("failed to serialize FFmpeg command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate command
|
||||||
|
editManager := queue.NewEditJobManager(s.jobQueue)
|
||||||
|
if err := editManager.ValidateCommand(&cmd); err != nil {
|
||||||
|
return fmt.Errorf("invalid FFmpeg command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build final command args
|
||||||
|
finalArgs := cmd.Args
|
||||||
|
if cmd.InputFile != "" {
|
||||||
|
finalArgs = append([]string{"-i", cmd.InputFile}, finalArgs...)
|
||||||
|
}
|
||||||
|
if cmd.OutputFile != "" {
|
||||||
|
finalArgs = append(finalArgs, cmd.OutputFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute FFmpeg command
|
||||||
|
ffmpegPath := utils.GetFFmpegPath()
|
||||||
|
fullCmd := append([]string{ffmpegPath}, finalArgs...)
|
||||||
|
|
||||||
|
logging.Info(logging.CatFFMPEG, "Executing edit job: %v", fullCmd)
|
||||||
|
|
||||||
|
// Create and execute command
|
||||||
|
execCmd := exec.CommandContext(ctx, fullCmd[0], fullCmd[1:]...)
|
||||||
|
|
||||||
|
// Set up pipes for stdout/stderr
|
||||||
|
stdout, err := execCmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create stdout pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr, err := execCmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create stderr pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start command
|
||||||
|
if err := execCmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("failed to start FFmpeg: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse output for progress
|
||||||
|
progressParser := utils.NewFFmpegProgressParser()
|
||||||
|
|
||||||
|
// Combine stdout and stderr for processing
|
||||||
|
go func() {
|
||||||
|
scanner := bufio.NewScanner(stdout)
|
||||||
|
for scanner.Scan() {
|
||||||
|
if progress := progressParser.ParseLine(scanner.Text()); progress >= 0 {
|
||||||
|
progressCallback(progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
scanner := bufio.NewScanner(stderr)
|
||||||
|
for scanner.Scan() {
|
||||||
|
if progress := progressParser.ParseLine(scanner.Text()); progress >= 0 {
|
||||||
|
progressCallback(progress)
|
||||||
|
}
|
||||||
|
// Log stderr for debugging
|
||||||
|
logging.Debug(logging.CatFFMPEG, "FFmpeg stderr: %s", scanner.Text())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for command to complete
|
||||||
|
err = execCmd.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("FFmpeg execution failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark job as completed
|
||||||
|
progressCallback(100.0)
|
||||||
|
logging.Info(logging.CatFFMPEG, "Edit job %s completed successfully", job.ID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
352
internal/ui/command_editor.go
Normal file
352
internal/ui/command_editor.go
Normal file
|
|
@ -0,0 +1,352 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
3
main.go
3
main.go
|
|
@ -72,7 +72,7 @@ var (
|
||||||
logsDirOnce sync.Once
|
logsDirOnce sync.Once
|
||||||
logsDirPath string
|
logsDirPath string
|
||||||
feedbackBundler = utils.NewFeedbackBundler()
|
feedbackBundler = utils.NewFeedbackBundler()
|
||||||
appVersion = "v0.1.0-dev21"
|
appVersion = "v0.1.0-dev22"
|
||||||
|
|
||||||
hwAccelProbeOnce sync.Once
|
hwAccelProbeOnce sync.Once
|
||||||
hwAccelSupported atomic.Value // map[string]bool
|
hwAccelSupported atomic.Value // map[string]bool
|
||||||
|
|
@ -5724,7 +5724,6 @@ func (s *appState) executeUpscaleJob(ctx context.Context, job *queue.Job, progre
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// buildFFmpegCommandFromJob builds an FFmpeg command string from a queue job with INPUT/OUTPUT placeholders
|
// buildFFmpegCommandFromJob builds an FFmpeg command string from a queue job with INPUT/OUTPUT placeholders
|
||||||
func buildFFmpegCommandFromJob(job *queue.Job) string {
|
func buildFFmpegCommandFromJob(job *queue.Job) string {
|
||||||
if job == nil || job.Config == nil {
|
if job == nil || job.Config == nil {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user