diff --git a/Ganimede/Ganimede/MainWindow.xaml b/Ganimede/Ganimede/MainWindow.xaml
index 4e0113f..f4c26b5 100644
--- a/Ganimede/Ganimede/MainWindow.xaml
+++ b/Ganimede/Ganimede/MainWindow.xaml
@@ -11,106 +11,55 @@
-
+
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
@@ -125,27 +74,25 @@
-
-
-
-
-
-
-
-
-
-
+ Tag="{Binding}" ToolTip="Rimuovi dalla coda"/>
+
+
+
+
+
+
-
+
+
@@ -155,10 +102,30 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Ganimede/Ganimede/MainWindow.xaml.cs b/Ganimede/Ganimede/MainWindow.xaml.cs
index 3a701aa..001b9e2 100644
--- a/Ganimede/Ganimede/MainWindow.xaml.cs
+++ b/Ganimede/Ganimede/MainWindow.xaml.cs
@@ -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();
diff --git a/Ganimede/Ganimede/Models/VideoJob.cs b/Ganimede/Ganimede/Models/VideoJob.cs
index 9cddb8b..bba4f22 100644
--- a/Ganimede/Ganimede/Models/VideoJob.cs
+++ b/Ganimede/Ganimede/Models/VideoJob.cs
@@ -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)
diff --git a/Ganimede/Ganimede/Properties/Settings.Designer.cs b/Ganimede/Ganimede/Properties/Settings.Designer.cs
index 497ea90..7a12c26 100644
--- a/Ganimede/Ganimede/Properties/Settings.Designer.cs
+++ b/Ganimede/Ganimede/Properties/Settings.Designer.cs
@@ -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;
+ }
+ }
}
}
diff --git a/Ganimede/Ganimede/Properties/Settings.settings b/Ganimede/Ganimede/Properties/Settings.settings
index 43e163b..a72fec3 100644
--- a/Ganimede/Ganimede/Properties/Settings.settings
+++ b/Ganimede/Ganimede/Properties/Settings.settings
@@ -26,5 +26,11 @@
custom
+
+ Full
+
+
+ False
+
\ No newline at end of file
diff --git a/Ganimede/Ganimede/Services/VideoProcessingService.cs b/Ganimede/Ganimede/Services/VideoProcessingService.cs
index 3b114aa..5223065 100644
--- a/Ganimede/Ganimede/Services/VideoProcessingService.cs
+++ b/Ganimede/Ganimede/Services/VideoProcessingService.cs
@@ -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(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
}
}
}
diff --git a/Ganimede/Ganimede/Windows/JobConfigWindow.xaml b/Ganimede/Ganimede/Windows/JobConfigWindow.xaml
index e35392e..0c4f837 100644
--- a/Ganimede/Ganimede/Windows/JobConfigWindow.xaml
+++ b/Ganimede/Ganimede/Windows/JobConfigWindow.xaml
@@ -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">
@@ -22,6 +22,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Ganimede/Ganimede/Windows/JobConfigWindow.xaml.cs b/Ganimede/Ganimede/Windows/JobConfigWindow.xaml.cs
index 69bcb33..5a246a7 100644
--- a/Ganimede/Ganimede/Windows/JobConfigWindow.xaml.cs
+++ b/Ganimede/Ganimede/Windows/JobConfigWindow.xaml.cs
@@ -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(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(namingItem.Tag?.ToString(), out var namingPattern))
diff --git a/Ganimede/Ganimede/Windows/SettingsWindow.xaml b/Ganimede/Ganimede/Windows/SettingsWindow.xaml
index e82dbad..e47a0e5 100644
--- a/Ganimede/Ganimede/Windows/SettingsWindow.xaml
+++ b/Ganimede/Ganimede/Windows/SettingsWindow.xaml
@@ -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">
@@ -69,12 +69,24 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Ganimede/Ganimede/Windows/SettingsWindow.xaml.cs b/Ganimede/Ganimede/Windows/SettingsWindow.xaml.cs
index fa70809..adcf44d 100644
--- a/Ganimede/Ganimede/Windows/SettingsWindow.xaml.cs
+++ b/Ganimede/Ganimede/Windows/SettingsWindow.xaml.cs
@@ -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();