Add Git overlay, operation tools, and virtualized preview so large folders stay responsive.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-24 15:04:04 +02:00
parent 9bf451932f
commit a3c54bbb03
127 changed files with 14748 additions and 633 deletions

View File

@@ -15,6 +15,7 @@ namespace Explorer.App;
public partial class MainWindow : Window
{
private DocumentationWindow? _docs;
private Point _dragStart;
private bool _dragPending;
private MouseButton _dragButton;
@@ -46,6 +47,7 @@ public partial class MainWindow : Window
_wiredVm.InlineRenameRequested += OnInlineRenameRequested;
_wiredVm.PropertyChanged += OnViewModelPropertyChanged;
RestoreLayout(_wiredVm);
_ = _wiredVm.RefreshUndoRenameAsync();
}
};
Closing += (_, _) => PersistLayout();
@@ -274,6 +276,37 @@ public partial class MainWindow : Window
await Vm.ForgetSourceAsync(path).ConfigureAwait(true);
}
private async void OnOpenRecycleBin(object sender, RoutedEventArgs e)
=> await Vm.ActivePane.NavigateAsync(LocationRoots.RecycleBin).ConfigureAwait(true);
private async void OnEmptyRecycleBin(object sender, RoutedEventArgs e)
{
if (MessageBox.Show(
this,
"Empty Recycle Bin?\n\nItems will be permanently deleted. This is queued and can be cancelled until it starts.",
"Empty Recycle Bin",
MessageBoxButton.YesNo,
MessageBoxImage.Warning) != MessageBoxResult.Yes)
{
return;
}
await Vm.EmptyRecycleBinAsync().ConfigureAwait(true);
}
private async void OnImportWindowsLocation(object sender, RoutedEventArgs e)
{
var item = Vm.ActivePane.SelectedItems.FirstOrDefault();
if (item is null)
{
return;
}
await Vm.ActivePane.OpenItemAsync(item).ConfigureAwait(true);
await Vm.Tree.ReloadAsync(item.FullPath).ConfigureAwait(true);
Vm.Footer = "Added Windows location. Indexing is optional.";
}
private static IEnumerable<NavNodeViewModel> FlattenTree(NavNodeViewModel node)
{
yield return node;
@@ -584,6 +617,7 @@ public partial class MainWindow : Window
if (sender is FrameworkElement { ContextMenu: { } menu })
{
menu.DataContext = DataContext;
_ = FillRunProfileMenuAsync(menu);
}
}
@@ -1045,7 +1079,6 @@ public partial class MainWindow : Window
private void OnCloseSearch(object sender, RoutedEventArgs e) => Vm.Search.IsOpen = false;
private void OnCloseAnalysis(object sender, RoutedEventArgs e) => Vm.Analysis.Close();
private void OnCloseDuplicates(object sender, RoutedEventArgs e) => Vm.Duplicates.IsOpen = false;
private void OnBuildIndex(object sender, RoutedEventArgs e) => Vm.BuildIndexCommand.Execute(null);
@@ -1084,6 +1117,12 @@ public partial class MainWindow : Window
private void OnCtxRename(object sender, RoutedEventArgs e)
{
if (Vm.ActivePane.SelectedItems.Count >= 2)
{
OnBatchRename(sender, e);
return;
}
var item = Vm.ActivePane.SelectedItems.FirstOrDefault();
if (item is not null)
{
@@ -1091,6 +1130,143 @@ public partial class MainWindow : Window
}
}
private async void OnBatchRename(object sender, RoutedEventArgs e)
{
var vm = Vm.CreateBatchRenameViewModel();
if (vm is null)
{
return;
}
var dlg = new BatchRenameWindow(vm) { Owner = this };
if (dlg.ShowDialog() == true)
{
Vm.Footer = "Rename queued.";
await Vm.RefreshUndoRenameAsync().ConfigureAwait(true);
}
}
private async void OnExtractHere(object sender, RoutedEventArgs e)
=> await Vm.ExtractSelectedAsync(null).ConfigureAwait(true);
private async void OnExtractTo(object sender, RoutedEventArgs e)
{
var picker = new Microsoft.Win32.OpenFolderDialog
{
Title = "Extract to",
Multiselect = false
};
if (picker.ShowDialog(this) != true || string.IsNullOrWhiteSpace(picker.FolderName))
{
return;
}
await Vm.ExtractSelectedAsync(picker.FolderName).ConfigureAwait(true);
}
private async void OnCompressZip(object sender, RoutedEventArgs e)
=> await Vm.CompressSelectedAsync(ArchiveFormat.Zip).ConfigureAwait(true);
private async void OnCompressSevenZip(object sender, RoutedEventArgs e)
=> await Vm.CompressSelectedAsync(ArchiveFormat.SevenZip).ConfigureAwait(true);
private async void OnAddToArchive(object sender, RoutedEventArgs e)
{
var dlg = new Microsoft.Win32.OpenFileDialog
{
Title = "Add to archive",
Filter = "Archives|*.zip;*.7z;*.zipx;*.cbz;*.cb7|All files|*.*",
CheckFileExists = true
};
if (dlg.ShowDialog(this) != true || string.IsNullOrWhiteSpace(dlg.FileName))
{
return;
}
await Vm.AddSelectedToArchiveAsync(dlg.FileName).ConfigureAwait(true);
}
private async void OnVerifyArchive(object sender, RoutedEventArgs e)
=> await Vm.VerifySelectedAsync().ConfigureAwait(true);
private async void OnFolderSync(object sender, RoutedEventArgs e)
{
var vm = Vm.CreateFolderSyncViewModel();
await vm.LoadAsync().ConfigureAwait(true);
var dlg = new FolderSyncWindow(vm) { Owner = this };
dlg.ShowDialog();
}
private async void OnOperationProfiles(object sender, RoutedEventArgs e)
{
var vm = Vm.CreateOperationProfilesViewModel();
await vm.LoadAsync().ConfigureAwait(true);
var dlg = new OperationProfilesWindow(vm) { Owner = this };
dlg.ShowDialog();
}
private void OnOrganizeFolder(object sender, RoutedEventArgs e)
{
var vm = Vm.CreateReorganizeViewModel();
var dlg = new ReorganizeWindow(vm) { Owner = this };
dlg.ShowDialog();
}
private async Task FillRunProfileMenuAsync(ContextMenu menu)
{
var host = menu.Items.OfType<MenuItem>().FirstOrDefault(i => Equals(i.Tag, "RunProfileMenu"));
if (host is null)
{
return;
}
host.Items.Clear();
IReadOnlyList<OperationProfile> profiles;
try
{
profiles = await Vm.ListOperationProfilesAsync().ConfigureAwait(true);
}
catch (Exception)
{
host.Items.Add(new MenuItem { Header = "Could not load profiles", IsEnabled = false });
return;
}
if (profiles.Count == 0)
{
host.Items.Add(new MenuItem { Header = "No profiles yet", IsEnabled = false });
return;
}
foreach (var profile in profiles)
{
var item = new MenuItem { Header = profile.Name, Tag = profile.Id };
item.Click += OnRunProfile;
host.Items.Add(item);
}
}
private async void OnRunProfile(object sender, RoutedEventArgs e)
{
if (sender is not MenuItem { Tag: long id })
{
return;
}
var sources = Vm.SelectedRealPaths();
if (sources.Count == 0)
{
Vm.Footer = "Select files or folders to run a profile.";
return;
}
var vm = Vm.CreateOperationProfilesViewModel();
await vm.LoadAsync().ConfigureAwait(true);
await vm.RunOnAsync(id, sources).ConfigureAwait(true);
var dlg = new OperationProfilesWindow(vm) { Owner = this };
dlg.ShowDialog();
}
private void BeginInlineRenameForPath(string path)
{
var item = Vm.ActivePane.Items.FirstOrDefault(i => NavigationTreeViewModel.PathsEqual(i.FullPath, path));
@@ -1299,6 +1475,24 @@ public partial class MainWindow : Window
await Vm.RefreshForgetActionAsync().ConfigureAwait(true);
}
private void OnDocumentation(object sender, RoutedEventArgs e) => ShowDocumentation();
private void OnAbout(object sender, RoutedEventArgs e)
=> new AboutWindow { Owner = this }.ShowDialog();
private void ShowDocumentation()
{
if (_docs is { IsVisible: true })
{
_docs.Activate();
return;
}
_docs = new DocumentationWindow { Owner = this };
_docs.Closed += (_, _) => _docs = null;
_docs.Show();
}
private void OnAddNetwork(object sender, RoutedEventArgs e)
{
var path = PromptWindow.Ask(this, "Add network location", "Network path (\\\\server\\share):", @"\\");
@@ -1350,6 +1544,13 @@ public partial class MainWindow : Window
return;
}
if (e.Key == Key.F1)
{
ShowDocumentation();
e.Handled = true;
return;
}
if (e.Key == Key.F5)
{
await Vm.RefreshAsync().ConfigureAwait(true);