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:
79
src/Explorer.Presentation/ViewModels/DuplicateViewModel.cs
Normal file
79
src/Explorer.Presentation/ViewModels/DuplicateViewModel.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Explorer.Application;
|
||||
using Explorer.Domain;
|
||||
using Explorer.Domain.Abstractions;
|
||||
|
||||
namespace Explorer.Presentation.ViewModels;
|
||||
|
||||
public sealed partial class DuplicateViewModel : ObservableObject
|
||||
{
|
||||
private readonly IIndexStore _store;
|
||||
private readonly SourceManager _sources;
|
||||
|
||||
[ObservableProperty] private bool _isOpen;
|
||||
[ObservableProperty] private bool _isBusy;
|
||||
[ObservableProperty] private string _status = "";
|
||||
|
||||
public DuplicateViewModel(IIndexStore store, SourceManager sources)
|
||||
{
|
||||
_store = store;
|
||||
_sources = sources;
|
||||
Groups = [];
|
||||
}
|
||||
|
||||
public ObservableCollection<string> Groups { get; }
|
||||
|
||||
[RelayCommand]
|
||||
public async Task OpenAsync()
|
||||
{
|
||||
IsOpen = true;
|
||||
IsBusy = true;
|
||||
Status = "Finding size collisions…";
|
||||
Groups.Clear();
|
||||
try
|
||||
{
|
||||
var lines = await Task.Run(async () =>
|
||||
{
|
||||
await _store.Hashes.EnqueueSizeCollisionsAsync(null).ConfigureAwait(false);
|
||||
var groups = await _store.Hashes.GetDuplicateGroupsAsync(null, null, 200).ConfigureAwait(false);
|
||||
var sources = (await _sources.RefreshOnlineStateAsync().ConfigureAwait(false)).ToDictionary(s => s.Id);
|
||||
var result = new List<string>();
|
||||
foreach (var g in groups)
|
||||
{
|
||||
if (g.SameFileId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var paths = string.Join(" | ", g.Entries.Select(e =>
|
||||
{
|
||||
sources.TryGetValue(e.SourceId, out var s);
|
||||
return PathRules.Combine(s?.LastRootPath ?? s?.DisplayName ?? "", e.PathRel);
|
||||
}));
|
||||
result.Add($"{Formatters.Size(g.SizeBytes)} · {g.Entries.Count} files · {paths}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}).ConfigureAwait(true);
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
Groups.Add(line);
|
||||
}
|
||||
|
||||
Status = Groups.Count == 0
|
||||
? "No confirmed duplicates yet. Hashing continues in the background."
|
||||
: $"{Groups.Count} duplicate groups";
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Status = "Could not load duplicates.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user