Add Explorer Workbench with hierarchical, off-UI Storage analysis.
Storage queries run in the background with cancellation and covering indexes so switching views no longer freezes the UI. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
664
src/Explorer.Presentation/ViewModels/MainViewModel.cs
Normal file
664
src/Explorer.Presentation/ViewModels/MainViewModel.cs
Normal file
@@ -0,0 +1,664 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Explorer.Application;
|
||||
using Explorer.Domain;
|
||||
using Explorer.FileOperations;
|
||||
using Explorer.Indexing;
|
||||
using Explorer.Search;
|
||||
using Explorer.Analysis;
|
||||
using Explorer.Domain.Abstractions;
|
||||
using Explorer.Plugin.Abstractions;
|
||||
|
||||
namespace Explorer.Presentation.ViewModels;
|
||||
|
||||
public sealed partial class MainViewModel : ObservableObject
|
||||
{
|
||||
private readonly BrowseService _browse;
|
||||
private readonly FileOperationService _ops;
|
||||
private readonly IndexingCoordinator _indexing;
|
||||
private readonly SourceManager _sources;
|
||||
private readonly PathHistoryStore _pathHistory;
|
||||
private readonly StorageProviderRegistry _providers;
|
||||
private readonly CloudPlaceStore _cloudPlaces;
|
||||
private List<string> _clipboard = [];
|
||||
private bool _clipboardIsCut;
|
||||
|
||||
[ObservableProperty] private ExplorerTabViewModel _activeTab = null!;
|
||||
[ObservableProperty] private string _pathText = "";
|
||||
[ObservableProperty] private string _theme = "Dark";
|
||||
[ObservableProperty] private string _footer = "";
|
||||
[ObservableProperty] private string? _promptUnc;
|
||||
[ObservableProperty] private bool _showCloudPin;
|
||||
[ObservableProperty] private bool _showCloudDehydrate;
|
||||
|
||||
private readonly IOsClipboard Clipboard;
|
||||
private readonly SynchronizationContext? _ui = SynchronizationContext.Current;
|
||||
|
||||
public MainViewModel(
|
||||
BrowseService browse,
|
||||
FileOperationService ops,
|
||||
IndexingCoordinator indexing,
|
||||
SourceManager sources,
|
||||
SearchService search,
|
||||
AnalysisService analysis,
|
||||
IIndexStore store,
|
||||
TransferQueue transfers,
|
||||
IOsClipboard clipboard,
|
||||
PathHistoryStore pathHistory,
|
||||
StorageProviderRegistry providers,
|
||||
CloudPlaceStore cloudPlaces)
|
||||
{
|
||||
_browse = browse;
|
||||
_ops = ops;
|
||||
_indexing = indexing;
|
||||
_sources = sources;
|
||||
_pathHistory = pathHistory;
|
||||
_providers = providers;
|
||||
_cloudPlaces = cloudPlaces;
|
||||
PathHistory = [];
|
||||
Tree = new NavigationTreeViewModel(sources, browse, providers, cloudPlaces);
|
||||
Search = new SearchViewModel(search, sources);
|
||||
Analysis = new AnalysisViewModel(analysis);
|
||||
Duplicates = new DuplicateViewModel(store, sources);
|
||||
Transfers = new TransferQueueViewModel(transfers);
|
||||
Tabs = [];
|
||||
Clipboard = clipboard;
|
||||
transfers.JobFinished += (_, job) =>
|
||||
{
|
||||
void Go() => _ = OnTransferFinishedAsync(job);
|
||||
if (_ui is { } ctx)
|
||||
{
|
||||
ctx.Post(_ => Go(), null);
|
||||
}
|
||||
else
|
||||
{
|
||||
Go();
|
||||
}
|
||||
};
|
||||
_indexing.ProgressChanged += (_, p) =>
|
||||
{
|
||||
var text = p.Status == ScanJobStatus.Done
|
||||
? $"Indexed {p.FilesSeen:N0} files"
|
||||
: $"Indexing… {p.FilesSeen:N0} files · {p.CurrentPath}";
|
||||
if (_ui is { } ctx)
|
||||
{
|
||||
ctx.Post(_ => Footer = text, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
Footer = text;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public ObservableCollection<ExplorerTabViewModel> Tabs { get; }
|
||||
public ObservableCollection<string> PathHistory { get; }
|
||||
public NavigationTreeViewModel Tree { get; }
|
||||
public SearchViewModel Search { get; }
|
||||
public AnalysisViewModel Analysis { get; }
|
||||
public DuplicateViewModel Duplicates { get; }
|
||||
public TransferQueueViewModel Transfers { get; }
|
||||
public ExplorerPaneViewModel ActivePane => ActiveTab.ActivePane;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await _sources.InitializeAsync().ConfigureAwait(true);
|
||||
foreach (var path in _pathHistory.Load())
|
||||
{
|
||||
PathHistory.Add(path);
|
||||
}
|
||||
|
||||
await NewTabAsync().ConfigureAwait(true);
|
||||
await Tree.ReloadAsync(ActivePane.CurrentPath).ConfigureAwait(true);
|
||||
Footer = "Ready";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task NewTabAsync()
|
||||
{
|
||||
var tab = new ExplorerTabViewModel(_browse, _ops, _indexing, _sources);
|
||||
WireTab(tab);
|
||||
Tabs.Add(tab);
|
||||
ActiveTab = tab;
|
||||
await tab.OpenInitialAsync().ConfigureAwait(true);
|
||||
PathText = tab.ActivePane.CurrentPath;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void CloseTab(ExplorerTabViewModel? tab)
|
||||
{
|
||||
tab ??= ActiveTab;
|
||||
if (Tabs.Count <= 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var index = Tabs.IndexOf(tab);
|
||||
Tabs.Remove(tab);
|
||||
ActiveTab = Tabs[Math.Clamp(index - 1, 0, Tabs.Count - 1)];
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void Split() => ActiveTab.ToggleSplit();
|
||||
|
||||
[RelayCommand]
|
||||
public async Task GoBreadcrumbAsync(BreadcrumbSegment? segment)
|
||||
{
|
||||
if (segment is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ActivePane.NavigateAsync(segment.Path).ConfigureAwait(true);
|
||||
PathText = ActivePane.CurrentPath;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void SetView(string? mode)
|
||||
=> ActivePane.ViewMode = mode?.ToLowerInvariant() switch
|
||||
{
|
||||
"list" => FolderViewMode.List,
|
||||
"preview" => FolderViewMode.Preview,
|
||||
_ => FolderViewMode.Details
|
||||
};
|
||||
|
||||
[RelayCommand]
|
||||
public void CancelTransfer(TransferJob? job)
|
||||
{
|
||||
if (job is not null)
|
||||
{
|
||||
Transfers.Cancel(job);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public Task BackAsync() => ActivePane.BackAsync();
|
||||
|
||||
[RelayCommand]
|
||||
public Task ForwardAsync() => ActivePane.ForwardAsync();
|
||||
|
||||
[RelayCommand]
|
||||
public Task UpAsync() => ActivePane.UpAsync();
|
||||
|
||||
[RelayCommand]
|
||||
public Task RefreshAsync() => ActivePane.RefreshAsync();
|
||||
|
||||
[RelayCommand]
|
||||
public async Task GoAsync()
|
||||
{
|
||||
var path = PathText.Trim();
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (PathRules.IsUnc(path))
|
||||
{
|
||||
await _sources.AddUncAsync(path).ConfigureAwait(true);
|
||||
await Tree.ReloadAsync(path).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
await ActivePane.NavigateAsync(path).ConfigureAwait(true);
|
||||
RememberEnteredPath(ActivePane.CurrentPath);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task TreeSelectAsync(NavNodeViewModel? node)
|
||||
{
|
||||
if (node is null || node.IsPlaceholder)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Tree.EnsureChildrenAsync(node).ConfigureAwait(true);
|
||||
if (!NavigationTreeViewModel.PathsEqual(node.Path, ActivePane.CurrentPath))
|
||||
{
|
||||
await ActivePane.NavigateAsync(node.Path).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
PathText = node.Path;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public Task OpenSelectedAsync()
|
||||
{
|
||||
var item = ActivePane.SelectedItems.FirstOrDefault() ?? ActivePane.Items.FirstOrDefault();
|
||||
return item is null ? Task.CompletedTask : ActivePane.OpenItemAsync(item);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void Copy()
|
||||
{
|
||||
_clipboard = SelectedPaths();
|
||||
_clipboardIsCut = false;
|
||||
CopyPathsToClipboard(_clipboard);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void Cut()
|
||||
{
|
||||
_clipboard = SelectedPaths();
|
||||
_clipboardIsCut = true;
|
||||
CopyPathsToClipboard(_clipboard);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task PasteAsync()
|
||||
{
|
||||
if (Clipboard.TryGetFiles(out var osFiles, out var cut) && osFiles.Count > 0)
|
||||
{
|
||||
_clipboard = osFiles.ToList();
|
||||
_clipboardIsCut = cut;
|
||||
}
|
||||
|
||||
if (_clipboard.Count == 0 || ActivePane.CurrentPath == "This PC" || ActivePane.IsOffline)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_clipboardIsCut)
|
||||
{
|
||||
await _ops.MoveAsync(_clipboard, ActivePane.CurrentPath).ConfigureAwait(true);
|
||||
_clipboard = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
await _ops.CopyAsync(_clipboard, ActivePane.CurrentPath).ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public Task DeleteAsync()
|
||||
{
|
||||
var paths = SelectedPaths();
|
||||
return paths.Count == 0 ? Task.CompletedTask : _ops.DeleteAsync(paths);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void CopyPath()
|
||||
{
|
||||
var paths = SelectedPaths();
|
||||
if (paths.Count == 0 && ActivePane.CurrentPath != "This PC")
|
||||
{
|
||||
paths = [ActivePane.CurrentPath];
|
||||
}
|
||||
|
||||
CopyPathsToClipboard(paths);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void NewFolder()
|
||||
{
|
||||
if (ActivePane.CurrentPath == "This PC" || ActivePane.IsOffline)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ops.NewFolder(ActivePane.CurrentPath);
|
||||
EnqueueReconcile(ActivePane.CurrentPath);
|
||||
_ = ActivePane.RefreshAsync();
|
||||
}
|
||||
|
||||
public void RenameSelected(string newName)
|
||||
{
|
||||
var item = ActivePane.SelectedItems.FirstOrDefault();
|
||||
if (item is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ops.Rename(item.FullPath, newName);
|
||||
EnqueueReconcile(ActivePane.CurrentPath);
|
||||
_ = ActivePane.RefreshAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task BuildIndexAsync()
|
||||
{
|
||||
var path = ActivePane.CurrentPath;
|
||||
if (path == "This PC")
|
||||
{
|
||||
path = ActivePane.SelectedItems.FirstOrDefault()?.FullPath ?? "";
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(path) || path == "This PC")
|
||||
{
|
||||
Footer = "Select a drive or folder to index.";
|
||||
return;
|
||||
}
|
||||
|
||||
var source = await _sources.EnsureForPathAsync(path).ConfigureAwait(true);
|
||||
if (source is null)
|
||||
{
|
||||
Footer = "This location could not be indexed.";
|
||||
return;
|
||||
}
|
||||
|
||||
ActivePane.CurrentSource = source;
|
||||
ActivePane.ShowIndexBanner = false;
|
||||
ActivePane.IndexBannerText = null;
|
||||
_indexing.EnqueueFullScan(source.Id);
|
||||
Footer = $"Indexing {source.DisplayName}…";
|
||||
await Tree.ReloadAsync(ActivePane.CurrentPath).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void RescanFolder() => ActivePane.RescanFolder();
|
||||
|
||||
public void RefreshCloudActions()
|
||||
{
|
||||
var path = ActivePane.SelectedItems.FirstOrDefault()?.FullPath ?? ActivePane.CurrentPath;
|
||||
ShowCloudPin = _providers.HasCapability(path, ProviderCapability.Pin);
|
||||
ShowCloudDehydrate = _providers.HasCapability(path, ProviderCapability.Dehydrate);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public Task PinCloudAsync() => InvokeCloudAsync(ProviderAction.Pin);
|
||||
|
||||
[RelayCommand]
|
||||
public Task UnpinCloudAsync() => InvokeCloudAsync(ProviderAction.Unpin);
|
||||
|
||||
[RelayCommand]
|
||||
public Task FreeUpCloudSpaceAsync() => InvokeCloudAsync(ProviderAction.Dehydrate);
|
||||
|
||||
private async Task InvokeCloudAsync(ProviderAction action)
|
||||
{
|
||||
var paths = SelectedPaths();
|
||||
if (paths.Count == 0 && ActivePane.CurrentPath != "This PC")
|
||||
{
|
||||
paths = [ActivePane.CurrentPath];
|
||||
}
|
||||
|
||||
var result = await _providers.InvokeAsync(action, paths).ConfigureAwait(true);
|
||||
Footer = result.Message ?? (result.Status == ProviderActionStatus.Succeeded
|
||||
? "Asked the cloud client to update these items."
|
||||
: "Cloud action was not available.");
|
||||
await ActivePane.RefreshAsync().ConfigureAwait(true);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void CancelIndex()
|
||||
{
|
||||
if (ActivePane.CurrentSource is not null)
|
||||
{
|
||||
_indexing.Cancel(ActivePane.CurrentSource.Id);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task OpenStorageHereAsync()
|
||||
{
|
||||
if (!Analysis.CanAct || Analysis.SelectedNavigatePath is not { } path)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ActivePane.NavigateAsync(path).ConfigureAwait(true);
|
||||
PathText = ActivePane.CurrentPath;
|
||||
Analysis.Close();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task OpenStorageOtherAsync()
|
||||
{
|
||||
if (!ActiveTab.IsSplit || !Analysis.CanAct || Analysis.SelectedNavigatePath is not { } path)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var other = ActiveTab.ActivePane == ActiveTab.Left ? ActiveTab.Right : ActiveTab.Left;
|
||||
ActiveTab.Activate(other);
|
||||
await other.NavigateAsync(path).ConfigureAwait(true);
|
||||
PathText = other.CurrentPath;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task OpenStorageTabAsync()
|
||||
{
|
||||
if (!Analysis.CanAct || Analysis.SelectedNavigatePath is not { } path)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await NewTabAsync().ConfigureAwait(true);
|
||||
await ActivePane.NavigateAsync(path).ConfigureAwait(true);
|
||||
PathText = ActivePane.CurrentPath;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task SearchStorageAsync()
|
||||
{
|
||||
if (!Analysis.CanAct || Analysis.SelectedNavigatePath is not { } path)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ActivePane.NavigateAsync(path).ConfigureAwait(true);
|
||||
PathText = ActivePane.CurrentPath;
|
||||
Search.Scope = SearchScopeKind.CurrentTree;
|
||||
Search.OpenWithoutSearch();
|
||||
Analysis.Close();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void RescanStorage()
|
||||
{
|
||||
if (Analysis.SelectedSourceId is not long id)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Analysis.SelectedIsSource)
|
||||
{
|
||||
_indexing.EnqueueFullScan(id);
|
||||
}
|
||||
else
|
||||
{
|
||||
_indexing.EnqueueFolderScan(id, Analysis.SelectedScanPathRel);
|
||||
}
|
||||
|
||||
Footer = "Queued an index rescan for this location.";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void CopyStoragePath()
|
||||
{
|
||||
if (Analysis.SelectedPath is { } path)
|
||||
{
|
||||
CopyPathsToClipboard([path]);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task AddNetworkAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(PromptUnc))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _sources.AddUncAsync(PromptUnc.Trim()).ConfigureAwait(true);
|
||||
PromptUnc = "";
|
||||
await Tree.ReloadAsync(ActivePane.CurrentPath).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
public async Task AddCloudFolderAsync(string path, string? displayName = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cloudPlaces.Add(OneDriveProviderId, path, displayName);
|
||||
Footer = "Added OneDrive folder to the navigation tree.";
|
||||
await Tree.ReloadAsync(path.Trim().TrimEnd('\\')).ConfigureAwait(true);
|
||||
await ActivePane.NavigateAsync(path).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
private const string OneDriveProviderId = "onedrive";
|
||||
|
||||
[RelayCommand]
|
||||
public Task SearchAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Search.Text) && !Search.IsOpen)
|
||||
{
|
||||
Search.OpenWithoutSearch();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return Search.RunAsync(ActivePane);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void ToggleTheme() => Theme = Theme == "Dark" ? "Light" : "Dark";
|
||||
|
||||
public async Task DropAsync(IReadOnlyList<string> files, string targetDirectory, bool move)
|
||||
{
|
||||
if (files.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (move)
|
||||
{
|
||||
await _ops.MoveAsync(files, targetDirectory).ConfigureAwait(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _ops.CopyAsync(files, targetDirectory).ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void WireTab(ExplorerTabViewModel tab)
|
||||
{
|
||||
tab.Left.PropertyChanged += (_, e) => OnPaneProperty(tab, e.PropertyName);
|
||||
tab.Right.PropertyChanged += (_, e) => OnPaneProperty(tab, e.PropertyName);
|
||||
tab.PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(ExplorerTabViewModel.ActivePane) && tab == ActiveTab)
|
||||
{
|
||||
PathText = tab.ActivePane.CurrentPath;
|
||||
OnPropertyChanged(nameof(ActivePane));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void OnPaneProperty(ExplorerTabViewModel tab, string? propertyName)
|
||||
{
|
||||
if (tab != ActiveTab)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (propertyName == nameof(ExplorerPaneViewModel.CurrentPath))
|
||||
{
|
||||
PathText = tab.ActivePane.CurrentPath;
|
||||
OnPropertyChanged(nameof(ActivePane));
|
||||
RefreshCloudActions();
|
||||
_ = Tree.RevealPathAsync(tab.ActivePane.CurrentPath);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnTransferFinishedAsync(TransferJob job)
|
||||
{
|
||||
var dirs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
void Add(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var part in path.Split('|', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var dir = Directory.Exists(part) ? part : PathRules.Parent(part);
|
||||
if (!string.IsNullOrEmpty(dir))
|
||||
{
|
||||
dirs.Add(dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Add(job.SourcePath);
|
||||
Add(job.DestinationPath);
|
||||
foreach (var extra in job.AdditionalSources)
|
||||
{
|
||||
Add(extra);
|
||||
}
|
||||
|
||||
foreach (var dir in dirs)
|
||||
{
|
||||
EnqueueReconcile(dir);
|
||||
}
|
||||
|
||||
await ActivePane.RefreshAsync().ConfigureAwait(true);
|
||||
if (ActiveTab.IsSplit)
|
||||
{
|
||||
var other = ActivePane == ActiveTab.Left ? ActiveTab.Right : ActiveTab.Left;
|
||||
await other.RefreshAsync().ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnqueueReconcile(string path)
|
||||
{
|
||||
_ = ReconcileAsync(path);
|
||||
}
|
||||
|
||||
private async Task ReconcileAsync(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var source = await _sources.FindByPathAsync(path).ConfigureAwait(true);
|
||||
if (source is not { IsIndexed: true } || source.LastRootPath is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var rel = PathRules.MakeRelative(source.LastRootPath, path);
|
||||
_indexing.EnqueueReconcile(source.Id, rel);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// local errors stay out of the UI
|
||||
}
|
||||
}
|
||||
|
||||
public void RememberEnteredPath(string path)
|
||||
{
|
||||
var next = PathHistoryStore.Remember(PathHistory, path);
|
||||
if (next.Count == PathHistory.Count
|
||||
&& PathHistory.Count > 0
|
||||
&& string.Equals(PathHistory[0], path, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PathHistory.Clear();
|
||||
foreach (var item in next)
|
||||
{
|
||||
PathHistory.Add(item);
|
||||
}
|
||||
|
||||
_pathHistory.Save(PathHistory);
|
||||
}
|
||||
|
||||
private List<string> SelectedPaths()
|
||||
=> ActivePane.SelectedItems.Select(i => i.FullPath).ToList();
|
||||
|
||||
private void CopyPathsToClipboard(IReadOnlyList<string> paths)
|
||||
{
|
||||
if (paths.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Clipboard.SetFiles(paths, _clipboardIsCut);
|
||||
}
|
||||
|
||||
partial void OnActiveTabChanged(ExplorerTabViewModel value)
|
||||
{
|
||||
PathText = value.ActivePane.CurrentPath;
|
||||
OnPropertyChanged(nameof(ActivePane));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user