Rifattorizzazione UI e logica di elaborazione video

- Aggiornamenti al layout della finestra principale.
- Introduzione della selezione della modalità di estrazione dei frame.
- Traduzione dell'interfaccia in italiano.
- Aggiunta del nuovo enum `ExtractionMode`.
- Modifiche ai servizi di elaborazione video per supportare la nuova logica.
- Aggiornamenti alle impostazioni per la modalità di estrazione predefinita.
- Rimozione di codice obsoleto e miglioramenti generali.

Aggiornamento alla versione 5.0.0.256
This commit is contained in:
Alberto Balbo
2025-09-11 17:47:44 +02:00
parent bf436d0926
commit 8879a9375f
10 changed files with 404 additions and 289 deletions

View File

@@ -11,71 +11,7 @@
<local:StatusColorConverter x:Key="StatusColorConverter"/>
</Window.Resources>
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<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="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,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 & 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 Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
@@ -83,34 +19,47 @@
<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"/>
<!-- Top controls condensed -->
<DockPanel Grid.Row="0" LastChildFill="False" Margin="0,0,0,10">
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left" Margin="0,0,15,0">
<Button x:Name="BrowseVideoButton" Content="Aggiungi Video" Width="120" Height="35" Margin="0,0,8,0" Click="BrowseVideoButton_Click"/>
<Button x:Name="ImportFolderButton" Content="Importa Cartella" Width="130" Height="35" Margin="0,0,8,0" Click="ImportFolderButton_Click"/>
<Button x:Name="SelectOutputFolderButton" Content="Cartella Output" Width="130" Height="35" Margin="0,0,8,0" Click="SelectOutputFolderButton_Click"/>
<Button x:Name="SettingsButton" Content="Impostazioni" Width="110" Height="35" Margin="0,0,8,0" Click="SettingsButton_Click"/>
</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"
<StackPanel Orientation="Horizontal" DockPanel.Dock="Right">
<Button x:Name="StartQueueButton" Content="▶ Avvia" Width="90" Height="35" Margin="0,0,8,0"
Click="StartQueueButton_Click" Background="#4FC3F7" Foreground="White" FontSize="12"/>
<Button x:Name="StopQueueButton" Content="⏹ Stop" Width="80" Height="30"
<Button x:Name="StopQueueButton" Content="⏹ Stop" Width="90" Height="35"
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"
</DockPanel>
<!-- Secondary controls -->
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,0,0,10" HorizontalAlignment="Center">
<Button x:Name="ConfigureSelectedButton" Content="⚙ Configura" Width="100" Height="30" Margin="0,0,8,0"
Click="ConfigureSelectedButton_Click" FontSize="11" IsEnabled="False"/>
<Button x:Name="ClearCompletedButton" Content="🗑 Clear Done" Width="80" Height="25"
<Button x:Name="ClearCompletedButton" Content="🗑 Pulisci Completati" Width="140" Height="30" Margin="0,0,8,0"
Click="ClearCompletedButton_Click" FontSize="11"/>
</StackPanel>
<Button x:Name="ClearAllButton" Content="🗑 Pulisci Tutto" Width="120" Height="30"
Click="ClearAllButton_Click" Background="#6C757D" Foreground="White"/>
</StackPanel>
<!-- Queue List -->
<ScrollViewer Grid.Row="2" VerticalScrollBarVisibility="Auto">
<!-- Queue List FULL AREA -->
<Grid Grid.Row="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10" HorizontalAlignment="Center">
<TextBlock Text="Coda Elaborazione" FontSize="18" FontWeight="Bold" Foreground="White" VerticalAlignment="Center"/>
<TextBlock x:Name="QueueCountText" Text="(0 elementi)" Foreground="#AAA" FontSize="12" Margin="10,0,0,0" VerticalAlignment="Center"/>
</StackPanel>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<ItemsControl x:Name="QueueItemsControl">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="#333" Margin="0,0,0,5" Padding="8" CornerRadius="5">
<Border Background="#2C2C2C" Margin="0,0,0,8" Padding="10" CornerRadius="6" BorderBrush="#444" BorderThickness="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
@@ -125,27 +74,25 @@
<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"
<Button Grid.Row="0" Grid.Column="2" Content="×" Width="22" Height="22" FontSize="14"
Click="RemoveQueueItem_Click" Background="Transparent" Foreground="#FF6B6B" BorderThickness="0"
Tag="{Binding}" ToolTip="Remove from queue"/>
Tag="{Binding}" ToolTip="Rimuovi dalla coda"/>
<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"/>
<TextBlock Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Text="{Binding StatusMessage}" Foreground="#AAA" FontSize="11" Margin="0,4,0,0"/>
<ProgressBar Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" Height="5" Margin="0,6,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="10" Margin="0,4,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 Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="3" FontSize="9" Foreground="#666" Margin="0,4,0,0">
<TextBlock.Text>
<MultiBinding StringFormat="{}📁 {0} | 📐 {1} | 🔄 {2} | 🏷 {3}">
<MultiBinding StringFormat="{}📁 {0} | 📐 {1} | 🔄 {2} | 🏷 {3} | 🎯 {4}">
<Binding Path="OutputFolderDisplay"/>
<Binding Path="FrameSizeDisplay"/>
<Binding Path="OverwriteModeDisplay"/>
<Binding Path="NamingPatternDisplay"/>
<Binding Path="ExtractionModeDisplay"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
@@ -155,10 +102,30 @@
</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>
<!-- Bottom status and thumbnails (thumbnails optional) -->
<DockPanel Grid.Row="3" Margin="0,10,0,0">
<StackPanel DockPanel.Dock="Left" Width="400">
<ProgressBar x:Name="ProgressBar" Height="20" Minimum="0" Maximum="100" Value="0" Background="#333" Foreground="#4FC3F7"/>
<TextBlock x:Name="StatusText" Foreground="#AAA" FontSize="13" Margin="0,6,0,0" TextWrapping="Wrap"/>
</StackPanel>
<ScrollViewer DockPanel.Dock="Right" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled" Height="80">
<ItemsControl x:Name="ThumbnailsPanel">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="2" BorderBrush="#555" BorderThickness="1" CornerRadius="4">
<Image Source="{Binding}" Width="100" Height="56" Stretch="UniformToFill"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
</Grid>
</Window>

View File

@@ -16,9 +16,6 @@ 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
{
@@ -64,7 +61,7 @@ namespace Ganimede
Dispatcher.Invoke(() =>
{
var count = _processingService.JobQueue.Count;
QueueCountText.Text = $"({count} items)";
QueueCountText.Text = $"({count} elementi)";
});
}
@@ -74,7 +71,7 @@ namespace Ganimede
{
StartQueueButton.IsEnabled = false;
StopQueueButton.IsEnabled = true;
StatusText.Text = "Processing queue started...";
StatusText.Text = "Elaborazione coda avviata...";
});
}
@@ -84,7 +81,7 @@ namespace Ganimede
{
StartQueueButton.IsEnabled = true;
StopQueueButton.IsEnabled = false;
StatusText.Text = "Processing queue stopped.";
StatusText.Text = "Elaborazione coda fermata.";
});
}
@@ -92,7 +89,7 @@ namespace Ganimede
{
Dispatcher.Invoke(() =>
{
StatusText.Text = $"✓ Completed: {job.VideoName}";
StatusText.Text = $"✓ Completato: {job.VideoName}";
// Load thumbnails for the completed job
LoadThumbnailsFromFolder(job.OutputFolder);
});
@@ -102,7 +99,7 @@ namespace Ganimede
{
Dispatcher.Invoke(() =>
{
StatusText.Text = $"✗ Failed: {job.VideoName} - {job.StatusMessage}";
StatusText.Text = $"✗ Fallito: {job.VideoName} - {job.StatusMessage}";
});
}
@@ -231,11 +228,41 @@ namespace Ganimede
}
}
private void ImportFolderButton_Click(object sender, RoutedEventArgs e)
{
using var dialog = new System.Windows.Forms.FolderBrowserDialog
{
Description = "Seleziona la cartella contenente i video da importare",
ShowNewFolderButton = false
};
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
var folder = dialog.SelectedPath;
try
{
var videoFiles = Directory.EnumerateFiles(folder, "*.*", SearchOption.TopDirectoryOnly)
.Where(IsVideoFile)
.ToArray();
if (videoFiles.Length == 0)
{
WpfMessageBox.Show("Nessun file video valido trovato nella cartella.", "Importa Cartella", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
AddVideosToQueue(videoFiles);
StatusText.Text = $"Importati {videoFiles.Length} video dalla cartella.";
}
catch (Exception ex)
{
WpfMessageBox.Show($"Errore durante l'importazione: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
private void AddVideosToQueue(string[] videoPaths)
{
if (string.IsNullOrEmpty(outputFolder))
{
WpfMessageBox.Show("Please select an output folder first.", "Output Folder Required",
WpfMessageBox.Show("Seleziona prima una cartella di output.", "Cartella Output Richiesta",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
@@ -248,7 +275,7 @@ namespace Ganimede
Debug.WriteLine($"[QUEUE] Added video to queue: {Path.GetFileName(videoPath)}");
}
StatusText.Text = $"Added {videoPaths.Length} video(s) to queue (Pending)";
StatusText.Text = $"Aggiunti {videoPaths.Length} video in coda (Pending)";
Settings.Default.LastVideoPath = videoPaths.FirstOrDefault();
Settings.Default.Save();
}
@@ -282,7 +309,7 @@ namespace Ganimede
{
// Reconfigure FFMpeg if settings changed
ConfigureFFMpeg();
StatusText.Text = "Settings updated successfully";
StatusText.Text = "Impostazioni aggiornate";
}
}
@@ -290,7 +317,7 @@ namespace Ganimede
{
if (_processingService.JobQueue.Count == 0)
{
WpfMessageBox.Show("No videos in queue. Please add some videos first.", "Queue Empty",
WpfMessageBox.Show("Nessun video in coda. Aggiungi prima dei video.", "Coda Vuota",
MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
@@ -298,7 +325,7 @@ namespace Ganimede
var pendingJobs = _processingService.JobQueue.Count(job => job.Status == JobStatus.Pending);
if (pendingJobs == 0)
{
WpfMessageBox.Show("No pending videos in queue.", "No Pending Jobs",
WpfMessageBox.Show("Nessun job in stato Pending.", "Nessun Job",
MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
@@ -310,7 +337,7 @@ namespace Ganimede
private void StopQueueButton_Click(object sender, RoutedEventArgs e)
{
_processingService.StopProcessing();
StatusText.Text = "Stopping queue processing...";
StatusText.Text = "Arresto elaborazione in corso...";
Debug.WriteLine("[QUEUE] Stop processing requested by user");
}
@@ -352,18 +379,25 @@ namespace Ganimede
if (configWindow.ShowDialog() == true)
{
StatusText.Text = $"Configuration applied to {_selectedJobs.Count} job(s)";
StatusText.Text = $"Configurazione applicata a {_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;
if (job.ExtractionMode == ExtractionMode.SingleFrame)
{
job.OutputFolder = outputFolder; // single frame directly
}
else
{
job.OutputFolder = createSubfolder
? Path.Combine(outputFolder, job.VideoName)
: outputFolder;
}
}
}
}
private void RemoveQueueItem_Click(object sender, RoutedEventArgs e)
{
@@ -380,7 +414,7 @@ namespace Ganimede
private void ClearCompletedButton_Click(object sender, RoutedEventArgs e)
{
_processingService.RemoveCompletedJobs();
StatusText.Text = "Completed jobs cleared";
StatusText.Text = "Job completati rimossi";
}
private void ClearAllButton_Click(object sender, RoutedEventArgs e)
@@ -390,26 +424,23 @@ namespace Ganimede
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",
$"Ci sono {processingJobs.Count} job in elaborazione.\n\n" +
"Sì: Ferma tutto e svuota la coda\n" +
"No: Rimuovi solo job completati/pending\n" +
"Annulla: Non fare nulla",
"Job in corso",
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";
StatusText.Text = "Tutti i job rimossi e elaborazione fermata";
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)
@@ -417,71 +448,26 @@ namespace Ganimede
_processingService.JobQueue.RemoveAt(i);
}
}
StatusText.Text = "Cleared completed/pending jobs (processing jobs continue)";
StatusText.Text = "Coda ripulita (job in corso mantenuti)";
break;
case MessageBoxResult.Cancel:
// Do nothing
return;
}
}
else
{
var result = WpfMessageBox.Show("Are you sure you want to clear all jobs from the queue?",
"Clear All Jobs", MessageBoxButton.YesNo, MessageBoxImage.Question);
var result = WpfMessageBox.Show("Sicuro di voler rimuovere tutti i job?",
"Pulisci Tutto", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
_processingService.JobQueue.Clear();
thumbnails.Clear();
StatusText.Text = "All jobs cleared";
StatusText.Text = "Tutti i job rimossi";
}
}
}
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));
}
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));
}
private static bool IsVideoFile(string filePath)
{
var extension = Path.GetExtension(filePath).ToLowerInvariant();

View File

@@ -40,6 +40,14 @@ namespace Ganimede.Models
VideoNameFrameProgressive
}
// NEW: extraction mode
public enum ExtractionMode
{
Full, // Extract all frames (default)
SingleFrame, // Extract only one representative frame
Auto // Let the system analyze and decide
}
public class VideoJob : INotifyPropertyChanged
{
private JobStatus _status = JobStatus.Pending;
@@ -51,6 +59,8 @@ namespace Ganimede.Models
private bool _customCreateSubfolder = true;
private NamingPattern? _customNamingPattern = null;
private string _customPrefix = string.Empty;
private ExtractionMode _extractionMode = ExtractionMode.Full; // user chosen / default
private ExtractionMode? _suggestedExtractionMode = null; // suggestion after analysis
public required string VideoPath { get; set; }
public string VideoName => System.IO.Path.GetFileNameWithoutExtension(VideoPath);
@@ -154,6 +164,30 @@ namespace Ganimede.Models
}
}
// NEW: extraction mode chosen by user (or default)
public ExtractionMode ExtractionMode
{
get => _extractionMode;
set
{
_extractionMode = value;
OnPropertyChanged(nameof(ExtractionMode));
OnPropertyChanged(nameof(ExtractionModeDisplay));
}
}
// NEW: suggested extraction mode discovered during quick analysis
public ExtractionMode? SuggestedExtractionMode
{
get => _suggestedExtractionMode;
set
{
_suggestedExtractionMode = value;
OnPropertyChanged(nameof(SuggestedExtractionMode));
OnPropertyChanged(nameof(ExtractionModeDisplay));
}
}
// Display properties for UI
public string OutputFolderDisplay =>
string.IsNullOrEmpty(CustomOutputFolder) ? "Default" : System.IO.Path.GetFileName(CustomOutputFolder) + (CustomCreateSubfolder ? "/??" : "");
@@ -169,6 +203,24 @@ namespace Ganimede.Models
CustomNamingPattern?.ToString() ?? "Default" +
(!string.IsNullOrEmpty(CustomPrefix) ? $" ({CustomPrefix}_)" : "");
// NEW: display extraction mode with suggestion
public string ExtractionModeDisplay
{
get
{
var mode = ExtractionMode.ToString();
if (SuggestedExtractionMode.HasValue && ExtractionMode == ExtractionMode.Full && SuggestedExtractionMode.Value == ExtractionMode.SingleFrame)
{
mode += " (Suggerito: SingleFrame)";
}
else if (SuggestedExtractionMode.HasValue && ExtractionMode == ExtractionMode.Auto)
{
mode += $" -> {SuggestedExtractionMode.Value}";
}
return mode;
}
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)

View File

@@ -118,5 +118,30 @@ namespace Ganimede.Properties {
this["DefaultCustomPrefix"] = value;
}
}
// NEW: default extraction mode (Full, SingleFrame, Auto)
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("Full")]
public string DefaultExtractionMode {
get {
return ((string)(this["DefaultExtractionMode"]));
}
set {
this["DefaultExtractionMode"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("False")]
public bool SingleFrameUseSubfolder {
get {
return ((bool)(this["SingleFrameUseSubfolder"]));
}
set {
this["SingleFrameUseSubfolder"] = value;
}
}
}
}

View File

@@ -26,5 +26,11 @@
<Setting Name="DefaultCustomPrefix" Type="System.String" Scope="User">
<Value Profile="(Default)">custom</Value>
</Setting>
<Setting Name="DefaultExtractionMode" Type="System.String" Scope="User">
<Value Profile="(Default)">Full</Value>
</Setting>
<Setting Name="SingleFrameUseSubfolder" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
</Settings>
</SettingsFile>

View File

@@ -28,16 +28,28 @@ namespace Ganimede.Services
public void AddJob(string videoPath, string outputFolder, bool createSubfolder = true)
{
// Determine default extraction mode from settings
var settingMode = Settings.Default.DefaultExtractionMode;
ExtractionMode extractionMode = ExtractionMode.Full;
if (!string.IsNullOrEmpty(settingMode) && Enum.TryParse<ExtractionMode>(settingMode, out var parsed))
{
extractionMode = parsed;
}
var useSingleSub = Settings.Default.SingleFrameUseSubfolder;
var jobOutput = createSubfolder && (extractionMode != ExtractionMode.SingleFrame || (extractionMode == ExtractionMode.SingleFrame && useSingleSub))
? Path.Combine(outputFolder, Path.GetFileNameWithoutExtension(videoPath))
: outputFolder;
var job = new VideoJob
{
VideoPath = videoPath,
OutputFolder = createSubfolder
? Path.Combine(outputFolder, Path.GetFileNameWithoutExtension(videoPath))
: outputFolder
OutputFolder = jobOutput,
ExtractionMode = extractionMode
};
_jobQueue.Add(job);
Debug.WriteLine($"[QUEUE] Added job: {job.VideoName} (Status: Pending)");
Debug.WriteLine($"[QUEUE] Added job: {job.VideoName} (Status: Pending) Mode={job.ExtractionMode}");
}
public async Task StartProcessingAsync()
@@ -134,21 +146,59 @@ namespace Ganimede.Services
{
Debug.WriteLine($"[PROCESS] Starting job: {job.VideoName}");
// Create output directory if it doesn't exist
Directory.CreateDirectory(job.OutputFolder);
// (Do not decide folder change yet; need analysis for Auto mode)
var mediaInfo = await FFProbe.AnalyseAsync(job.VideoPath);
int frameRate = (int)(mediaInfo.PrimaryVideoStream?.FrameRate ?? 24);
int totalFrames = (int)(mediaInfo.Duration.TotalSeconds * frameRate);
Debug.WriteLine($"[INFO] Video {job.VideoName}: {totalFrames} frames at {frameRate} fps, duration {mediaInfo.Duration}");
Debug.WriteLine($"[INFO] Processing {totalFrames} frames at {frameRate} fps");
// Heuristic suggestion
var suggestSingleFrame = false;
try
{
if (totalFrames <= frameRate * 2)
suggestSingleFrame = true;
else if (mediaInfo.Duration.TotalSeconds >= 3 && mediaInfo.Duration.TotalSeconds <= 45)
{
var primary = mediaInfo.PrimaryVideoStream;
if (primary != null && primary.BitRate > 0 && primary.Width > 0 && primary.Height > 0)
{
double pixels = primary.Width * primary.Height;
if (primary.BitRate < pixels * 0.3)
suggestSingleFrame = true;
}
}
}
catch { }
job.SuggestedExtractionMode = suggestSingleFrame ? ExtractionMode.SingleFrame : ExtractionMode.Full;
var effectiveMode = job.ExtractionMode == ExtractionMode.Auto
? (job.SuggestedExtractionMode ?? ExtractionMode.Full)
: job.ExtractionMode;
// ADJUST OUTPUT FOLDER NOW based on effective mode (includes Auto->SingleFrame)
if (effectiveMode == ExtractionMode.SingleFrame && !Settings.Default.SingleFrameUseSubfolder)
{
// If current output folder ends with the video name (subfolder), move up
if (Path.GetFileName(job.OutputFolder).Equals(job.VideoName, StringComparison.OrdinalIgnoreCase))
{
var parent = Directory.GetParent(job.OutputFolder);
if (parent != null)
{
Debug.WriteLine($"[PROCESS] Adjusting output folder for single frame (removing subfolder): {job.OutputFolder} -> {parent.FullName}");
job.OutputFolder = parent.FullName;
}
}
}
Directory.CreateDirectory(job.OutputFolder);
var frameSize = GetFrameSize(job);
var overwriteMode = GetOverwriteMode(job);
var namingPattern = GetNamingPattern(job);
var customPrefix = GetCustomPrefix(job);
// Check for existing files if needed (using naming pattern)
var existingFiles = Directory.GetFiles(job.OutputFolder, "*.png");
if (existingFiles.Length > 0 && overwriteMode == OverwriteMode.Ask)
{
@@ -161,14 +211,32 @@ namespace Ganimede.Services
System.Windows.MessageBoxButton.YesNo,
System.Windows.MessageBoxImage.Question);
});
overwriteMode = dialogResult == System.Windows.MessageBoxResult.Yes ? OverwriteMode.Overwrite : OverwriteMode.Skip;
}
// Process frames sequentially
if (effectiveMode == ExtractionMode.SingleFrame)
{
int targetIndex = totalFrames > 0 ? totalFrames / 2 : 0;
var frameTime = TimeSpan.FromSeconds((double)targetIndex / Math.Max(frameRate,1));
var fileName = NamingHelper.GenerateFileName(namingPattern, job, targetIndex, frameTime, customPrefix);
string framePath = Path.Combine(job.OutputFolder, fileName);
if (File.Exists(framePath) && overwriteMode == OverwriteMode.Skip)
job.StatusMessage = "Frame already exists (skipped)";
else
{
await ExtractFrameAsync(job, targetIndex, frameRate, frameSize, framePath);
job.StatusMessage = "Single frame extracted";
}
job.Progress = 100;
job.Status = JobStatus.Completed;
JobCompleted?.Invoke(job);
Debug.WriteLine($"[SUCCESS] Single frame extraction completed: {job.VideoName}");
return;
}
// Full extraction loop (unchanged)
int processedFrames = 0;
int skippedFrames = 0;
for (int i = 0; i < totalFrames; i++)
{
if (cancellationToken.IsCancellationRequested)
@@ -178,38 +246,23 @@ namespace Ganimede.Services
Debug.WriteLine($"[CANCELLED] Job cancelled: {job.VideoName}");
return;
}
var frameTime = TimeSpan.FromSeconds((double)i / frameRate);
var fileName = NamingHelper.GenerateFileName(namingPattern, job, i, frameTime, customPrefix);
string framePath = Path.Combine(job.OutputFolder, fileName);
// 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.StatusMessage = $"Processed {processedFrames}/{totalFrames} frames ({job.Progress:F1}%)" + (skippedFrames > 0 ? $" - Skipped {skippedFrames}" : "");
if (i % 10 == 0) await Task.Delay(1, cancellationToken);
}
}
job.Status = JobStatus.Completed;
job.StatusMessage = $"Completed - {processedFrames} frames processed" +
(skippedFrames > 0 ? $", {skippedFrames} skipped" : "");
job.StatusMessage = $"Completed - {processedFrames} frames processed" + (skippedFrames > 0 ? $", {skippedFrames} skipped" : "");
job.Progress = 100;
Debug.WriteLine($"[SUCCESS] Completed job: {job.VideoName}");
JobCompleted?.Invoke(job);
}
@@ -230,22 +283,20 @@ namespace Ganimede.Services
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) || frameSize == "original")
return (-1, -1); // Special value indicating original size
return (-1, -1);
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 (-1, -1); // Default to original size if parsing fails
return (-1, -1);
}
private OverwriteMode GetOverwriteMode(VideoJob job)
{
// Use job-specific overwrite mode if set, otherwise use default setting
if (job.CustomOverwriteMode.HasValue)
return job.CustomOverwriteMode.Value;
@@ -258,7 +309,6 @@ namespace Ganimede.Services
private NamingPattern GetNamingPattern(VideoJob job)
{
// Use job-specific naming pattern if set, otherwise use default setting
if (job.CustomNamingPattern.HasValue)
return job.CustomNamingPattern.Value;
@@ -271,7 +321,6 @@ namespace Ganimede.Services
private string GetCustomPrefix(VideoJob job)
{
// Use job-specific custom prefix if set, otherwise use default setting
if (!string.IsNullOrEmpty(job.CustomPrefix))
return job.CustomPrefix;
@@ -284,10 +333,8 @@ namespace Ganimede.Services
try
{
// Check if we should use original size
if (frameSize.width == -1 && frameSize.height == -1)
{
// Extract frame with original video size (no resize)
try
{
await FFMpegArguments
@@ -301,7 +348,6 @@ namespace Ganimede.Services
}
catch
{
// Fallback without codec specification
await FFMpegArguments
.FromFileInput(job.VideoPath)
.OutputToFile(framePath, true, options => options
@@ -312,7 +358,6 @@ namespace Ganimede.Services
}
}
// Extract frame with specified resize
try
{
await FFMpegArguments
@@ -326,7 +371,6 @@ namespace Ganimede.Services
}
catch
{
// Fallback without codec specification
await FFMpegArguments
.FromFileInput(job.VideoPath)
.OutputToFile(framePath, true, options => options
@@ -339,7 +383,6 @@ namespace Ganimede.Services
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

@@ -4,7 +4,7 @@
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"
Title="Configure Selected Jobs" Height="560" Width="600"
Background="#222" WindowStartupLocation="CenterOwner">
<Grid Margin="20">
<Grid.RowDefinitions>
@@ -22,6 +22,19 @@
<!-- Settings Content -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel>
<!-- Extraction Mode Settings -->
<GroupBox Header="Modalita Estrazione" Foreground="White" BorderBrush="#444" Margin="0,0,0,20">
<StackPanel Margin="10">
<TextBlock Text="Seleziona come estrarre i frame:" Foreground="#CCC" Margin="0,0,0,8"/>
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<RadioButton x:Name="ExtractionFullRadio" Content="Completa" GroupName="ExtractionMode" IsChecked="True" Foreground="White" Margin="0,0,15,0"/>
<RadioButton x:Name="ExtractionSingleRadio" Content="Singolo Frame" GroupName="ExtractionMode" Foreground="White" Margin="0,0,15,0"/>
<RadioButton x:Name="ExtractionAutoRadio" Content="Auto" GroupName="ExtractionMode" Foreground="White"/>
</StackPanel>
<TextBlock Text="Auto: il sistema analizza il video e decide se conviene estrarre tutti i frame o solo uno (tipico dei video social con immagine statica)." TextWrapping="Wrap" Foreground="#888" FontSize="11"/>
</StackPanel>
</GroupBox>
<!-- Output Settings -->
<GroupBox Header="Output Settings" Foreground="White" BorderBrush="#444" Margin="0,0,0,20">
<StackPanel Margin="10">

View File

@@ -22,12 +22,37 @@ namespace Ganimede.Windows
LoadCurrentSettings();
}
private void SetExtractionModeRadio(ExtractionMode mode)
{
var fullObj = FindName("ExtractionFullRadio");
if (fullObj is System.Windows.Controls.RadioButton fullRb)
fullRb.IsChecked = mode == ExtractionMode.Full;
var singleObj = FindName("ExtractionSingleRadio");
if (singleObj is System.Windows.Controls.RadioButton singleRb)
singleRb.IsChecked = mode == ExtractionMode.SingleFrame;
var autoObj = FindName("ExtractionAutoRadio");
if (autoObj is System.Windows.Controls.RadioButton autoRb)
autoRb.IsChecked = mode == ExtractionMode.Auto;
}
private ExtractionMode GetSelectedExtractionMode()
{
var singleObj = FindName("ExtractionSingleRadio");
if (singleObj is System.Windows.Controls.RadioButton singleRb && singleRb.IsChecked == true)
return ExtractionMode.SingleFrame;
var autoObj = FindName("ExtractionAutoRadio");
if (autoObj is System.Windows.Controls.RadioButton autoRb && autoRb.IsChecked == true)
return ExtractionMode.Auto;
return ExtractionMode.Full;
}
private void LoadCurrentSettings()
{
// Load current settings from first job or defaults
var firstJob = _selectedJobs.FirstOrDefault();
if (firstJob != null)
{
SetExtractionModeRadio(firstJob.ExtractionMode);
// Output settings
if (!string.IsNullOrEmpty(firstJob.CustomOutputFolder))
{
@@ -80,13 +105,10 @@ namespace Ganimede.Windows
}
}
// Set default selections if nothing is selected
if (CustomFrameSizeComboBox.SelectedItem == null)
CustomFrameSizeComboBox.SelectedIndex = 0;
if (CustomOverwriteComboBox.SelectedItem == null)
CustomOverwriteComboBox.SelectedIndex = 0;
if (CustomNamingComboBox.SelectedItem == null)
CustomNamingComboBox.SelectedIndex = 0;
@@ -117,35 +139,12 @@ namespace Ganimede.Windows
}
}
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 UseCustomNamingCheckBox_CheckedChanged(object sender, RoutedEventArgs e)
{
UpdateJobNamingPreview();
}
private void CustomNamingComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
UpdateJobNamingPreview();
}
private void CustomNamingPrefixTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
UpdateJobNamingPreview();
}
private void UseCustomOutputCheckBox_CheckedChanged(object sender, RoutedEventArgs e) { }
private void UseCustomFrameSizeCheckBox_CheckedChanged(object sender, RoutedEventArgs e) { }
private void UseCustomOverwriteCheckBox_CheckedChanged(object sender, RoutedEventArgs e) { }
private void UseCustomNamingCheckBox_CheckedChanged(object sender, RoutedEventArgs e) { UpdateJobNamingPreview(); }
private void CustomNamingComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { UpdateJobNamingPreview(); }
private void CustomNamingPrefixTextBox_TextChanged(object sender, TextChangedEventArgs e) { UpdateJobNamingPreview(); }
private void BrowseCustomOutputButton_Click(object sender, RoutedEventArgs e)
{
@@ -168,26 +167,31 @@ namespace Ganimede.Windows
{
try
{
var selectedExtraction = GetSelectedExtractionMode();
foreach (var job in _selectedJobs)
{
// Apply output settings
job.ExtractionMode = selectedExtraction;
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;
// Do NOT create per-video folder if SingleFrame
if (job.CustomCreateSubfolder && job.ExtractionMode != ExtractionMode.SingleFrame)
{
job.OutputFolder = System.IO.Path.Combine(job.CustomOutputFolder, job.VideoName);
}
else
{
job.OutputFolder = job.CustomOutputFolder; // single frame goes directly here
}
}
else
{
job.CustomOutputFolder = string.Empty;
// Reset to default output folder logic will be handled in MainWindow
// OutputFolder will be recalculated in MainWindow if needed
}
// Apply frame size settings
if (UseCustomFrameSizeCheckBox.IsChecked == true && CustomFrameSizeComboBox.SelectedItem is ComboBoxItem frameSizeItem)
{
job.CustomFrameSize = frameSizeItem.Tag?.ToString() ?? string.Empty;
@@ -197,7 +201,6 @@ namespace Ganimede.Windows
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))
@@ -210,7 +213,6 @@ namespace Ganimede.Windows
job.CustomOverwriteMode = null;
}
// Apply naming settings
if (UseCustomNamingCheckBox.IsChecked == true && CustomNamingComboBox.SelectedItem is ComboBoxItem namingItem)
{
if (Enum.TryParse<NamingPattern>(namingItem.Tag?.ToString(), out var namingPattern))

View File

@@ -4,7 +4,7 @@
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"
Title="Settings" Height="560" Width="600"
Background="#222" WindowStartupLocation="CenterOwner">
<Grid Margin="20">
<Grid.RowDefinitions>
@@ -69,12 +69,24 @@
<ComboBoxItem Content="1920x1080 (Full HD)" Tag="1920,1080" Foreground="Black" Background="White"/>
</ComboBox>
<TextBlock Text="Default File Overwrite Behavior:" Foreground="#CCC" Margin="0,15,0,5"/>
<TextBlock Text="Default File Overwrite Behavior:" Foreground="#CCC" Margin="15,15,0,5"/>
<ComboBox x:Name="OverwriteModeComboBox" Height="30" Background="#333" Foreground="White" BorderBrush="#555">
<ComboBoxItem Content="Ask each time" Tag="Ask" IsSelected="True" Foreground="Black" Background="White"/>
<ComboBoxItem Content="Skip existing files" Tag="Skip" Foreground="Black" Background="White"/>
<ComboBoxItem Content="Overwrite existing files" Tag="Overwrite" Foreground="Black" Background="White"/>
</ComboBox>
<!-- New default extraction mode -->
<TextBlock Text="Default Extraction Mode:" Foreground="#CCC" Margin="15,15,0,5"/>
<StackPanel Orientation="Horizontal" Margin="0,0,0,0">
<RadioButton x:Name="DefaultModeFullRadio" Content="Full" GroupName="DefExtraction" Foreground="White" Margin="0,0,15,0" IsChecked="True"/>
<RadioButton x:Name="DefaultModeSingleRadio" Content="Single Frame" GroupName="DefExtraction" Foreground="White" Margin="0,0,15,0"/>
<RadioButton x:Name="DefaultModeAutoRadio" Content="Auto" GroupName="DefExtraction" Foreground="White"/>
</StackPanel>
<TextBlock Text="Auto: analyze video to decide if extracting all frames or only one." Foreground="#777" FontSize="10" TextWrapping="Wrap" Margin="0,4,0,0"/>
<TextBlock Text="Single Frame Output (default behaviour = direct in output folder):" Foreground="#CCC" Margin="15,15,0,5"/>
<CheckBox x:Name="SingleFrameUseSubfolderCheckBox" Content="Create per-video subfolder for single frame jobs (uncheck = save directly)" Foreground="White" IsChecked="False" ToolTip="Unchecked: single frame saved directly in main output folder. Checked: create video-named subfolder."/>
</StackPanel>
</GroupBox>

View File

@@ -1,7 +1,6 @@
using System;
using System.IO;
using System.Windows;
using Microsoft.Win32;
using Ganimede.Properties;
using System.Diagnostics;
using WpfMessageBox = System.Windows.MessageBox;
@@ -16,11 +15,17 @@ namespace Ganimede.Windows
LoadSettings();
}
private System.Windows.Controls.RadioButton? GetDefaultModeRadio(string name) => FindName(name) as System.Windows.Controls.RadioButton;
private System.Windows.Controls.CheckBox? GetCheckBox(string name) => FindName(name) as System.Windows.Controls.CheckBox;
private void LoadSettings()
{
FFmpegPathTextBox.Text = Settings.Default.FFmpegBinFolder;
DefaultOutputTextBox.Text = Settings.Default.LastOutputFolder;
CreateSubfolderCheckBox.IsChecked = Settings.Default.CreateSubfolder;
var singleFrameChk = GetCheckBox("SingleFrameUseSubfolderCheckBox");
if (singleFrameChk != null)
singleFrameChk.IsChecked = Settings.Default.SingleFrameUseSubfolder;
var frameSize = Settings.Default.FrameSize;
foreach (System.Windows.Controls.ComboBoxItem item in FrameSizeComboBox.Items)
@@ -42,13 +47,32 @@ namespace Ganimede.Windows
}
}
// TODO: Load naming pattern settings when controls are generated
// var namingPattern = Settings.Default.DefaultNamingPattern;
// CustomPrefixTextBox.Text = Settings.Default.DefaultCustomPrefix;
// Default extraction mode
switch (Settings.Default.DefaultExtractionMode)
{
case "SingleFrame":
if (GetDefaultModeRadio("DefaultModeSingleRadio") is { } r1) r1.IsChecked = true; break;
case "Auto":
if (GetDefaultModeRadio("DefaultModeAutoRadio") is { } r2) r2.IsChecked = true; break;
default:
if (GetDefaultModeRadio("DefaultModeFullRadio") is { } r3) r3.IsChecked = true; break;
}
UpdateFFmpegStatus();
}
private string GetSelectedDefaultExtractionMode()
{
if (GetDefaultModeRadio("DefaultModeSingleRadio")?.IsChecked == true) return "SingleFrame";
if (GetDefaultModeRadio("DefaultModeAutoRadio")?.IsChecked == true) return "Auto";
return "Full";
}
private bool GetSingleFrameUseSubfolder()
{
return GetCheckBox("SingleFrameUseSubfolderCheckBox")?.IsChecked == true;
}
private void UpdateFFmpegStatus()
{
var path = FFmpegPathTextBox.Text;
@@ -115,38 +139,23 @@ namespace Ganimede.Windows
}
}
// TODO: Implement naming pattern event handlers when controls are generated
private void NamingPatternComboBox_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
// UpdateNamingPreview();
}
private void CustomPrefixTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
// UpdateNamingPreview();
}
private void NamingPatternComboBox_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) { }
private void CustomPrefixTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e) { }
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";
// TODO: Save naming settings when controls are available
// Settings.Default.DefaultNamingPattern = ...
// Settings.Default.DefaultCustomPrefix = ...
Settings.Default.DefaultExtractionMode = GetSelectedDefaultExtractionMode();
Settings.Default.SingleFrameUseSubfolder = GetSingleFrameUseSubfolder();
Settings.Default.Save();
Debug.WriteLine("[SETTINGS] Settings saved successfully");
DialogResult = true;
Close();