Aggiornamento interfaccia e gestione lavori video

* Aggiornato il titolo e le dimensioni della finestra principale.
* Riorganizzata la struttura dell'interfaccia con nuovi controlli.
* Implementata la classe `VideoProcessingService` per gestire la coda di elaborazione.
* Aggiunta la finestra di configurazione per i lavori selezionati.
* Introdotta la classe `StatusColorConverter` per la visualizzazione degli stati.
* Modifiche ai file di impostazioni per nuove opzioni di configurazione.
* Aggiornamento alla versione 1.0.0.0
This commit is contained in:
Alberto Balbo
2025-09-07 01:03:46 +02:00
parent bb5b0f2d52
commit 91695f350c
11 changed files with 1500 additions and 198 deletions

View File

@@ -0,0 +1,33 @@
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
using Ganimede.Models;
namespace Ganimede
{
public class StatusColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is JobStatus status)
{
return status switch
{
JobStatus.Pending => new SolidColorBrush(Colors.Orange),
JobStatus.Processing => new SolidColorBrush(Colors.LightBlue),
JobStatus.Completed => new SolidColorBrush(Colors.LightGreen),
JobStatus.Failed => new SolidColorBrush(Colors.Red),
JobStatus.Cancelled => new SolidColorBrush(Colors.Gray),
_ => new SolidColorBrush(Colors.White)
};
}
return new SolidColorBrush(Colors.White);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@@ -5,9 +5,13 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Ganimede"
mc:Ignorable="d"
Title="Frame Extractor" Height="600" Width="900"
Title="Ganimede - Frame Extractor" Height="800" Width="1200"
Background="#222">
<Grid Margin="40">
<Window.Resources>
<local:StatusColorConverter x:Key="StatusColorConverter"/>
</Window.Resources>
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
@@ -15,34 +19,145 @@
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Main Content Area -->
<Grid Grid.Row="0" Grid.RowSpan="4" Grid.Column="0" Margin="0,0,20,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Drag & Drop Area -->
<Border x:Name="DragDropArea" Grid.Row="0" Height="180" CornerRadius="16" BorderBrush="#444" BorderThickness="2" Background="#282828" AllowDrop="True" Drop="DragDropArea_Drop" MouseEnter="DragDropArea_MouseEnter" MouseLeave="DragDropArea_MouseLeave">
<TextBlock Text="Drag and drop video here" Foreground="#AAA" FontSize="22" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Border x:Name="DragDropArea" Grid.Row="0" Height="160" CornerRadius="16" BorderBrush="#444" BorderThickness="2"
Background="#282828" AllowDrop="True" Drop="DragDropArea_Drop" DragEnter="DragDropArea_DragEnter"
MouseEnter="DragDropArea_MouseEnter" MouseLeave="DragDropArea_MouseLeave">
<StackPanel VerticalAlignment="Center">
<TextBlock Text="Drag and drop videos here" Foreground="#AAA" FontSize="18" HorizontalAlignment="Center"/>
<TextBlock Text="(Multiple videos supported)" Foreground="#777" FontSize="12" HorizontalAlignment="Center" Margin="0,5,0,0"/>
</StackPanel>
</Border>
<!-- Buttons -->
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,24,0,0" >
<Button x:Name="BrowseVideoButton" Content="Browse Video" Width="160" Height="48" Margin="0,0,16,0" Click="BrowseVideoButton_Click"/>
<Button x:Name="SelectOutputFolderButton" Content="Select Output Folder" Width="160" Height="48" Click="SelectOutputFolderButton_Click"/>
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,20,0,0">
<Button x:Name="BrowseVideoButton" Content="Browse Videos" Width="120" Height="40" Margin="0,0,10,0" Click="BrowseVideoButton_Click"/>
<Button x:Name="SelectOutputFolderButton" Content="Output Folder" Width="120" Height="40" Margin="0,0,10,0" Click="SelectOutputFolderButton_Click"/>
<Button x:Name="SettingsButton" Content="Settings" Width="80" Height="40" Margin="0,0,10,0" Click="SettingsButton_Click"/>
</StackPanel>
<!-- Extract Frames Button -->
<Button Grid.Row="2" x:Name="ExtractFramesButton" Content="Extract Frames" Width="220" Height="54" HorizontalAlignment="Center" Margin="0,24,0,0" Click="ExtractFramesButton_Click"/>
<!-- Progress & Thumbnails -->
<StackPanel Grid.Row="3" Margin="0,32,0,0">
<!-- Progress & Status -->
<StackPanel Grid.Row="2" Margin="0,20,0,0">
<ProgressBar x:Name="ProgressBar" Height="24" Minimum="0" Maximum="100" Value="0" Background="#333" Foreground="#4FC3F7"/>
<TextBlock x:Name="StatusText" Foreground="#AAA" FontSize="16" Margin="0,12,0,0"/>
<ItemsControl x:Name="ThumbnailsPanel" Margin="0,24,0,0" Height="120" HorizontalAlignment="Center">
<TextBlock x:Name="StatusText" Foreground="#AAA" FontSize="14" Margin="0,10,0,0" TextWrapping="Wrap"/>
</StackPanel>
<!-- Thumbnails -->
<ScrollViewer Grid.Row="3" Margin="0,20,0,0" VerticalScrollBarVisibility="Auto">
<ItemsControl x:Name="ThumbnailsPanel">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="2" BorderBrush="#555" BorderThickness="1" CornerRadius="4">
<Image Source="{Binding}" Width="160" Height="90" Stretch="UniformToFill"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
<!-- Queue Panel -->
<Grid Grid.Row="0" Grid.RowSpan="4" Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Queue Header -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="Processing Queue" FontSize="16" FontWeight="Bold" Foreground="White" VerticalAlignment="Center"/>
<TextBlock x:Name="QueueCountText" Text="(0 items)" Foreground="#AAA" FontSize="12" Margin="10,0,0,0" VerticalAlignment="Center"/>
</StackPanel>
<!-- Queue Controls (moved above the list) -->
<StackPanel Grid.Row="1" Margin="0,0,0,10">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,5">
<Button x:Name="StartQueueButton" Content="▶ Start" Width="80" Height="30" Margin="0,0,5,0"
Click="StartQueueButton_Click" Background="#4FC3F7" Foreground="White" FontSize="12"/>
<Button x:Name="StopQueueButton" Content="⏹ Stop" Width="80" Height="30"
Click="StopQueueButton_Click" Background="#DC3545" Foreground="White" FontSize="12" IsEnabled="False"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button x:Name="ConfigureSelectedButton" Content="⚙ Configure" Width="80" Height="25" Margin="0,0,5,0"
Click="ConfigureSelectedButton_Click" FontSize="11" IsEnabled="False"/>
<Button x:Name="ClearCompletedButton" Content="🗑 Clear Done" Width="80" Height="25"
Click="ClearCompletedButton_Click" FontSize="11"/>
</StackPanel>
</StackPanel>
<!-- Queue List -->
<ScrollViewer Grid.Row="2" VerticalScrollBarVisibility="Auto">
<ItemsControl x:Name="QueueItemsControl">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="#333" Margin="0,0,0,5" Padding="8" CornerRadius="5">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Selection checkbox -->
<CheckBox x:Name="JobCheckBox" Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="0,0,8,0"
Checked="JobCheckBox_CheckedChanged" Unchecked="JobCheckBox_CheckedChanged" Tag="{Binding}"/>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding VideoName}" Foreground="White" FontWeight="SemiBold" TextTrimming="CharacterEllipsis"/>
<Button Grid.Row="0" Grid.Column="2" Content="×" Width="18" Height="18" FontSize="12"
Click="RemoveQueueItem_Click" Background="Transparent" Foreground="#FF6B6B" BorderThickness="0"
Tag="{Binding}" ToolTip="Remove from queue"/>
<TextBlock Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Text="{Binding StatusMessage}" Foreground="#AAA" FontSize="10" Margin="0,2,0,0"/>
<ProgressBar Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" Height="3" Margin="0,3,0,0" Value="{Binding Progress}" Background="#555" Foreground="#4FC3F7"/>
<TextBlock Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3" Text="{Binding Status}" Foreground="{Binding Status, Converter={StaticResource StatusColorConverter}}" FontSize="9" Margin="0,2,0,0"/>
<!-- Job specific settings display -->
<TextBlock Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="3" FontSize="8" Foreground="#666" Margin="0,2,0,0">
<TextBlock.Text>
<MultiBinding StringFormat="{}📁 {0} | 📐 {1} | 🔄 {2}">
<Binding Path="OutputFolderDisplay"/>
<Binding Path="FrameSizeDisplay"/>
<Binding Path="OverwriteModeDisplay"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<!-- Clear All Button -->
<Button Grid.Row="3" x:Name="ClearAllButton" Content="🗑 Clear All" Height="30" Margin="0,10,0,0"
Click="ClearAllButton_Click" Background="#6C757D" Foreground="White"/>
</Grid>
</Grid>
</Window>

View File

@@ -5,10 +5,20 @@ using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
using Microsoft.Win32;
using FFMpegCore;
using System.Diagnostics;
using System.Linq;
using System.Collections.Generic;
using FFMpegCore;
using Ganimede.Properties;
using Ganimede.Services;
using Ganimede.Models;
using Ganimede.Windows;
using WpfMessageBox = System.Windows.MessageBox;
using WpfOpenFileDialog = Microsoft.Win32.OpenFileDialog;
using WpfButton = System.Windows.Controls.Button;
using WpfDataFormats = System.Windows.DataFormats;
using WpfDragEventArgs = System.Windows.DragEventArgs;
using WpfDragDropEffects = System.Windows.DragDropEffects;
namespace Ganimede
{
@@ -17,64 +27,135 @@ namespace Ganimede
/// </summary>
public partial class MainWindow : Window
{
private string? videoPath;
private string? outputFolder;
private ObservableCollection<BitmapImage> thumbnails = new();
private readonly VideoProcessingService _processingService = new();
private readonly ObservableCollection<BitmapImage> thumbnails = new();
private readonly List<VideoJob> _selectedJobs = new();
public MainWindow()
{
InitializeComponent();
ThumbnailsPanel.ItemsSource = thumbnails;
Debug.WriteLine("[INIT] MainWindow initialized.");
// Carica i percorsi salvati
outputFolder = Settings.Default.LastOutputFolder;
videoPath = Settings.Default.LastVideoPath;
if (!string.IsNullOrEmpty(outputFolder))
StatusText.Text = $"Last output folder: {outputFolder}";
if (!string.IsNullOrEmpty(videoPath))
StatusText.Text += $"\nLast video: {System.IO.Path.GetFileName(videoPath)}";
// Configura FFMpegCore con percorso binari
InitializeUI();
ConfigureFFMpeg();
}
private void InitializeUI()
{
ThumbnailsPanel.ItemsSource = thumbnails;
QueueItemsControl.ItemsSource = _processingService.JobQueue;
// Load saved settings
outputFolder = Settings.Default.LastOutputFolder;
if (!string.IsNullOrEmpty(outputFolder))
StatusText.Text = $"Output folder: {outputFolder}";
// Subscribe to processing service events
_processingService.JobCompleted += OnJobCompleted;
_processingService.JobFailed += OnJobFailed;
_processingService.ProcessingStarted += OnProcessingStarted;
_processingService.ProcessingStopped += OnProcessingStopped;
_processingService.JobQueue.CollectionChanged += (s, e) => UpdateQueueCount();
Debug.WriteLine("[INIT] MainWindow initialized with queue support.");
}
private void UpdateQueueCount()
{
Dispatcher.Invoke(() =>
{
var count = _processingService.JobQueue.Count;
QueueCountText.Text = $"({count} items)";
});
}
private void OnProcessingStarted()
{
Dispatcher.Invoke(() =>
{
StartQueueButton.IsEnabled = false;
StopQueueButton.IsEnabled = true;
StatusText.Text = "Processing queue started...";
});
}
private void OnProcessingStopped()
{
Dispatcher.Invoke(() =>
{
StartQueueButton.IsEnabled = true;
StopQueueButton.IsEnabled = false;
StatusText.Text = "Processing queue stopped.";
});
}
private void OnJobCompleted(VideoJob job)
{
Dispatcher.Invoke(() =>
{
StatusText.Text = $"✓ Completed: {job.VideoName}";
// Load thumbnails for the completed job
LoadThumbnailsFromFolder(job.OutputFolder);
});
}
private void OnJobFailed(VideoJob job)
{
Dispatcher.Invoke(() =>
{
StatusText.Text = $"✗ Failed: {job.VideoName} - {job.StatusMessage}";
});
}
private void LoadThumbnailsFromFolder(string folder)
{
try
{
if (!Directory.Exists(folder)) return;
var imageFiles = Directory.GetFiles(folder, "*.png")
.OrderBy(f => f)
.Take(20) // Load max 20 thumbnails for preview
.ToList();
foreach (var imagePath in imageFiles)
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = new Uri(imagePath);
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
thumbnails.Add(bitmap);
}
}
catch (Exception ex)
{
Debug.WriteLine($"[ERROR] Failed to load thumbnails: {ex.Message}");
}
}
private void ConfigureFFMpeg()
{
var ffmpegBin = Settings.Default.FFmpegBinFolder;
// Verifica se i binari esistono nella cartella specificata
if (!string.IsNullOrEmpty(ffmpegBin) && ValidateFFMpegBinaries(ffmpegBin))
{
FFMpegCore.GlobalFFOptions.Configure(options => options.BinaryFolder = ffmpegBin);
Debug.WriteLine($"[CONFIG] FFMpeg bin folder set: {ffmpegBin}");
StatusText.Text += "\n[SUCCESS] FFMpeg configured successfully.";
}
else
{
// Prova a utilizzare FFMpeg dal PATH di sistema
if (TryUseSystemFFMpeg())
else if (TryUseSystemFFMpeg())
{
Debug.WriteLine("[CONFIG] Using system FFMpeg from PATH");
StatusText.Text += "\n[INFO] Using system FFMpeg installation.";
}
else
{
// Se manca solo ffmpeg.exe, copia da ffprobe.exe se possibile
if (TryFixMissingFFMpeg(ffmpegBin))
else if (TryFixMissingFFMpeg(ffmpegBin))
{
FFMpegCore.GlobalFFOptions.Configure(options => options.BinaryFolder = ffmpegBin);
Debug.WriteLine($"[CONFIG] FFMpeg fixed and configured: {ffmpegBin}");
StatusText.Text += "\n[FIXED] Missing ffmpeg.exe resolved.";
}
else
{
StatusText.Text += "\n[ERROR] FFMpeg not properly configured. Please ensure ffmpeg.exe is available.";
Debug.WriteLine("[ERROR] FFMpeg configuration failed.");
}
}
}
}
private bool ValidateFFMpegBinaries(string binFolder)
{
@@ -84,20 +165,13 @@ namespace Ganimede
var ffmpegPath = Path.Combine(binFolder, "ffmpeg.exe");
var ffprobePath = Path.Combine(binFolder, "ffprobe.exe");
bool ffmpegExists = File.Exists(ffmpegPath);
bool ffprobeExists = File.Exists(ffprobePath);
Debug.WriteLine($"[CHECK] ffmpeg.exe exists: {ffmpegExists}");
Debug.WriteLine($"[CHECK] ffprobe.exe exists: {ffprobeExists}");
return ffmpegExists && ffprobeExists;
return File.Exists(ffmpegPath) && File.Exists(ffprobePath);
}
private bool TryUseSystemFFMpeg()
{
try
{
// Verifica se ffmpeg è disponibile nel PATH di sistema
var processInfo = new ProcessStartInfo
{
FileName = "ffmpeg",
@@ -124,8 +198,6 @@ namespace Ganimede
var ffmpegPath = Path.Combine(binFolder, "ffmpeg.exe");
var ffprobePath = Path.Combine(binFolder, "ffprobe.exe");
// Se esiste ffprobe.exe ma non ffmpeg.exe, prova a copiare ffprobe come ffmpeg
// (questo è un workaround temporaneo - ffprobe può fare alcune operazioni di ffmpeg)
if (!File.Exists(ffmpegPath) && File.Exists(ffprobePath))
{
try
@@ -147,172 +219,294 @@ namespace Ganimede
private void BrowseVideoButton_Click(object sender, RoutedEventArgs e)
{
Debug.WriteLine("[UI] BrowseVideoButton_Click invoked.");
var dialog = new Microsoft.Win32.OpenFileDialog { Filter = "Video files (*.mp4;*.avi;*.mov)|*.mp4;*.avi;*.mov|All files (*.*)|*.*" };
var dialog = new WpfOpenFileDialog
{
Filter = "Video files (*.mp4;*.avi;*.mov;*.mkv;*.wmv)|*.mp4;*.avi;*.mov;*.mkv;*.wmv|All files (*.*)|*.*",
Multiselect = true
};
if (dialog.ShowDialog() == true)
{
videoPath = dialog.FileName;
StatusText.Text = $"Selected video: {Path.GetFileName(videoPath)}";
Settings.Default.LastVideoPath = videoPath;
Settings.Default.Save();
Debug.WriteLine($"[INFO] Video selected: {videoPath}");
AddVideosToQueue(dialog.FileNames);
}
else
}
private void AddVideosToQueue(string[] videoPaths)
{
Debug.WriteLine("[INFO] Video selection cancelled.");
if (string.IsNullOrEmpty(outputFolder))
{
WpfMessageBox.Show("Please select an output folder first.", "Output Folder Required",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var createSubfolder = Settings.Default.CreateSubfolder;
foreach (var videoPath in videoPaths)
{
_processingService.AddJob(videoPath, outputFolder, createSubfolder);
Debug.WriteLine($"[QUEUE] Added video to queue: {Path.GetFileName(videoPath)}");
}
StatusText.Text = $"Added {videoPaths.Length} video(s) to queue (Pending)";
Settings.Default.LastVideoPath = videoPaths.FirstOrDefault();
Settings.Default.Save();
}
private void SelectOutputFolderButton_Click(object sender, RoutedEventArgs e)
{
Debug.WriteLine("[UI] SelectOutputFolderButton_Click invoked.");
using (var dialog = new System.Windows.Forms.FolderBrowserDialog())
{
using var dialog = new System.Windows.Forms.FolderBrowserDialog();
if (!string.IsNullOrEmpty(outputFolder))
dialog.SelectedPath = outputFolder;
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
outputFolder = dialog.SelectedPath;
StatusText.Text = $"Selected output folder: {outputFolder}";
StatusText.Text = $"Output folder: {outputFolder}";
Settings.Default.LastOutputFolder = outputFolder;
Settings.Default.Save();
Debug.WriteLine($"[INFO] Output folder selected: {outputFolder}");
}
else
{
Debug.WriteLine("[INFO] Output folder selection cancelled.");
}
private void SettingsButton_Click(object sender, RoutedEventArgs e)
{
var settingsWindow = new SettingsWindow
{
Owner = this
};
if (settingsWindow.ShowDialog() == true)
{
// Reconfigure FFMpeg if settings changed
ConfigureFFMpeg();
StatusText.Text = "Settings updated successfully";
}
}
private async void ExtractFramesButton_Click(object sender, RoutedEventArgs e)
private async void StartQueueButton_Click(object sender, RoutedEventArgs e)
{
Debug.WriteLine("[UI] ExtractFramesButton_Click invoked.");
if (string.IsNullOrEmpty(videoPath) || string.IsNullOrEmpty(outputFolder))
if (_processingService.JobQueue.Count == 0)
{
StatusText.Text = "Please select a video and output folder.";
Debug.WriteLine("[ERROR] Video path or output folder not set.");
WpfMessageBox.Show("No videos in queue. Please add some videos first.", "Queue Empty",
MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
ExtractFramesButton.IsEnabled = false;
ProgressBar.Value = 0;
var pendingJobs = _processingService.JobQueue.Count(job => job.Status == JobStatus.Pending);
if (pendingJobs == 0)
{
WpfMessageBox.Show("No pending videos in queue.", "No Pending Jobs",
MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
Debug.WriteLine("[QUEUE] Starting queue processing manually");
await _processingService.StartProcessingAsync();
}
private void StopQueueButton_Click(object sender, RoutedEventArgs e)
{
_processingService.StopProcessing();
StatusText.Text = "Stopping queue processing...";
Debug.WriteLine("[QUEUE] Stop processing requested by user");
}
private void JobCheckBox_CheckedChanged(object sender, RoutedEventArgs e)
{
UpdateSelectedJobs();
}
private void UpdateSelectedJobs()
{
_selectedJobs.Clear();
// Find all checked checkboxes in the ItemsControl
var checkBoxes = FindVisualChildren<System.Windows.Controls.CheckBox>(QueueItemsControl).Where(cb => cb.Name == "JobCheckBox");
foreach (var checkBox in checkBoxes)
{
if (checkBox.IsChecked == true && checkBox.Tag is VideoJob job)
{
_selectedJobs.Add(job);
}
}
// Update configure button state
ConfigureSelectedButton.IsEnabled = _selectedJobs.Count > 0;
Debug.WriteLine($"[SELECTION] {_selectedJobs.Count} jobs selected");
}
private void ConfigureSelectedButton_Click(object sender, RoutedEventArgs e)
{
if (_selectedJobs.Count == 0)
return;
var configWindow = new JobConfigWindow(_selectedJobs.ToList())
{
Owner = this
};
if (configWindow.ShowDialog() == true)
{
StatusText.Text = $"Configuration applied to {_selectedJobs.Count} job(s)";
// Reset job output folders for those without custom settings
foreach (var job in _selectedJobs.Where(j => string.IsNullOrEmpty(j.CustomOutputFolder)))
{
var createSubfolder = Settings.Default.CreateSubfolder;
job.OutputFolder = createSubfolder
? Path.Combine(outputFolder, job.VideoName)
: outputFolder;
}
}
}
private void RemoveQueueItem_Click(object sender, RoutedEventArgs e)
{
if (sender is WpfButton button && button.Tag is VideoJob job)
{
_processingService.CancelJob(job);
if (job.Status == JobStatus.Cancelled || job.Status == JobStatus.Pending)
{
_processingService.JobQueue.Remove(job);
}
}
}
private void ClearCompletedButton_Click(object sender, RoutedEventArgs e)
{
_processingService.RemoveCompletedJobs();
StatusText.Text = "Completed jobs cleared";
}
private void ClearAllButton_Click(object sender, RoutedEventArgs e)
{
var processingJobs = _processingService.JobQueue.Where(j => j.Status == JobStatus.Processing).ToList();
if (processingJobs.Count > 0)
{
var result = WpfMessageBox.Show(
$"There are {processingJobs.Count} job(s) currently processing.\n\n" +
"Yes: Stop all processes and clear queue\n" +
"No: Clear only completed/pending jobs\n" +
"Cancel: Don't clear anything",
"Jobs in Progress",
MessageBoxButton.YesNoCancel,
MessageBoxImage.Question);
switch (result)
{
case MessageBoxResult.Yes:
// Stop processing and clear all
_processingService.StopProcessing();
_processingService.JobQueue.Clear();
thumbnails.Clear();
StatusText.Text = "Analyzing video...";
Debug.WriteLine($"[PROCESS] Starting analysis for video: {videoPath}");
StatusText.Text = "All jobs cleared and processing stopped";
break;
try
case MessageBoxResult.No:
// Clear only non-processing jobs
for (int i = _processingService.JobQueue.Count - 1; i >= 0; i--)
{
var mediaInfo = await FFProbe.AnalyseAsync(videoPath);
Debug.WriteLine($"[INFO] Video duration: {mediaInfo.Duration}, FrameRate: 24");
int frameRate = 24;
int frameCount = (int)mediaInfo.Duration.TotalSeconds * frameRate;
Debug.WriteLine($"[INFO] Total frames to extract: {frameCount}");
for (int i = 0; i < frameCount; i++)
if (_processingService.JobQueue[i].Status != JobStatus.Processing)
{
var frameTime = TimeSpan.FromSeconds((double)i / frameRate);
string framePath = Path.Combine(outputFolder, $"frame_{i:D6}.png");
Debug.WriteLine($"[PROCESS] Extracting frame {i + 1}/{frameCount} at {frameTime}");
try
{
await FFMpegArguments
.FromFileInput(videoPath)
.OutputToFile(framePath, true, options => options
.Seek(frameTime)
.WithFrameOutputCount(1)
.WithVideoCodec("png") // Usa codec PNG invece di ForceFormat
.Resize(320, 180))
.ProcessAsynchronously();
}
catch (Exception frameEx)
{
Debug.WriteLine($"[ERROR] Failed to extract frame {i}: {frameEx.Message}");
// Fallback: prova senza specificare il codec
try
{
await FFMpegArguments
.FromFileInput(videoPath)
.OutputToFile(framePath, true, options => options
.Seek(frameTime)
.WithFrameOutputCount(1)
.Resize(320, 180))
.ProcessAsynchronously();
Debug.WriteLine($"[INFO] Frame extracted successfully with fallback method: {i}");
}
catch (Exception fallbackEx)
{
Debug.WriteLine($"[ERROR] Fallback also failed for frame {i}: {fallbackEx.Message}");
continue; // Salta questo frame e continua con il prossimo
_processingService.JobQueue.RemoveAt(i);
}
}
StatusText.Text = "Cleared completed/pending jobs (processing jobs continue)";
break;
if (File.Exists(framePath))
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = new Uri(framePath);
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
thumbnails.Add(bitmap);
Debug.WriteLine($"[INFO] Frame saved and loaded: {framePath}");
case MessageBoxResult.Cancel:
// Do nothing
return;
}
}
else
{
Debug.WriteLine($"[ERROR] Frame file not found: {framePath}");
}
var result = WpfMessageBox.Show("Are you sure you want to clear all jobs from the queue?",
"Clear All Jobs", MessageBoxButton.YesNo, MessageBoxImage.Question);
ProgressBar.Value = (i + 1) * 100 / frameCount;
StatusText.Text = $"Extracting frames {i + 1}/{frameCount} ({ProgressBar.Value:F1}%) - Processing frame {i + 1}.";
}
StatusText.Text = "Extraction complete!";
Debug.WriteLine("[SUCCESS] Extraction complete.");
}
catch (Exception ex)
if (result == MessageBoxResult.Yes)
{
StatusText.Text = $"Error: {ex.Message}";
Debug.WriteLine($"[EXCEPTION] {ex.GetType()}: {ex.Message}\n{ex.StackTrace}");
_processingService.JobQueue.Clear();
thumbnails.Clear();
StatusText.Text = "All jobs cleared";
}
}
}
ExtractFramesButton.IsEnabled = true;
}
private void DragDropArea_Drop(object sender, System.Windows.DragEventArgs e)
private void DragDropArea_Drop(object sender, WpfDragEventArgs e)
{
Debug.WriteLine("[UI] DragDropArea_Drop invoked.");
if (e.Data.GetDataPresent(System.Windows.DataFormats.FileDrop))
if (e.Data.GetDataPresent(WpfDataFormats.FileDrop))
{
var files = (string[])e.Data.GetData(System.Windows.DataFormats.FileDrop);
if (files.Length > 0)
var files = (string[])e.Data.GetData(WpfDataFormats.FileDrop);
var videoFiles = files.Where(f => IsVideoFile(f)).ToArray();
if (videoFiles.Length > 0)
{
videoPath = files[0];
StatusText.Text = $"Selected video: {Path.GetFileName(videoPath)}";
Settings.Default.LastVideoPath = videoPath;
Settings.Default.Save();
Debug.WriteLine($"[INFO] Video selected via drag & drop: {videoPath}");
AddVideosToQueue(videoFiles);
Debug.WriteLine($"[INFO] {videoFiles.Length} videos added via drag & drop");
}
else
{
Debug.WriteLine("[WARN] Drag & drop did not contain files.");
StatusText.Text = "No video files found in dropped items";
}
}
}
private void DragDropArea_DragEnter(object sender, WpfDragEventArgs e)
{
if (e.Data.GetDataPresent(WpfDataFormats.FileDrop))
{
var files = (string[])e.Data.GetData(WpfDataFormats.FileDrop);
e.Effects = files.Any(IsVideoFile) ? WpfDragDropEffects.Copy : WpfDragDropEffects.None;
}
else
{
Debug.WriteLine("[WARN] Drag & drop did not contain file drop format.");
e.Effects = WpfDragDropEffects.None;
}
}
private void DragDropArea_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
{
DragDropArea.BorderBrush = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(79, 195, 247));
Debug.WriteLine("[UI] DragDropArea_MouseEnter");
}
private void DragDropArea_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
{
DragDropArea.BorderBrush = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(68, 68, 68));
Debug.WriteLine("[UI] DragDropArea_MouseLeave");
}
private static bool IsVideoFile(string filePath)
{
var extension = Path.GetExtension(filePath).ToLowerInvariant();
return extension is ".mp4" or ".avi" or ".mov" or ".mkv" or ".wmv" or ".flv" or ".webm";
}
// Helper method to find visual children
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj != null)
{
for (int i = 0; i < System.Windows.Media.VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = System.Windows.Media.VisualTreeHelper.GetChild(depObj, i);
if (child != null && child is T)
{
yield return (T)child;
}
foreach (T childOfChild in FindVisualChildren<T>(child))
{
yield return childOfChild;
}
}
}
}
}
}

View File

@@ -0,0 +1,129 @@
using System;
using System.ComponentModel;
namespace Ganimede.Models
{
public enum JobStatus
{
Pending,
Processing,
Completed,
Failed,
Cancelled
}
public enum OverwriteMode
{
Ask,
Skip,
Overwrite
}
public class VideoJob : INotifyPropertyChanged
{
private JobStatus _status = JobStatus.Pending;
private double _progress = 0;
private string _statusMessage = "Pending";
private string _customOutputFolder = string.Empty;
private string _customFrameSize = string.Empty;
private OverwriteMode? _customOverwriteMode = null;
private bool _customCreateSubfolder = true;
public required string VideoPath { get; set; }
public string VideoName => System.IO.Path.GetFileNameWithoutExtension(VideoPath);
public required string OutputFolder { get; set; }
public DateTime AddedTime { get; set; } = DateTime.Now;
public JobStatus Status
{
get => _status;
set
{
_status = value;
OnPropertyChanged(nameof(Status));
}
}
public double Progress
{
get => _progress;
set
{
_progress = value;
OnPropertyChanged(nameof(Progress));
}
}
public string StatusMessage
{
get => _statusMessage;
set
{
_statusMessage = value;
OnPropertyChanged(nameof(StatusMessage));
}
}
// Custom settings for individual jobs
public string CustomOutputFolder
{
get => _customOutputFolder;
set
{
_customOutputFolder = value;
OnPropertyChanged(nameof(CustomOutputFolder));
OnPropertyChanged(nameof(OutputFolderDisplay));
}
}
public string CustomFrameSize
{
get => _customFrameSize;
set
{
_customFrameSize = value;
OnPropertyChanged(nameof(CustomFrameSize));
OnPropertyChanged(nameof(FrameSizeDisplay));
}
}
public OverwriteMode? CustomOverwriteMode
{
get => _customOverwriteMode;
set
{
_customOverwriteMode = value;
OnPropertyChanged(nameof(CustomOverwriteMode));
OnPropertyChanged(nameof(OverwriteModeDisplay));
}
}
public bool CustomCreateSubfolder
{
get => _customCreateSubfolder;
set
{
_customCreateSubfolder = value;
OnPropertyChanged(nameof(CustomCreateSubfolder));
OnPropertyChanged(nameof(OutputFolderDisplay));
}
}
// Display properties for UI
public string OutputFolderDisplay =>
string.IsNullOrEmpty(CustomOutputFolder) ? "Default" : System.IO.Path.GetFileName(CustomOutputFolder) + (CustomCreateSubfolder ? "/??" : "");
public string FrameSizeDisplay =>
string.IsNullOrEmpty(CustomFrameSize) ? "Default" : CustomFrameSize;
public string OverwriteModeDisplay =>
CustomOverwriteMode?.ToString() ?? "Default";
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@@ -58,5 +58,41 @@ namespace Ganimede.Properties {
this["FFmpegBinFolder"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("True")]
public bool CreateSubfolder {
get {
return ((bool)(this["CreateSubfolder"]));
}
set {
this["CreateSubfolder"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("320,180")]
public string FrameSize {
get {
return ((string)(this["FrameSize"]));
}
set {
this["FrameSize"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("Ask")]
public string DefaultOverwriteMode {
get {
return ((string)(this["DefaultOverwriteMode"]));
}
set {
this["DefaultOverwriteMode"] = value;
}
}
}
}

View File

@@ -11,5 +11,14 @@
<Setting Name="FFmpegBinFolder" Type="System.String" Scope="User">
<Value Profile="(Default)">C:\Users\balbo\source\repos\Ganimede\Ganimede\Ganimede\FFMpeg</Value>
</Setting>
<Setting Name="CreateSubfolder" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">True</Value>
</Setting>
<Setting Name="FrameSize" Type="System.String" Scope="User">
<Value Profile="(Default)">320,180</Value>
</Setting>
<Setting Name="DefaultOverwriteMode" Type="System.String" Scope="User">
<Value Profile="(Default)">Ask</Value>
</Setting>
</Settings>
</SettingsFile>

View File

@@ -0,0 +1,291 @@
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Threading;
using System.Diagnostics;
using FFMpegCore;
using System.IO;
using Ganimede.Models;
using Ganimede.Properties;
namespace Ganimede.Services
{
public class VideoProcessingService
{
private readonly ObservableCollection<VideoJob> _jobQueue = new();
private readonly SemaphoreSlim _processingSemaphore = new(1, 1);
private bool _isProcessing = false;
private CancellationTokenSource _cancellationTokenSource = new();
public ObservableCollection<VideoJob> JobQueue => _jobQueue;
public bool IsProcessing => _isProcessing;
public event Action<VideoJob>? JobCompleted;
public event Action<VideoJob>? JobFailed;
public event Action? ProcessingStarted;
public event Action? ProcessingStopped;
public void AddJob(string videoPath, string outputFolder, bool createSubfolder = true)
{
var job = new VideoJob
{
VideoPath = videoPath,
OutputFolder = createSubfolder
? Path.Combine(outputFolder, Path.GetFileNameWithoutExtension(videoPath))
: outputFolder
};
_jobQueue.Add(job);
Debug.WriteLine($"[QUEUE] Added job: {job.VideoName} (Status: Pending)");
}
public async Task StartProcessingAsync()
{
if (_isProcessing)
return;
await _processingSemaphore.WaitAsync();
_isProcessing = true;
_cancellationTokenSource = new CancellationTokenSource();
ProcessingStarted?.Invoke();
Debug.WriteLine("[QUEUE] Processing started by user");
try
{
while (true)
{
if (_cancellationTokenSource.Token.IsCancellationRequested)
{
Debug.WriteLine("[QUEUE] Processing cancelled by user");
break;
}
var nextJob = GetNextPendingJob();
if (nextJob == null)
{
Debug.WriteLine("[QUEUE] No more pending jobs");
break;
}
await ProcessJobAsync(nextJob, _cancellationTokenSource.Token);
}
}
finally
{
_isProcessing = false;
ProcessingStopped?.Invoke();
_processingSemaphore.Release();
Debug.WriteLine("[QUEUE] Processing stopped");
}
}
public void StopProcessing()
{
if (_isProcessing)
{
_cancellationTokenSource.Cancel();
Debug.WriteLine("[QUEUE] Stop processing requested");
}
}
public void CancelJob(VideoJob job)
{
if (job.Status == JobStatus.Pending)
{
job.Status = JobStatus.Cancelled;
job.StatusMessage = "Cancelled";
Debug.WriteLine($"[QUEUE] Cancelled job: {job.VideoName}");
}
}
public void RemoveCompletedJobs()
{
for (int i = _jobQueue.Count - 1; i >= 0; i--)
{
if (_jobQueue[i].Status == JobStatus.Completed ||
_jobQueue[i].Status == JobStatus.Failed ||
_jobQueue[i].Status == JobStatus.Cancelled)
{
_jobQueue.RemoveAt(i);
}
}
Debug.WriteLine("[QUEUE] Removed completed jobs");
}
private VideoJob? GetNextPendingJob()
{
foreach (var job in _jobQueue)
{
if (job.Status == JobStatus.Pending)
return job;
}
return null;
}
private async Task ProcessJobAsync(VideoJob job, CancellationToken cancellationToken)
{
job.Status = JobStatus.Processing;
job.StatusMessage = "Analyzing video...";
job.Progress = 0;
try
{
Debug.WriteLine($"[PROCESS] Starting job: {job.VideoName}");
// Create output directory if it doesn't exist
Directory.CreateDirectory(job.OutputFolder);
var mediaInfo = await FFProbe.AnalyseAsync(job.VideoPath);
int frameRate = (int)(mediaInfo.PrimaryVideoStream?.FrameRate ?? 24);
int totalFrames = (int)(mediaInfo.Duration.TotalSeconds * frameRate);
Debug.WriteLine($"[INFO] Processing {totalFrames} frames at {frameRate} fps");
var frameSize = GetFrameSize(job);
var overwriteMode = GetOverwriteMode(job);
// Check for existing files if needed
var existingFiles = Directory.GetFiles(job.OutputFolder, "frame_*.png");
if (existingFiles.Length > 0 && overwriteMode == OverwriteMode.Ask)
{
var dialogResult = System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
return System.Windows.MessageBox.Show(
$"Found {existingFiles.Length} existing frame files in:\n{job.OutputFolder}\n\n" +
"Do you want to overwrite them?",
$"Existing Files - {job.VideoName}",
System.Windows.MessageBoxButton.YesNo,
System.Windows.MessageBoxImage.Question);
});
overwriteMode = dialogResult == System.Windows.MessageBoxResult.Yes ? OverwriteMode.Overwrite : OverwriteMode.Skip;
}
// Process frames sequentially
int processedFrames = 0;
int skippedFrames = 0;
for (int i = 0; i < totalFrames; i++)
{
if (cancellationToken.IsCancellationRequested)
{
job.Status = JobStatus.Cancelled;
job.StatusMessage = "Cancelled by user";
Debug.WriteLine($"[CANCELLED] Job cancelled: {job.VideoName}");
return;
}
string framePath = Path.Combine(job.OutputFolder, $"frame_{i:D6}.png");
// Check if file exists and handle according to overwrite mode
if (File.Exists(framePath) && overwriteMode == OverwriteMode.Skip)
{
skippedFrames++;
}
else
{
await ExtractFrameAsync(job, i, frameRate, frameSize, framePath);
processedFrames++;
}
job.Progress = (double)(i + 1) / totalFrames * 100;
job.StatusMessage = $"Processed {processedFrames}/{totalFrames} frames ({job.Progress:F1}%)" +
(skippedFrames > 0 ? $" - Skipped {skippedFrames}" : "");
// Small delay to prevent UI freezing and allow cancellation
if (i % 10 == 0) // Check every 10 frames
{
await Task.Delay(1, cancellationToken);
}
}
job.Status = JobStatus.Completed;
job.StatusMessage = $"Completed - {processedFrames} frames processed" +
(skippedFrames > 0 ? $", {skippedFrames} skipped" : "");
job.Progress = 100;
Debug.WriteLine($"[SUCCESS] Completed job: {job.VideoName}");
JobCompleted?.Invoke(job);
}
catch (OperationCanceledException)
{
job.Status = JobStatus.Cancelled;
job.StatusMessage = "Cancelled by user";
Debug.WriteLine($"[CANCELLED] Job cancelled: {job.VideoName}");
}
catch (Exception ex)
{
job.Status = JobStatus.Failed;
job.StatusMessage = $"Error: {ex.Message}";
Debug.WriteLine($"[ERROR] Job failed for {job.VideoName}: {ex.Message}");
JobFailed?.Invoke(job);
}
}
private (int width, int height) GetFrameSize(VideoJob job)
{
// Use job-specific frame size if set, otherwise use default setting
var frameSize = !string.IsNullOrEmpty(job.CustomFrameSize) ? job.CustomFrameSize : Settings.Default.FrameSize;
if (string.IsNullOrEmpty(frameSize))
return (320, 180);
var parts = frameSize.Split(',');
if (parts.Length == 2 && int.TryParse(parts[0], out int width) && int.TryParse(parts[1], out int height))
return (width, height);
return (320, 180);
}
private OverwriteMode GetOverwriteMode(VideoJob job)
{
// Use job-specific overwrite mode if set, otherwise use default setting
if (job.CustomOverwriteMode.HasValue)
return job.CustomOverwriteMode.Value;
var defaultMode = Settings.Default.DefaultOverwriteMode;
if (Enum.TryParse<OverwriteMode>(defaultMode, out var mode))
return mode;
return OverwriteMode.Ask;
}
private async Task ExtractFrameAsync(VideoJob job, int frameIndex, int frameRate, (int width, int height) frameSize, string framePath)
{
var frameTime = TimeSpan.FromSeconds((double)frameIndex / frameRate);
try
{
// Try with PNG codec first
await FFMpegArguments
.FromFileInput(job.VideoPath)
.OutputToFile(framePath, true, options => options
.Seek(frameTime)
.WithFrameOutputCount(1)
.WithVideoCodec("png")
.Resize(frameSize.width, frameSize.height))
.ProcessAsynchronously();
}
catch
{
// Fallback without codec specification
try
{
await FFMpegArguments
.FromFileInput(job.VideoPath)
.OutputToFile(framePath, true, options => options
.Seek(frameTime)
.WithFrameOutputCount(1)
.Resize(frameSize.width, frameSize.height))
.ProcessAsynchronously();
}
catch (Exception ex)
{
Debug.WriteLine($"[ERROR] Failed to extract frame {frameIndex} from {job.VideoName}: {ex.Message}");
// Continue processing other frames even if this one fails
}
}
}
}
}

View File

@@ -0,0 +1,88 @@
<Window x:Class="Ganimede.Windows.JobConfigWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="Configure Selected Jobs" Height="500" Width="600"
Background="#222" WindowStartupLocation="CenterOwner">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Title -->
<StackPanel Grid.Row="0" Margin="0,0,0,20">
<TextBlock Text="Configure Selected Jobs" FontSize="20" FontWeight="Bold" Foreground="White"/>
<TextBlock x:Name="SelectedJobsText" Text="2 jobs selected" FontSize="12" Foreground="#AAA" Margin="0,5,0,0"/>
</StackPanel>
<!-- Settings Content -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel>
<!-- Output Settings -->
<GroupBox Header="Output Settings" Foreground="White" BorderBrush="#444" Margin="0,0,0,20">
<StackPanel Margin="10">
<CheckBox x:Name="UseCustomOutputCheckBox" Content="Use custom output folder for selected jobs"
Foreground="White" Margin="0,0,0,10" Checked="UseCustomOutputCheckBox_CheckedChanged" Unchecked="UseCustomOutputCheckBox_CheckedChanged"/>
<Grid IsEnabled="{Binding IsChecked, ElementName=UseCustomOutputCheckBox}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="CustomOutputTextBox" Grid.Column="0" Height="30" VerticalContentAlignment="Center"
Background="#333" Foreground="White" BorderBrush="#555" Margin="0,0,10,0"/>
<Button x:Name="BrowseCustomOutputButton" Grid.Column="1" Content="Browse" Width="80" Height="30"
Click="BrowseCustomOutputButton_Click"/>
</Grid>
<CheckBox x:Name="CreateSubfolderCheckBox" Content="Create subfolder for each video"
Foreground="White" IsChecked="True" Margin="0,10,0,0"/>
</StackPanel>
</GroupBox>
<!-- Frame Settings -->
<GroupBox Header="Frame Settings" Foreground="White" BorderBrush="#444" Margin="0,0,0,20">
<StackPanel Margin="10">
<CheckBox x:Name="UseCustomFrameSizeCheckBox" Content="Use custom frame size for selected jobs"
Foreground="White" Margin="0,0,0,10" Checked="UseCustomFrameSizeCheckBox_CheckedChanged" Unchecked="UseCustomFrameSizeCheckBox_CheckedChanged"/>
<ComboBox x:Name="CustomFrameSizeComboBox" Height="30" Background="#333" Foreground="White" BorderBrush="#555"
IsEnabled="{Binding IsChecked, ElementName=UseCustomFrameSizeCheckBox}">
<ComboBoxItem Content="320x180 (Fast)" Tag="320,180"/>
<ComboBoxItem Content="640x360 (Medium)" Tag="640,360"/>
<ComboBoxItem Content="1280x720 (High)" Tag="1280,720"/>
<ComboBoxItem Content="1920x1080 (Full HD)" Tag="1920,1080"/>
</ComboBox>
</StackPanel>
</GroupBox>
<!-- Overwrite Settings -->
<GroupBox Header="File Overwrite Settings" Foreground="White" BorderBrush="#444" Margin="0,0,0,20">
<StackPanel Margin="10">
<CheckBox x:Name="UseCustomOverwriteCheckBox" Content="Use custom overwrite behavior for selected jobs"
Foreground="White" Margin="0,0,0,10" Checked="UseCustomOverwriteCheckBox_CheckedChanged" Unchecked="UseCustomOverwriteCheckBox_CheckedChanged"/>
<ComboBox x:Name="CustomOverwriteComboBox" Height="30" Background="#333" Foreground="White" BorderBrush="#555"
IsEnabled="{Binding IsChecked, ElementName=UseCustomOverwriteCheckBox}">
<ComboBoxItem Content="Ask each time" Tag="Ask"/>
<ComboBoxItem Content="Skip existing files" Tag="Skip"/>
<ComboBoxItem Content="Overwrite existing files" Tag="Overwrite"/>
</ComboBox>
</StackPanel>
</GroupBox>
</StackPanel>
</ScrollViewer>
<!-- Buttons -->
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,20,0,0">
<Button x:Name="ApplyButton" Content="Apply to Selected" Width="120" Height="35" Margin="0,0,10,0"
Click="ApplyButton_Click" Background="#4FC3F7" Foreground="White" BorderThickness="0"/>
<Button x:Name="CancelButton" Content="Cancel" Width="80" Height="35"
Click="CancelButton_Click"/>
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,169 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using Ganimede.Models;
using WpfMessageBox = System.Windows.MessageBox;
namespace Ganimede.Windows
{
public partial class JobConfigWindow : Window
{
private List<VideoJob> _selectedJobs;
public JobConfigWindow(List<VideoJob> selectedJobs)
{
InitializeComponent();
_selectedJobs = selectedJobs;
SelectedJobsText.Text = $"{selectedJobs.Count} job(s) selected";
LoadCurrentSettings();
}
private void LoadCurrentSettings()
{
// Load current settings from first job or defaults
var firstJob = _selectedJobs.FirstOrDefault();
if (firstJob != null)
{
// Output settings
if (!string.IsNullOrEmpty(firstJob.CustomOutputFolder))
{
UseCustomOutputCheckBox.IsChecked = true;
CustomOutputTextBox.Text = firstJob.CustomOutputFolder;
}
CreateSubfolderCheckBox.IsChecked = firstJob.CustomCreateSubfolder;
// Frame size settings
if (!string.IsNullOrEmpty(firstJob.CustomFrameSize))
{
UseCustomFrameSizeCheckBox.IsChecked = true;
foreach (ComboBoxItem item in CustomFrameSizeComboBox.Items)
{
if (item.Tag?.ToString() == firstJob.CustomFrameSize)
{
CustomFrameSizeComboBox.SelectedItem = item;
break;
}
}
}
// Overwrite settings
if (firstJob.CustomOverwriteMode.HasValue)
{
UseCustomOverwriteCheckBox.IsChecked = true;
foreach (ComboBoxItem item in CustomOverwriteComboBox.Items)
{
if (item.Tag?.ToString() == firstJob.CustomOverwriteMode.Value.ToString())
{
CustomOverwriteComboBox.SelectedItem = item;
break;
}
}
}
}
// Set default selections if nothing is selected
if (CustomFrameSizeComboBox.SelectedItem == null)
CustomFrameSizeComboBox.SelectedIndex = 0;
if (CustomOverwriteComboBox.SelectedItem == null)
CustomOverwriteComboBox.SelectedIndex = 0;
}
private void UseCustomOutputCheckBox_CheckedChanged(object sender, RoutedEventArgs e)
{
// Enable/disable controls handled by binding
}
private void UseCustomFrameSizeCheckBox_CheckedChanged(object sender, RoutedEventArgs e)
{
// Enable/disable controls handled by binding
}
private void UseCustomOverwriteCheckBox_CheckedChanged(object sender, RoutedEventArgs e)
{
// Enable/disable controls handled by binding
}
private void BrowseCustomOutputButton_Click(object sender, RoutedEventArgs e)
{
using var dialog = new System.Windows.Forms.FolderBrowserDialog
{
Description = "Select custom output folder for selected jobs",
ShowNewFolderButton = true
};
if (!string.IsNullOrEmpty(CustomOutputTextBox.Text))
dialog.SelectedPath = CustomOutputTextBox.Text;
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
CustomOutputTextBox.Text = dialog.SelectedPath;
}
}
private void ApplyButton_Click(object sender, RoutedEventArgs e)
{
try
{
foreach (var job in _selectedJobs)
{
// Apply output settings
if (UseCustomOutputCheckBox.IsChecked == true)
{
job.CustomOutputFolder = CustomOutputTextBox.Text;
job.CustomCreateSubfolder = CreateSubfolderCheckBox.IsChecked ?? true;
// Update the actual output folder
job.OutputFolder = job.CustomCreateSubfolder
? System.IO.Path.Combine(job.CustomOutputFolder, job.VideoName)
: job.CustomOutputFolder;
}
else
{
job.CustomOutputFolder = string.Empty;
// Reset to default output folder logic will be handled in MainWindow
}
// Apply frame size settings
if (UseCustomFrameSizeCheckBox.IsChecked == true && CustomFrameSizeComboBox.SelectedItem is ComboBoxItem frameSizeItem)
{
job.CustomFrameSize = frameSizeItem.Tag?.ToString() ?? string.Empty;
}
else
{
job.CustomFrameSize = string.Empty;
}
// Apply overwrite settings
if (UseCustomOverwriteCheckBox.IsChecked == true && CustomOverwriteComboBox.SelectedItem is ComboBoxItem overwriteItem)
{
if (Enum.TryParse<OverwriteMode>(overwriteItem.Tag?.ToString(), out var overwriteMode))
{
job.CustomOverwriteMode = overwriteMode;
}
}
else
{
job.CustomOverwriteMode = null;
}
}
DialogResult = true;
Close();
}
catch (Exception ex)
{
WpfMessageBox.Show($"Error applying settings: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
}
}

View File

@@ -0,0 +1,90 @@
<Window x:Class="Ganimede.Windows.SettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="Settings" Height="500" Width="600"
Background="#222" WindowStartupLocation="CenterOwner">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Title -->
<TextBlock Grid.Row="0" Text="Settings" FontSize="24" FontWeight="Bold" Foreground="White" Margin="0,0,0,20"/>
<!-- Settings Content -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel>
<!-- FFmpeg Settings -->
<GroupBox Header="FFmpeg Configuration" Foreground="White" BorderBrush="#444" Margin="0,0,0,20">
<StackPanel Margin="10">
<TextBlock Text="FFmpeg Binary Folder:" Foreground="#CCC" Margin="0,0,0,5"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="FFmpegPathTextBox" Grid.Column="0" Height="30" VerticalContentAlignment="Center"
Background="#333" Foreground="White" BorderBrush="#555" Margin="0,0,10,0"/>
<Button x:Name="BrowseFFmpegButton" Grid.Column="1" Content="Browse" Width="80" Height="30"
Click="BrowseFFmpegButton_Click"/>
</Grid>
<TextBlock x:Name="FFmpegStatusText" Foreground="#AAA" FontSize="12" Margin="0,5,0,0"/>
</StackPanel>
</GroupBox>
<!-- Output Settings -->
<GroupBox Header="Output Settings" Foreground="White" BorderBrush="#444" Margin="0,0,0,20">
<StackPanel Margin="10">
<CheckBox x:Name="CreateSubfolderCheckBox" Content="Create subfolder for each video"
Foreground="White" IsChecked="True" Margin="0,0,0,10"/>
<TextBlock Text="Default Output Folder:" Foreground="#CCC" Margin="0,0,0,5"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="DefaultOutputTextBox" Grid.Column="0" Height="30" VerticalContentAlignment="Center"
Background="#333" Foreground="White" BorderBrush="#555" Margin="0,0,10,0"/>
<Button x:Name="BrowseOutputButton" Grid.Column="1" Content="Browse" Width="80" Height="30"
Click="BrowseOutputButton_Click"/>
</Grid>
</StackPanel>
</GroupBox>
<!-- Performance Settings -->
<GroupBox Header="Default Processing Settings" Foreground="White" BorderBrush="#444" Margin="0,0,0,20">
<StackPanel Margin="10">
<TextBlock Text="Default Frame Size:" Foreground="#CCC" Margin="0,0,0,5"/>
<ComboBox x:Name="FrameSizeComboBox" Height="30" Background="#333" Foreground="White" BorderBrush="#555">
<ComboBoxItem Content="320x180 (Fast)" Tag="320,180" IsSelected="True"/>
<ComboBoxItem Content="640x360 (Medium)" Tag="640,360"/>
<ComboBoxItem Content="1280x720 (High)" Tag="1280,720"/>
<ComboBoxItem Content="1920x1080 (Full HD)" Tag="1920,1080"/>
</ComboBox>
<TextBlock Text="Default File Overwrite Behavior:" Foreground="#CCC" Margin="0,15,0,5"/>
<ComboBox x:Name="OverwriteModeComboBox" Height="30" Background="#333" Foreground="White" BorderBrush="#555">
<ComboBoxItem Content="Ask each time" Tag="Ask" IsSelected="True"/>
<ComboBoxItem Content="Skip existing files" Tag="Skip"/>
<ComboBoxItem Content="Overwrite existing files" Tag="Overwrite"/>
</ComboBox>
</StackPanel>
</GroupBox>
</StackPanel>
</ScrollViewer>
<!-- Buttons -->
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,20,0,0">
<Button x:Name="SaveButton" Content="Save" Width="80" Height="35" Margin="0,0,10,0"
Click="SaveButton_Click" Background="#4FC3F7" Foreground="White" BorderThickness="0"/>
<Button x:Name="CancelButton" Content="Cancel" Width="80" Height="35"
Click="CancelButton_Click"/>
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,148 @@
using System;
using System.IO;
using System.Windows;
using Microsoft.Win32;
using Ganimede.Properties;
using System.Diagnostics;
using WpfMessageBox = System.Windows.MessageBox;
namespace Ganimede.Windows
{
public partial class SettingsWindow : Window
{
public SettingsWindow()
{
InitializeComponent();
LoadSettings();
}
private void LoadSettings()
{
FFmpegPathTextBox.Text = Settings.Default.FFmpegBinFolder;
DefaultOutputTextBox.Text = Settings.Default.LastOutputFolder;
CreateSubfolderCheckBox.IsChecked = Settings.Default.CreateSubfolder;
var frameSize = Settings.Default.FrameSize;
foreach (System.Windows.Controls.ComboBoxItem item in FrameSizeComboBox.Items)
{
if (item.Tag?.ToString() == frameSize)
{
FrameSizeComboBox.SelectedItem = item;
break;
}
}
var overwriteMode = Settings.Default.DefaultOverwriteMode;
foreach (System.Windows.Controls.ComboBoxItem item in OverwriteModeComboBox.Items)
{
if (item.Tag?.ToString() == overwriteMode)
{
OverwriteModeComboBox.SelectedItem = item;
break;
}
}
UpdateFFmpegStatus();
}
private void UpdateFFmpegStatus()
{
var path = FFmpegPathTextBox.Text;
if (string.IsNullOrEmpty(path))
{
FFmpegStatusText.Text = "No path specified";
FFmpegStatusText.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Orange);
}
else if (ValidateFFMpegPath(path))
{
FFmpegStatusText.Text = "? Valid FFmpeg installation found";
FFmpegStatusText.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.LightGreen);
}
else
{
FFmpegStatusText.Text = "? FFmpeg binaries not found in specified path";
FFmpegStatusText.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Red);
}
}
private bool ValidateFFMpegPath(string path)
{
if (!Directory.Exists(path))
return false;
var ffmpegPath = Path.Combine(path, "ffmpeg.exe");
var ffprobePath = Path.Combine(path, "ffprobe.exe");
return File.Exists(ffmpegPath) && File.Exists(ffprobePath);
}
private void BrowseFFmpegButton_Click(object sender, RoutedEventArgs e)
{
using var dialog = new System.Windows.Forms.FolderBrowserDialog
{
Description = "Select FFmpeg binary folder",
ShowNewFolderButton = false
};
if (!string.IsNullOrEmpty(FFmpegPathTextBox.Text))
dialog.SelectedPath = FFmpegPathTextBox.Text;
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
FFmpegPathTextBox.Text = dialog.SelectedPath;
UpdateFFmpegStatus();
}
}
private void BrowseOutputButton_Click(object sender, RoutedEventArgs e)
{
using var dialog = new System.Windows.Forms.FolderBrowserDialog
{
Description = "Select default output folder",
ShowNewFolderButton = true
};
if (!string.IsNullOrEmpty(DefaultOutputTextBox.Text))
dialog.SelectedPath = DefaultOutputTextBox.Text;
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
DefaultOutputTextBox.Text = dialog.SelectedPath;
}
}
private void SaveButton_Click(object sender, RoutedEventArgs e)
{
try
{
// Save settings
Settings.Default.FFmpegBinFolder = FFmpegPathTextBox.Text;
Settings.Default.LastOutputFolder = DefaultOutputTextBox.Text;
Settings.Default.CreateSubfolder = CreateSubfolderCheckBox.IsChecked ?? true;
var selectedFrameSizeItem = FrameSizeComboBox.SelectedItem as System.Windows.Controls.ComboBoxItem;
Settings.Default.FrameSize = selectedFrameSizeItem?.Tag?.ToString() ?? "320,180";
var selectedOverwriteItem = OverwriteModeComboBox.SelectedItem as System.Windows.Controls.ComboBoxItem;
Settings.Default.DefaultOverwriteMode = selectedOverwriteItem?.Tag?.ToString() ?? "Ask";
Settings.Default.Save();
Debug.WriteLine("[SETTINGS] Settings saved successfully");
DialogResult = true;
Close();
}
catch (Exception ex)
{
WpfMessageBox.Show($"Error saving settings: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
Debug.WriteLine($"[ERROR] Failed to save settings: {ex.Message}");
}
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
}
}