Sostituzione FFMpegCore con FFMediaToolkit

- Rimosso FFMpegCore e introdotto FFMediaToolkit per semplificare
  la gestione dei video e migliorare le prestazioni.
- Aggiunti `FrameExtractor` e `VideoAnalyzer` per l'estrazione
  dei frame e l'analisi dei metadati video.
- Riprogettata l'interfaccia utente con tema moderno e navigazione
  a schede (Processing, Library, Settings).
- Integrate le impostazioni nella scheda "Settings", eliminando
  la finestra legacy.
- Aggiornato `VideoProcessingService` per utilizzare i nuovi
  wrapper e migliorata la gestione della coda.
- Tradotti i testi dell'interfaccia dall'italiano all'inglese.
- Aggiornata la documentazione (`README.md`) con dettagli sulle
  funzionalità, lo stack tecnologico e la struttura del progetto.
- Ottimizzate le prestazioni e migliorata la gestione degli errori.
- Aggiunto supporto per nuovi formati video (FLV, WebM).
- Rimossi codice e risorse obsolete, migliorando la manutenibilità.
This commit is contained in:
2025-12-07 23:34:48 +01:00
parent 959fdad037
commit 11931854c7
11 changed files with 1150 additions and 461 deletions

View File

@@ -13,9 +13,6 @@
<setting name="LastVideoPath" serializeAs="String">
<value />
</setting>
<setting name="FFmpegBinFolder" serializeAs="String">
<value>C:\Users\balbo\source\repos\Ganimede\Ganimede\Ganimede\FFMpeg</value>
</setting>
</Ganimede.Properties.Settings>
</userSettings>
</configuration>

View File

@@ -7,10 +7,12 @@
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FFMpegCore" Version="5.2.0" />
<PackageReference Include="FFMediaToolkit" Version="4.8.1" />
<PackageReference Include="System.Drawing.Common" Version="10.0.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -5,48 +5,47 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Ganimede"
mc:Ignorable="d"
Title="Estrattore Frame Video" Height="800" Width="1250"
Background="#1E2228" WindowStartupLocation="CenterScreen">
Title="Ganimede - Video Frame Extractor" Height="750" Width="1200"
Background="#F5F7FA" WindowStartupLocation="CenterScreen">
<Window.Resources>
<local:StatusColorConverter x:Key="StatusColorConverter"/>
<!-- Color resources -->
<Color x:Key="AccentColor">#268BFF</Color>
<SolidColorBrush x:Key="AccentBrush" Color="{StaticResource AccentColor}"/>
<SolidColorBrush x:Key="AccentBrushLight" Color="#39A3FF"/>
<SolidColorBrush x:Key="BaseBrush" Color="#1E2228"/>
<SolidColorBrush x:Key="PanelBrush" Color="#242A31"/>
<SolidColorBrush x:Key="PanelSubBrush" Color="#2C333B"/>
<SolidColorBrush x:Key="BorderBrushColor" Color="#38424D"/>
<SolidColorBrush x:Key="TextPrimaryBrush" Color="#FFFFFF"/>
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#B5BDC7"/>
<!-- Modern Color Palette -->
<SolidColorBrush x:Key="PrimaryBrush" Color="#6366F1"/>
<SolidColorBrush x:Key="PrimaryDarkBrush" Color="#4F46E5"/>
<SolidColorBrush x:Key="SuccessBrush" Color="#10B981"/>
<SolidColorBrush x:Key="DangerBrush" Color="#EF4444"/>
<SolidColorBrush x:Key="WarningBrush" Color="#F59E0B"/>
<SolidColorBrush x:Key="BackgroundBrush" Color="#F5F7FA"/>
<SolidColorBrush x:Key="SurfaceBrush" Color="#FFFFFF"/>
<SolidColorBrush x:Key="BorderBrush" Color="#E5E7EB"/>
<SolidColorBrush x:Key="TextPrimaryBrush" Color="#111827"/>
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#6B7280"/>
<SolidColorBrush x:Key="TextMutedBrush" Color="#9CA3AF"/>
<!-- Button Style -->
<Style TargetType="Button" x:Key="ToolbarButton">
<!-- Modern Button Style -->
<Style TargetType="Button" x:Key="ModernButton">
<Setter Property="Background" Value="{StaticResource PrimaryBrush}"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Background" Value="#2F3740"/>
<Setter Property="BorderBrush" Value="#3F4A55"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="14 8"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="16,10"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinHeight" Value="40"/>
<Setter Property="FontWeight" Value="Medium"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="8" SnapsToDevicePixels="True">
<Border Background="{TemplateBinding Background}"
CornerRadius="8"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#39444F"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#46525E"/>
<Setter Property="Background" Value="{StaticResource PrimaryDarkBrush}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.4"/>
<Setter Property="Opacity" Value="0.5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
@@ -54,59 +53,133 @@
</Setter>
</Style>
<!-- Accent Button -->
<Style TargetType="Button" x:Key="AccentButton" BasedOn="{StaticResource ToolbarButton}">
<Setter Property="Background" Value="{StaticResource AccentBrush}"/>
<Setter Property="BorderBrush" Value="#1673D5"/>
<Setter Property="Foreground" Value="White"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{StaticResource AccentBrushLight}"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- Danger Button -->
<Style TargetType="Button" x:Key="DangerButton" BasedOn="{StaticResource ToolbarButton}">
<Setter Property="Background" Value="#D9534F"/>
<Setter Property="BorderBrush" Value="#B33E3B"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#E36460"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- Flat small badge button -->
<Style TargetType="Button" x:Key="SmallGhostButton" BasedOn="{StaticResource ToolbarButton}">
<Setter Property="FontSize" Value="13"/>
<Setter Property="Padding" Value="12 6"/>
<Setter Property="MinHeight" Value="36"/>
<Setter Property="Background" Value="#2F3740"/>
</Style>
<!-- ProgressBar -->
<Style TargetType="ProgressBar">
<Setter Property="Height" Value="6"/>
<Setter Property="Foreground" Value="{StaticResource AccentBrush}"/>
<Setter Property="Background" Value="#313941"/>
<Style TargetType="Button" x:Key="OutlineButton" BasedOn="{StaticResource ModernButton}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
<Setter Property="BorderThickness" Value="1.5"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ProgressBar">
<Border Background="{TemplateBinding Background}" CornerRadius="3">
<Grid x:Name="PART_Track">
<Rectangle x:Name="PART_Indicator" Fill="{TemplateBinding Foreground}" RadiusX="3" RadiusY="3"/>
</Grid>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="8"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#F9FAFB"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="Button" x:Key="DangerButton" BasedOn="{StaticResource ModernButton}">
<Setter Property="Background" Value="{StaticResource DangerBrush}"/>
</Style>
<Style TargetType="Button" x:Key="SuccessButton" BasedOn="{StaticResource ModernButton}">
<Setter Property="Background" Value="{StaticResource SuccessBrush}"/>
</Style>
<!-- Modern TabControl Style -->
<Style TargetType="TabControl">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
<Style TargetType="TabItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TabItem">
<Border Name="Border"
Background="Transparent"
BorderThickness="0,0,0,3"
BorderBrush="Transparent"
Padding="20,12"
Margin="0,0,8,0">
<ContentPresenter x:Name="ContentSite"
VerticalAlignment="Center"
HorizontalAlignment="Center"
ContentSource="Header"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Border" Property="BorderBrush" Value="{StaticResource PrimaryBrush}"/>
<Setter Property="Foreground" Value="{StaticResource PrimaryBrush}"/>
</Trigger>
<Trigger Property="IsSelected" Value="False">
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="#F9FAFB"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="FontSize" Value="15"/>
<Setter Property="FontWeight" Value="Medium"/>
</Style>
<!-- Modern Card Style -->
<Style x:Key="Card" TargetType="Border">
<Setter Property="Background" Value="{StaticResource SurfaceBrush}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="12"/>
<Setter Property="Padding" Value="20"/>
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect Color="#000000" Opacity="0.05" BlurRadius="10" ShadowDepth="0"/>
</Setter.Value>
</Setter>
</Style>
<!-- Modern TextBox Style -->
<Style TargetType="TextBox">
<Setter Property="Background" Value="{StaticResource SurfaceBrush}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
<Setter Property="BorderThickness" Value="1.5"/>
<Setter Property="Padding" Value="12,8"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="8">
<ScrollViewer x:Name="PART_ContentHost" Margin="{TemplateBinding Padding}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ScrollViewer styling for thin scrollbar -->
<Style TargetType="ScrollBar">
<Setter Property="Width" Value="10"/>
<Setter Property="Background" Value="#20252B"/>
<!-- Modern ProgressBar Style -->
<Style TargetType="ProgressBar">
<Setter Property="Height" Value="8"/>
<Setter Property="Background" Value="#E5E7EB"/>
<Setter Property="Foreground" Value="{StaticResource PrimaryBrush}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ProgressBar">
<Border Background="{TemplateBinding Background}" CornerRadius="4">
<Rectangle Name="PART_Indicator"
Fill="{TemplateBinding Foreground}"
RadiusX="4" RadiusY="4"
HorizontalAlignment="Left"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
@@ -117,132 +190,374 @@
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- TOOLBAR -->
<Border Background="#242A31" Padding="16 12" BorderBrush="#303840" BorderThickness="0,0,0,1">
<DockPanel LastChildFill="False">
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left">
<Button x:Name="BrowseVideoButton" Style="{StaticResource AccentButton}" Content=" Aggiungi Video" Click="BrowseVideoButton_Click" Margin="0,0,10,0"/>
<Button x:Name="ImportFolderButton" Style="{StaticResource ToolbarButton}" Content="📁 Importa Cartella" Click="ImportFolderButton_Click" Margin="0,0,10,0"/>
<Button x:Name="SelectOutputFolderButton" Style="{StaticResource ToolbarButton}" Content="🗂 Seleziona Cartella Output" Click="SelectOutputFolderButton_Click" Margin="0,0,10,0"/>
</StackPanel>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Right">
<Button x:Name="ConfigureSelectedButton" Style="{StaticResource ToolbarButton}" Content="⚙ Configura Selezionati" Width="195" IsEnabled="False" Click="ConfigureSelectedButton_Click" Margin="0,0,10,0"/>
<Button x:Name="StartQueueButton" Style="{StaticResource AccentButton}" Content="▶ Avvia Coda" Width="150" Click="StartQueueButton_Click" Margin="0,0,10,0"/>
<Button x:Name="StopQueueButton" Style="{StaticResource DangerButton}" Content="⏹ Ferma" Width="110" IsEnabled="False" Click="StopQueueButton_Click" Margin="0,0,10,0"/>
<Button x:Name="ClearCompletedButton" Style="{StaticResource SmallGhostButton}" Content="🧹 Pulisci Completati" Click="ClearCompletedButton_Click" Margin="0,0,10,0"/>
<Button x:Name="ClearAllButton" Style="{StaticResource SmallGhostButton}" Content="🗑 Pulisci Tutto" Click="ClearAllButton_Click" Margin="0,0,10,0"/>
<Button x:Name="SettingsButton" Style="{StaticResource SmallGhostButton}" Content="⚙ Impostazioni" Click="SettingsButton_Click"/>
<!-- Header -->
<Border Background="{StaticResource SurfaceBrush}"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="0,0,0,1"
Padding="24,16">
<DockPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="🎬" FontSize="28" Margin="0,0,12,0"/>
<StackPanel>
<TextBlock Text="Ganimede"
FontSize="20"
FontWeight="Bold"
Foreground="{StaticResource TextPrimaryBrush}"/>
<TextBlock Text="Video Frame Extractor"
FontSize="12"
Foreground="{StaticResource TextSecondaryBrush}"/>
</StackPanel>
</StackPanel>
</DockPanel>
</Border>
<!-- CONTENUTO PRINCIPALE -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="370"/>
</Grid.ColumnDefinitions>
<!-- Main Content with Tabs -->
<TabControl Grid.Row="1" Margin="24,16,24,16">
<!-- Processing Tab -->
<TabItem Header="🎥 Processing">
<Grid Margin="0,24,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Coda -->
<Grid Margin="18 12 8 12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="0 0 0 10" VerticalAlignment="Center">
<TextBlock Text="Coda Job" FontSize="18" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}"/>
<TextBlock x:Name="QueueCountText" Text="(0)" Foreground="{StaticResource TextSecondaryBrush}" Margin="8,4,0,0"/>
</StackPanel>
<!-- Actions Bar -->
<Border Style="{StaticResource Card}" Margin="0,0,0,16">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Border Grid.Row="1" Background="{StaticResource PanelBrush}" BorderBrush="{StaticResource BorderBrushColor}" BorderThickness="1" CornerRadius="8" Padding="4">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl x:Name="QueueItemsControl">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{StaticResource PanelSubBrush}" Margin="6" Padding="10" CornerRadius="6" BorderBrush="#3A454F" BorderThickness="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Button Style="{StaticResource ModernButton}"
Content=" Add Videos"
Click="BrowseVideoButton_Click"
Margin="0,0,12,0"/>
<Button Style="{StaticResource OutlineButton}"
Content="📁 Import Folder"
Click="ImportFolderButton_Click"
Margin="0,0,12,0"/>
<Button Style="{StaticResource OutlineButton}"
Content="⚙️ Configure Selected"
x:Name="ConfigureSelectedButton"
IsEnabled="False"
Click="ConfigureSelectedButton_Click"/>
</StackPanel>
<CheckBox x:Name="JobCheckBox" Grid.Row="0" Grid.Column="0" Margin="0,0,10,0" VerticalAlignment="Center" Tag="{Binding}" Checked="JobCheckBox_CheckedChanged" Unchecked="JobCheckBox_CheckedChanged"/>
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="{Binding VideoName}" Foreground="White" FontWeight="SemiBold" TextTrimming="CharacterEllipsis" Margin="0,0,8,0"/>
<TextBlock Text="{Binding Progress, StringFormat={}{0:0}%}" Foreground="{StaticResource TextSecondaryBrush}" FontSize="11"/>
</StackPanel>
<Button Grid.Row="0" Grid.Column="2" Content="✕" Width="30" Height="26" Style="{StaticResource SmallGhostButton}" Tag="{Binding}" Click="RemoveQueueItem_Click" ToolTip="Rimuovi"/>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button Style="{StaticResource SuccessButton}"
Content="▶️ Start Queue"
Width="140"
x:Name="StartQueueButton"
Click="StartQueueButton_Click"
Margin="0,0,8,0"/>
<Button Style="{StaticResource DangerButton}"
Content="⏹️ Stop"
Width="100"
x:Name="StopQueueButton"
IsEnabled="False"
Click="StopQueueButton_Click"
Margin="0,0,8,0"/>
<Button Style="{StaticResource OutlineButton}"
Content="🧹 Clear"
Click="ClearCompletedButton_Click"/>
</StackPanel>
</Grid>
</Border>
<ProgressBar Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Margin="0,8,0,0" Value="{Binding Progress}"/>
<TextBlock Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" Text="{Binding StatusMessage}" Foreground="{StaticResource TextSecondaryBrush}" FontSize="11" Margin="0,6,0,0" TextTrimming="CharacterEllipsis"/>
<TextBlock Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3" FontSize="10" Foreground="#87939F" Margin="0,8,0,0" TextWrapping="Wrap">
<TextBlock.Text>
<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>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
</Grid>
<!-- Queue List -->
<Border Grid.Row="1" Style="{StaticResource Card}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Pannello destro -->
<StackPanel Grid.Column="1" Margin="8 12 18 12">
<Border Background="{StaticResource PanelBrush}" BorderBrush="{StaticResource BorderBrushColor}" BorderThickness="1" CornerRadius="8" Padding="14">
<StackPanel>
<TextBlock Text="Impostazioni Globali" FontSize="16" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}"/>
<TextBlock Text="Cartella Output" Foreground="{StaticResource TextSecondaryBrush}" FontSize="12" Margin="0,10,0,2"/>
<DockPanel LastChildFill="True">
<TextBox x:Name="GlobalOutputFolderTextBox" Height="34" Margin="0,0,10,0" IsReadOnly="True" Background="#2C333B" BorderBrush="#3A434C" Foreground="White" BorderThickness="1"/>
<Button Content="Sfoglia" Width="80" Style="{StaticResource SmallGhostButton}" Click="SelectOutputFolderButton_Click"/>
</DockPanel>
<TextBlock Text="Anteprime (Thumbnails)" Foreground="{StaticResource TextSecondaryBrush}" FontSize="12" Margin="0,14,0,4"/>
<Border Background="{StaticResource PanelSubBrush}" BorderBrush="#3A454F" BorderThickness="1" CornerRadius="6" Padding="6" Height="260">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl x:Name="ThumbnailsPanel">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<DockPanel Margin="0,0,0,16">
<TextBlock Text="Processing Queue"
FontSize="18"
FontWeight="SemiBold"
Foreground="{StaticResource TextPrimaryBrush}"/>
<TextBlock x:Name="QueueCountText"
Text="(0)"
FontSize="16"
Foreground="{StaticResource TextMutedBrush}"
Margin="8,2,0,0"/>
</DockPanel>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<ItemsControl x:Name="QueueItemsControl">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="4" BorderBrush="#3D4853" BorderThickness="1" CornerRadius="4">
<Image Source="{Binding}" Width="90" Height="52" Stretch="UniformToFill"/>
<Border Background="#F9FAFB"
Margin="0,0,0,12"
Padding="16"
CornerRadius="8"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<CheckBox x:Name="JobCheckBox"
Grid.Column="0"
VerticalAlignment="Center"
Margin="0,0,12,0"
Tag="{Binding}"
Checked="JobCheckBox_CheckedChanged"
Unchecked="JobCheckBox_CheckedChanged"/>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Text="{Binding VideoName}"
FontWeight="Medium"
FontSize="15"
Foreground="{StaticResource TextPrimaryBrush}"/>
<TextBlock Text="{Binding StatusMessage}"
FontSize="13"
Foreground="{StaticResource TextSecondaryBrush}"
Margin="0,4,0,0"/>
</StackPanel>
<TextBlock Grid.Column="2"
Text="{Binding Progress, StringFormat={}{0:0}%}"
FontSize="15"
FontWeight="SemiBold"
Foreground="{StaticResource PrimaryBrush}"
VerticalAlignment="Center"
Margin="0,0,16,0"/>
<Button Grid.Column="3"
Content="✕"
Width="32"
Height="32"
FontSize="16"
Style="{StaticResource OutlineButton}"
Padding="0"
Tag="{Binding}"
Click="RemoveQueueItem_Click"/>
</Grid>
<ProgressBar Grid.Row="1"
Value="{Binding Progress}"
Margin="0,12,0,0"/>
<TextBlock Grid.Row="2"
FontSize="12"
Foreground="{StaticResource TextMutedBrush}"
Margin="0,8,0,0">
<Run Text="Mode:"/>
<Run Text="{Binding ExtractionModeDisplay}" FontWeight="Medium"/>
<Run Text=" • Output:"/>
<Run Text="{Binding OutputFolderDisplay}" FontWeight="Medium"/>
</TextBlock>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
</StackPanel>
</Border>
</StackPanel>
</Grid>
</Grid>
</Border>
</Grid>
</TabItem>
<!-- BARRA STATO -->
<Border Grid.Row="2" Background="#242A31" BorderBrush="#303840" BorderThickness="1,1,1,0" Padding="16 6" CornerRadius="6 6 0 0" Margin="14 0 14 14">
<!-- Library Tab -->
<TabItem Header="📚 Library">
<Grid Margin="0,24,0,0">
<Border Style="{StaticResource Card}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Text="Output Preview"
FontSize="18"
FontWeight="SemiBold"
Foreground="{StaticResource TextPrimaryBrush}"
Margin="0,0,0,8"/>
<DockPanel Grid.Row="1" Margin="0,0,0,16">
<TextBlock Text="Output Folder:"
Foreground="{StaticResource TextSecondaryBrush}"
VerticalAlignment="Center"
Margin="0,0,12,0"/>
<Button DockPanel.Dock="Right"
Content="Browse"
Style="{StaticResource OutlineButton}"
Padding="12,6"
Click="SelectOutputFolderButton_Click"
Margin="12,0,0,0"/>
<TextBox x:Name="GlobalOutputFolderTextBox"
IsReadOnly="True"
VerticalContentAlignment="Center"/>
</DockPanel>
<Border Grid.Row="2"
Background="#F9FAFB"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="12">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl x:Name="ThumbnailsPanel">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="6"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="1"
CornerRadius="6"
Background="White">
<Image Source="{Binding}"
Width="140"
Height="80"
Stretch="UniformToFill"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
</Grid>
</Border>
</Grid>
</TabItem>
<!-- Settings Tab -->
<TabItem Header="⚙️ Settings">
<ScrollViewer Margin="0,24,0,0" VerticalScrollBarVisibility="Auto">
<StackPanel MaxWidth="800">
<!-- Frame Settings Card -->
<Border Style="{StaticResource Card}" Margin="0,0,0,16">
<StackPanel>
<TextBlock Text="Frame Settings"
FontSize="18"
FontWeight="SemiBold"
Foreground="{StaticResource TextPrimaryBrush}"
Margin="0,0,0,16"/>
<TextBlock Text="Default Frame Size"
Foreground="{StaticResource TextSecondaryBrush}"
Margin="0,0,0,8"/>
<ComboBox x:Name="FrameSizeComboBox"
Height="42"
FontSize="14"
Margin="0,0,0,16">
<ComboBoxItem Content="Original Size" Tag="original" IsSelected="True"/>
<ComboBoxItem Content="320x180 (Fast)" Tag="320,180"/>
<ComboBoxItem Content="640x360 (Medium)" Tag="640,360"/>
<ComboBoxItem Content="1280x720 (HD)" Tag="1280,720"/>
<ComboBoxItem Content="1920x1080 (Full HD)" Tag="1920,1080"/>
</ComboBox>
<TextBlock Text="Extraction Mode"
Foreground="{StaticResource TextSecondaryBrush}"
Margin="0,0,0,8"/>
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
<RadioButton x:Name="DefaultModeFullRadio"
Content="Full Extraction"
GroupName="DefExtraction"
IsChecked="True"
Margin="0,0,24,0"/>
<RadioButton x:Name="DefaultModeSingleRadio"
Content="Single Frame"
GroupName="DefExtraction"
Margin="0,0,24,0"/>
<RadioButton x:Name="DefaultModeAutoRadio"
Content="Auto Detect"
GroupName="DefExtraction"/>
</StackPanel>
<TextBlock Text="Auto mode analyzes the video and decides the best extraction method."
FontSize="12"
Foreground="{StaticResource TextMutedBrush}"
TextWrapping="Wrap"/>
</StackPanel>
</Border>
<!-- Output Settings Card -->
<Border Style="{StaticResource Card}" Margin="0,0,0,16">
<StackPanel>
<TextBlock Text="Output Settings"
FontSize="18"
FontWeight="SemiBold"
Foreground="{StaticResource TextPrimaryBrush}"
Margin="0,0,0,16"/>
<CheckBox x:Name="CreateSubfolderCheckBox"
Content="Create subfolder for each video"
IsChecked="True"
Margin="0,0,0,16"/>
<CheckBox x:Name="SingleFrameUseSubfolderCheckBox"
Content="Use subfolder for single frame extraction"
Margin="0,0,0,16"/>
<TextBlock Text="Overwrite Behavior"
Foreground="{StaticResource TextSecondaryBrush}"
Margin="0,0,0,8"/>
<ComboBox x:Name="OverwriteModeComboBox"
Height="42"
FontSize="14">
<ComboBoxItem Content="Ask before overwrite" Tag="Ask" IsSelected="True"/>
<ComboBoxItem Content="Skip existing files" Tag="Skip"/>
<ComboBoxItem Content="Overwrite existing files" Tag="Overwrite"/>
</ComboBox>
</StackPanel>
</Border>
<!-- Action Buttons -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="Save Settings"
Style="{StaticResource ModernButton}"
Width="140"
Click="SaveSettings_Click"/>
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
<!-- Footer Status Bar -->
<Border Grid.Row="2"
Background="{StaticResource SurfaceBrush}"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="0,1,0,0"
Padding="24,12">
<DockPanel>
<TextBlock x:Name="StatusText" Foreground="{StaticResource TextSecondaryBrush}" FontSize="13" VerticalAlignment="Center" Text="Pronto"/>
<TextBlock Text=" | " Foreground="#55606B" Margin="6,0"/>
<TextBlock Text="Job:" Foreground="#77818B" Margin="0,0,4,0"/>
<TextBlock x:Name="JobsSummaryText" Foreground="#4F5962" FontSize="11" VerticalAlignment="Center"/>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left">
<TextBlock Text="●"
Foreground="{StaticResource SuccessBrush}"
FontSize="16"
Margin="0,0,8,0"/>
<TextBlock x:Name="StatusText"
Text="Ready"
Foreground="{StaticResource TextSecondaryBrush}"
FontSize="13"/>
</StackPanel>
<TextBlock x:Name="JobsSummaryText"
DockPanel.Dock="Right"
Foreground="{StaticResource TextMutedBrush}"
FontSize="12"
HorizontalAlignment="Right"/>
</DockPanel>
</Border>
</Grid>

View File

@@ -8,7 +8,6 @@ using System.Windows.Media.Imaging;
using System.Diagnostics;
using System.Linq;
using System.Collections.Generic;
using FFMpegCore;
using Ganimede.Properties;
using Ganimede.Services;
using Ganimede.Models;
@@ -30,7 +29,6 @@ namespace Ganimede
{
InitializeComponent();
InitializeUI();
ConfigureFFMpeg();
}
private void InitializeUI()
@@ -51,9 +49,67 @@ namespace Ganimede
_processingService.ProcessingStopped += OnProcessingStopped;
_processingService.JobQueue.CollectionChanged += (s, e) => UpdateQueueCount();
// Initialize settings controls
LoadSettingsControls();
UpdateQueueCount();
}
private void LoadSettingsControls()
{
// Controls are loaded after InitializeComponent, so we can't access them in constructor
// We'll load settings when the Settings tab is first accessed instead
Dispatcher.InvokeAsync(() =>
{
try
{
// Load frame size
var frameSize = Settings.Default.FrameSize;
foreach (ComboBoxItem item in FrameSizeComboBox.Items)
{
if (item.Tag?.ToString() == frameSize)
{
FrameSizeComboBox.SelectedItem = item;
break;
}
}
// Load overwrite mode
var overwriteMode = Settings.Default.DefaultOverwriteMode;
foreach (ComboBoxItem item in OverwriteModeComboBox.Items)
{
if (item.Tag?.ToString() == overwriteMode)
{
OverwriteModeComboBox.SelectedItem = item;
break;
}
}
// Load extraction mode
switch (Settings.Default.DefaultExtractionMode)
{
case "SingleFrame":
DefaultModeSingleRadio.IsChecked = true;
break;
case "Auto":
DefaultModeAutoRadio.IsChecked = true;
break;
default:
DefaultModeFullRadio.IsChecked = true;
break;
}
// Load folder settings
CreateSubfolderCheckBox.IsChecked = Settings.Default.CreateSubfolder;
SingleFrameUseSubfolderCheckBox.IsChecked = Settings.Default.SingleFrameUseSubfolder;
}
catch (Exception ex)
{
Debug.WriteLine($"[ERROR] Failed to load settings controls: {ex.Message}");
}
}, System.Windows.Threading.DispatcherPriority.Loaded);
}
private void UpdateQueueCount()
{
Dispatcher.Invoke(() =>
@@ -71,7 +127,7 @@ namespace Ganimede
var failed = _processingService.JobQueue.Count(j => j.Status == JobStatus.Failed);
Dispatcher.Invoke(() =>
{
JobsSummaryText.Text = $"In attesa: {pending} | In corso: {processing} | Completati: {completed} | Falliti: {failed}";
JobsSummaryText.Text = $"Pending: {pending} | Processing: {processing} | Completed: {completed} | Failed: {failed}";
});
}
@@ -81,7 +137,7 @@ namespace Ganimede
{
StartQueueButton.IsEnabled = false;
StopQueueButton.IsEnabled = true;
StatusText.Text = "Elaborazione coda...";
StatusText.Text = "Processing queue...";
UpdateJobsSummary();
});
}
@@ -92,7 +148,7 @@ namespace Ganimede
{
StartQueueButton.IsEnabled = true;
StopQueueButton.IsEnabled = false;
StatusText.Text = "Coda fermata";
StatusText.Text = "Queue stopped";
UpdateJobsSummary();
});
}
@@ -101,7 +157,7 @@ namespace Ganimede
{
Dispatcher.Invoke(() =>
{
StatusText.Text = $"✓ Completato: {job.VideoName}";
StatusText.Text = $"✓ Completed: {job.VideoName}";
LoadThumbnailsFromFolder(job.OutputFolder);
UpdateJobsSummary();
});
@@ -111,7 +167,7 @@ namespace Ganimede
{
Dispatcher.Invoke(() =>
{
StatusText.Text = $"✗ Fallito: {job.VideoName}";
StatusText.Text = $"✗ Failed: {job.VideoName}";
UpdateJobsSummary();
});
}
@@ -139,122 +195,16 @@ namespace Ganimede
}
}
private void ConfigureFFMpeg()
{
var ffmpegBin = Settings.Default.FFmpegBinFolder;
if (!string.IsNullOrEmpty(ffmpegBin) && ValidateFFMpegBinaries(ffmpegBin))
FFMpegCore.GlobalFFOptions.Configure(o => o.BinaryFolder = ffmpegBin);
else if (TryUseSystemFFMpeg()) { }
else if (TryFixMissingFFMpeg(ffmpegBin))
FFMpegCore.GlobalFFOptions.Configure(o => o.BinaryFolder = ffmpegBin);
}
private bool ValidateFFMpegBinaries(string binFolder) =>
Directory.Exists(binFolder) &&
File.Exists(Path.Combine(binFolder, "ffmpeg.exe")) &&
File.Exists(Path.Combine(binFolder, "ffprobe.exe"));
private bool TryUseSystemFFMpeg()
{
try
{
var psi = new ProcessStartInfo { FileName = "ffmpeg", Arguments = "-version", UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true };
using var p = Process.Start(psi);
return p != null && p.WaitForExit(4000) && p.ExitCode == 0;
}
catch { return false; }
}
private bool TryFixMissingFFMpeg(string binFolder)
{
if (string.IsNullOrEmpty(binFolder) || !Directory.Exists(binFolder)) return false;
var ffmpegPath = Path.Combine(binFolder, "ffmpeg.exe");
var ffprobePath = Path.Combine(binFolder, "ffprobe.exe");
if (!File.Exists(ffmpegPath) && File.Exists(ffprobePath))
{
try { File.Copy(ffprobePath, ffmpegPath, true); return File.Exists(ffmpegPath); } catch { return false; }
}
return false;
}
private void BrowseVideoButton_Click(object sender, RoutedEventArgs e)
{
var dialog = new WpfOpenFileDialog { Filter = "Video (*.mp4;*.avi;*.mov;*.mkv;*.wmv)|*.mp4;*.avi;*.mov;*.mkv;*.wmv|Tutti i file (*.*)|*.*", Multiselect = true };
if (dialog.ShowDialog() == true) AddVideosToQueue(dialog.FileNames);
}
private void ImportFolderButton_Click(object sender, RoutedEventArgs e)
{
using var dialog = new System.Windows.Forms.FolderBrowserDialog { Description = "Seleziona la cartella con i video", ShowNewFolderButton = false };
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
try
{
var files = Directory.EnumerateFiles(dialog.SelectedPath, "*.*", SearchOption.TopDirectoryOnly).Where(IsVideoFile).ToArray();
if (files.Length == 0)
{
WpfMessageBox.Show("Nessun file video valido trovato.", "Importa Cartella", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
AddVideosToQueue(files);
StatusText.Text = $"Importati {files.Length} video.";
}
catch (Exception ex)
{
WpfMessageBox.Show($"Errore: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
private void AddVideosToQueue(string[] paths)
{
if (string.IsNullOrEmpty(outputFolder))
{
WpfMessageBox.Show("Seleziona prima una cartella di output.", "Cartella Output Richiesta", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var createSub = Settings.Default.CreateSubfolder;
foreach (var p in paths) _processingService.AddJob(p, outputFolder, createSub);
StatusText.Text = $"Aggiunti {paths.Length} video (In attesa)";
Settings.Default.LastVideoPath = paths.FirstOrDefault();
Settings.Default.Save();
UpdateQueueCount();
}
private void SelectOutputFolderButton_Click(object sender, RoutedEventArgs e)
{
using var dialog = new System.Windows.Forms.FolderBrowserDialog();
if (!string.IsNullOrEmpty(outputFolder)) dialog.SelectedPath = outputFolder;
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
outputFolder = dialog.SelectedPath;
GlobalOutputFolderTextBox.Text = outputFolder;
StatusText.Text = "Cartella output aggiornata";
Settings.Default.LastOutputFolder = outputFolder;
Settings.Default.Save();
}
}
private void SettingsButton_Click(object sender, RoutedEventArgs e)
{
var win = new SettingsWindow { Owner = this };
if (win.ShowDialog() == true)
{
ConfigureFFMpeg();
StatusText.Text = "Impostazioni aggiornate";
}
}
private async void StartQueueButton_Click(object sender, RoutedEventArgs e)
{
if (_processingService.JobQueue.Count == 0)
{
WpfMessageBox.Show("Nessun video in coda.", "Coda Vuota", MessageBoxButton.OK, MessageBoxImage.Information);
WpfMessageBox.Show("No videos in queue.", "Empty Queue", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
if (_processingService.JobQueue.All(j => j.Status != JobStatus.Pending))
{
WpfMessageBox.Show("Nessun job in stato In attesa.", "Nessun Job", MessageBoxButton.OK, MessageBoxImage.Information);
WpfMessageBox.Show("No pending jobs in queue.", "No Jobs", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
await _processingService.StartProcessingAsync();
@@ -264,7 +214,7 @@ namespace Ganimede
private void StopQueueButton_Click(object sender, RoutedEventArgs e)
{
_processingService.StopProcessing();
StatusText.Text = "Arresto in corso...";
StatusText.Text = "Stopping...";
UpdateJobsSummary();
}
@@ -281,15 +231,24 @@ namespace Ganimede
private void ConfigureSelectedButton_Click(object sender, RoutedEventArgs e)
{
if (_selectedJobs.Count == 0) return;
var cfg = new JobConfigWindow(_selectedJobs.ToList()) { Owner = this };
if (cfg.ShowDialog() == true)
{
StatusText.Text = $"Configurazione applicata a {_selectedJobs.Count} job";
foreach (var job in _selectedJobs.Where(j => string.IsNullOrEmpty(j.CustomOutputFolder)))
StatusText.Text = $"Configuration applied to {_selectedJobs.Count} job(s)";
// Update output folders only if outputFolder is set
if (!string.IsNullOrEmpty(outputFolder))
{
var createSub = Settings.Default.CreateSubfolder;
job.OutputFolder = job.ExtractionMode == ExtractionMode.SingleFrame ? outputFolder : (createSub ? Path.Combine(outputFolder, job.VideoName) : outputFolder);
foreach (var job in _selectedJobs.Where(j => string.IsNullOrEmpty(j.CustomOutputFolder)))
{
var createSub = Settings.Default.CreateSubfolder;
job.OutputFolder = job.ExtractionMode == ExtractionMode.SingleFrame
? outputFolder
: (createSub ? Path.Combine(outputFolder, job.VideoName) : outputFolder);
}
}
UpdateJobsSummary();
}
}
@@ -308,7 +267,7 @@ namespace Ganimede
private void ClearCompletedButton_Click(object sender, RoutedEventArgs e)
{
_processingService.RemoveCompletedJobs();
StatusText.Text = "Job completati rimossi";
StatusText.Text = "Completed jobs removed";
UpdateQueueCount();
}
@@ -317,7 +276,7 @@ namespace Ganimede
var processing = _processingService.JobQueue.Any(j => j.Status == JobStatus.Processing);
if (processing)
{
var res = WpfMessageBox.Show("Ci sono job in elaborazione.\n\nSi: Ferma e svuota la coda\nNo: Rimuovi solo job non in elaborazione\nAnnulla: Annulla", "Conferma", MessageBoxButton.YesNoCancel, MessageBoxImage.Question);
var res = WpfMessageBox.Show("There are jobs being processed.\n\nYes: Stop and clear queue\nNo: Remove only non-processing jobs\nCancel: Cancel operation", "Confirm", MessageBoxButton.YesNoCancel, MessageBoxImage.Question);
if (res == MessageBoxResult.Cancel) return;
if (res == MessageBoxResult.Yes)
{
@@ -334,16 +293,107 @@ namespace Ganimede
}
else
{
if (WpfMessageBox.Show("Rimuovere tutti i job?", "Conferma", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
if (WpfMessageBox.Show("Remove all jobs from queue?", "Confirm", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
{
_processingService.JobQueue.Clear();
thumbnails.Clear();
}
}
StatusText.Text = "Coda aggiornata";
StatusText.Text = "Queue updated";
UpdateQueueCount();
}
private void BrowseVideoButton_Click(object sender, RoutedEventArgs e)
{
var dialog = new WpfOpenFileDialog { Filter = "Video Files|*.mp4;*.avi;*.mov;*.mkv;*.wmv;*.flv;*.webm|All Files|*.*", Multiselect = true };
if (dialog.ShowDialog() == true) AddVideosToQueue(dialog.FileNames);
}
private void ImportFolderButton_Click(object sender, RoutedEventArgs e)
{
using var dialog = new System.Windows.Forms.FolderBrowserDialog { Description = "Select folder containing videos", ShowNewFolderButton = false };
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
try
{
var files = Directory.EnumerateFiles(dialog.SelectedPath, "*.*", SearchOption.TopDirectoryOnly).Where(IsVideoFile).ToArray();
if (files.Length == 0)
{
WpfMessageBox.Show("No valid video files found.", "Import Folder", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
AddVideosToQueue(files);
StatusText.Text = $"Imported {files.Length} video(s)";
}
catch (Exception ex)
{
WpfMessageBox.Show($"Error: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
private void AddVideosToQueue(string[] paths)
{
if (string.IsNullOrEmpty(outputFolder))
{
WpfMessageBox.Show("Please select an output folder first.", "Output Folder Required", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var createSub = Settings.Default.CreateSubfolder;
foreach (var p in paths) _processingService.AddJob(p, outputFolder, createSub);
StatusText.Text = $"Added {paths.Length} video(s)";
Settings.Default.LastVideoPath = paths.FirstOrDefault();
Settings.Default.Save();
UpdateQueueCount();
}
private void SelectOutputFolderButton_Click(object sender, RoutedEventArgs e)
{
using var dialog = new System.Windows.Forms.FolderBrowserDialog();
if (!string.IsNullOrEmpty(outputFolder)) dialog.SelectedPath = outputFolder;
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
outputFolder = dialog.SelectedPath;
GlobalOutputFolderTextBox.Text = outputFolder;
StatusText.Text = "Output folder updated";
Settings.Default.LastOutputFolder = outputFolder;
Settings.Default.Save();
}
}
private void SaveSettings_Click(object sender, RoutedEventArgs e)
{
try
{
var selectedFrameSize = FrameSizeComboBox.SelectedItem as ComboBoxItem;
Settings.Default.FrameSize = selectedFrameSize?.Tag?.ToString() ?? "original";
var selectedOverwrite = OverwriteModeComboBox.SelectedItem as ComboBoxItem;
Settings.Default.DefaultOverwriteMode = selectedOverwrite?.Tag?.ToString() ?? "Ask";
if (DefaultModeSingleRadio.IsChecked == true)
Settings.Default.DefaultExtractionMode = "SingleFrame";
else if (DefaultModeAutoRadio.IsChecked == true)
Settings.Default.DefaultExtractionMode = "Auto";
else
Settings.Default.DefaultExtractionMode = "Full";
Settings.Default.CreateSubfolder = CreateSubfolderCheckBox.IsChecked ?? true;
Settings.Default.SingleFrameUseSubfolder = SingleFrameUseSubfolderCheckBox.IsChecked ?? false;
Settings.Default.Save();
StatusText.Text = "✓ Settings saved successfully";
Debug.WriteLine("[SETTINGS] Settings saved from inline tab");
}
catch (Exception ex)
{
StatusText.Text = "✗ Failed to save settings";
Debug.WriteLine($"[ERROR] Failed to save settings: {ex.Message}");
WpfMessageBox.Show($"Error saving settings: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private static bool IsVideoFile(string path)
{
var ext = Path.GetExtension(path).ToLowerInvariant();

View File

@@ -47,18 +47,6 @@ namespace Ganimede.Properties {
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("C:\\Users\\balbo\\source\\repos\\Ganimede\\Ganimede\\Ganimede\\FFMpeg")]
public string FFmpegBinFolder {
get {
return ((string)(this["FFmpegBinFolder"]));
}
set {
this["FFmpegBinFolder"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("True")]

150
Ganimede/Ganimede/README.md Normal file
View File

@@ -0,0 +1,150 @@
# Ganimede - Video Frame Extractor
## ?? Overview
Ganimede is a modern .NET 8 WPF application for extracting frames from video files. The application features a clean, Material Design-inspired interface with tabbed navigation.
## ? Key Features
### Video Processing
- **Multiple video formats supported**: MP4, AVI, MOV, MKV, WMV, FLV, WebM
- **Batch processing**: Add multiple videos to queue
- **Folder import**: Import entire folders of videos
- **Three extraction modes**:
- **Full**: Extract all frames from video
- **Single Frame**: Extract one representative frame
- **Auto**: Automatically determine best extraction method
### Modern UI
- **Tab-based navigation**:
- ?? **Processing**: Manage video queue and processing
- ?? **Library**: Preview extracted frames
- ?? **Settings**: Configure application preferences
- **Clean Material Design** aesthetic
- **Real-time progress tracking**
- **Thumbnail preview** of extracted frames
### Settings
- **Frame size options**: Original, 320x180, 640x360, 1280x720, 1920x1080
- **Overwrite behavior**: Ask, Skip, Overwrite
- **Subfolder creation** options
- **Extraction mode** defaults
## ??? Technology Stack
### Core Technologies
- **.NET 8** (Windows)
- **WPF** (Windows Presentation Foundation)
- **XAML** for UI design
### Video Processing
- **FFMediaToolkit 4.8.1** - Native .NET video processing
- **FFmpeg.AutoGen 7.1.1** - FFmpeg bindings
- **System.Drawing.Common 10.0.0** - Image manipulation
### Architecture
- **MVVM-inspired** pattern
- **Async/await** for responsive UI
- **ObservableCollection** for data binding
- **Custom wrapper classes** for video operations:
- `VideoAnalyzer` - Video metadata extraction
- `FrameExtractor` - Frame extraction operations
- `VideoProcessingService` - Queue management
## ?? Project Structure
```
Ganimede/
??? VideoProcessing/
? ??? VideoAnalyzer.cs # Video analysis wrapper
? ??? FrameExtractor.cs # Frame extraction wrapper
??? Services/
? ??? VideoProcessingService.cs # Processing queue management
??? Models/
? ??? VideoJob.cs # Job data model
??? Windows/
? ??? JobConfigWindow.xaml # Job configuration dialog
? ??? SettingsWindow.xaml # Legacy settings window (unused)
??? Helpers/
? ??? NamingHelper.cs # File naming utilities
??? Converters/
? ??? StatusColorConverter.cs # Status-to-color converter
??? MainWindow.xaml # Main application window
```
## ?? Recent Updates
### UI Redesign (v2.0)
- Complete UI overhaul with modern Material Design
- Tab-based navigation replacing sidebar layout
- Integrated settings (no separate window needed)
- Light theme with clean aesthetics
- Improved color palette (Indigo primary)
- Enhanced card-based layouts
- Refined typography and spacing
### Video Processing Refactor (v2.0)
- **Replaced FFMpegCore** with FFMediaToolkit
- **No external FFmpeg binaries required**
- Custom wrapper architecture for video operations
- Improved performance with direct memory access
- Simplified configuration (no FFmpeg path needed)
## ?? Requirements
- **Windows** operating system
- **.NET 8 Runtime** (or SDK for development)
- **FFmpeg libraries** (automatically included via FFMediaToolkit)
## ?? Color Palette
- **Primary**: #6366F1 (Indigo)
- **Success**: #10B981 (Green)
- **Danger**: #EF4444 (Red)
- **Warning**: #F59E0B (Amber)
- **Background**: #F5F7FA (Light Gray)
- **Surface**: #FFFFFF (White)
- **Border**: #E5E7EB (Gray)
## ?? Usage
1. **Add Videos**: Click "Add Videos" or "Import Folder"
2. **Select Output**: Choose output folder for extracted frames
3. **Configure** (Optional): Configure individual jobs or use default settings
4. **Start Queue**: Process all pending videos
5. **View Results**: Switch to Library tab to preview extracted frames
## ?? Development
### Building
```bash
dotnet build
```
### Running
```bash
dotnet run --project Ganimede\Ganimede.csproj
```
### Configuration
Settings are stored in `Settings.settings` and persisted between sessions:
- Default output folder
- Frame size preferences
- Extraction mode defaults
- Overwrite behavior
- Subfolder creation options
## ?? License
[Your License Here]
## ?? Author
[Your Name/Organization]
## ?? Known Issues
None currently reported.
## ?? Support
[Your Support Contact]

View File

@@ -3,11 +3,11 @@ using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Threading;
using System.Diagnostics;
using FFMpegCore;
using System.IO;
using Ganimede.Models;
using Ganimede.Properties;
using Ganimede.Helpers;
using Ganimede.VideoProcessing;
namespace Ganimede.Services
{
@@ -146,11 +146,10 @@ namespace Ganimede.Services
{
Debug.WriteLine($"[PROCESS] Starting job: {job.VideoName}");
// (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);
// Analyze video using VideoAnalyzer
var mediaInfo = await Task.Run(() => VideoAnalyzer.Analyze(job.VideoPath), cancellationToken);
int frameRate = (int)mediaInfo.FrameRate;
int totalFrames = mediaInfo.TotalFrames;
Debug.WriteLine($"[INFO] Video {job.VideoName}: {totalFrames} frames at {frameRate} fps, duration {mediaInfo.Duration}");
// Heuristic suggestion
@@ -161,11 +160,10 @@ namespace Ganimede.Services
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)
if (mediaInfo.BitRate > 0 && mediaInfo.Width > 0 && mediaInfo.Height > 0)
{
double pixels = primary.Width * primary.Height;
if (primary.BitRate < pixels * 0.3)
double pixels = mediaInfo.Width * mediaInfo.Height;
if (mediaInfo.BitRate < pixels * 0.3)
suggestSingleFrame = true;
}
}
@@ -224,7 +222,7 @@ namespace Ganimede.Services
job.StatusMessage = "Frame already exists (skipped)";
else
{
await ExtractFrameAsync(job, targetIndex, frameRate, frameSize, framePath);
await ExtractFrameAsync(job, frameTime, frameSize, framePath, cancellationToken);
job.StatusMessage = "Single frame extracted";
}
job.Progress = 100;
@@ -234,32 +232,47 @@ namespace Ganimede.Services
return;
}
// Full extraction loop (unchanged)
// Full extraction using FrameExtractor
int processedFrames = 0;
int skippedFrames = 0;
for (int i = 0; i < totalFrames; i++)
await Task.Run(() =>
{
if (cancellationToken.IsCancellationRequested)
{
job.Status = JobStatus.Cancelled;
job.StatusMessage = "Cancelled by user";
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);
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}" : "");
if (i % 10 == 0) await Task.Delay(1, cancellationToken);
FrameExtractor.ExtractAllFrames(
job.VideoPath,
job.OutputFolder,
(frameIndex, timePosition) => NamingHelper.GenerateFileName(namingPattern, job, frameIndex, timePosition, customPrefix),
frameSize.width,
frameSize.height,
(current, total) =>
{
if (cancellationToken.IsCancellationRequested)
return;
job.Progress = (double)current / total * 100;
job.StatusMessage = $"Processed {current}/{total} frames ({job.Progress:F1}%)";
processedFrames = current;
},
(framePath) =>
{
if (File.Exists(framePath) && overwriteMode == OverwriteMode.Skip)
{
skippedFrames++;
return true;
}
return false;
}
);
}, cancellationToken);
if (cancellationToken.IsCancellationRequested)
{
job.Status = JobStatus.Cancelled;
job.StatusMessage = "Cancelled by user";
Debug.WriteLine($"[CANCELLED] Job cancelled: {job.VideoName}");
return;
}
job.Status = JobStatus.Completed;
job.StatusMessage = $"Completed - {processedFrames} frames processed" + (skippedFrames > 0 ? $", {skippedFrames} skipped" : "");
job.Progress = 100;
@@ -327,62 +340,25 @@ namespace Ganimede.Services
return Settings.Default.DefaultCustomPrefix ?? "custom";
}
private async Task ExtractFrameAsync(VideoJob job, int frameIndex, int frameRate, (int width, int height) frameSize, string framePath)
private async Task ExtractFrameAsync(VideoJob job, TimeSpan frameTime, (int width, int height) frameSize, string framePath, CancellationToken cancellationToken)
{
var frameTime = TimeSpan.FromSeconds((double)frameIndex / frameRate);
try
{
if (frameSize.width == -1 && frameSize.height == -1)
await Task.Run(() =>
{
try
{
await FFMpegArguments
.FromFileInput(job.VideoPath)
.OutputToFile(framePath, true, options => options
.Seek(frameTime)
.WithFrameOutputCount(1)
.WithVideoCodec("png"))
.ProcessAsynchronously();
return;
}
catch
{
await FFMpegArguments
.FromFileInput(job.VideoPath)
.OutputToFile(framePath, true, options => options
.Seek(frameTime)
.WithFrameOutputCount(1))
.ProcessAsynchronously();
return;
}
}
try
{
await FFMpegArguments
.FromFileInput(job.VideoPath)
.OutputToFile(framePath, true, options => options
.Seek(frameTime)
.WithFrameOutputCount(1)
.WithVideoCodec("png")
.Resize(frameSize.width, frameSize.height))
.ProcessAsynchronously();
}
catch
{
await FFMpegArguments
.FromFileInput(job.VideoPath)
.OutputToFile(framePath, true, options => options
.Seek(frameTime)
.WithFrameOutputCount(1)
.Resize(frameSize.width, frameSize.height))
.ProcessAsynchronously();
}
FrameExtractor.ExtractFrame(
job.VideoPath,
frameTime,
framePath,
frameSize.width,
frameSize.height
);
}, cancellationToken);
}
catch (Exception ex)
{
Debug.WriteLine($"[ERROR] Failed to extract frame {frameIndex} from {job.VideoName}: {ex.Message}");
Debug.WriteLine($"[ERROR] Failed to extract frame from {job.VideoName}: {ex.Message}");
throw;
}
}
}

View File

@@ -0,0 +1,194 @@
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using FFMediaToolkit;
using FFMediaToolkit.Decoding;
using FFMediaToolkit.Graphics;
namespace Ganimede.VideoProcessing
{
/// <summary>
/// Provides frame extraction capabilities from video files using FFMediaToolkit
/// </summary>
public class FrameExtractor
{
/// <summary>
/// Extracts a single frame from a video at a specific time position
/// </summary>
/// <param name="videoPath">Path to the video file</param>
/// <param name="timePosition">Time position in the video</param>
/// <param name="outputPath">Output path for the PNG image</param>
/// <param name="targetWidth">Target width for resizing (optional, -1 for original)</param>
/// <param name="targetHeight">Target height for resizing (optional, -1 for original)</param>
public static void ExtractFrame(
string videoPath,
TimeSpan timePosition,
string outputPath,
int targetWidth = -1,
int targetHeight = -1)
{
if (!File.Exists(videoPath))
throw new FileNotFoundException($"Video file not found: {videoPath}");
try
{
using var file = MediaFile.Open(videoPath, new MediaOptions { StreamsToLoad = MediaMode.Video, VideoPixelFormat = ImagePixelFormat.Bgr24 });
if (!file.HasVideo)
throw new InvalidOperationException("The file does not contain a video stream");
// Get the frame at the specified time
var imageData = file.Video.GetFrame(timePosition);
// Create bitmap and copy data
using var bitmap = new Bitmap(imageData.ImageSize.Width, imageData.ImageSize.Height, PixelFormat.Format24bppRgb);
var rect = new Rectangle(Point.Empty, bitmap.Size);
var bitmapData = bitmap.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
unsafe
{
var dst = (byte*)bitmapData.Scan0;
var rowSize = imageData.ImageSize.Width * 3;
for (int y = 0; y < imageData.ImageSize.Height; y++)
{
var srcRow = imageData.Data.Slice(y * imageData.Stride, rowSize);
var dstRow = new Span<byte>(dst + y * bitmapData.Stride, rowSize);
srcRow.CopyTo(dstRow);
}
}
bitmap.UnlockBits(bitmapData);
// Resize if needed
if (targetWidth > 0 && targetHeight > 0 && (targetWidth != imageData.ImageSize.Width || targetHeight != imageData.ImageSize.Height))
{
using var resized = new Bitmap(bitmap, targetWidth, targetHeight);
SaveBitmapAsPng(resized, outputPath);
}
else
{
SaveBitmapAsPng(bitmap, outputPath);
}
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to extract frame: {ex.Message}", ex);
}
}
/// <summary>
/// Extracts all frames from a video
/// </summary>
/// <param name="videoPath">Path to the video file</param>
/// <param name="outputFolder">Output folder for PNG images</param>
/// <param name="fileNameGenerator">Function to generate file names for each frame</param>
/// <param name="targetWidth">Target width for resizing (optional, -1 for original)</param>
/// <param name="targetHeight">Target height for resizing (optional, -1 for original)</param>
/// <param name="onProgress">Progress callback (frame index, total frames)</param>
/// <param name="shouldSkipFrame">Function to determine if a frame should be skipped</param>
public static void ExtractAllFrames(
string videoPath,
string outputFolder,
Func<int, TimeSpan, string> fileNameGenerator,
int targetWidth = -1,
int targetHeight = -1,
Action<int, int>? onProgress = null,
Func<string, bool>? shouldSkipFrame = null)
{
if (!File.Exists(videoPath))
throw new FileNotFoundException($"Video file not found: {videoPath}");
if (!Directory.Exists(outputFolder))
Directory.CreateDirectory(outputFolder);
try
{
using var file = MediaFile.Open(videoPath, new MediaOptions { StreamsToLoad = MediaMode.Video, VideoPixelFormat = ImagePixelFormat.Bgr24 });
if (!file.HasVideo)
throw new InvalidOperationException("The file does not contain a video stream");
var video = file.Video;
var info = video.Info;
// Calculate total frames
double frameRate = info.AvgFrameRate;
int totalFrames = info.NumberOfFrames ?? (int)(info.Duration.TotalSeconds * frameRate);
int frameIndex = 0;
// Create a reusable buffer
var buffer = new byte[video.Info.FrameSize.Width * video.Info.FrameSize.Height * 3];
while (video.TryGetNextFrame(buffer))
{
var timePosition = video.Position;
var fileName = fileNameGenerator(frameIndex, timePosition);
var fullPath = Path.Combine(outputFolder, fileName);
// Check if frame should be skipped
if (shouldSkipFrame != null && shouldSkipFrame(fullPath))
{
frameIndex++;
onProgress?.Invoke(frameIndex, totalFrames);
continue;
}
// Create bitmap from buffer
using var bitmap = new Bitmap(info.FrameSize.Width, info.FrameSize.Height, PixelFormat.Format24bppRgb);
var rect = new Rectangle(Point.Empty, bitmap.Size);
var bitmapData = bitmap.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
unsafe
{
var dst = (byte*)bitmapData.Scan0;
var rowSize = info.FrameSize.Width * 3;
for (int y = 0; y < info.FrameSize.Height; y++)
{
var srcRow = buffer.AsSpan(y * rowSize, rowSize);
var dstRow = new Span<byte>(dst + y * bitmapData.Stride, rowSize);
srcRow.CopyTo(dstRow);
}
}
bitmap.UnlockBits(bitmapData);
// Resize if needed
if (targetWidth > 0 && targetHeight > 0)
{
using var resized = new Bitmap(bitmap, targetWidth, targetHeight);
SaveBitmapAsPng(resized, fullPath);
}
else
{
SaveBitmapAsPng(bitmap, fullPath);
}
frameIndex++;
onProgress?.Invoke(frameIndex, totalFrames);
}
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to extract frames: {ex.Message}", ex);
}
}
/// <summary>
/// Saves a bitmap as PNG file
/// </summary>
private static void SaveBitmapAsPng(Bitmap bitmap, string outputPath)
{
// Ensure directory exists
var directory = Path.GetDirectoryName(outputPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
Directory.CreateDirectory(directory);
// Save as PNG
bitmap.Save(outputPath, ImageFormat.Png);
}
}
}

View File

@@ -0,0 +1,76 @@
using System;
using System.IO;
using FFMediaToolkit;
using FFMediaToolkit.Decoding;
namespace Ganimede.VideoProcessing
{
/// <summary>
/// Provides video analysis capabilities using FFMediaToolkit
/// </summary>
public class VideoAnalyzer
{
/// <summary>
/// Analyzes a video file and returns its metadata
/// </summary>
public static VideoMetadata Analyze(string videoPath)
{
if (!File.Exists(videoPath))
throw new FileNotFoundException($"Video file not found: {videoPath}");
try
{
using var file = MediaFile.Open(videoPath);
if (!file.HasVideo)
throw new InvalidOperationException("The file does not contain a video stream");
var video = file.Video;
var info = video.Info;
// Get frame rate
double frameRate = info.AvgFrameRate;
// Calculate total frames from duration and frame rate
var duration = info.Duration;
int totalFrames = info.NumberOfFrames ?? (int)(duration.TotalSeconds * frameRate);
// Get video dimensions
int width = info.FrameSize.Width;
int height = info.FrameSize.Height;
// Estimate bitrate from file info
long bitrate = file.Info.Bitrate;
return new VideoMetadata
{
Duration = duration,
FrameRate = frameRate,
TotalFrames = totalFrames,
Width = width,
Height = height,
BitRate = bitrate,
CodecName = info.CodecName ?? "unknown"
};
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to analyze video: {ex.Message}", ex);
}
}
}
/// <summary>
/// Contains metadata information about a video file
/// </summary>
public class VideoMetadata
{
public TimeSpan Duration { get; set; }
public double FrameRate { get; set; }
public int TotalFrames { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public long BitRate { get; set; }
public string CodecName { get; set; } = string.Empty;
}
}

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="Impostazioni" Height="600" Width="640"
Title="Impostazioni" Height="550" Width="640"
Background="#1E2228" WindowStartupLocation="CenterOwner">
<Grid Margin="22">
<Grid.RowDefinitions>
@@ -19,24 +19,6 @@
<!-- Settings Content -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel>
<!-- FFmpeg Settings -->
<GroupBox Header="Configurazione FFmpeg" Foreground="White" BorderBrush="#444" Margin="0,0,0,18">
<StackPanel Margin="12">
<TextBlock Text="Cartella Binari FFmpeg:" Foreground="#CCC" Margin="0,0,0,6"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="FFmpegPathTextBox" Grid.Column="0" Height="34" VerticalContentAlignment="Center"
Background="#333" Foreground="White" BorderBrush="#555" Margin="0,0,10,0"/>
<Button x:Name="BrowseFFmpegButton" Grid.Column="1" Content="Sfoglia" Width="90" Height="34"
Click="BrowseFFmpegButton_Click"/>
</Grid>
<TextBlock x:Name="FFmpegStatusText" Foreground="#AAA" FontSize="12" Margin="0,6,0,0"/>
</StackPanel>
</GroupBox>
<!-- Output Settings -->
<GroupBox Header="Output" Foreground="White" BorderBrush="#444" Margin="0,0,0,18">
<StackPanel Margin="12">

View File

@@ -20,7 +20,6 @@ namespace Ganimede.Windows
private void LoadSettings()
{
FFmpegPathTextBox.Text = Settings.Default.FFmpegBinFolder;
DefaultOutputTextBox.Text = Settings.Default.LastOutputFolder;
CreateSubfolderCheckBox.IsChecked = Settings.Default.CreateSubfolder;
var singleFrameChk = GetCheckBox("SingleFrameUseSubfolderCheckBox");
@@ -55,8 +54,6 @@ namespace Ganimede.Windows
default:
GetDefaultModeRadio("DefaultModeFullRadio")!.IsChecked = true; break;
}
UpdateFFmpegStatus();
}
private string GetSelectedDefaultExtractionMode()
@@ -68,43 +65,6 @@ namespace Ganimede.Windows
private bool GetSingleFrameUseSubfolder() => GetCheckBox("SingleFrameUseSubfolderCheckBox")?.IsChecked == true;
private void UpdateFFmpegStatus()
{
var path = FFmpegPathTextBox.Text;
if (string.IsNullOrEmpty(path))
{
FFmpegStatusText.Text = "Nessun percorso specificato";
FFmpegStatusText.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Orange);
}
else if (ValidateFFMpegPath(path))
{
FFmpegStatusText.Text = "? FFmpeg valido";
FFmpegStatusText.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.LightGreen);
}
else
{
FFmpegStatusText.Text = "? Binari FFmpeg non trovati";
FFmpegStatusText.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Red);
}
}
private bool ValidateFFMpegPath(string path)
{
if (!Directory.Exists(path)) return false;
return File.Exists(Path.Combine(path, "ffmpeg.exe")) && File.Exists(Path.Combine(path, "ffprobe.exe"));
}
private void BrowseFFmpegButton_Click(object sender, RoutedEventArgs e)
{
using var dialog = new System.Windows.Forms.FolderBrowserDialog { Description = "Seleziona cartella binari FFmpeg", ShowNewFolderButton = false };
if (!string.IsNullOrEmpty(FFmpegPathTextBox.Text)) dialog.SelectedPath = FFmpegPathTextBox.Text;
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
FFmpegPathTextBox.Text = dialog.SelectedPath;
UpdateFFmpegStatus();
}
}
private void BrowseOutputButton_Click(object sender, RoutedEventArgs e)
{
using var dialog = new System.Windows.Forms.FolderBrowserDialog { Description = "Seleziona cartella output predefinita", ShowNewFolderButton = true };
@@ -119,7 +79,6 @@ namespace Ganimede.Windows
{
try
{
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;