Add settings, cloud places, and optional archive-content indexing.
Keep official clients in charge of sync while Explorer can group locations, persist UI prefs, and list zip/rar/7z members without extracting them. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -48,7 +48,7 @@ public sealed partial class ExplorerPaneViewModel : ObservableObject
|
||||
public IReadOnlyList<BreadcrumbSegment> Breadcrumb => BuildBreadcrumb(CurrentPath);
|
||||
public bool CanGoBack => _history.CanGoBack;
|
||||
public bool CanGoForward => _history.CanGoForward;
|
||||
public bool CanGoUp => CurrentPath is not "This PC";
|
||||
public bool CanGoUp => !LocationRoots.IsVirtual(CurrentPath) || CurrentPath is LocationRoots.Network or LocationRoots.Cloud;
|
||||
|
||||
public async Task NavigateAsync(string path, bool addHistory = true)
|
||||
{
|
||||
@@ -69,9 +69,9 @@ public sealed partial class ExplorerPaneViewModel : ObservableObject
|
||||
OnPropertyChanged(nameof(CanGoUp));
|
||||
OnPropertyChanged(nameof(Breadcrumb));
|
||||
|
||||
if (path == "This PC")
|
||||
if (LocationRoots.IsVirtual(path))
|
||||
{
|
||||
await LoadThisPcAsync(ct).ConfigureAwait(true);
|
||||
await LoadVirtualRootAsync(path, ct).ConfigureAwait(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -108,7 +108,8 @@ public sealed partial class ExplorerPaneViewModel : ObservableObject
|
||||
Items.Add(new FolderItemViewModel(item, sizeFromIndex && item.IsDirectory));
|
||||
}
|
||||
|
||||
if (CurrentSource is { IsIndexed: true, Status: SourceStatus.Online } src)
|
||||
if (CurrentSource is { IsIndexed: true, Status: SourceStatus.Online } src
|
||||
&& Directory.Exists(path))
|
||||
{
|
||||
var rel = PathRules.MakeRelative(src.LastRootPath ?? path, path);
|
||||
_indexing.EnqueueReconcile(src.Id, rel);
|
||||
@@ -124,13 +125,18 @@ public sealed partial class ExplorerPaneViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadThisPcAsync(CancellationToken cancellationToken)
|
||||
private async Task LoadVirtualRootAsync(string path, CancellationToken cancellationToken)
|
||||
{
|
||||
IsOffline = false;
|
||||
ShowIndexBanner = false;
|
||||
CurrentSource = null;
|
||||
Items.Clear();
|
||||
var listing = await _browse.ListThisPcAsync(cancellationToken).ConfigureAwait(true);
|
||||
var listing = path switch
|
||||
{
|
||||
LocationRoots.Network => await _browse.ListNetworkAsync(cancellationToken).ConfigureAwait(true),
|
||||
LocationRoots.Cloud => await _browse.ListCloudAsync(cancellationToken).ConfigureAwait(true),
|
||||
_ => await _browse.ListThisPcAsync(cancellationToken).ConfigureAwait(true)
|
||||
};
|
||||
foreach (var item in listing.Items)
|
||||
{
|
||||
Items.Add(new FolderItemViewModel(item, sizeFromIndex: item.SizeBytes > 0));
|
||||
@@ -158,12 +164,13 @@ public sealed partial class ExplorerPaneViewModel : ObservableObject
|
||||
[RelayCommand]
|
||||
public Task UpAsync()
|
||||
{
|
||||
if (CurrentPath == "This PC")
|
||||
if (CurrentPath == LocationRoots.ThisPc)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (PathRules.IsDriveRoot(CurrentPath)
|
||||
if (LocationRoots.IsVirtual(CurrentPath)
|
||||
|| PathRules.IsDriveRoot(CurrentPath)
|
||||
|| (PathRules.IsUnc(CurrentPath)
|
||||
&& CurrentPath.Equals(PathRules.CanonicalUncRoot(CurrentPath), StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
@@ -178,7 +185,7 @@ public sealed partial class ExplorerPaneViewModel : ObservableObject
|
||||
|
||||
public Task OpenItemAsync(FolderItemViewModel item)
|
||||
{
|
||||
if (item.IsDirectory)
|
||||
if (item.IsDirectory || _browse.CanBrowseArchive(item.Item.Name))
|
||||
{
|
||||
return NavigateAsync(item.FullPath);
|
||||
}
|
||||
@@ -202,7 +209,7 @@ public sealed partial class ExplorerPaneViewModel : ObservableObject
|
||||
|
||||
public void RescanFolder()
|
||||
{
|
||||
if (CurrentSource is null || CurrentPath == "This PC")
|
||||
if (CurrentSource is null || LocationRoots.IsVirtual(CurrentPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -216,12 +223,21 @@ public sealed partial class ExplorerPaneViewModel : ObservableObject
|
||||
|
||||
private static IReadOnlyList<BreadcrumbSegment> BuildBreadcrumb(string path)
|
||||
{
|
||||
if (path == "This PC")
|
||||
if (path == LocationRoots.ThisPc)
|
||||
{
|
||||
return [new BreadcrumbSegment("This PC", "This PC", IsLast: true)];
|
||||
return [new BreadcrumbSegment(LocationRoots.ThisPc, LocationRoots.ThisPc, IsLast: true)];
|
||||
}
|
||||
|
||||
var parts = new List<BreadcrumbSegment> { new("This PC", "This PC") };
|
||||
if (path is LocationRoots.Network or LocationRoots.Cloud)
|
||||
{
|
||||
return
|
||||
[
|
||||
new BreadcrumbSegment(LocationRoots.ThisPc, LocationRoots.ThisPc),
|
||||
new BreadcrumbSegment(path, path, IsLast: true)
|
||||
];
|
||||
}
|
||||
|
||||
var parts = new List<BreadcrumbSegment> { new(LocationRoots.ThisPc, LocationRoots.ThisPc) };
|
||||
var p = PathRules.FromExtended(path);
|
||||
if (PathRules.IsUnc(p))
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Explorer.Application;
|
||||
using Explorer.Domain;
|
||||
using Explorer.FileOperations;
|
||||
using Explorer.Indexing;
|
||||
|
||||
@@ -33,7 +34,9 @@ public sealed partial class ExplorerTabViewModel : ObservableObject
|
||||
{
|
||||
if (e.PropertyName == nameof(ExplorerPaneViewModel.CurrentPath))
|
||||
{
|
||||
Title = Left.CurrentPath == "This PC" ? "This PC" : Path.GetFileName(Left.CurrentPath.TrimEnd('\\'));
|
||||
Title = LocationRoots.IsVirtual(Left.CurrentPath)
|
||||
? Left.CurrentPath
|
||||
: Path.GetFileName(Left.CurrentPath.TrimEnd('\\'));
|
||||
if (string.IsNullOrEmpty(Title))
|
||||
{
|
||||
Title = Left.CurrentPath;
|
||||
|
||||
@@ -21,6 +21,7 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
private readonly PathHistoryStore _pathHistory;
|
||||
private readonly StorageProviderRegistry _providers;
|
||||
private readonly CloudPlaceStore _cloudPlaces;
|
||||
private readonly UiPreferencesStore _preferences;
|
||||
private List<string> _clipboard = [];
|
||||
private bool _clipboardIsCut;
|
||||
|
||||
@@ -31,6 +32,7 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
[ObservableProperty] private string? _promptUnc;
|
||||
[ObservableProperty] private bool _showCloudPin;
|
||||
[ObservableProperty] private bool _showCloudDehydrate;
|
||||
[ObservableProperty] private bool _showForgetSource;
|
||||
|
||||
private readonly IOsClipboard Clipboard;
|
||||
private readonly SynchronizationContext? _ui = SynchronizationContext.Current;
|
||||
@@ -47,7 +49,8 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
IOsClipboard clipboard,
|
||||
PathHistoryStore pathHistory,
|
||||
StorageProviderRegistry providers,
|
||||
CloudPlaceStore cloudPlaces)
|
||||
CloudPlaceStore cloudPlaces,
|
||||
UiPreferencesStore preferences)
|
||||
{
|
||||
_browse = browse;
|
||||
_ops = ops;
|
||||
@@ -56,8 +59,11 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
_pathHistory = pathHistory;
|
||||
_providers = providers;
|
||||
_cloudPlaces = cloudPlaces;
|
||||
_preferences = preferences;
|
||||
var prefs = preferences.Load();
|
||||
Theme = prefs.Theme;
|
||||
PathHistory = [];
|
||||
Tree = new NavigationTreeViewModel(sources, browse, providers, cloudPlaces);
|
||||
Tree = new NavigationTreeViewModel(sources, browse, providers, cloudPlaces, preferences);
|
||||
Search = new SearchViewModel(search, sources);
|
||||
Analysis = new AnalysisViewModel(analysis);
|
||||
Duplicates = new DuplicateViewModel(store, sources);
|
||||
@@ -252,7 +258,7 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
_clipboardIsCut = cut;
|
||||
}
|
||||
|
||||
if (_clipboard.Count == 0 || ActivePane.CurrentPath == "This PC" || ActivePane.IsOffline)
|
||||
if (_clipboard.Count == 0 || LocationRoots.IsVirtual(ActivePane.CurrentPath) || ActivePane.IsOffline)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -279,7 +285,7 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
public void CopyPath()
|
||||
{
|
||||
var paths = SelectedPaths();
|
||||
if (paths.Count == 0 && ActivePane.CurrentPath != "This PC")
|
||||
if (paths.Count == 0 && !LocationRoots.IsVirtual(ActivePane.CurrentPath))
|
||||
{
|
||||
paths = [ActivePane.CurrentPath];
|
||||
}
|
||||
@@ -290,7 +296,7 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
[RelayCommand]
|
||||
public void NewFolder()
|
||||
{
|
||||
if (ActivePane.CurrentPath == "This PC" || ActivePane.IsOffline)
|
||||
if (LocationRoots.IsVirtual(ActivePane.CurrentPath) || ActivePane.IsOffline)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -317,12 +323,12 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
public async Task BuildIndexAsync()
|
||||
{
|
||||
var path = ActivePane.CurrentPath;
|
||||
if (path == "This PC")
|
||||
if (LocationRoots.IsVirtual(path))
|
||||
{
|
||||
path = ActivePane.SelectedItems.FirstOrDefault()?.FullPath ?? "";
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(path) || path == "This PC")
|
||||
if (string.IsNullOrWhiteSpace(path) || LocationRoots.IsVirtual(path))
|
||||
{
|
||||
Footer = "Select a drive or folder to index.";
|
||||
return;
|
||||
@@ -351,6 +357,59 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
var path = ActivePane.SelectedItems.FirstOrDefault()?.FullPath ?? ActivePane.CurrentPath;
|
||||
ShowCloudPin = _providers.HasCapability(path, ProviderCapability.Pin);
|
||||
ShowCloudDehydrate = _providers.HasCapability(path, ProviderCapability.Dehydrate);
|
||||
_ = RefreshForgetActionAsync();
|
||||
}
|
||||
|
||||
public async Task RefreshForgetActionAsync()
|
||||
{
|
||||
ShowForgetSource = ActivePane.CurrentPath is LocationRoots.ThisPc or LocationRoots.Network
|
||||
&& ActivePane.SelectedItems.Count == 1
|
||||
&& await _sources.CanForgetPathAsync(ActivePane.SelectedItems[0].FullPath).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
public async Task<bool> ForgetSourceAsync(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var name = Path.GetFileName(path.TrimEnd('\\'));
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
name = path;
|
||||
}
|
||||
|
||||
var removed = await _sources.ForgetDisconnectedAsync(path).ConfigureAwait(true);
|
||||
if (!removed)
|
||||
{
|
||||
Footer = "This location is still connected in Windows, so Explorer keeps it.";
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var tab in Tabs.ToList())
|
||||
{
|
||||
foreach (var pane in new[] { tab.Left, tab.Right })
|
||||
{
|
||||
if (pane.CurrentPath != "This PC"
|
||||
&& (NavigationTreeViewModel.PathsEqual(pane.CurrentPath, path)
|
||||
|| CloudPath.IsUnder(path, pane.CurrentPath)))
|
||||
{
|
||||
await pane.NavigateAsync("This PC").ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Tree.ReloadAsync("This PC").ConfigureAwait(true);
|
||||
if (ActivePane.CurrentPath == "This PC")
|
||||
{
|
||||
await ActivePane.RefreshAsync().ConfigureAwait(true);
|
||||
}
|
||||
|
||||
PathText = ActivePane.CurrentPath;
|
||||
Footer = $"Removed {name} and its index data.";
|
||||
ShowForgetSource = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -365,7 +424,7 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
private async Task InvokeCloudAsync(ProviderAction action)
|
||||
{
|
||||
var paths = SelectedPaths();
|
||||
if (paths.Count == 0 && ActivePane.CurrentPath != "This PC")
|
||||
if (paths.Count == 0 && !LocationRoots.IsVirtual(ActivePane.CurrentPath))
|
||||
{
|
||||
paths = [ActivePane.CurrentPath];
|
||||
}
|
||||
@@ -483,20 +542,51 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
await Tree.ReloadAsync(ActivePane.CurrentPath).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
public async Task AddCloudFolderAsync(string path, string? displayName = null)
|
||||
public async Task AddCloudFolderAsync(string path, string? providerId = null, 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);
|
||||
var trimmed = path.Trim().TrimEnd('\\');
|
||||
var id = providerId
|
||||
?? _providers.Find(trimmed)?.Manifest.Id
|
||||
?? GuessCloudProvider(trimmed);
|
||||
var name = string.IsNullOrWhiteSpace(displayName) ? CloudProviderLabel(id) : displayName;
|
||||
_cloudPlaces.Add(id, trimmed, name);
|
||||
Footer = $"Added {name} to the navigation tree.";
|
||||
await Tree.ReloadAsync(trimmed).ConfigureAwait(true);
|
||||
await ActivePane.NavigateAsync(trimmed).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
private const string OneDriveProviderId = "onedrive";
|
||||
public const string OneDriveProviderId = "onedrive";
|
||||
public const string GoogleDriveProviderId = "googledrive";
|
||||
public const string NextcloudProviderId = "nextcloud";
|
||||
|
||||
private static string GuessCloudProvider(string path)
|
||||
{
|
||||
if (path.Contains("Google Drive", StringComparison.OrdinalIgnoreCase)
|
||||
|| path.Contains(@"\My Drive", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return GoogleDriveProviderId;
|
||||
}
|
||||
|
||||
if (path.Contains("Nextcloud", StringComparison.OrdinalIgnoreCase)
|
||||
|| path.Contains("ownCloud", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return NextcloudProviderId;
|
||||
}
|
||||
|
||||
return OneDriveProviderId;
|
||||
}
|
||||
|
||||
private static string CloudProviderLabel(string providerId) => providerId switch
|
||||
{
|
||||
GoogleDriveProviderId => "Google Drive",
|
||||
NextcloudProviderId => "Nextcloud",
|
||||
_ => "OneDrive"
|
||||
};
|
||||
|
||||
[RelayCommand]
|
||||
public Task SearchAsync()
|
||||
@@ -513,6 +603,41 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
[RelayCommand]
|
||||
public void ToggleTheme() => Theme = Theme == "Dark" ? "Light" : "Dark";
|
||||
|
||||
public UiPreferences CurrentPreferences()
|
||||
{
|
||||
var stored = _preferences.Load();
|
||||
return stored with { Theme = UiPreferencesStore.NormalizeTheme(Theme) };
|
||||
}
|
||||
|
||||
public async Task ApplyPreferencesAsync(UiPreferences preferences)
|
||||
{
|
||||
var normalized = preferences with { Theme = UiPreferencesStore.NormalizeTheme(preferences.Theme) };
|
||||
_preferences.Save(normalized);
|
||||
Theme = normalized.Theme;
|
||||
await Tree.ReloadAsync(ActivePane.CurrentPath).ConfigureAwait(true);
|
||||
foreach (var tab in Tabs)
|
||||
{
|
||||
foreach (var pane in new[] { tab.Left, tab.Right })
|
||||
{
|
||||
if (LocationRoots.IsVirtual(pane.CurrentPath))
|
||||
{
|
||||
await pane.RefreshAsync().ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Footer = "Settings saved.";
|
||||
}
|
||||
|
||||
partial void OnThemeChanged(string value)
|
||||
{
|
||||
var stored = _preferences.Load();
|
||||
if (!stored.Theme.Equals(value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_preferences.Save(stored with { Theme = UiPreferencesStore.NormalizeTheme(value) });
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DropAsync(IReadOnlyList<string> files, string targetDirectory, bool move)
|
||||
{
|
||||
if (files.Count == 0)
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Explorer.Application;
|
||||
using Explorer.Domain;
|
||||
using Explorer.Plugin.Abstractions;
|
||||
|
||||
namespace Explorer.Presentation.ViewModels;
|
||||
|
||||
@@ -18,6 +19,9 @@ public sealed partial class NavNodeViewModel : ObservableObject
|
||||
public ObservableCollection<NavNodeViewModel> Children { get; } = [];
|
||||
public string Glyph { get; init; } = "\uE8B7";
|
||||
public bool IsPlaceholder { get; init; }
|
||||
public bool IsGroup { get; init; }
|
||||
public long? SourceId { get; init; }
|
||||
public bool CanRemove { get; init; }
|
||||
}
|
||||
|
||||
public sealed class NavigationTreeViewModel
|
||||
@@ -26,17 +30,20 @@ public sealed class NavigationTreeViewModel
|
||||
private readonly BrowseService _browse;
|
||||
private readonly StorageProviderRegistry _providers;
|
||||
private readonly CloudPlaceStore _cloudPlaces;
|
||||
private readonly UiPreferencesStore _preferences;
|
||||
|
||||
public NavigationTreeViewModel(
|
||||
SourceManager sources,
|
||||
BrowseService browse,
|
||||
StorageProviderRegistry providers,
|
||||
CloudPlaceStore cloudPlaces)
|
||||
CloudPlaceStore cloudPlaces,
|
||||
UiPreferencesStore preferences)
|
||||
{
|
||||
_sources = sources;
|
||||
_browse = browse;
|
||||
_providers = providers;
|
||||
_cloudPlaces = cloudPlaces;
|
||||
_preferences = preferences;
|
||||
Roots = [];
|
||||
}
|
||||
|
||||
@@ -55,47 +62,80 @@ public sealed class NavigationTreeViewModel
|
||||
try
|
||||
{
|
||||
Roots.Clear();
|
||||
var thisPc = new NavNodeViewModel { Label = "This PC", Path = "This PC", Glyph = "\uE977", IsExpanded = true, ChildrenLoaded = true };
|
||||
var prefs = _preferences.Load();
|
||||
var thisPc = new NavNodeViewModel
|
||||
{
|
||||
Label = LocationRoots.ThisPc,
|
||||
Path = LocationRoots.ThisPc,
|
||||
Glyph = "\uE977",
|
||||
IsExpanded = true,
|
||||
ChildrenLoaded = true,
|
||||
IsGroup = true
|
||||
};
|
||||
Roots.Add(thisPc);
|
||||
|
||||
var sources = await _sources.RefreshOnlineStateAsync(cancellationToken).ConfigureAwait(true);
|
||||
foreach (var source in sources)
|
||||
foreach (var source in sources.Where(s => !s.Kind.IsNetwork()))
|
||||
{
|
||||
var node = new NavNodeViewModel
|
||||
{
|
||||
Label = source.DisplayName,
|
||||
Path = source.LastRootPath ?? source.DisplayName,
|
||||
Status = source.Status switch
|
||||
{
|
||||
SourceStatus.Offline => source.LastSeenUtc is null
|
||||
? "Offline"
|
||||
: $"Offline · Last seen {source.LastSeenUtc.Value.ToLocalTime():d}",
|
||||
SourceStatus.Scanning => "Indexing",
|
||||
SourceStatus.Stale => "May be out of date",
|
||||
SourceStatus.Error => "Error",
|
||||
_ => source.IsIndexed ? "Indexed" : ""
|
||||
},
|
||||
IsOffline = source.Status == SourceStatus.Offline,
|
||||
Glyph = source.Kind == SourceKind.Removable ? "\uE88E" : source.Kind == SourceKind.Smb ? "\uE968" : "\uEDA2"
|
||||
};
|
||||
AddPlaceholder(node);
|
||||
thisPc.Children.Add(node);
|
||||
thisPc.Children.Add(CreateSourceNode(source, _sources.CanForget(source)));
|
||||
}
|
||||
|
||||
foreach (var place in CloudPlaceStore.Merge(_providers.GetPlaces(), _cloudPlaces.Load())
|
||||
.OrderBy(p => p.DisplayName, StringComparer.CurrentCultureIgnoreCase))
|
||||
var network = sources.Where(s => s.Kind.IsNetwork()).ToList();
|
||||
if (prefs.GroupNetworkPlaces && network.Count > 0)
|
||||
{
|
||||
var exists = Directory.Exists(place.Path);
|
||||
var node = new NavNodeViewModel
|
||||
var group = new NavNodeViewModel
|
||||
{
|
||||
Label = place.DisplayName,
|
||||
Path = place.Path,
|
||||
Glyph = "\uE753",
|
||||
Status = exists ? "" : "Offline",
|
||||
IsOffline = !exists
|
||||
Label = LocationRoots.Network,
|
||||
Path = LocationRoots.Network,
|
||||
Glyph = "\uE968",
|
||||
IsExpanded = true,
|
||||
ChildrenLoaded = true,
|
||||
IsGroup = true
|
||||
};
|
||||
AddPlaceholder(node);
|
||||
Roots.Add(node);
|
||||
foreach (var source in network)
|
||||
{
|
||||
group.Children.Add(CreateSourceNode(source, _sources.CanForget(source)));
|
||||
}
|
||||
|
||||
Roots.Add(group);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var source in network)
|
||||
{
|
||||
Roots.Add(CreateSourceNode(source, _sources.CanForget(source)));
|
||||
}
|
||||
}
|
||||
|
||||
var places = CloudPlaceStore.Merge(_providers.GetPlaces(), _cloudPlaces.Load())
|
||||
.OrderBy(p => ProviderOrder(p.ProviderId))
|
||||
.ThenBy(p => p.DisplayName, StringComparer.CurrentCultureIgnoreCase)
|
||||
.Select(CreateCloudNode)
|
||||
.ToList();
|
||||
if (prefs.GroupCloudPlaces && places.Count > 0)
|
||||
{
|
||||
var group = new NavNodeViewModel
|
||||
{
|
||||
Label = LocationRoots.Cloud,
|
||||
Path = LocationRoots.Cloud,
|
||||
Glyph = "\uE753",
|
||||
IsExpanded = true,
|
||||
ChildrenLoaded = true,
|
||||
IsGroup = true
|
||||
};
|
||||
foreach (var place in places)
|
||||
{
|
||||
group.Children.Add(place);
|
||||
}
|
||||
|
||||
Roots.Add(group);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var place in places)
|
||||
{
|
||||
Roots.Add(place);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var path in expanded)
|
||||
@@ -123,7 +163,7 @@ public sealed class NavigationTreeViewModel
|
||||
|
||||
public async Task EnsureChildrenAsync(NavNodeViewModel node)
|
||||
{
|
||||
if (node.IsPlaceholder || node.ChildrenLoaded || node.Path == "This PC")
|
||||
if (node.IsPlaceholder || node.ChildrenLoaded || node.IsGroup || LocationRoots.IsVirtual(node.Path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -155,13 +195,13 @@ public sealed class NavigationTreeViewModel
|
||||
IsRevealing = true;
|
||||
try
|
||||
{
|
||||
if (PathsEqual(path, "This PC"))
|
||||
if (LocationRoots.IsVirtual(path))
|
||||
{
|
||||
var thisPc = Roots.FirstOrDefault(r => r.Path == "This PC");
|
||||
if (thisPc is not null)
|
||||
var virtualRoot = Roots.FirstOrDefault(r => r.Path == path);
|
||||
if (virtualRoot is not null)
|
||||
{
|
||||
thisPc.IsExpanded = true;
|
||||
SelectOnly(thisPc);
|
||||
virtualRoot.IsExpanded = true;
|
||||
SelectOnly(virtualRoot);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -224,6 +264,61 @@ public sealed class NavigationTreeViewModel
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static NavNodeViewModel CreateCloudNode(ProviderPlace place)
|
||||
{
|
||||
var exists = Directory.Exists(place.Path);
|
||||
var node = new NavNodeViewModel
|
||||
{
|
||||
Label = place.DisplayName,
|
||||
Path = place.Path,
|
||||
Glyph = place.Glyph ?? CloudGlyph(place.ProviderId),
|
||||
Status = exists ? "" : "Offline",
|
||||
IsOffline = !exists
|
||||
};
|
||||
AddPlaceholder(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
private static NavNodeViewModel CreateSourceNode(Source source, bool canRemove)
|
||||
{
|
||||
var node = new NavNodeViewModel
|
||||
{
|
||||
Label = source.DisplayName,
|
||||
Path = source.LastRootPath ?? source.DisplayName,
|
||||
Status = source.Status switch
|
||||
{
|
||||
SourceStatus.Offline => source.LastSeenUtc is null
|
||||
? "Offline"
|
||||
: $"Offline · Last seen {source.LastSeenUtc.Value.ToLocalTime():d}",
|
||||
SourceStatus.Scanning => "Indexing",
|
||||
SourceStatus.Stale => "May be out of date",
|
||||
SourceStatus.Error => "Error",
|
||||
_ => source.IsIndexed ? "Indexed" : ""
|
||||
},
|
||||
IsOffline = source.Status == SourceStatus.Offline,
|
||||
Glyph = source.Kind == SourceKind.Removable ? "\uE88E" : source.Kind.IsNetwork() ? "\uE968" : "\uEDA2",
|
||||
SourceId = source.Id,
|
||||
CanRemove = canRemove
|
||||
};
|
||||
AddPlaceholder(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
private static int ProviderOrder(string providerId) => providerId switch
|
||||
{
|
||||
"onedrive" => 0,
|
||||
"googledrive" => 1,
|
||||
"nextcloud" => 2,
|
||||
_ => 9
|
||||
};
|
||||
|
||||
private static string CloudGlyph(string providerId) => providerId switch
|
||||
{
|
||||
"googledrive" => "\uE753",
|
||||
"nextcloud" => "\uE753",
|
||||
_ => "\uE753"
|
||||
};
|
||||
|
||||
private static NavNodeViewModel? FindBestRoot(IEnumerable<NavNodeViewModel> roots, string path)
|
||||
{
|
||||
var normalized = PathRules.FromExtended(path).TrimEnd('\\');
|
||||
@@ -232,7 +327,7 @@ public sealed class NavigationTreeViewModel
|
||||
|
||||
void Consider(NavNodeViewModel node)
|
||||
{
|
||||
if (node.IsPlaceholder || node.Path == "This PC")
|
||||
if (node.IsPlaceholder || node.IsGroup || LocationRoots.IsVirtual(node.Path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -251,11 +346,11 @@ public sealed class NavigationTreeViewModel
|
||||
|
||||
foreach (var root in roots)
|
||||
{
|
||||
if (root.Path == "This PC")
|
||||
if (root.IsGroup || LocationRoots.IsVirtual(root.Path))
|
||||
{
|
||||
foreach (var drive in root.Children.Where(c => !c.IsPlaceholder))
|
||||
foreach (var child in root.Children.Where(c => !c.IsPlaceholder))
|
||||
{
|
||||
Consider(drive);
|
||||
Consider(child);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user