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:
@@ -11,106 +11,55 @@
|
||||
<local:StatusColorConverter x:Key="StatusColorConverter"/>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Margin="20">
|
||||
<Grid Margin="10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</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">
|
||||
<!-- 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>
|
||||
<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="90" Height="35"
|
||||
Click="StopQueueButton_Click" Background="#DC3545" Foreground="White" FontSize="12" IsEnabled="False"/>
|
||||
</StackPanel>
|
||||
</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="🗑 Pulisci Completati" Width="140" Height="30" Margin="0,0,8,0"
|
||||
Click="ClearCompletedButton_Click" FontSize="11"/>
|
||||
<Button x:Name="ClearAllButton" Content="🗑 Pulisci Tutto" Width="120" Height="30"
|
||||
Click="ClearAllButton_Click" Background="#6C757D" Foreground="White"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Queue List FULL AREA -->
|
||||
<Grid Grid.Row="2">
|
||||
<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 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>
|
||||
|
||||
<!-- 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">
|
||||
<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>
|
||||
|
||||
@@ -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,15 +379,22 @@ 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;
|
||||
job.OutputFolder = createSubfolder
|
||||
? Path.Combine(outputFolder, job.VideoName)
|
||||
: outputFolder;
|
||||
if (job.ExtractionMode == ExtractionMode.SingleFrame)
|
||||
{
|
||||
job.OutputFolder = outputFolder; // single frame directly
|
||||
}
|
||||
else
|
||||
{
|
||||
job.OutputFolder = createSubfolder
|
||||
? Path.Combine(outputFolder, job.VideoName)
|
||||
: outputFolder;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
25
Ganimede/Ganimede/Properties/Settings.Designer.cs
generated
25
Ganimede/Ganimede/Properties/Settings.Designer.cs
generated
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user