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:
33
Ganimede/Ganimede/Converters/StatusColorConverter.cs
Normal file
33
Ganimede/Ganimede/Converters/StatusColorConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
<!-- 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>
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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>
|
||||
<!-- Drag & Drop Area -->
|
||||
<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>
|
||||
|
||||
<!-- 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"/>
|
||||
<!-- Buttons -->
|
||||
<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>
|
||||
|
||||
<!-- Progress & Thumbnails -->
|
||||
<StackPanel Grid.Row="3" Margin="0,32,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">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
<!-- 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="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>
|
||||
|
||||
@@ -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,62 +27,133 @@ 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 if (TryUseSystemFFMpeg())
|
||||
{
|
||||
Debug.WriteLine("[CONFIG] Using system FFMpeg from PATH");
|
||||
}
|
||||
else if (TryFixMissingFFMpeg(ffmpegBin))
|
||||
{
|
||||
FFMpegCore.GlobalFFOptions.Configure(options => options.BinaryFolder = ffmpegBin);
|
||||
Debug.WriteLine($"[CONFIG] FFMpeg fixed and configured: {ffmpegBin}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Prova a utilizzare FFMpeg dal PATH di sistema
|
||||
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))
|
||||
{
|
||||
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.");
|
||||
}
|
||||
}
|
||||
Debug.WriteLine("[ERROR] FFMpeg configuration failed.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
if (string.IsNullOrEmpty(outputFolder))
|
||||
{
|
||||
Debug.WriteLine("[INFO] Video selection cancelled.");
|
||||
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)
|
||||
{
|
||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
||||
{
|
||||
outputFolder = dialog.SelectedPath;
|
||||
StatusText.Text = $"Selected 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.");
|
||||
}
|
||||
outputFolder = dialog.SelectedPath;
|
||||
StatusText.Text = $"Output folder: {outputFolder}";
|
||||
Settings.Default.LastOutputFolder = outputFolder;
|
||||
Settings.Default.Save();
|
||||
Debug.WriteLine($"[INFO] Output folder selected: {outputFolder}");
|
||||
}
|
||||
}
|
||||
|
||||
private async void ExtractFramesButton_Click(object sender, RoutedEventArgs e)
|
||||
private void SettingsButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Debug.WriteLine("[UI] ExtractFramesButton_Click invoked.");
|
||||
if (string.IsNullOrEmpty(videoPath) || string.IsNullOrEmpty(outputFolder))
|
||||
var settingsWindow = new SettingsWindow
|
||||
{
|
||||
StatusText.Text = "Please select a video and output folder.";
|
||||
Debug.WriteLine("[ERROR] Video path or output folder not set.");
|
||||
Owner = this
|
||||
};
|
||||
|
||||
if (settingsWindow.ShowDialog() == true)
|
||||
{
|
||||
// Reconfigure FFMpeg if settings changed
|
||||
ConfigureFFMpeg();
|
||||
StatusText.Text = "Settings updated successfully";
|
||||
}
|
||||
}
|
||||
|
||||
private async void StartQueueButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_processingService.JobQueue.Count == 0)
|
||||
{
|
||||
WpfMessageBox.Show("No videos in queue. Please add some videos first.", "Queue Empty",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
ExtractFramesButton.IsEnabled = false;
|
||||
ProgressBar.Value = 0;
|
||||
thumbnails.Clear();
|
||||
StatusText.Text = "Analyzing video...";
|
||||
Debug.WriteLine($"[PROCESS] Starting analysis for video: {videoPath}");
|
||||
|
||||
try
|
||||
|
||||
var pendingJobs = _processingService.JobQueue.Count(job => job.Status == JobStatus.Pending);
|
||||
if (pendingJobs == 0)
|
||||
{
|
||||
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++)
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine($"[ERROR] Frame file not found: {framePath}");
|
||||
}
|
||||
|
||||
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.");
|
||||
WpfMessageBox.Show("No pending videos in queue.", "No Pending Jobs",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText.Text = $"Error: {ex.Message}";
|
||||
Debug.WriteLine($"[EXCEPTION] {ex.GetType()}: {ex.Message}\n{ex.StackTrace}");
|
||||
}
|
||||
|
||||
ExtractFramesButton.IsEnabled = true;
|
||||
|
||||
Debug.WriteLine("[QUEUE] Starting queue processing manually");
|
||||
await _processingService.StartProcessingAsync();
|
||||
}
|
||||
|
||||
private void DragDropArea_Drop(object sender, System.Windows.DragEventArgs e)
|
||||
private void StopQueueButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Debug.WriteLine("[UI] DragDropArea_Drop invoked.");
|
||||
if (e.Data.GetDataPresent(System.Windows.DataFormats.FileDrop))
|
||||
_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)
|
||||
{
|
||||
var files = (string[])e.Data.GetData(System.Windows.DataFormats.FileDrop);
|
||||
if (files.Length > 0)
|
||||
if (checkBox.IsChecked == true && checkBox.Tag is VideoJob job)
|
||||
{
|
||||
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}");
|
||||
_selectedJobs.Add(job);
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
// 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)))
|
||||
{
|
||||
Debug.WriteLine("[WARN] Drag & drop did not contain files.");
|
||||
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 = "All jobs cleared and processing stopped";
|
||||
break;
|
||||
|
||||
case MessageBoxResult.No:
|
||||
// Clear only non-processing jobs
|
||||
for (int i = _processingService.JobQueue.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (_processingService.JobQueue[i].Status != JobStatus.Processing)
|
||||
{
|
||||
_processingService.JobQueue.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
StatusText.Text = "Cleared completed/pending jobs (processing jobs continue)";
|
||||
break;
|
||||
|
||||
case MessageBoxResult.Cancel:
|
||||
// Do nothing
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("[WARN] Drag & drop did not contain file drop format.");
|
||||
var result = WpfMessageBox.Show("Are you sure you want to clear all jobs from the queue?",
|
||||
"Clear All Jobs", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||
|
||||
if (result == MessageBoxResult.Yes)
|
||||
{
|
||||
_processingService.JobQueue.Clear();
|
||||
thumbnails.Clear();
|
||||
StatusText.Text = "All jobs cleared";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DragDropArea_Drop(object sender, WpfDragEventArgs e)
|
||||
{
|
||||
Debug.WriteLine("[UI] DragDropArea_Drop invoked.");
|
||||
if (e.Data.GetDataPresent(WpfDataFormats.FileDrop))
|
||||
{
|
||||
var files = (string[])e.Data.GetData(WpfDataFormats.FileDrop);
|
||||
var videoFiles = files.Where(f => IsVideoFile(f)).ToArray();
|
||||
|
||||
if (videoFiles.Length > 0)
|
||||
{
|
||||
AddVideosToQueue(videoFiles);
|
||||
Debug.WriteLine($"[INFO] {videoFiles.Length} videos added via drag & drop");
|
||||
}
|
||||
else
|
||||
{
|
||||
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
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
129
Ganimede/Ganimede/Models/VideoJob.cs
Normal file
129
Ganimede/Ganimede/Models/VideoJob.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Ganimede/Ganimede/Properties/Settings.Designer.cs
generated
36
Ganimede/Ganimede/Properties/Settings.Designer.cs
generated
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
291
Ganimede/Ganimede/Services/VideoProcessingService.cs
Normal file
291
Ganimede/Ganimede/Services/VideoProcessingService.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
88
Ganimede/Ganimede/Windows/JobConfigWindow.xaml
Normal file
88
Ganimede/Ganimede/Windows/JobConfigWindow.xaml
Normal 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>
|
||||
169
Ganimede/Ganimede/Windows/JobConfigWindow.xaml.cs
Normal file
169
Ganimede/Ganimede/Windows/JobConfigWindow.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
90
Ganimede/Ganimede/Windows/SettingsWindow.xaml
Normal file
90
Ganimede/Ganimede/Windows/SettingsWindow.xaml
Normal 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>
|
||||
148
Ganimede/Ganimede/Windows/SettingsWindow.xaml.cs
Normal file
148
Ganimede/Ganimede/Windows/SettingsWindow.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user