Files
Futonizer/MainWindow.xaml
T
l4kr c7c125d40b Match and mux in dropped external subtitles
Drag in subtitle files alongside .mkv files and they're matched to
the right video by episode code (S04E06, zero-padding and separator
insensitive, plus 4x06 as a fallback) and muxed in as extra subtitle
tracks. Any subtitle format mkvmerge accepts is recognized: SRT,
(S)SA, VobSub, WebVTT, PGS/SUP, USF, SAMI, TTML/DFXP, STL, Kate.

Matching works regardless of drop order — subtitles dropped before
their video are held pending until a matching video shows up, and
vice versa. Language, forced, and SDH flags are guessed from common
filename tokens (e.g. "eng", "forced", "sdh") to set the muxed
track's language/name/flags; unmatched subtitles are reported in the
status bar.
2026-07-08 10:37:56 +02:00

422 lines
27 KiB
XML

<ui:FluentWindow x:Class="Futonizer.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
Title="Futonizer"
Icon="pack://application:,,,/Assets/app.png"
Height="900" Width="1600" MinHeight="650" MinWidth="1100"
ExtendsContentIntoTitleBar="True"
WindowBackdropType="Mica"
WindowCornerPreference="Round"
WindowStartupLocation="CenterScreen"
AllowDrop="True"
DragOver="Window_DragOver"
Drop="Window_Drop"
PreviewKeyDown="Window_PreviewKeyDown">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
<Style x:Key="QueueListBoxItemStyle" TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Padding" Value="8,5"/>
<Setter Property="Margin" Value="3,1"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border x:Name="ItemBorder"
Background="Transparent"
CornerRadius="6"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="True">
<Grid>
<Border x:Name="AccentBar"
Width="3" HorizontalAlignment="Left"
Margin="-8,-5,0,-5"
CornerRadius="2"
Background="{DynamicResource AccentFillColorDefaultBrush}"
Visibility="Collapsed"/>
<ContentPresenter/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="ItemBorder" Property="Background"
Value="{DynamicResource ControlFillColorSecondaryBrush}"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="ItemBorder" Property="Background"
Value="{DynamicResource AccentFillColorSecondaryBrush}"/>
<Setter TargetName="AccentBar" Property="Visibility" Value="Visible"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ui:TitleBar Grid.Row="0" Title="Futonizer">
<ui:TitleBar.Icon>
<ui:ImageIcon Source="pack://application:,,,/Assets/app.png"/>
</ui:TitleBar.Icon>
</ui:TitleBar>
<Grid Grid.Row="1" Margin="16,4,16,16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- ── Toolbar ───────────────────────────────────────────────── -->
<Grid Grid.Row="0" Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ui:Button Grid.Column="0" Content="Settings" Icon="{ui:SymbolIcon Settings24}"
Appearance="Secondary"
Click="SettingsButton_Click"/>
<TextBlock x:Name="StatusText" Grid.Column="2" VerticalAlignment="Center"
Margin="16,0" FontWeight="SemiBold" FontSize="12"/>
<ui:Button x:Name="RunButton" Grid.Column="3"
Content="Strip Tracks" Appearance="Primary"
Icon="{ui:SymbolIcon Delete24}"
Click="RunButton_Click"/>
</Grid>
<!-- ── Two-pane content ──────────────────────────────────────── -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="280"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="*" MinWidth="300"/>
</Grid.ColumnDefinitions>
<!-- File queue panel -->
<Border Grid.Column="0" CornerRadius="8" ClipToBounds="True"
Background="{DynamicResource ControlFillColorDefaultBrush}"
BorderBrush="{DynamicResource ControlElevationBorderBrush}"
BorderThickness="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Queue header -->
<Grid Grid.Row="0" Margin="10,8,6,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Queue"
FontWeight="SemiBold" FontSize="13"
VerticalAlignment="Center"/>
<Button x:Name="ClearQueueButton" Grid.Column="1"
Content="Clear" Click="ClearQueueButton_Click"
ToolTip="Remove all files from the queue"
Background="Transparent" BorderThickness="0"
Padding="6,2" Cursor="Hand" FontSize="11"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
IsEnabled="False"/>
</Grid>
<Grid Grid.Row="1">
<TextBlock x:Name="DropHint"
HorizontalAlignment="Center" VerticalAlignment="Center"
Text="Drop .mkv files (and subtitles)&#10;onto this window"
Opacity="0.4" FontSize="14" TextAlignment="Center"
IsHitTestVisible="False"/>
<ListBox x:Name="FileQueueList"
Background="Transparent" BorderThickness="0"
HorizontalContentAlignment="Stretch"
SelectionMode="Extended"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
VirtualizingPanel.ScrollUnit="Pixel"
PreviewMouseWheel="SmoothList_PreviewMouseWheel"
PreviewKeyDown="FileQueueList_PreviewKeyDown"
ItemContainerStyle="{StaticResource QueueListBoxItemStyle}"
SelectionChanged="FileQueueList_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid ToolTip="{Binding Status}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="18"/>
</Grid.ColumnDefinitions>
<!-- Status glyphs (only one visible at a time) -->
<TextBlock Grid.Column="0" Text="&#x27F3;" FontSize="12"
VerticalAlignment="Center"
Visibility="{Binding IsLoading, Converter={StaticResource BoolToVis}}"/>
<TextBlock Grid.Column="0" Text="&#x25CF;" FontSize="9"
Opacity="0.5" VerticalAlignment="Center"
Visibility="{Binding IsLoadedNeutral, Converter={StaticResource BoolToVis}}"/>
<TextBlock Grid.Column="0" Text="&#x2713;" FontSize="12"
Foreground="#4CAF50" VerticalAlignment="Center"
Visibility="{Binding IsCorrect, Converter={StaticResource BoolToVis}}"/>
<TextBlock Grid.Column="0" Text="&#x2717;" FontSize="12"
Foreground="#F44336" VerticalAlignment="Center"
Visibility="{Binding IsIncorrect, Converter={StaticResource BoolToVis}}"/>
<TextBlock Grid.Column="1" Text="{Binding DisplayFileName}"
FontSize="12" VerticalAlignment="Center" Margin="6,0"
TextTrimming="CharacterEllipsis"/>
<Border Grid.Column="2" CornerRadius="8" Padding="5,1" Margin="2,0"
VerticalAlignment="Center"
Background="{DynamicResource AccentFillColorSecondaryBrush}"
Visibility="{Binding HasExternalSubtitles, Converter={StaticResource BoolToVis}}"
ToolTip="{Binding ExternalSubtitlesTooltip}">
<TextBlock Text="{Binding ExternalSubtitles.Count, StringFormat={}+{0} sub}"
FontSize="9" FontWeight="SemiBold"/>
</Border>
<TextBlock Grid.Column="3" Text="{Binding FileSizeDisplay}"
FontSize="11" VerticalAlignment="Center" Margin="4,0"
Opacity="0.6"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"/>
<Button Grid.Column="4" Content="&#xD7;"
Tag="{Binding}"
Click="RemoveFile_Click"
Background="Transparent" BorderThickness="0"
Padding="2,0" Cursor="Hand" FontSize="13"
VerticalAlignment="Center"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Grid>
</Border>
<!-- GridSplitter -->
<GridSplitter Grid.Column="1" Width="6"
HorizontalAlignment="Center" Background="Transparent"/>
<!-- Track table panel -->
<Border Grid.Column="2" CornerRadius="8" ClipToBounds="True"
Background="{DynamicResource ControlFillColorDefaultBrush}"
BorderBrush="{DynamicResource ControlElevationBorderBrush}"
BorderThickness="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- External subtitles matched to the selected file -->
<Border x:Name="ExternalSubsPanel" Grid.Row="0" Margin="10,8,10,0"
Padding="8,6" CornerRadius="6" Visibility="Collapsed"
Background="{DynamicResource ControlFillColorSecondaryBrush}">
<StackPanel>
<TextBlock Text="External subtitles to mux in" FontSize="11"
FontWeight="SemiBold" Opacity="0.7" Margin="0,0,0,4"/>
<ItemsControl x:Name="ExternalSubsList">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{DynamicResource ControlFillColorDefaultBrush}"
BorderBrush="{DynamicResource ControlElevationBorderBrush}"
BorderThickness="1" CornerRadius="4"
Margin="0,0,6,6" Padding="6,3">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding LanguageDisplay}" FontSize="11"
FontWeight="SemiBold" VerticalAlignment="Center"/>
<TextBlock Text="{Binding FileName}" FontSize="11" Opacity="0.6"
Margin="6,0,0,0" VerticalAlignment="Center"
MaxWidth="220" TextTrimming="CharacterEllipsis"/>
<TextBlock Text="DEFAULT" FontSize="9" FontWeight="Bold"
Foreground="{DynamicResource AccentTextFillColorPrimaryBrush}"
Margin="6,0,0,0" VerticalAlignment="Center"
Visibility="{Binding IsDefault, Converter={StaticResource BoolToVis}}"/>
<Button Content="&#xD7;" Tag="{Binding}"
Click="RemoveExternalSubtitle_Click"
Background="Transparent" BorderThickness="0"
Padding="6,0,0,0" Cursor="Hand" FontSize="13"
VerticalAlignment="Center"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"/>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
<Grid Grid.Row="1">
<TextBlock x:Name="NoSelectionHint"
HorizontalAlignment="Center" VerticalAlignment="Center"
Text="Select a file to view its tracks"
Opacity="0.4" FontSize="14"
IsHitTestVisible="False"/>
<DataGrid x:Name="TrackGrid"
Visibility="Collapsed"
AutoGenerateColumns="False"
CanUserAddRows="False"
CanUserDeleteRows="False"
CanUserReorderColumns="False"
IsReadOnly="False"
SelectionMode="Single"
HeadersVisibility="Column"
Background="Transparent"
BorderThickness="0"
RowHeight="30"
GridLinesVisibility="None"
VirtualizingPanel.ScrollUnit="Pixel"
PreviewMouseWheel="SmoothList_PreviewMouseWheel"
PreviewKeyDown="TrackGrid_PreviewKeyDown"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto">
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{DynamicResource ControlElevationBorderBrush}"/>
<Setter Property="BorderThickness" Value="0"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsGroupStart}" Value="True">
<Setter Property="BorderThickness" Value="0,1,0,0"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="video">
<Setter Property="Opacity" Value="0.5"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="chapters">
<Setter Property="Opacity" Value="0.5"/>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Type}" Value="audio"/>
<Condition Binding="{Binding Copy}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter Property="Opacity" Value="0.5"/>
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Type}" Value="subtitles"/>
<Condition Binding="{Binding Copy}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter Property="Opacity" Value="0.5"/>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
<DataGrid.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}"/>
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.CellStyle>
<DataGrid.Columns>
<!-- Copy (editable checkbox) -->
<DataGridTemplateColumn Header="Copy" Width="55"
CanUserSort="False" CanUserResize="False">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding Copy, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding IsEditable}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="ID"
Binding="{Binding IdDisplay}" Width="45" IsReadOnly="True"/>
<DataGridTextColumn Header="Language"
Binding="{Binding Language}" Width="100" IsReadOnly="True"/>
<DataGridTextColumn Header="Name"
Binding="{Binding DisplayName}" Width="*" IsReadOnly="True"/>
<DataGridTextColumn Header="Codec"
Binding="{Binding Codec}" Width="185" IsReadOnly="True"/>
<!-- Default (read-only indicator) -->
<DataGridTemplateColumn Header="Default" Width="70"
CanUserSort="False" CanUserResize="False">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding DefaultTrack, Mode=OneWay}"
IsHitTestVisible="False" IsTabStop="False"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- Forced (read-only indicator) -->
<DataGridTemplateColumn Header="Forced" Width="70"
CanUserSort="False" CanUserResize="False">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding ForcedDisplay, Mode=OneWay}"
IsHitTestVisible="False" IsTabStop="False"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Grid>
</Border>
<!-- Processing overlay: dims the entire content area while stripping is running -->
<Border x:Name="ProcessingOverlay"
Grid.ColumnSpan="3"
Background="#99000000"
CornerRadius="8"
Visibility="Collapsed"
IsHitTestVisible="True"
Panel.ZIndex="10"/>
<!-- Loading overlay: blocks interaction until every queued file has finished loading -->
<Border x:Name="LoadingOverlay"
Grid.ColumnSpan="3"
Background="#B3000000"
CornerRadius="8"
Visibility="Collapsed"
IsHitTestVisible="True">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Width="240">
<TextBlock x:Name="LoadingOverlayText" Text="Loading files..."
FontSize="14" FontWeight="Medium" Foreground="White"
HorizontalAlignment="Center" Margin="0,0,0,10"/>
<ProgressBar x:Name="LoadingProgressBar"
Height="4" Minimum="0" Maximum="1" Value="0"/>
</StackPanel>
</Border>
</Grid>
</Grid>
</Grid>
</ui:FluentWindow>