Compare commits
5 Commits
46e94e8ee4
...
bd7e71d67c
| Author | SHA1 | Date | |
|---|---|---|---|
| bd7e71d67c | |||
| 8879a9375f | |||
| bf436d0926 | |||
| 91695f350c | |||
| bb5b0f2d52 |
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<configSections>
|
||||
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
|
||||
<section name="Ganimede.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
|
||||
</sectionGroup>
|
||||
</configSections>
|
||||
<userSettings>
|
||||
<Ganimede.Properties.Settings>
|
||||
<setting name="LastOutputFolder" serializeAs="String">
|
||||
<value />
|
||||
</setting>
|
||||
<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>
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
using Ganimede.Models;
|
||||
|
||||
namespace Ganimede
|
||||
{
|
||||
public class StatusColorConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is JobStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
JobStatus.Pending => new SolidColorBrush(Colors.Orange),
|
||||
JobStatus.Processing => new SolidColorBrush(Colors.LightBlue),
|
||||
JobStatus.Completed => new SolidColorBrush(Colors.LightGreen),
|
||||
JobStatus.Failed => new SolidColorBrush(Colors.Red),
|
||||
JobStatus.Cancelled => new SolidColorBrush(Colors.Gray),
|
||||
_ => new SolidColorBrush(Colors.White)
|
||||
};
|
||||
}
|
||||
return new SolidColorBrush(Colors.White);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -13,10 +13,6 @@
|
||||
<PackageReference Include="FFMpegCore" Version="5.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Settings.Designer.cs">
|
||||
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using Ganimede.Models;
|
||||
|
||||
namespace Ganimede.Helpers
|
||||
{
|
||||
public static class NamingHelper
|
||||
{
|
||||
public static string GenerateFileName(NamingPattern pattern, VideoJob job, int frameIndex, TimeSpan frameTime, string customPrefix = "")
|
||||
{
|
||||
var videoName = Path.GetFileNameWithoutExtension(job.VideoPath);
|
||||
var progressiveNumber = frameIndex + 1; // Start from 1 instead of 0
|
||||
|
||||
return pattern switch
|
||||
{
|
||||
NamingPattern.VideoNameProgressive =>
|
||||
$"{videoName}_{progressiveNumber:D6}.png",
|
||||
|
||||
NamingPattern.FrameProgressive =>
|
||||
$"frame_{progressiveNumber:D6}.png",
|
||||
|
||||
NamingPattern.VideoNameTimestamp =>
|
||||
$"{videoName}_{(int)frameTime.TotalMilliseconds:D6}ms.png",
|
||||
|
||||
NamingPattern.CustomProgressive =>
|
||||
$"{(string.IsNullOrEmpty(customPrefix) ? "custom" : customPrefix)}_{progressiveNumber:D6}.png",
|
||||
|
||||
NamingPattern.TimestampOnly =>
|
||||
$"{(int)frameTime.Hours:D2}h{frameTime.Minutes:D2}m{frameTime.Seconds:D2}s{frameTime.Milliseconds:D3}ms.png",
|
||||
|
||||
NamingPattern.VideoNameFrameProgressive =>
|
||||
$"{videoName}_frame_{progressiveNumber:D6}.png",
|
||||
|
||||
_ => $"frame_{progressiveNumber:D6}.png"
|
||||
};
|
||||
}
|
||||
|
||||
public static string GetPatternDescription(NamingPattern pattern)
|
||||
{
|
||||
var field = pattern.GetType().GetField(pattern.ToString());
|
||||
var attribute = field?.GetCustomAttribute<DescriptionAttribute>();
|
||||
return attribute?.Description ?? pattern.ToString();
|
||||
}
|
||||
|
||||
public static string GetPatternExample(NamingPattern pattern, string videoName = "VID20250725", string customPrefix = "custom")
|
||||
{
|
||||
var job = new VideoJob
|
||||
{
|
||||
VideoPath = $"{videoName}.mp4",
|
||||
OutputFolder = "",
|
||||
CustomPrefix = customPrefix
|
||||
};
|
||||
|
||||
var frameTime = new TimeSpan(0, 12, 34, 567); // 12:34.567
|
||||
var example = GenerateFileName(pattern, job, 0, frameTime, customPrefix); // frameIndex 0 will become 1
|
||||
|
||||
return example;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,44 +5,245 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Ganimede"
|
||||
mc:Ignorable="d"
|
||||
Title="Frame Extractor" Height="600" Width="900"
|
||||
Background="#222">
|
||||
<Grid Margin="40">
|
||||
Title="Estrattore Frame Video" Height="800" Width="1250"
|
||||
Background="#1E2228" 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"/>
|
||||
|
||||
<!-- Button Style -->
|
||||
<Style TargetType="Button" x:Key="ToolbarButton">
|
||||
<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="FontSize" Value="14"/>
|
||||
<Setter Property="MinHeight" Value="40"/>
|
||||
<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">
|
||||
<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"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.4"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</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"/>
|
||||
<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>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ScrollViewer styling for thin scrollbar -->
|
||||
<Style TargetType="ScrollBar">
|
||||
<Setter Property="Width" Value="10"/>
|
||||
<Setter Property="Background" Value="#20252B"/>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Drag & Drop Area -->
|
||||
<Border x:Name="DragDropArea" Grid.Row="0" Height="180" CornerRadius="16" BorderBrush="#444" BorderThickness="2" Background="#282828" AllowDrop="True" Drop="DragDropArea_Drop" MouseEnter="DragDropArea_MouseEnter" MouseLeave="DragDropArea_MouseLeave">
|
||||
<TextBlock Text="Drag and drop video here" Foreground="#AAA" FontSize="22" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<!-- 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"/>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Buttons -->
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,24,0,0" >
|
||||
<Button x:Name="BrowseVideoButton" Content="Browse Video" Width="160" Height="48" Margin="0,0,16,0" Click="BrowseVideoButton_Click"/>
|
||||
<Button x:Name="SelectOutputFolderButton" Content="Select Output Folder" Width="160" Height="48" Click="SelectOutputFolderButton_Click"/>
|
||||
</StackPanel>
|
||||
<!-- CONTENUTO PRINCIPALE -->
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="370"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Extract Frames Button -->
|
||||
<Button Grid.Row="2" x:Name="ExtractFramesButton" Content="Extract Frames" Width="220" Height="54" HorizontalAlignment="Center" Margin="0,24,0,0" Click="ExtractFramesButton_Click"/>
|
||||
<!-- 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>
|
||||
|
||||
<!-- Progress & Thumbnails -->
|
||||
<StackPanel Grid.Row="3" Margin="0,32,0,0">
|
||||
<ProgressBar x:Name="ProgressBar" Height="24" Minimum="0" Maximum="100" Value="0" Background="#333" Foreground="#4FC3F7"/>
|
||||
<TextBlock x:Name="StatusText" Foreground="#AAA" FontSize="16" Margin="0,12,0,0"/>
|
||||
<ItemsControl x:Name="ThumbnailsPanel" Margin="0,24,0,0" Height="120" HorizontalAlignment="Center">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
<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>
|
||||
|
||||
<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"/>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Margin="4" BorderBrush="#3D4853" BorderThickness="1" CornerRadius="4">
|
||||
<Image Source="{Binding}" Width="90" Height="52" Stretch="UniformToFill"/>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 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">
|
||||
<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"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -5,185 +5,360 @@ using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Microsoft.Win32;
|
||||
using FFMpegCore;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using FFMpegCore;
|
||||
using Ganimede.Properties;
|
||||
using Ganimede.Services;
|
||||
using Ganimede.Models;
|
||||
using Ganimede.Windows;
|
||||
using WpfMessageBox = System.Windows.MessageBox;
|
||||
using WpfOpenFileDialog = Microsoft.Win32.OpenFileDialog;
|
||||
using WpfButton = System.Windows.Controls.Button;
|
||||
|
||||
namespace Ganimede
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private string? videoPath;
|
||||
private string? outputFolder;
|
||||
private ObservableCollection<BitmapImage> thumbnails = new();
|
||||
private readonly VideoProcessingService _processingService = new();
|
||||
private readonly ObservableCollection<BitmapImage> thumbnails = new();
|
||||
private readonly List<VideoJob> _selectedJobs = new();
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
InitializeUI();
|
||||
ConfigureFFMpeg();
|
||||
}
|
||||
|
||||
private void InitializeUI()
|
||||
{
|
||||
ThumbnailsPanel.ItemsSource = thumbnails;
|
||||
Debug.WriteLine("[INIT] MainWindow initialized.");
|
||||
QueueItemsControl.ItemsSource = _processingService.JobQueue;
|
||||
|
||||
// Carica i percorsi salvati
|
||||
outputFolder = Settings.Default.LastOutputFolder;
|
||||
videoPath = Settings.Default.LastVideoPath;
|
||||
if (!string.IsNullOrEmpty(outputFolder))
|
||||
StatusText.Text = $"Last output folder: {outputFolder}";
|
||||
if (!string.IsNullOrEmpty(videoPath))
|
||||
StatusText.Text += $"\nLast video: {System.IO.Path.GetFileName(videoPath)}";
|
||||
{
|
||||
StatusText.Text = "Pronto";
|
||||
GlobalOutputFolderTextBox.Text = outputFolder;
|
||||
}
|
||||
|
||||
// Configura FFMpegCore con percorso binari
|
||||
_processingService.JobCompleted += OnJobCompleted;
|
||||
_processingService.JobFailed += OnJobFailed;
|
||||
_processingService.ProcessingStarted += OnProcessingStarted;
|
||||
_processingService.ProcessingStopped += OnProcessingStopped;
|
||||
_processingService.JobQueue.CollectionChanged += (s, e) => UpdateQueueCount();
|
||||
|
||||
UpdateQueueCount();
|
||||
}
|
||||
|
||||
private void UpdateQueueCount()
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
QueueCountText.Text = $"({_processingService.JobQueue.Count})";
|
||||
UpdateJobsSummary();
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateJobsSummary()
|
||||
{
|
||||
var pending = _processingService.JobQueue.Count(j => j.Status == JobStatus.Pending);
|
||||
var processing = _processingService.JobQueue.Count(j => j.Status == JobStatus.Processing);
|
||||
var completed = _processingService.JobQueue.Count(j => j.Status == JobStatus.Completed);
|
||||
var failed = _processingService.JobQueue.Count(j => j.Status == JobStatus.Failed);
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
JobsSummaryText.Text = $"In attesa: {pending} | In corso: {processing} | Completati: {completed} | Falliti: {failed}";
|
||||
});
|
||||
}
|
||||
|
||||
private void OnProcessingStarted()
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
StartQueueButton.IsEnabled = false;
|
||||
StopQueueButton.IsEnabled = true;
|
||||
StatusText.Text = "Elaborazione coda...";
|
||||
UpdateJobsSummary();
|
||||
});
|
||||
}
|
||||
|
||||
private void OnProcessingStopped()
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
StartQueueButton.IsEnabled = true;
|
||||
StopQueueButton.IsEnabled = false;
|
||||
StatusText.Text = "Coda fermata";
|
||||
UpdateJobsSummary();
|
||||
});
|
||||
}
|
||||
|
||||
private void OnJobCompleted(VideoJob job)
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
StatusText.Text = $"✓ Completato: {job.VideoName}";
|
||||
LoadThumbnailsFromFolder(job.OutputFolder);
|
||||
UpdateJobsSummary();
|
||||
});
|
||||
}
|
||||
|
||||
private void OnJobFailed(VideoJob job)
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
StatusText.Text = $"✗ Fallito: {job.VideoName}";
|
||||
UpdateJobsSummary();
|
||||
});
|
||||
}
|
||||
|
||||
private void LoadThumbnailsFromFolder(string folder)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(folder)) return;
|
||||
var imageFiles = Directory.GetFiles(folder, "*.png").OrderBy(f => f).Take(60).ToList();
|
||||
thumbnails.Clear();
|
||||
foreach (var imagePath in imageFiles)
|
||||
{
|
||||
var bmp = new BitmapImage();
|
||||
bmp.BeginInit();
|
||||
bmp.UriSource = new Uri(imagePath);
|
||||
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bmp.EndInit();
|
||||
thumbnails.Add(bmp);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[ERROR] Impossibile caricare thumbnails: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfigureFFMpeg()
|
||||
{
|
||||
var ffmpegBin = Settings.Default.FFmpegBinFolder;
|
||||
if (!string.IsNullOrEmpty(ffmpegBin) && Directory.Exists(ffmpegBin))
|
||||
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
|
||||
{
|
||||
FFMpegCore.GlobalFFOptions.Configure(options => options.BinaryFolder = ffmpegBin);
|
||||
Debug.WriteLine($"[CONFIG] FFMpeg bin folder set: {ffmpegBin}");
|
||||
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;
|
||||
}
|
||||
else
|
||||
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))
|
||||
{
|
||||
StatusText.Text += "\n[WARNING] ffmpeg/ffprobe path not set. Configure in settings.";
|
||||
Debug.WriteLine("[WARNING] ffmpeg/ffprobe path not set or invalid.");
|
||||
try { File.Copy(ffprobePath, ffmpegPath, true); return File.Exists(ffmpegPath); } catch { return false; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void BrowseVideoButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Debug.WriteLine("[UI] BrowseVideoButton_Click invoked.");
|
||||
var dialog = new Microsoft.Win32.OpenFileDialog { Filter = "Video files (*.mp4;*.avi;*.mov)|*.mp4;*.avi;*.mov|All files (*.*)|*.*" };
|
||||
if (dialog.ShowDialog() == true)
|
||||
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)
|
||||
{
|
||||
videoPath = dialog.FileName;
|
||||
StatusText.Text = $"Selected video: {Path.GetFileName(videoPath)}";
|
||||
Settings.Default.LastVideoPath = videoPath;
|
||||
Settings.Default.Save();
|
||||
Debug.WriteLine($"[INFO] Video selected: {videoPath}");
|
||||
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);
|
||||
}
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
private void AddVideosToQueue(string[] paths)
|
||||
{
|
||||
if (string.IsNullOrEmpty(outputFolder))
|
||||
{
|
||||
Debug.WriteLine("[INFO] Video selection cancelled.");
|
||||
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)
|
||||
{
|
||||
Debug.WriteLine("[UI] SelectOutputFolderButton_Click invoked.");
|
||||
using (var dialog = new System.Windows.Forms.FolderBrowserDialog())
|
||||
using var dialog = new System.Windows.Forms.FolderBrowserDialog();
|
||||
if (!string.IsNullOrEmpty(outputFolder)) dialog.SelectedPath = outputFolder;
|
||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
||||
{
|
||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
||||
{
|
||||
outputFolder = dialog.SelectedPath;
|
||||
StatusText.Text = $"Selected output folder: {outputFolder}";
|
||||
Settings.Default.LastOutputFolder = outputFolder;
|
||||
Settings.Default.Save();
|
||||
Debug.WriteLine($"[INFO] Output folder selected: {outputFolder}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("[INFO] Output folder selection cancelled.");
|
||||
}
|
||||
outputFolder = dialog.SelectedPath;
|
||||
GlobalOutputFolderTextBox.Text = outputFolder;
|
||||
StatusText.Text = "Cartella output aggiornata";
|
||||
Settings.Default.LastOutputFolder = outputFolder;
|
||||
Settings.Default.Save();
|
||||
}
|
||||
}
|
||||
|
||||
private async void ExtractFramesButton_Click(object sender, RoutedEventArgs e)
|
||||
private void SettingsButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Debug.WriteLine("[UI] ExtractFramesButton_Click invoked.");
|
||||
if (string.IsNullOrEmpty(videoPath) || string.IsNullOrEmpty(outputFolder))
|
||||
var win = new SettingsWindow { Owner = this };
|
||||
if (win.ShowDialog() == true)
|
||||
{
|
||||
StatusText.Text = "Please select a video and output folder.";
|
||||
Debug.WriteLine("[ERROR] Video path or output folder not set.");
|
||||
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);
|
||||
return;
|
||||
}
|
||||
ExtractFramesButton.IsEnabled = false;
|
||||
ProgressBar.Value = 0;
|
||||
thumbnails.Clear();
|
||||
StatusText.Text = "Analyzing video...";
|
||||
Debug.WriteLine($"[PROCESS] Starting analysis for video: {videoPath}");
|
||||
try
|
||||
if (_processingService.JobQueue.All(j => j.Status != JobStatus.Pending))
|
||||
{
|
||||
var mediaInfo = await FFProbe.AnalyseAsync(videoPath);
|
||||
Debug.WriteLine($"[INFO] Video duration: {mediaInfo.Duration}, FrameRate: 24");
|
||||
int frameRate = 24;
|
||||
int frameCount = (int)mediaInfo.Duration.TotalSeconds * frameRate;
|
||||
Debug.WriteLine($"[INFO] Total frames to extract: {frameCount}");
|
||||
for (int i = 0; i < frameCount; i++)
|
||||
{
|
||||
var frameTime = TimeSpan.FromSeconds((double)i / frameRate);
|
||||
string framePath = Path.Combine(outputFolder, $"frame_{i}.png");
|
||||
Debug.WriteLine($"[PROCESS] Extracting frame {i + 1}/{frameCount} at {frameTime}");
|
||||
await FFMpegArguments
|
||||
.FromFileInput(videoPath)
|
||||
.OutputToFile(framePath, false, options => options
|
||||
.Seek(frameTime)
|
||||
.WithFrameOutputCount(1)
|
||||
.ForceFormat("png")
|
||||
.Resize(320, 180))
|
||||
.ProcessAsynchronously();
|
||||
if (File.Exists(framePath))
|
||||
{
|
||||
var bitmap = new BitmapImage();
|
||||
bitmap.BeginInit();
|
||||
bitmap.UriSource = new Uri(framePath);
|
||||
bitmap.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bitmap.EndInit();
|
||||
thumbnails.Add(bitmap);
|
||||
Debug.WriteLine($"[INFO] Frame saved and loaded: {framePath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine($"[ERROR] Frame file not found: {framePath}");
|
||||
}
|
||||
ProgressBar.Value = (i + 1) * 100 / frameCount;
|
||||
StatusText.Text = $"Extracting frames {i + 1}/{frameCount} ({ProgressBar.Value}%) - Processing frame {i + 1}.";
|
||||
}
|
||||
StatusText.Text = "Extraction complete!";
|
||||
Debug.WriteLine("[SUCCESS] Extraction complete.");
|
||||
WpfMessageBox.Show("Nessun job in stato In attesa.", "Nessun Job", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText.Text = $"Error: {ex.Message}";
|
||||
Debug.WriteLine($"[EXCEPTION] {ex.GetType()}: {ex.Message}\n{ex.StackTrace}");
|
||||
}
|
||||
ExtractFramesButton.IsEnabled = true;
|
||||
await _processingService.StartProcessingAsync();
|
||||
UpdateJobsSummary();
|
||||
}
|
||||
|
||||
private void DragDropArea_Drop(object sender, System.Windows.DragEventArgs e)
|
||||
private void StopQueueButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Debug.WriteLine("[UI] DragDropArea_Drop invoked.");
|
||||
if (e.Data.GetDataPresent(System.Windows.DataFormats.FileDrop))
|
||||
_processingService.StopProcessing();
|
||||
StatusText.Text = "Arresto in corso...";
|
||||
UpdateJobsSummary();
|
||||
}
|
||||
|
||||
private void JobCheckBox_CheckedChanged(object sender, RoutedEventArgs e) => UpdateSelectedJobs();
|
||||
|
||||
private void UpdateSelectedJobs()
|
||||
{
|
||||
_selectedJobs.Clear();
|
||||
var boxes = FindVisualChildren<System.Windows.Controls.CheckBox>(QueueItemsControl).Where(cb => cb.Name == "JobCheckBox");
|
||||
foreach (var cb in boxes) if (cb.IsChecked == true && cb.Tag is VideoJob job) _selectedJobs.Add(job);
|
||||
ConfigureSelectedButton.IsEnabled = _selectedJobs.Count > 0;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var files = (string[])e.Data.GetData(System.Windows.DataFormats.FileDrop);
|
||||
if (files.Length > 0)
|
||||
StatusText.Text = $"Configurazione applicata a {_selectedJobs.Count} job";
|
||||
foreach (var job in _selectedJobs.Where(j => string.IsNullOrEmpty(j.CustomOutputFolder)))
|
||||
{
|
||||
videoPath = files[0];
|
||||
StatusText.Text = $"Selected video: {Path.GetFileName(videoPath)}";
|
||||
Settings.Default.LastVideoPath = videoPath;
|
||||
Settings.Default.Save();
|
||||
Debug.WriteLine($"[INFO] Video selected via drag & drop: {videoPath}");
|
||||
var createSub = Settings.Default.CreateSubfolder;
|
||||
job.OutputFolder = job.ExtractionMode == ExtractionMode.SingleFrame ? outputFolder : (createSub ? Path.Combine(outputFolder, job.VideoName) : outputFolder);
|
||||
}
|
||||
else
|
||||
UpdateJobsSummary();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveQueueItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is WpfButton btn && btn.Tag is VideoJob job)
|
||||
{
|
||||
_processingService.CancelJob(job);
|
||||
if (job.Status == JobStatus.Cancelled || job.Status == JobStatus.Pending)
|
||||
_processingService.JobQueue.Remove(job);
|
||||
UpdateQueueCount();
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearCompletedButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_processingService.RemoveCompletedJobs();
|
||||
StatusText.Text = "Job completati rimossi";
|
||||
UpdateQueueCount();
|
||||
}
|
||||
|
||||
private void ClearAllButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
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);
|
||||
if (res == MessageBoxResult.Cancel) return;
|
||||
if (res == MessageBoxResult.Yes)
|
||||
{
|
||||
Debug.WriteLine("[WARN] Drag & drop did not contain files.");
|
||||
_processingService.StopProcessing();
|
||||
_processingService.JobQueue.Clear();
|
||||
thumbnails.Clear();
|
||||
}
|
||||
else if (res == MessageBoxResult.No)
|
||||
{
|
||||
for (int i = _processingService.JobQueue.Count - 1; i >= 0; i--)
|
||||
if (_processingService.JobQueue[i].Status != JobStatus.Processing)
|
||||
_processingService.JobQueue.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("[WARN] Drag & drop did not contain file drop format.");
|
||||
if (WpfMessageBox.Show("Rimuovere tutti i job?", "Conferma", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
|
||||
{
|
||||
_processingService.JobQueue.Clear();
|
||||
thumbnails.Clear();
|
||||
}
|
||||
}
|
||||
StatusText.Text = "Coda aggiornata";
|
||||
UpdateQueueCount();
|
||||
}
|
||||
|
||||
private void DragDropArea_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
|
||||
private static bool IsVideoFile(string path)
|
||||
{
|
||||
DragDropArea.BorderBrush = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(79, 195, 247));
|
||||
Debug.WriteLine("[UI] DragDropArea_MouseEnter");
|
||||
var ext = Path.GetExtension(path).ToLowerInvariant();
|
||||
return ext is ".mp4" or ".avi" or ".mov" or ".mkv" or ".wmv" or ".flv" or ".webm";
|
||||
}
|
||||
|
||||
private void DragDropArea_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
|
||||
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject dep) where T : DependencyObject
|
||||
{
|
||||
DragDropArea.BorderBrush = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(68, 68, 68));
|
||||
Debug.WriteLine("[UI] DragDropArea_MouseLeave");
|
||||
if (dep == null) yield break;
|
||||
for (int i = 0; i < System.Windows.Media.VisualTreeHelper.GetChildrenCount(dep); i++)
|
||||
{
|
||||
var child = System.Windows.Media.VisualTreeHelper.GetChild(dep, i);
|
||||
if (child is T t) yield return t;
|
||||
foreach (var c in FindVisualChildren<T>(child)) yield return c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace Ganimede.Models
|
||||
{
|
||||
// Modelli futuri (es. VideoInfo, FrameInfo)
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Ganimede.Models
|
||||
{
|
||||
public enum JobStatus
|
||||
{
|
||||
Pending,
|
||||
Processing,
|
||||
Completed,
|
||||
Failed,
|
||||
Cancelled
|
||||
}
|
||||
|
||||
public enum OverwriteMode
|
||||
{
|
||||
Ask,
|
||||
Skip,
|
||||
Overwrite
|
||||
}
|
||||
|
||||
public enum NamingPattern
|
||||
{
|
||||
[Description("VideoName + Progressive (VID20250725_000001)")]
|
||||
VideoNameProgressive,
|
||||
|
||||
[Description("Frame + Progressive (frame_000001)")]
|
||||
FrameProgressive,
|
||||
|
||||
[Description("VideoName + Timestamp (VID20250725_001234ms)")]
|
||||
VideoNameTimestamp,
|
||||
|
||||
[Description("Custom Prefix + Progressive (custom_000001)")]
|
||||
CustomProgressive,
|
||||
|
||||
[Description("Timestamp Only (00h12m34s567ms)")]
|
||||
TimestampOnly,
|
||||
|
||||
[Description("VideoName + Frame + Progressive (VID20250725_frame_000001)")]
|
||||
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;
|
||||
private double _progress = 0;
|
||||
private string _statusMessage = "Pending";
|
||||
private string _customOutputFolder = string.Empty;
|
||||
private string _customFrameSize = string.Empty;
|
||||
private OverwriteMode? _customOverwriteMode = null;
|
||||
private bool _customCreateSubfolder = true;
|
||||
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);
|
||||
public required string OutputFolder { get; set; }
|
||||
public DateTime AddedTime { get; set; } = DateTime.Now;
|
||||
|
||||
public JobStatus Status
|
||||
{
|
||||
get => _status;
|
||||
set
|
||||
{
|
||||
_status = value;
|
||||
OnPropertyChanged(nameof(Status));
|
||||
}
|
||||
}
|
||||
|
||||
public double Progress
|
||||
{
|
||||
get => _progress;
|
||||
set
|
||||
{
|
||||
_progress = value;
|
||||
OnPropertyChanged(nameof(Progress));
|
||||
}
|
||||
}
|
||||
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
set
|
||||
{
|
||||
_statusMessage = value;
|
||||
OnPropertyChanged(nameof(StatusMessage));
|
||||
}
|
||||
}
|
||||
|
||||
// Custom settings for individual jobs
|
||||
public string CustomOutputFolder
|
||||
{
|
||||
get => _customOutputFolder;
|
||||
set
|
||||
{
|
||||
_customOutputFolder = value;
|
||||
OnPropertyChanged(nameof(CustomOutputFolder));
|
||||
OnPropertyChanged(nameof(OutputFolderDisplay));
|
||||
}
|
||||
}
|
||||
|
||||
public string CustomFrameSize
|
||||
{
|
||||
get => _customFrameSize;
|
||||
set
|
||||
{
|
||||
_customFrameSize = value;
|
||||
OnPropertyChanged(nameof(CustomFrameSize));
|
||||
OnPropertyChanged(nameof(FrameSizeDisplay));
|
||||
}
|
||||
}
|
||||
|
||||
public OverwriteMode? CustomOverwriteMode
|
||||
{
|
||||
get => _customOverwriteMode;
|
||||
set
|
||||
{
|
||||
_customOverwriteMode = value;
|
||||
OnPropertyChanged(nameof(CustomOverwriteMode));
|
||||
OnPropertyChanged(nameof(OverwriteModeDisplay));
|
||||
}
|
||||
}
|
||||
|
||||
public bool CustomCreateSubfolder
|
||||
{
|
||||
get => _customCreateSubfolder;
|
||||
set
|
||||
{
|
||||
_customCreateSubfolder = value;
|
||||
OnPropertyChanged(nameof(CustomCreateSubfolder));
|
||||
OnPropertyChanged(nameof(OutputFolderDisplay));
|
||||
}
|
||||
}
|
||||
|
||||
public NamingPattern? CustomNamingPattern
|
||||
{
|
||||
get => _customNamingPattern;
|
||||
set
|
||||
{
|
||||
_customNamingPattern = value;
|
||||
OnPropertyChanged(nameof(CustomNamingPattern));
|
||||
OnPropertyChanged(nameof(NamingPatternDisplay));
|
||||
}
|
||||
}
|
||||
|
||||
public string CustomPrefix
|
||||
{
|
||||
get => _customPrefix;
|
||||
set
|
||||
{
|
||||
_customPrefix = value;
|
||||
OnPropertyChanged(nameof(CustomPrefix));
|
||||
OnPropertyChanged(nameof(NamingPatternDisplay));
|
||||
}
|
||||
}
|
||||
|
||||
// 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 ? "/??" : "");
|
||||
|
||||
public string FrameSizeDisplay =>
|
||||
string.IsNullOrEmpty(CustomFrameSize) ? "Default" :
|
||||
(CustomFrameSize == "original" ? "Original" : CustomFrameSize);
|
||||
|
||||
public string OverwriteModeDisplay =>
|
||||
CustomOverwriteMode?.ToString() ?? "Default";
|
||||
|
||||
public string NamingPatternDisplay =>
|
||||
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)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// Il codice è stato generato da uno strumento.
|
||||
// Versione runtime:4.0.30319.42000
|
||||
//
|
||||
// Le modifiche apportate a questo file possono provocare un comportamento non corretto e andranno perse se
|
||||
// il codice viene rigenerato.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Ganimede.Properties {
|
||||
|
||||
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.14.0.0")]
|
||||
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
|
||||
|
||||
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
|
||||
|
||||
public static Settings Default {
|
||||
get {
|
||||
return defaultInstance;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("")]
|
||||
public string LastOutputFolder {
|
||||
get {
|
||||
return ((string)(this["LastOutputFolder"]));
|
||||
}
|
||||
set {
|
||||
this["LastOutputFolder"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("")]
|
||||
public string LastVideoPath {
|
||||
get {
|
||||
return ((string)(this["LastVideoPath"]));
|
||||
}
|
||||
set {
|
||||
this["LastVideoPath"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[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")]
|
||||
public bool CreateSubfolder {
|
||||
get {
|
||||
return ((bool)(this["CreateSubfolder"]));
|
||||
}
|
||||
set {
|
||||
this["CreateSubfolder"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("original")]
|
||||
public string FrameSize {
|
||||
get {
|
||||
return ((string)(this["FrameSize"]));
|
||||
}
|
||||
set {
|
||||
this["FrameSize"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("Ask")]
|
||||
public string DefaultOverwriteMode {
|
||||
get {
|
||||
return ((string)(this["DefaultOverwriteMode"]));
|
||||
}
|
||||
set {
|
||||
this["DefaultOverwriteMode"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("VideoNameProgressive")]
|
||||
public string DefaultNamingPattern {
|
||||
get {
|
||||
return ((string)(this["DefaultNamingPattern"]));
|
||||
}
|
||||
set {
|
||||
this["DefaultNamingPattern"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("custom")]
|
||||
public string DefaultCustomPrefix {
|
||||
get {
|
||||
return ((string)(this["DefaultCustomPrefix"]));
|
||||
}
|
||||
set {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version='1.0' encoding='iso-8859-1'?>
|
||||
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="Ganimede.Properties" GeneratedClassName="Settings">
|
||||
<Profiles />
|
||||
<Settings>
|
||||
<Setting Name="LastOutputFolder" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)" />
|
||||
</Setting>
|
||||
<Setting Name="LastVideoPath" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)" />
|
||||
</Setting>
|
||||
<Setting Name="FFmpegBinFolder" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)">C:\Users\balbo\source\repos\Ganimede\Ganimede\Ganimede\FFMpeg</Value>
|
||||
</Setting>
|
||||
<Setting Name="CreateSubfolder" Type="System.Boolean" Scope="User">
|
||||
<Value Profile="(Default)">True</Value>
|
||||
</Setting>
|
||||
<Setting Name="FrameSize" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)">original</Value>
|
||||
</Setting>
|
||||
<Setting Name="DefaultOverwriteMode" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)">Ask</Value>
|
||||
</Setting>
|
||||
<Setting Name="DefaultNamingPattern" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)">VideoNameProgressive</Value>
|
||||
</Setting>
|
||||
<Setting Name="DefaultCustomPrefix" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)">custom</Value>
|
||||
</Setting>
|
||||
<Setting Name="DefaultExtractionMode" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)">Full</Value>
|
||||
</Setting>
|
||||
<Setting Name="SingleFrameUseSubfolder" Type="System.Boolean" Scope="User">
|
||||
<Value Profile="(Default)">False</Value>
|
||||
</Setting>
|
||||
</Settings>
|
||||
</SettingsFile>
|
||||
@@ -0,0 +1,389 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Diagnostics;
|
||||
using FFMpegCore;
|
||||
using System.IO;
|
||||
using Ganimede.Models;
|
||||
using Ganimede.Properties;
|
||||
using Ganimede.Helpers;
|
||||
|
||||
namespace Ganimede.Services
|
||||
{
|
||||
public class VideoProcessingService
|
||||
{
|
||||
private readonly ObservableCollection<VideoJob> _jobQueue = new();
|
||||
private readonly SemaphoreSlim _processingSemaphore = new(1, 1);
|
||||
private bool _isProcessing = false;
|
||||
private CancellationTokenSource _cancellationTokenSource = new();
|
||||
|
||||
public ObservableCollection<VideoJob> JobQueue => _jobQueue;
|
||||
public bool IsProcessing => _isProcessing;
|
||||
|
||||
public event Action<VideoJob>? JobCompleted;
|
||||
public event Action<VideoJob>? JobFailed;
|
||||
public event Action? ProcessingStarted;
|
||||
public event Action? ProcessingStopped;
|
||||
|
||||
public void AddJob(string videoPath, string outputFolder, bool createSubfolder = true)
|
||||
{
|
||||
// Determine default extraction mode from settings
|
||||
var settingMode = Settings.Default.DefaultExtractionMode;
|
||||
ExtractionMode extractionMode = ExtractionMode.Full;
|
||||
if (!string.IsNullOrEmpty(settingMode) && Enum.TryParse<ExtractionMode>(settingMode, out var parsed))
|
||||
{
|
||||
extractionMode = parsed;
|
||||
}
|
||||
|
||||
var useSingleSub = Settings.Default.SingleFrameUseSubfolder;
|
||||
var jobOutput = createSubfolder && (extractionMode != ExtractionMode.SingleFrame || (extractionMode == ExtractionMode.SingleFrame && useSingleSub))
|
||||
? Path.Combine(outputFolder, Path.GetFileNameWithoutExtension(videoPath))
|
||||
: outputFolder;
|
||||
|
||||
var job = new VideoJob
|
||||
{
|
||||
VideoPath = videoPath,
|
||||
OutputFolder = jobOutput,
|
||||
ExtractionMode = extractionMode
|
||||
};
|
||||
|
||||
_jobQueue.Add(job);
|
||||
Debug.WriteLine($"[QUEUE] Added job: {job.VideoName} (Status: Pending) Mode={job.ExtractionMode}");
|
||||
}
|
||||
|
||||
public async Task StartProcessingAsync()
|
||||
{
|
||||
if (_isProcessing)
|
||||
return;
|
||||
|
||||
await _processingSemaphore.WaitAsync();
|
||||
_isProcessing = true;
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
ProcessingStarted?.Invoke();
|
||||
Debug.WriteLine("[QUEUE] Processing started by user");
|
||||
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (_cancellationTokenSource.Token.IsCancellationRequested)
|
||||
{
|
||||
Debug.WriteLine("[QUEUE] Processing cancelled by user");
|
||||
break;
|
||||
}
|
||||
|
||||
var nextJob = GetNextPendingJob();
|
||||
if (nextJob == null)
|
||||
{
|
||||
Debug.WriteLine("[QUEUE] No more pending jobs");
|
||||
break;
|
||||
}
|
||||
|
||||
await ProcessJobAsync(nextJob, _cancellationTokenSource.Token);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isProcessing = false;
|
||||
ProcessingStopped?.Invoke();
|
||||
_processingSemaphore.Release();
|
||||
Debug.WriteLine("[QUEUE] Processing stopped");
|
||||
}
|
||||
}
|
||||
|
||||
public void StopProcessing()
|
||||
{
|
||||
if (_isProcessing)
|
||||
{
|
||||
_cancellationTokenSource.Cancel();
|
||||
Debug.WriteLine("[QUEUE] Stop processing requested");
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelJob(VideoJob job)
|
||||
{
|
||||
if (job.Status == JobStatus.Pending)
|
||||
{
|
||||
job.Status = JobStatus.Cancelled;
|
||||
job.StatusMessage = "Cancelled";
|
||||
Debug.WriteLine($"[QUEUE] Cancelled job: {job.VideoName}");
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveCompletedJobs()
|
||||
{
|
||||
for (int i = _jobQueue.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (_jobQueue[i].Status == JobStatus.Completed ||
|
||||
_jobQueue[i].Status == JobStatus.Failed ||
|
||||
_jobQueue[i].Status == JobStatus.Cancelled)
|
||||
{
|
||||
_jobQueue.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
Debug.WriteLine("[QUEUE] Removed completed jobs");
|
||||
}
|
||||
|
||||
private VideoJob? GetNextPendingJob()
|
||||
{
|
||||
foreach (var job in _jobQueue)
|
||||
{
|
||||
if (job.Status == JobStatus.Pending)
|
||||
return job;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task ProcessJobAsync(VideoJob job, CancellationToken cancellationToken)
|
||||
{
|
||||
job.Status = JobStatus.Processing;
|
||||
job.StatusMessage = "Analyzing video...";
|
||||
job.Progress = 0;
|
||||
|
||||
try
|
||||
{
|
||||
Debug.WriteLine($"[PROCESS] Starting job: {job.VideoName}");
|
||||
|
||||
// (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}");
|
||||
|
||||
// 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);
|
||||
|
||||
var existingFiles = Directory.GetFiles(job.OutputFolder, "*.png");
|
||||
if (existingFiles.Length > 0 && overwriteMode == OverwriteMode.Ask)
|
||||
{
|
||||
var dialogResult = System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
return System.Windows.MessageBox.Show(
|
||||
$"Found {existingFiles.Length} existing frame files in:\n{job.OutputFolder}\n\n" +
|
||||
"Do you want to overwrite them?",
|
||||
$"Existing Files - {job.VideoName}",
|
||||
System.Windows.MessageBoxButton.YesNo,
|
||||
System.Windows.MessageBoxImage.Question);
|
||||
});
|
||||
overwriteMode = dialogResult == System.Windows.MessageBoxResult.Yes ? OverwriteMode.Overwrite : OverwriteMode.Skip;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
job.Status = JobStatus.Completed;
|
||||
job.StatusMessage = $"Completed - {processedFrames} frames processed" + (skippedFrames > 0 ? $", {skippedFrames} skipped" : "");
|
||||
job.Progress = 100;
|
||||
Debug.WriteLine($"[SUCCESS] Completed job: {job.VideoName}");
|
||||
JobCompleted?.Invoke(job);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
job.Status = JobStatus.Cancelled;
|
||||
job.StatusMessage = "Cancelled by user";
|
||||
Debug.WriteLine($"[CANCELLED] Job cancelled: {job.VideoName}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
job.Status = JobStatus.Failed;
|
||||
job.StatusMessage = $"Error: {ex.Message}";
|
||||
Debug.WriteLine($"[ERROR] Job failed for {job.VideoName}: {ex.Message}");
|
||||
JobFailed?.Invoke(job);
|
||||
}
|
||||
}
|
||||
|
||||
private (int width, int height) GetFrameSize(VideoJob job)
|
||||
{
|
||||
var frameSize = !string.IsNullOrEmpty(job.CustomFrameSize) ? job.CustomFrameSize : Settings.Default.FrameSize;
|
||||
|
||||
if (string.IsNullOrEmpty(frameSize) || frameSize == "original")
|
||||
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);
|
||||
}
|
||||
|
||||
private OverwriteMode GetOverwriteMode(VideoJob job)
|
||||
{
|
||||
if (job.CustomOverwriteMode.HasValue)
|
||||
return job.CustomOverwriteMode.Value;
|
||||
|
||||
var defaultMode = Settings.Default.DefaultOverwriteMode;
|
||||
if (Enum.TryParse<OverwriteMode>(defaultMode, out var mode))
|
||||
return mode;
|
||||
|
||||
return OverwriteMode.Ask;
|
||||
}
|
||||
|
||||
private NamingPattern GetNamingPattern(VideoJob job)
|
||||
{
|
||||
if (job.CustomNamingPattern.HasValue)
|
||||
return job.CustomNamingPattern.Value;
|
||||
|
||||
var defaultPattern = Settings.Default.DefaultNamingPattern;
|
||||
if (Enum.TryParse<NamingPattern>(defaultPattern, out var pattern))
|
||||
return pattern;
|
||||
|
||||
return NamingPattern.VideoNameProgressive;
|
||||
}
|
||||
|
||||
private string GetCustomPrefix(VideoJob job)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(job.CustomPrefix))
|
||||
return job.CustomPrefix;
|
||||
|
||||
return Settings.Default.DefaultCustomPrefix ?? "custom";
|
||||
}
|
||||
|
||||
private async Task ExtractFrameAsync(VideoJob job, int frameIndex, int frameRate, (int width, int height) frameSize, string framePath)
|
||||
{
|
||||
var frameTime = TimeSpan.FromSeconds((double)frameIndex / frameRate);
|
||||
|
||||
try
|
||||
{
|
||||
if (frameSize.width == -1 && frameSize.height == -1)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[ERROR] Failed to extract frame {frameIndex} from {job.VideoName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace Ganimede.ViewModels
|
||||
{
|
||||
// ViewModel principale e futuri ViewModel
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace Ganimede.Views
|
||||
{
|
||||
// Views aggiuntive se necessario
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<Window x:Class="Ganimede.Windows.JobConfigWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Title="Configurazione Job" Height="640" Width="640"
|
||||
Background="#1E2228" WindowStartupLocation="CenterOwner">
|
||||
<Window.Resources>
|
||||
<SolidColorBrush x:Key="PanelBrush" Color="#242A31"/>
|
||||
<SolidColorBrush x:Key="PanelSubBrush" Color="#2C333B"/>
|
||||
<SolidColorBrush x:Key="BorderBrushColor" Color="#38424D"/>
|
||||
<SolidColorBrush x:Key="AccentBrush" Color="#268BFF"/>
|
||||
<Style TargetType="GroupBox">
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="GroupBox">
|
||||
<Border Background="{StaticResource PanelBrush}" BorderBrush="{StaticResource BorderBrushColor}" BorderThickness="1" CornerRadius="8" Padding="12" >
|
||||
<DockPanel>
|
||||
<Border Background="#2C333B" CornerRadius="4" Padding="6 4" Margin="0 0 0 10" DockPanel.Dock="Top">
|
||||
<ContentPresenter ContentSource="Header"/>
|
||||
</Border>
|
||||
<ContentPresenter/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style TargetType="Button" x:Key="PrimaryButton">
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="Background" Value="#268BFF"/>
|
||||
<Setter Property="BorderBrush" Value="#1776DF"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="14 8"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="6">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#3595FF"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="#1570D4"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.45"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style TargetType="Button" x:Key="FlatButton" BasedOn="{StaticResource PrimaryButton}">
|
||||
<Setter Property="Background" Value="#2F3740"/>
|
||||
<Setter Property="BorderBrush" Value="#404B55"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#38434D"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
<Style TargetType="TextBox">
|
||||
<Setter Property="Background" Value="#2C333B"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderBrush" Value="#3A444E"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="6 4"/>
|
||||
</Style>
|
||||
<Style TargetType="ComboBox">
|
||||
<Setter Property="Background" Value="#2C333B"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderBrush" Value="#3A444E"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="4 2"/>
|
||||
</Style>
|
||||
<Style TargetType="CheckBox">
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="Margin" Value="0 2 0 6"/>
|
||||
</Style>
|
||||
<Style TargetType="RadioButton">
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="Margin" Value="0 0 14 0"/>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Margin="18">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel Grid.Row="0" Margin="0 0 0 14">
|
||||
<TextBlock Text="Configurazione Job" FontSize="22" FontWeight="SemiBold" Foreground="White"/>
|
||||
<TextBlock x:Name="SelectedJobsText" Text="0 job selezionati" Foreground="#9BA5AF" FontSize="12" Margin="0 4 0 0"/>
|
||||
</StackPanel>
|
||||
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
<!-- Output -->
|
||||
<GroupBox Header="Cartella di Output (Override facoltativo)" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<CheckBox x:Name="UseCustomOutputCheckBox" Content="Usa cartella di output personalizzata" Checked="UseCustomOutputCheckBox_CheckedChanged" Unchecked="UseCustomOutputCheckBox_CheckedChanged"/>
|
||||
<DockPanel Margin="0 4 0 0" IsEnabled="{Binding IsChecked, ElementName=UseCustomOutputCheckBox}">
|
||||
<TextBox x:Name="CustomOutputTextBox" Height="34" VerticalContentAlignment="Center" DockPanel.Dock="Left" MinWidth="320" Margin="0 0 10 0"/>
|
||||
<Button x:Name="BrowseCustomOutputButton" Content="Sfoglia" Width="90" Height="34" Style="{StaticResource FlatButton}" Click="BrowseCustomOutputButton_Click"/>
|
||||
</DockPanel>
|
||||
<CheckBox x:Name="CreateSubfolderCheckBox" Content="Crea sottocartella per ogni video" IsChecked="True"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- Extraction Mode -->
|
||||
<GroupBox Header="Modalita di Estrazione" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
|
||||
<RadioButton x:Name="ExtractionFullRadio" Content="Completa" GroupName="ExtractionMode" IsChecked="True"/>
|
||||
<RadioButton x:Name="ExtractionSingleRadio" Content="Singolo Frame" GroupName="ExtractionMode"/>
|
||||
<RadioButton x:Name="ExtractionAutoRadio" Content="Auto" GroupName="ExtractionMode"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="Auto: analizza il video e decide se estrarre tutti i frame o solo uno." Foreground="#8D969F" FontSize="11" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- Naming Pattern -->
|
||||
<GroupBox Header="Pattern Nome File (Override facoltativo)" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<CheckBox x:Name="UseCustomNamingCheckBox" Content="Usa pattern di naming personalizzato" Checked="UseCustomNamingCheckBox_CheckedChanged" Unchecked="UseCustomNamingCheckBox_CheckedChanged"/>
|
||||
<ComboBox x:Name="CustomNamingComboBox" Height="32" Margin="0 6 0 0" SelectionChanged="CustomNamingComboBox_SelectionChanged" IsEnabled="{Binding IsChecked, ElementName=UseCustomNamingCheckBox}">
|
||||
<ComboBoxItem Content="NomeVideo + Progressivo" Tag="VideoNameProgressive"/>
|
||||
<ComboBoxItem Content="Frame + Progressivo" Tag="FrameProgressive"/>
|
||||
<ComboBoxItem Content="NomeVideo + Timestamp" Tag="VideoNameTimestamp"/>
|
||||
<ComboBoxItem Content="Prefisso Custom + Progressivo" Tag="CustomProgressive"/>
|
||||
<ComboBoxItem Content="Solo Timestamp" Tag="TimestampOnly"/>
|
||||
<ComboBoxItem Content="NomeVideo + Frame + Progressivo" Tag="VideoNameFrameProgressive"/>
|
||||
</ComboBox>
|
||||
<StackPanel Orientation="Horizontal" Margin="0 6 0 0" IsEnabled="{Binding IsChecked, ElementName=UseCustomNamingCheckBox}">
|
||||
<TextBlock Text="Prefisso:" Foreground="#9BA5AF" VerticalAlignment="Center" Margin="0 0 8 0"/>
|
||||
<TextBox x:Name="CustomNamingPrefixTextBox" Width="160" Text="custom" Height="30" VerticalContentAlignment="Center" TextChanged="CustomNamingPrefixTextBox_TextChanged"/>
|
||||
</StackPanel>
|
||||
<Border Background="#2F3740" CornerRadius="4" Padding="6" Margin="0 6 0 0" IsEnabled="{Binding IsChecked, ElementName=UseCustomNamingCheckBox}">
|
||||
<TextBlock x:Name="JobNamingPreviewText" Text="Video1_000001.png" FontFamily="Consolas" Foreground="#4FA6FF" FontSize="12"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- Overwrite -->
|
||||
<GroupBox Header="Modalita Sovrascrittura (Override facoltativo)" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<CheckBox x:Name="UseCustomOverwriteCheckBox" Content="Usa comportamento di sovrascrittura personalizzato" Checked="UseCustomOverwriteCheckBox_CheckedChanged" Unchecked="UseCustomOverwriteCheckBox_CheckedChanged"/>
|
||||
<ComboBox x:Name="CustomOverwriteComboBox" Height="32" Margin="0 6 0 0" IsEnabled="{Binding IsChecked, ElementName=UseCustomOverwriteCheckBox}">
|
||||
<ComboBoxItem Content="Chiedi" Tag="Ask"/>
|
||||
<ComboBoxItem Content="Salta" Tag="Skip"/>
|
||||
<ComboBoxItem Content="Sovrascrivi" Tag="Overwrite"/>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- Frame size -->
|
||||
<GroupBox Header="Ridimensionamento Frame (Override facoltativo)" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<CheckBox x:Name="UseCustomFrameSizeCheckBox" Content="Usa dimensione frame personalizzata" Checked="UseCustomFrameSizeCheckBox_CheckedChanged" Unchecked="UseCustomFrameSizeCheckBox_CheckedChanged"/>
|
||||
<ComboBox x:Name="CustomFrameSizeComboBox" Height="32" Margin="0 6 0 0" IsEnabled="{Binding IsChecked, ElementName=UseCustomFrameSizeCheckBox}">
|
||||
<ComboBoxItem Content="Risoluzione Originale" Tag="original"/>
|
||||
<ComboBoxItem Content="320x180 (Veloce)" Tag="320,180"/>
|
||||
<ComboBoxItem Content="640x360 (Media)" Tag="640,360"/>
|
||||
<ComboBoxItem Content="1280x720 (HD)" Tag="1280,720"/>
|
||||
<ComboBoxItem Content="1920x1080 (Full HD)" Tag="1920,1080"/>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0 16 0 0">
|
||||
<Button x:Name="ApplyButton" Content="Applica" Width="140" Height="42" Style="{StaticResource PrimaryButton}" Click="ApplyButton_Click" Margin="0,0,12,0"/>
|
||||
<Button x:Name="CancelButton" Content="Annulla" Width="110" Height="42" Style="{StaticResource FlatButton}" Click="CancelButton_Click"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,194 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Ganimede.Models;
|
||||
using Ganimede.Helpers;
|
||||
using WpfMessageBox = System.Windows.MessageBox;
|
||||
|
||||
namespace Ganimede.Windows
|
||||
{
|
||||
public partial class JobConfigWindow : Window
|
||||
{
|
||||
private List<VideoJob> _selectedJobs;
|
||||
|
||||
public JobConfigWindow(List<VideoJob> selectedJobs)
|
||||
{
|
||||
InitializeComponent();
|
||||
_selectedJobs = selectedJobs;
|
||||
SelectedJobsText.Text = $"{selectedJobs.Count} job selezionati";
|
||||
LoadCurrentSettings();
|
||||
}
|
||||
|
||||
private void SetExtractionModeRadio(ExtractionMode mode)
|
||||
{
|
||||
if (FindName("ExtractionFullRadio") is System.Windows.Controls.RadioButton fullRb) fullRb.IsChecked = mode == ExtractionMode.Full;
|
||||
if (FindName("ExtractionSingleRadio") is System.Windows.Controls.RadioButton singleRb) singleRb.IsChecked = mode == ExtractionMode.SingleFrame;
|
||||
if (FindName("ExtractionAutoRadio") is System.Windows.Controls.RadioButton autoRb) autoRb.IsChecked = mode == ExtractionMode.Auto;
|
||||
}
|
||||
|
||||
private ExtractionMode GetSelectedExtractionMode()
|
||||
{
|
||||
if (FindName("ExtractionSingleRadio") is System.Windows.Controls.RadioButton singleRb && singleRb.IsChecked == true) return ExtractionMode.SingleFrame;
|
||||
if (FindName("ExtractionAutoRadio") is System.Windows.Controls.RadioButton autoRb && autoRb.IsChecked == true) return ExtractionMode.Auto;
|
||||
return ExtractionMode.Full;
|
||||
}
|
||||
|
||||
private void LoadCurrentSettings()
|
||||
{
|
||||
var firstJob = _selectedJobs.FirstOrDefault();
|
||||
if (firstJob != null)
|
||||
{
|
||||
SetExtractionModeRadio(firstJob.ExtractionMode);
|
||||
if (!string.IsNullOrEmpty(firstJob.CustomOutputFolder))
|
||||
{
|
||||
UseCustomOutputCheckBox.IsChecked = true;
|
||||
CustomOutputTextBox.Text = firstJob.CustomOutputFolder;
|
||||
}
|
||||
CreateSubfolderCheckBox.IsChecked = firstJob.CustomCreateSubfolder;
|
||||
|
||||
if (!string.IsNullOrEmpty(firstJob.CustomFrameSize))
|
||||
{
|
||||
UseCustomFrameSizeCheckBox.IsChecked = true;
|
||||
foreach (ComboBoxItem item in CustomFrameSizeComboBox.Items)
|
||||
{
|
||||
if (item.Tag?.ToString() == firstJob.CustomFrameSize)
|
||||
{
|
||||
CustomFrameSizeComboBox.SelectedItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (firstJob.CustomOverwriteMode.HasValue)
|
||||
{
|
||||
UseCustomOverwriteCheckBox.IsChecked = true;
|
||||
foreach (ComboBoxItem item in CustomOverwriteComboBox.Items)
|
||||
{
|
||||
if (item.Tag?.ToString() == firstJob.CustomOverwriteMode.Value.ToString())
|
||||
{
|
||||
CustomOverwriteComboBox.SelectedItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (firstJob.CustomNamingPattern.HasValue)
|
||||
{
|
||||
UseCustomNamingCheckBox.IsChecked = true;
|
||||
foreach (ComboBoxItem item in CustomNamingComboBox.Items)
|
||||
{
|
||||
if (item.Tag?.ToString() == firstJob.CustomNamingPattern.Value.ToString())
|
||||
{
|
||||
CustomNamingComboBox.SelectedItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
CustomNamingPrefixTextBox.Text = firstJob.CustomPrefix ?? "custom";
|
||||
}
|
||||
}
|
||||
|
||||
if (CustomFrameSizeComboBox.SelectedItem == null) CustomFrameSizeComboBox.SelectedIndex = 0;
|
||||
if (CustomOverwriteComboBox.SelectedItem == null) CustomOverwriteComboBox.SelectedIndex = 0;
|
||||
if (CustomNamingComboBox.SelectedItem == null) CustomNamingComboBox.SelectedIndex = 0;
|
||||
|
||||
UpdateJobNamingPreview();
|
||||
}
|
||||
|
||||
private void UpdateJobNamingPreview()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (UseCustomNamingCheckBox.IsChecked == true &&
|
||||
CustomNamingComboBox.SelectedItem is ComboBoxItem selectedItem &&
|
||||
Enum.TryParse<NamingPattern>(selectedItem.Tag?.ToString(), out var pattern))
|
||||
{
|
||||
var firstVideoName = _selectedJobs.FirstOrDefault()?.VideoName ?? "Video1";
|
||||
var customPrefix = string.IsNullOrWhiteSpace(CustomNamingPrefixTextBox.Text) ? "custom" : CustomNamingPrefixTextBox.Text;
|
||||
var example = NamingHelper.GetPatternExample(pattern, firstVideoName, customPrefix);
|
||||
JobNamingPreviewText.Text = example;
|
||||
}
|
||||
else
|
||||
{
|
||||
JobNamingPreviewText.Text = "Video1_000001.png (default)";
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
JobNamingPreviewText.Text = "Video1_000001.png";
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
using var dialog = new System.Windows.Forms.FolderBrowserDialog { Description = "Seleziona cartella di output personalizzata", ShowNewFolderButton = true };
|
||||
if (!string.IsNullOrEmpty(CustomOutputTextBox.Text)) dialog.SelectedPath = CustomOutputTextBox.Text;
|
||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) CustomOutputTextBox.Text = dialog.SelectedPath;
|
||||
}
|
||||
|
||||
private void ApplyButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var selectedExtraction = GetSelectedExtractionMode();
|
||||
foreach (var job in _selectedJobs)
|
||||
{
|
||||
job.ExtractionMode = selectedExtraction;
|
||||
|
||||
if (UseCustomOutputCheckBox.IsChecked == true)
|
||||
{
|
||||
job.CustomOutputFolder = CustomOutputTextBox.Text;
|
||||
job.CustomCreateSubfolder = CreateSubfolderCheckBox.IsChecked ?? true;
|
||||
if (job.CustomCreateSubfolder && job.ExtractionMode != ExtractionMode.SingleFrame)
|
||||
job.OutputFolder = System.IO.Path.Combine(job.CustomOutputFolder, job.VideoName);
|
||||
else
|
||||
job.OutputFolder = job.CustomOutputFolder;
|
||||
}
|
||||
else
|
||||
{
|
||||
job.CustomOutputFolder = string.Empty;
|
||||
}
|
||||
|
||||
if (UseCustomFrameSizeCheckBox.IsChecked == true && CustomFrameSizeComboBox.SelectedItem is ComboBoxItem frameSizeItem)
|
||||
job.CustomFrameSize = frameSizeItem.Tag?.ToString() ?? string.Empty;
|
||||
else job.CustomFrameSize = string.Empty;
|
||||
|
||||
if (UseCustomOverwriteCheckBox.IsChecked == true && CustomOverwriteComboBox.SelectedItem is ComboBoxItem overwriteItem && Enum.TryParse<OverwriteMode>(overwriteItem.Tag?.ToString(), out var overwriteMode))
|
||||
job.CustomOverwriteMode = overwriteMode;
|
||||
else job.CustomOverwriteMode = null;
|
||||
|
||||
if (UseCustomNamingCheckBox.IsChecked == true && CustomNamingComboBox.SelectedItem is ComboBoxItem namingItem && Enum.TryParse<NamingPattern>(namingItem.Tag?.ToString(), out var namingPattern))
|
||||
{
|
||||
job.CustomNamingPattern = namingPattern;
|
||||
job.CustomPrefix = string.IsNullOrWhiteSpace(CustomNamingPrefixTextBox.Text) ? "custom" : CustomNamingPrefixTextBox.Text;
|
||||
}
|
||||
else
|
||||
{
|
||||
job.CustomNamingPattern = null;
|
||||
job.CustomPrefix = string.Empty;
|
||||
}
|
||||
}
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WpfMessageBox.Show($"Errore durante l'applicazione delle impostazioni: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DialogResult = false;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<Window x:Class="Ganimede.Windows.SettingsWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Title="Impostazioni" Height="600" Width="640"
|
||||
Background="#1E2228" WindowStartupLocation="CenterOwner">
|
||||
<Grid Margin="22">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Title -->
|
||||
<TextBlock Grid.Row="0" Text="Impostazioni" FontSize="26" FontWeight="Bold" Foreground="White" Margin="0,0,0,18"/>
|
||||
|
||||
<!-- 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">
|
||||
<CheckBox x:Name="CreateSubfolderCheckBox" Content="Crea sottocartella per ogni video"
|
||||
Foreground="White" IsChecked="True" Margin="0,0,0,10"/>
|
||||
|
||||
<TextBlock Text="Cartella Output Predefinita:" Foreground="#CCC" Margin="0,0,0,6"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox x:Name="DefaultOutputTextBox" Grid.Column="0" Height="34" VerticalContentAlignment="Center"
|
||||
Background="#333" Foreground="White" BorderBrush="#555" Margin="0,0,10,0"/>
|
||||
<Button x:Name="BrowseOutputButton" Grid.Column="1" Content="Sfoglia" Width="90" Height="34"
|
||||
Click="BrowseOutputButton_Click"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- Performance Settings -->
|
||||
<GroupBox Header="Impostazioni di Elaborazione" Foreground="White" BorderBrush="#444" Margin="0,0,0,18">
|
||||
<StackPanel Margin="12">
|
||||
<TextBlock Text="Dimensione Frame Predefinita:" Foreground="#CCC" Margin="0,0,0,6"/>
|
||||
<ComboBox x:Name="FrameSizeComboBox" Height="32" Background="#333" Foreground="White" BorderBrush="#555">
|
||||
<ComboBoxItem Content="Dimensione Originale" Tag="original" IsSelected="True" Foreground="Black" Background="White"/>
|
||||
<ComboBoxItem Content="320x180 (Veloce)" Tag="320,180" Foreground="Black" Background="White"/>
|
||||
<ComboBoxItem Content="640x360 (Media)" Tag="640,360" Foreground="Black" Background="White"/>
|
||||
<ComboBoxItem Content="1280x720 (Alta)" Tag="1280,720" Foreground="Black" Background="White"/>
|
||||
<ComboBoxItem Content="1920x1080 (Full HD)" Tag="1920,1080" Foreground="Black" Background="White"/>
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Text="Comportamento Sovrascrittura Predefinito:" Foreground="#CCC" Margin="16,16,0,6"/>
|
||||
<ComboBox x:Name="OverwriteModeComboBox" Height="32" Background="#333" Foreground="White" BorderBrush="#555">
|
||||
<ComboBoxItem Content="Chiedi" Tag="Ask" IsSelected="True" Foreground="Black" Background="White"/>
|
||||
<ComboBoxItem Content="Salta" Tag="Skip" Foreground="Black" Background="White"/>
|
||||
<ComboBoxItem Content="Sovrascrivi" Tag="Overwrite" Foreground="Black" Background="White"/>
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Text="Modalita di Estrazione Predefinita:" Foreground="#CCC" Margin="16,16,0,6"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<RadioButton x:Name="DefaultModeFullRadio" Content="Completa" GroupName="DefExtraction" Foreground="White" Margin="0,0,18,0" IsChecked="True"/>
|
||||
<RadioButton x:Name="DefaultModeSingleRadio" Content="Singolo Frame" GroupName="DefExtraction" Foreground="White" Margin="0,0,18,0"/>
|
||||
<RadioButton x:Name="DefaultModeAutoRadio" Content="Auto" GroupName="DefExtraction" Foreground="White"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="Auto: analizza il video e decide cosa estrarre." Foreground="#777" FontSize="10" TextWrapping="Wrap" Margin="0,4,0,0"/>
|
||||
|
||||
<TextBlock Text="Singolo Frame: salvataggio" Foreground="#CCC" Margin="16,16,0,6"/>
|
||||
<CheckBox x:Name="SingleFrameUseSubfolderCheckBox" Content="Crea sottocartella anche per job a singolo frame" Foreground="White" IsChecked="False"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- File Naming Settings -->
|
||||
<GroupBox Header="Naming File" Foreground="White" BorderBrush="#444" Margin="0,0,0,18">
|
||||
<StackPanel Margin="12">
|
||||
<TextBlock Text="Pattern di Naming Predefinito:" Foreground="#CCC" Margin="0,0,0,6"/>
|
||||
<ComboBox x:Name="NamingPatternComboBox" Height="32" Background="#333" Foreground="White" BorderBrush="#555"
|
||||
SelectionChanged="NamingPatternComboBox_SelectionChanged">
|
||||
<ComboBoxItem Content="NomeVideo + Progressivo" Tag="VideoNameProgressive" IsSelected="True"/>
|
||||
<ComboBoxItem Content="Frame + Progressivo" Tag="FrameProgressive"/>
|
||||
<ComboBoxItem Content="NomeVideo + Timestamp" Tag="VideoNameTimestamp"/>
|
||||
<ComboBoxItem Content="Prefisso Custom + Progressivo" Tag="CustomProgressive"/>
|
||||
<ComboBoxItem Content="Solo Timestamp" Tag="TimestampOnly"/>
|
||||
<ComboBoxItem Content="NomeVideo + Frame + Progressivo" Tag="VideoNameFrameProgressive"/>
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Text="Prefisso Custom Predefinito:" Foreground="#CCC" Margin="0,12,0,6"/>
|
||||
<TextBox x:Name="CustomPrefixTextBox" Height="32" Background="#333" Foreground="White" BorderBrush="#555"
|
||||
VerticalContentAlignment="Center" Text="custom" TextChanged="CustomPrefixTextBox_TextChanged"/>
|
||||
|
||||
<TextBlock Text="Anteprima:" Foreground="#CCC" Margin="0,12,0,4" FontSize="11"/>
|
||||
<TextBlock x:Name="NamingPreviewText" Text="Video1_000001.png" Foreground="#4FC3F7" FontSize="11"
|
||||
FontFamily="Consolas" Background="#1A1A1A" Padding="6" Margin="0,0,0,6"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Buttons -->
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,20,0,0">
|
||||
<Button x:Name="SaveButton" Content="Salva" Width="95" Height="40" Margin="0,0,12,0"
|
||||
Click="SaveButton_Click" Background="#4FC3F7" Foreground="White" BorderThickness="0"/>
|
||||
<Button x:Name="CancelButton" Content="Annulla" Width="100" Height="40"
|
||||
Click="CancelButton_Click"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,149 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using Ganimede.Properties;
|
||||
using System.Diagnostics;
|
||||
using WpfMessageBox = System.Windows.MessageBox;
|
||||
|
||||
namespace Ganimede.Windows
|
||||
{
|
||||
public partial class SettingsWindow : Window
|
||||
{
|
||||
public SettingsWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
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)
|
||||
{
|
||||
if (item.Tag?.ToString() == frameSize)
|
||||
{
|
||||
FrameSizeComboBox.SelectedItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var overwriteMode = Settings.Default.DefaultOverwriteMode;
|
||||
foreach (System.Windows.Controls.ComboBoxItem item in OverwriteModeComboBox.Items)
|
||||
{
|
||||
if (item.Tag?.ToString() == overwriteMode)
|
||||
{
|
||||
OverwriteModeComboBox.SelectedItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (Settings.Default.DefaultExtractionMode)
|
||||
{
|
||||
case "SingleFrame":
|
||||
GetDefaultModeRadio("DefaultModeSingleRadio")!.IsChecked = true; break;
|
||||
case "Auto":
|
||||
GetDefaultModeRadio("DefaultModeAutoRadio")!.IsChecked = true; break;
|
||||
default:
|
||||
GetDefaultModeRadio("DefaultModeFullRadio")!.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() => 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 };
|
||||
if (!string.IsNullOrEmpty(DefaultOutputTextBox.Text)) dialog.SelectedPath = DefaultOutputTextBox.Text;
|
||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) DefaultOutputTextBox.Text = dialog.SelectedPath;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
Settings.Default.FFmpegBinFolder = FFmpegPathTextBox.Text;
|
||||
Settings.Default.LastOutputFolder = DefaultOutputTextBox.Text;
|
||||
Settings.Default.CreateSubfolder = CreateSubfolderCheckBox.IsChecked ?? true;
|
||||
var selectedFrameSizeItem = FrameSizeComboBox.SelectedItem as System.Windows.Controls.ComboBoxItem;
|
||||
Settings.Default.FrameSize = selectedFrameSizeItem?.Tag?.ToString() ?? "320,180";
|
||||
var selectedOverwriteItem = OverwriteModeComboBox.SelectedItem as System.Windows.Controls.ComboBoxItem;
|
||||
Settings.Default.DefaultOverwriteMode = selectedOverwriteItem?.Tag?.ToString() ?? "Ask";
|
||||
Settings.Default.DefaultExtractionMode = GetSelectedDefaultExtractionMode();
|
||||
Settings.Default.SingleFrameUseSubfolder = GetSingleFrameUseSubfolder();
|
||||
Settings.Default.Save();
|
||||
Debug.WriteLine("[SETTINGS] Salvate");
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WpfMessageBox.Show($"Errore durante il salvataggio: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
Debug.WriteLine($"[ERROR] Failed to save settings: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DialogResult = false;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user