Add patterned rename and Move to, and keep settings, selection, and queue speed from resetting.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-26 18:29:17 +02:00
parent 66ea25993f
commit ff070beba4
62 changed files with 3172 additions and 92 deletions

View File

@@ -68,7 +68,14 @@
<TextBlock Text="Extension" FontWeight="SemiBold" Margin="0,0,0,8"/>
<CheckBox Content="Change extension" IsChecked="{Binding ChangeExtension}" Margin="0,0,0,8"/>
<TextBox Text="{Binding NewExtension, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding ChangeExtension}"/>
IsEnabled="{Binding ChangeExtension}" Margin="0,0,0,16"/>
<TextBlock Text="Name pattern" FontWeight="SemiBold" Margin="0,0,0,8"/>
<ComboBox IsEditable="True" ItemsSource="{Binding PatternChoices}"
Text="{Binding NamePattern, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,8"/>
<Button Content="Save pattern" Height="28" Command="{Binding SavePatternCommand}" Margin="0,0,0,8"/>
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgMuted}" FontSize="12"
Text="Optional. Replaces the current name. Placeholders: {Artist} {Title} {Album} {Track} {Year} {Genre} {CreatedDate} {TakenDate} {ModifiedDate} {Width} {Height} {Name} {Extension} {Parent} {Project} {Counter}. Dates accept {TakenDate:yyyyMMdd}."/>
</StackPanel>
</ScrollViewer>
<ListView Grid.Column="2" ItemsSource="{Binding Rows}"

View File

@@ -34,6 +34,10 @@
<MenuItem Header="Rename" Click="OnCtxRename"/>
<MenuItem Header="Batch rename…" Click="OnBatchRename"
Visibility="{Binding ShowBatchRename, Converter={StaticResource BoolVis}}"/>
<MenuItem Header="Tags…" Click="OnTags"
Visibility="{Binding ShowTags, Converter={StaticResource BoolVis}}"/>
<MenuItem Header="Move to…" Click="OnMoveTo"
Visibility="{Binding ShowMoveTo, Converter={StaticResource BoolVis}}"/>
<MenuItem Header="Run profile" Tag="RunProfileMenu"
IsEnabled="{Binding ShowRunProfile}">
<MenuItem Header="No profiles yet" IsEnabled="False"/>
@@ -149,6 +153,10 @@
</MenuItem>
<MenuItem Header="_File Operations">
<MenuItem Header="_Batch rename…" Click="OnBatchRename"/>
<MenuItem Header="_Tags…" Click="OnTags"
IsEnabled="{Binding ShowTags}"/>
<MenuItem Header="_Move to…" Click="OnMoveTo"
IsEnabled="{Binding ShowMoveTo}"/>
<MenuItem Header="_Undo last rename batch" Command="{Binding UndoRenameBatchCommand}"
IsEnabled="{Binding CanUndoRenameBatch}"/>
<Separator/>

View File

@@ -22,6 +22,10 @@ public partial class MainWindow : Window
private bool _dragPending;
private MouseButton _dragButton;
private FolderItemViewModel? _dragItem;
private ListView? _dragList;
private FolderItemViewModel[] _dragSelection = [];
private bool _dragFromMultiSelect;
private bool _syncingSelection;
private readonly ListMarquee _marquee = new();
private bool _suppressItemContextMenu;
private bool _incomingRightDrag;
@@ -577,12 +581,17 @@ public partial class MainWindow : Window
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is not ListView { IsVisible: true } list)
if (_syncingSelection || sender is not ListView { IsVisible: true } list)
{
return;
}
ActivatePaneFromList(list);
if (_dragFromMultiSelect && list == _dragList && _dragSelection.Length > 1)
{
RestoreListSelection(list, _dragSelection);
}
if (_clickRenameItem is not null && !list.SelectedItems.Contains(_clickRenameItem))
{
CancelClickRename();
@@ -597,6 +606,26 @@ public partial class MainWindow : Window
Vm.RefreshCloudActions();
}
private void RestoreListSelection(ListView list, IReadOnlyList<FolderItemViewModel> items)
{
_syncingSelection = true;
try
{
list.SelectedItems.Clear();
foreach (var item in items)
{
if (list.Items.Contains(item))
{
list.SelectedItems.Add(item);
}
}
}
finally
{
_syncingSelection = false;
}
}
private void OnPaneFocus(object sender, RoutedEventArgs e)
{
if (sender is ListView list)
@@ -620,11 +649,35 @@ public partial class MainWindow : Window
private void OnListMouseDown(object sender, MouseButtonEventArgs e)
{
if (IsInsideInlineRenameBox(e.OriginalSource as DependencyObject))
{
_dragPending = false;
_marquee.Disarm();
CancelClickRename();
return;
}
_suppressItemContextMenu = false;
_dragStart = e.GetPosition(null);
_dragPending = true;
_dragButton = e.ChangedButton;
_dragList = sender as ListView;
_dragItem = HitTestFolderItem(sender as DependencyObject, e.GetPosition((IInputElement)sender));
_dragSelection = _dragList is not null
? _dragList.SelectedItems.OfType<FolderItemViewModel>().ToArray()
: [];
var additive = (Keyboard.Modifiers & (ModifierKeys.Control | ModifierKeys.Shift)) != 0;
_dragFromMultiSelect = !additive
&& _dragItem is not null
&& _dragSelection.Length > 1
&& Array.IndexOf(_dragSelection, _dragItem) >= 0;
if (_dragFromMultiSelect && e.ChangedButton == MouseButton.Left && _dragList is not null)
{
e.Handled = true;
_dragList.Focus();
_dragList.CaptureMouse();
}
if (sender is ListView list
&& _dragItem is null
&& ListMarquee.IsBackground(e.OriginalSource as DependencyObject))
@@ -800,6 +853,13 @@ public partial class MainWindow : Window
return;
}
if (IsInsideInlineRenameBox(e.OriginalSource as DependencyObject)
|| _dragItem is { IsRenaming: true })
{
_dragPending = false;
return;
}
var pos = e.GetPosition(null);
if (Math.Abs(pos.X - _dragStart.X) < SystemParameters.MinimumHorizontalDragDistance
&& Math.Abs(pos.Y - _dragStart.Y) < SystemParameters.MinimumVerticalDragDistance)
@@ -809,18 +869,13 @@ public partial class MainWindow : Window
_dragPending = false;
CancelClickRename();
var selected = Vm.ActivePane.SelectedItems.Select(i => i.FullPath).ToList();
IReadOnlyList<string> paths;
if (_dragItem is not null && (selected.Count == 0
|| selected.TrueForAll(p => !p.Equals(_dragItem.FullPath, StringComparison.OrdinalIgnoreCase))))
if (_dragList is { IsMouseCaptured: true })
{
paths = [_dragItem.FullPath];
}
else
{
paths = selected;
_dragList.ReleaseMouseCapture();
}
var paths = DragPaths();
_dragFromMultiSelect = false;
if (paths.Count == 0)
{
return;
@@ -878,11 +933,42 @@ public partial class MainWindow : Window
}
base.OnPreviewMouseUp(e);
if (e.ChangedButton == _dragButton)
if (e.ChangedButton != _dragButton)
{
_dragPending = false;
_marquee.Disarm();
return;
}
if (_dragFromMultiSelect && _dragPending && _dragList is not null && _dragItem is not null)
{
_dragFromMultiSelect = false;
_dragList.SelectedItems.Clear();
_dragList.SelectedItem = _dragItem;
}
_dragPending = false;
_dragFromMultiSelect = false;
_marquee.Disarm();
if (_dragList is { IsMouseCaptured: true })
{
_dragList.ReleaseMouseCapture();
}
}
private IReadOnlyList<string> DragPaths()
{
if (_dragFromMultiSelect && _dragSelection.Length > 0)
{
return _dragSelection.Select(i => i.FullPath).ToList();
}
var selected = Vm.ActivePane.SelectedItems.Select(i => i.FullPath).ToList();
if (_dragItem is not null && (selected.Count == 0
|| selected.TrueForAll(p => !p.Equals(_dragItem.FullPath, StringComparison.OrdinalIgnoreCase))))
{
return [_dragItem.FullPath];
}
return selected;
}
private bool CompleteMarqueeMouseUp(MouseButtonEventArgs e)
@@ -1293,6 +1379,36 @@ public partial class MainWindow : Window
}
}
private void OnTags(object sender, RoutedEventArgs e)
{
var vm = Vm.CreateTagRenameViewModel();
if (vm is null)
{
return;
}
var dlg = new TagRenameWindow(vm) { Owner = this };
if (dlg.ShowDialog() == true)
{
Vm.Footer = "Tag or rename work queued.";
}
}
private void OnMoveTo(object sender, RoutedEventArgs e)
{
var vm = Vm.CreateMoveToViewModel();
if (vm is null)
{
return;
}
var dlg = new MoveToWindow(vm) { Owner = this };
if (dlg.ShowDialog() == true)
{
Vm.Footer = "Move queued.";
}
}
private async void OnExtractHere(object sender, RoutedEventArgs e)
=> await Vm.ExtractSelectedAsync(null).ConfigureAwait(true);

View File

@@ -0,0 +1,66 @@
<Window x:Class="Explorer.App.MoveToWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Move to"
Icon="pack://application:,,,/Assets/explorer-workbench.ico"
Height="560" Width="820"
MinHeight="420" MinWidth="640"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource Bg}" Foreground="{DynamicResource Fg}">
<DockPanel Margin="16">
<DockPanel DockPanel.Dock="Bottom" Margin="0,12,0,0">
<Button DockPanel.Dock="Right" Content="Cancel" MinWidth="88" Height="32" IsCancel="True" Margin="8,0,0,0"/>
<Button DockPanel.Dock="Right" Content="Queue" MinWidth="88" Height="32" IsDefault="True"
Command="{Binding QueueCommand}" IsEnabled="{Binding CanQueue}"/>
<TextBlock Text="{Binding Status}" VerticalAlignment="Center" Foreground="{DynamicResource FgMuted}" TextWrapping="Wrap"/>
</DockPanel>
<StackPanel DockPanel.Dock="Top" Margin="0,0,0,12">
<TextBlock Text="Destination pattern" FontWeight="SemiBold" Margin="0,0,0,8"/>
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="PatternBox" MinHeight="32" VerticalContentAlignment="Center" Padding="8,4"
Text="{Binding Pattern, UpdateSourceTrigger=PropertyChanged}"/>
<Button Grid.Column="1" Content="Browse…" MinWidth="88" Height="32" Click="OnBrowse" Margin="8,0,0,0"/>
<Button Grid.Column="2" Content="Save" MinWidth="72" Height="32" Command="{Binding SavePatternCommand}" Margin="8,0,0,0"/>
</Grid>
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Recent" VerticalAlignment="Center" Margin="0,0,8,0" Foreground="{DynamicResource FgMuted}"/>
<ComboBox Grid.Column="1" MinHeight="28" ItemsSource="{Binding PatternChoices}"
SelectedItem="{Binding SelectedChoice}" SelectedIndex="-1"/>
</Grid>
<ItemsControl ItemsSource="{Binding Tokens}" Margin="0,0,0,8">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding}" MinWidth="0" Height="26" Padding="8,0" Margin="0,0,8,8"
Click="OnInsertToken" Tag="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgMuted}" FontSize="12"
Text="Type a full UNC or drive path, then click a token to insert it at the caret. Example: \\10.0.0.31\media\movies\%filename_noext% creates a folder named after the file and places the file inside it. %year% / %month% use last-write time. Online-only cloud files are skipped."/>
</StackPanel>
<ListView ItemsSource="{Binding Rows}"
Background="{DynamicResource Panel}" Foreground="{DynamicResource Fg}">
<ListView.View>
<GridView>
<GridViewColumn Header="Action" Width="70" DisplayMemberBinding="{Binding Action}"/>
<GridViewColumn Header="Source" Width="200" DisplayMemberBinding="{Binding Detail}"/>
<GridViewColumn Header="Destination" Width="460" DisplayMemberBinding="{Binding Path}"/>
</GridView>
</ListView.View>
</ListView>
</DockPanel>
</Window>

View File

@@ -0,0 +1,69 @@
using System.Windows;
using System.Windows.Controls;
using Explorer.Presentation.ViewModels;
namespace Explorer.App;
public partial class MoveToWindow : Window
{
public MoveToWindow(MoveToViewModel vm)
{
InitializeComponent();
DataContext = vm;
vm.CloseRequested += (_, _) =>
{
try
{
DialogResult = true;
}
catch (InvalidOperationException)
{
// not shown as a dialog
}
Close();
};
Loaded += (_, _) =>
{
PatternBox.CaretIndex = PatternBox.Text?.Length ?? 0;
PatternBox.Focus();
};
}
private void OnBrowse(object sender, RoutedEventArgs e)
{
var picker = new Microsoft.Win32.OpenFolderDialog
{
Title = "Move to",
Multiselect = false
};
if (picker.ShowDialog(this) != true || string.IsNullOrWhiteSpace(picker.FolderName))
{
return;
}
if (DataContext is MoveToViewModel vm)
{
vm.Pattern = picker.FolderName;
}
PatternBox.CaretIndex = PatternBox.Text?.Length ?? 0;
PatternBox.Focus();
}
private void OnInsertToken(object sender, RoutedEventArgs e)
{
if (sender is not Button { Tag: string token } || DataContext is not MoveToViewModel vm)
{
return;
}
var text = PatternBox.Text ?? vm.Pattern ?? "";
var caret = Math.Clamp(PatternBox.CaretIndex, 0, text.Length);
var slash = caret > 0 && text[caret - 1] != '\\' ? "\\" : "";
var insert = slash + token;
vm.Pattern = text.Insert(caret, insert);
PatternBox.Focus();
PatternBox.CaretIndex = caret + insert.Length;
}
}

View File

@@ -1,7 +1,6 @@
<UserControl x:Class="Explorer.App.Settings.Pages.AdvancedSettingsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
DataContext="{Binding Draft, RelativeSource={RelativeSource AncestorType=Window}}">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel>
<TextBlock Text="Advanced" Style="{StaticResource SettingsHeading}"
AutomationProperties.HeadingLevel="Level1"/>

View File

@@ -10,7 +10,7 @@ public partial class AdvancedSettingsPage : UserControl
private void OnBrowseGit(object sender, RoutedEventArgs e)
{
if (DataContext is SettingsDraft draft
if (SettingsPageContext.DraftOf(this) is { } draft
&& SettingsPathBrowse.TryPick(
Window.GetWindow(this),
"Git executable",

View File

@@ -1,7 +1,6 @@
<UserControl x:Class="Explorer.App.Settings.Pages.AppearanceSettingsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
DataContext="{Binding Draft, RelativeSource={RelativeSource AncestorType=Window}}">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel>
<TextBlock Text="Appearance" Style="{StaticResource SettingsHeading}"
AutomationProperties.HeadingLevel="Level1"/>

View File

@@ -1,7 +1,6 @@
<UserControl x:Class="Explorer.App.Settings.Pages.FileOperationsSettingsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
DataContext="{Binding Draft, RelativeSource={RelativeSource AncestorType=Window}}">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel>
<TextBlock Text="File Operations" Style="{StaticResource SettingsHeading}"
AutomationProperties.HeadingLevel="Level1"/>

View File

@@ -10,7 +10,7 @@ public partial class FileOperationsSettingsPage : UserControl
private void OnBrowseSevenZip(object sender, RoutedEventArgs e)
{
if (DataContext is SettingsDraft draft
if (SettingsPageContext.DraftOf(this) is { } draft
&& SettingsPathBrowse.TryPick(
Window.GetWindow(this),
"7-Zip executable",
@@ -24,7 +24,7 @@ public partial class FileOperationsSettingsPage : UserControl
private void OnBrowseFfmpeg(object sender, RoutedEventArgs e)
{
if (DataContext is SettingsDraft draft
if (SettingsPageContext.DraftOf(this) is { } draft
&& SettingsPathBrowse.TryPick(
Window.GetWindow(this),
"FFmpeg executable",

View File

@@ -1,7 +1,6 @@
<UserControl x:Class="Explorer.App.Settings.Pages.GeneralSettingsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
DataContext="{Binding Draft, RelativeSource={RelativeSource AncestorType=Window}}">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel>
<TextBlock Text="General" Style="{StaticResource SettingsHeading}"
AutomationProperties.HeadingLevel="Level1"/>

View File

@@ -1,7 +1,7 @@
<UserControl x:Class="Explorer.App.Settings.Pages.IndexingSettingsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
DataContext="{Binding Draft, RelativeSource={RelativeSource AncestorType=Window}}">
xmlns:sys="clr-namespace:System;assembly=System.Runtime">
<StackPanel>
<TextBlock Text="Indexing" Style="{StaticResource SettingsHeading}"
AutomationProperties.HeadingLevel="Level1"/>
@@ -31,16 +31,23 @@
Text="The host watches Windows idle time even if this window is closed. Conservative defaults wait 10 minutes and prefer AC power."/>
<TextBlock Text="Idle for" Foreground="{DynamicResource FgMuted}" Margin="0,0,0,8"
IsEnabled="{Binding BackgroundMaintenanceWhenIdle}"/>
<DockPanel LastChildFill="False" Margin="0,0,0,14">
<ComboBox Width="80" HorizontalAlignment="Left"
ItemsSource="{Binding IdleThresholdChoices}"
SelectedItem="{Binding IdleMaintenanceMinutes}"
<StackPanel Orientation="Horizontal" Margin="0,0,0,14">
<ComboBox MinWidth="88" Width="88"
SelectedItem="{Binding IdleMaintenanceMinutes, Mode=TwoWay}"
IsEnabled="{Binding BackgroundMaintenanceWhenIdle}"
AutomationProperties.Name="Idle threshold in minutes"/>
AutomationProperties.Name="Idle threshold in minutes">
<ComboBox.ItemsSource>
<x:Array Type="sys:Int32">
<sys:Int32>5</sys:Int32>
<sys:Int32>10</sys:Int32>
<sys:Int32>30</sys:Int32>
</x:Array>
</ComboBox.ItemsSource>
</ComboBox>
<TextBlock Text="minutes" VerticalAlignment="Center" Margin="10,0,0,0"
Foreground="{DynamicResource FgMuted}"
IsEnabled="{Binding BackgroundMaintenanceWhenIdle}"/>
</DockPanel>
</StackPanel>
<CheckBox Margin="0,0,0,6"
Content="Only run expensive maintenance on AC power"
IsChecked="{Binding IdleMaintenanceAcOnly}"

View File

@@ -1,7 +1,6 @@
<UserControl x:Class="Explorer.App.Settings.Pages.NavigationSettingsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
DataContext="{Binding Draft, RelativeSource={RelativeSource AncestorType=Window}}">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel>
<TextBlock Text="Navigation" Style="{StaticResource SettingsHeading}"
AutomationProperties.HeadingLevel="Level1"/>

View File

@@ -0,0 +1,11 @@
using System.Windows;
namespace Explorer.App.Settings;
internal static class SettingsPageContext
{
public static SettingsDraft? DraftOf(FrameworkElement page)
=> page.DataContext as SettingsDraft
?? (page.DataContext as SettingsShell)?.Draft
?? (Window.GetWindow(page) as SettingsWindow)?.Draft;
}

View File

@@ -63,9 +63,7 @@
BorderThickness="1" Padding="16">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled"
KeyboardNavigation.TabIndex="1">
<ContentControl Content="{Binding SelectedCategory.Page}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Top"/>
<Grid x:Name="PageHost" HorizontalAlignment="Stretch" VerticalAlignment="Top"/>
</ScrollViewer>
</Border>
</Grid>

View File

@@ -12,16 +12,46 @@ public partial class SettingsWindow : Window
private readonly SettingsDraft _draft;
private readonly string _originalTheme;
public SettingsDraft Draft => _draft;
public SettingsWindow(MainViewModel vm)
{
InitializeComponent();
_vm = vm;
var prefs = vm.CurrentPreferences();
_originalTheme = prefs.Theme;
_draft = SettingsDraft.From(prefs);
_draft.PropertyChanged += OnDraftChanged;
DataContext = new SettingsShell(_draft, SettingsCatalog.Create());
Closed += (_, _) => _draft.PropertyChanged -= OnDraftChanged;
var shell = new SettingsShell(_draft, SettingsCatalog.Create());
DataContext = shell;
InitializeComponent();
shell.PropertyChanged += OnShellChanged;
ShowSelectedPage();
Closed += (_, _) =>
{
_draft.PropertyChanged -= OnDraftChanged;
shell.PropertyChanged -= OnShellChanged;
};
}
private void OnShellChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(SettingsShell.SelectedCategory) or null)
{
ShowSelectedPage();
}
}
private void ShowSelectedPage()
{
PageHost.Children.Clear();
if (DataContext is not SettingsShell shell)
{
return;
}
var page = shell.SelectedCategory.Page;
PageHost.Children.Add(page);
page.DataContext = _draft;
}
private void OnDraftChanged(object? sender, PropertyChangedEventArgs e)

View File

@@ -0,0 +1,50 @@
<Window x:Class="Explorer.App.TagRenameWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Tags"
Icon="pack://application:,,,/Assets/explorer-workbench.ico"
Height="640" Width="980"
MinHeight="480" MinWidth="760"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource Bg}" Foreground="{DynamicResource Fg}">
<DockPanel Margin="16">
<DockPanel DockPanel.Dock="Bottom" Margin="0,12,0,0">
<Button DockPanel.Dock="Right" Content="Cancel" MinWidth="88" Height="32" IsCancel="True" Margin="8,0,0,0"/>
<Button DockPanel.Dock="Right" Content="Queue rename" MinWidth="120" Height="32"
Command="{Binding QueueRenameCommand}" IsEnabled="{Binding CanQueueRename}" Margin="8,0,0,0"/>
<Button DockPanel.Dock="Right" Content="Queue tags" MinWidth="110" Height="32" IsDefault="True"
Command="{Binding QueueTagsCommand}" IsEnabled="{Binding CanQueueTags}"/>
<TextBlock Text="{Binding Status}" VerticalAlignment="Center" Foreground="{DynamicResource FgMuted}" TextWrapping="Wrap"/>
</DockPanel>
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,12">
<TextBlock DockPanel.Dock="Top" Text="Filename pattern" FontWeight="SemiBold" Margin="0,0,0,8"/>
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" Margin="8,0,0,0">
<Button Content="Save pattern" MinWidth="110" Height="28" Command="{Binding SavePatternCommand}" Margin="0,0,8,0"/>
<Button Content="Filename → tags" MinWidth="130" Height="28" Command="{Binding ApplyFilenameToTagsCommand}" Margin="0,0,8,0"/>
<Button Content="Tags → filename" MinWidth="130" Height="28" Command="{Binding ApplyTagsToNamesCommand}"/>
</StackPanel>
<ComboBox IsEditable="True" ItemsSource="{Binding PatternChoices}"
Text="{Binding Pattern, UpdateSourceTrigger=PropertyChanged}"/>
</DockPanel>
<TextBlock DockPanel.Dock="Top" Margin="0,0,0,12" TextWrapping="Wrap" Foreground="{DynamicResource FgMuted}" FontSize="12"
Text="Edit tags, then Queue tags to write them into the files (ID3 on audio, EXIF on photos). Tags → filename fills New name from the pattern; Queue rename applies those names. Online-only cloud files are skipped. {Project} is the Git repo or parent folder."/>
<DataGrid ItemsSource="{Binding Rows}" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False"
HeadersVisibility="Column" GridLinesVisibility="Horizontal"
Background="{DynamicResource Panel}" Foreground="{DynamicResource Fg}"
BorderBrush="{DynamicResource Stroke}" RowBackground="{DynamicResource Panel}"
AlternatingRowBackground="{DynamicResource Panel}"
HorizontalGridLinesBrush="{DynamicResource Stroke}">
<DataGrid.Columns>
<DataGridTextColumn Header="File" Binding="{Binding FileName}" Width="180" IsReadOnly="True"/>
<DataGridTextColumn Header="Artist" Binding="{Binding Artist, UpdateSourceTrigger=PropertyChanged}" Width="120"/>
<DataGridTextColumn Header="Title" Binding="{Binding Title, UpdateSourceTrigger=PropertyChanged}" Width="160"/>
<DataGridTextColumn Header="Album" Binding="{Binding Album, UpdateSourceTrigger=PropertyChanged}" Width="140"/>
<DataGridTextColumn Header="Track" Binding="{Binding Track, UpdateSourceTrigger=PropertyChanged}" Width="60"/>
<DataGridTextColumn Header="Year" Binding="{Binding Year, UpdateSourceTrigger=PropertyChanged}" Width="60"/>
<DataGridTextColumn Header="Genre" Binding="{Binding Genre, UpdateSourceTrigger=PropertyChanged}" Width="100"/>
<DataGridTextColumn Header="New name" Binding="{Binding ProposedName}" Width="180" IsReadOnly="True"/>
<DataGridTextColumn Header="Status" Binding="{Binding Status}" Width="120" IsReadOnly="True"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</Window>

View File

@@ -0,0 +1,26 @@
using System.Windows;
using Explorer.Presentation.ViewModels;
namespace Explorer.App;
public partial class TagRenameWindow : Window
{
public TagRenameWindow(TagRenameViewModel vm)
{
InitializeComponent();
DataContext = vm;
vm.CloseRequested += (_, _) =>
{
try
{
DialogResult = true;
}
catch (InvalidOperationException)
{
// not shown as a dialog
}
Close();
};
}
}