diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 0000000..d7694ae
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,8 @@
+{
+ "permissions": {
+ "allow": [
+ "mcp__serena__list_dir",
+ "Bash(dotnet build:*)"
+ ]
+ }
+}
diff --git a/src/AStar.Dev.File.App/AStar.Dev.File.App.csproj b/src/AStar.Dev.File.App/AStar.Dev.File.App.csproj
index a0590be..ee69c06 100644
--- a/src/AStar.Dev.File.App/AStar.Dev.File.App.csproj
+++ b/src/AStar.Dev.File.App/AStar.Dev.File.App.csproj
@@ -13,6 +13,10 @@
+
+
+
+
@@ -29,7 +33,8 @@
All
-
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/src/AStar.Dev.File.App/FodyWeavers.xml b/src/AStar.Dev.File.App/FodyWeavers.xml
new file mode 100644
index 0000000..63fc148
--- /dev/null
+++ b/src/AStar.Dev.File.App/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/AStar.Dev.File.App/Program.cs b/src/AStar.Dev.File.App/Program.cs
index a83f283..9f6a371 100644
--- a/src/AStar.Dev.File.App/Program.cs
+++ b/src/AStar.Dev.File.App/Program.cs
@@ -1,4 +1,5 @@
using Avalonia;
+using Avalonia.ReactiveUI;
using System;
namespace AStar.Dev.File.App;
@@ -17,5 +18,6 @@ public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure()
.UsePlatformDetect()
.WithInterFont()
- .LogToTrace();
+ .LogToTrace()
+ .UseReactiveUI();
}
diff --git a/src/AStar.Dev.File.App/ViewModels/DeletePendingViewModel.cs b/src/AStar.Dev.File.App/ViewModels/DeletePendingViewModel.cs
index e509385..f56060c 100644
--- a/src/AStar.Dev.File.App/ViewModels/DeletePendingViewModel.cs
+++ b/src/AStar.Dev.File.App/ViewModels/DeletePendingViewModel.cs
@@ -1,34 +1,35 @@
using AStar.Dev.File.App.Data;
using AStar.Dev.File.App.Services;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
using Microsoft.EntityFrameworkCore;
+using ReactiveUI;
+using ReactiveUI.Fody.Helpers;
using System;
using System.Collections.ObjectModel;
using System.Linq;
+using System.Reactive;
using System.Threading.Tasks;
namespace AStar.Dev.File.App.ViewModels;
-public partial class DeletePendingViewModel : ViewModelBase
+public class DeletePendingViewModel : ViewModelBase
{
private readonly IDbContextFactory _dbContextFactory;
private readonly IFileDeleteService _fileDeleteService;
private readonly IFileViewerService _fileViewerService;
- [ObservableProperty]
- private bool _isDeleting;
-
- [ObservableProperty]
- private int _pendingDeleteCount;
-
- [ObservableProperty]
- private string _statusMessage = string.Empty;
+ [Reactive] public bool IsDeleting { get; set; }
+ [Reactive] public int PendingDeleteCount { get; set; }
+ [Reactive] public string StatusMessage { get; set; } = string.Empty;
public ObservableCollection PendingDeleteFiles { get; } = [];
public event Action? ViewFileRequested;
+ public ReactiveCommand TogglePendingDeleteCommand { get; }
+ public ReactiveCommand DeleteAllCommand { get; }
+ public ReactiveCommand ClearMarkingsCommand { get; }
+ public ReactiveCommand ViewFileCommand { get; }
+
public DeletePendingViewModel(
IDbContextFactory dbContextFactory,
IFileDeleteService fileDeleteService,
@@ -39,10 +40,18 @@ public DeletePendingViewModel(
_fileViewerService = fileViewerService;
_fileViewerService.FileViewRequested += item => ViewFileRequested?.Invoke(item);
+ TogglePendingDeleteCommand = ReactiveCommand.CreateFromTask(TogglePendingDelete);
+
+ var canDeleteAll = this.WhenAnyValue(x => x.IsDeleting, x => x.PendingDeleteCount,
+ (deleting, count) => !deleting && count > 0);
+ DeleteAllCommand = ReactiveCommand.CreateFromTask(DeleteAll, canDeleteAll);
+
+ ClearMarkingsCommand = ReactiveCommand.CreateFromTask(ClearMarkings);
+ ViewFileCommand = ReactiveCommand.CreateFromTask(ViewFile);
+
_ = LoadPendingFilesAsync();
}
- [RelayCommand]
private async Task TogglePendingDelete(ScannedFileDisplayItem? item)
{
if (item is null) return;
@@ -58,7 +67,6 @@ private async Task TogglePendingDelete(ScannedFileDisplayItem? item)
await LoadPendingFilesAsync();
}
- [RelayCommand(CanExecute = nameof(CanDeleteAll))]
private async Task DeleteAll()
{
if (PendingDeleteFiles.Count == 0)
@@ -95,9 +103,6 @@ private async Task DeleteAll()
}
}
- private bool CanDeleteAll() => !IsDeleting && PendingDeleteFiles.Count > 0;
-
- [RelayCommand]
private async Task ClearMarkings()
{
if (PendingDeleteFiles.Count == 0)
@@ -116,7 +121,6 @@ private async Task ClearMarkings()
await LoadPendingFilesAsync();
}
- [RelayCommand]
private async Task ViewFile(ScannedFileDisplayItem? item)
{
await _fileViewerService.ViewFileAsync(item);
@@ -137,7 +141,6 @@ private async Task LoadPendingFilesAsync()
files.ForEach(file => PendingDeleteFiles.Add(new ScannedFileDisplayItem(file)));
PendingDeleteCount = PendingDeleteFiles.Count;
- DeleteAllCommand.NotifyCanExecuteChanged();
}
catch (Exception ex)
{
diff --git a/src/AStar.Dev.File.App/ViewModels/MainWindowViewModel.cs b/src/AStar.Dev.File.App/ViewModels/MainWindowViewModel.cs
index 6decac8..ccac9d4 100644
--- a/src/AStar.Dev.File.App/ViewModels/MainWindowViewModel.cs
+++ b/src/AStar.Dev.File.App/ViewModels/MainWindowViewModel.cs
@@ -1,349 +1,337 @@
-using AStar.Dev.File.App.Data;
-using AStar.Dev.File.App.Models;
-using AStar.Dev.File.App.Services;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-using Microsoft.EntityFrameworkCore;
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace AStar.Dev.File.App.ViewModels;
-
-public partial class MainWindowViewModel : ViewModelBase
-{
- private readonly IFileScannerService _fileScannerService;
- private readonly IFolderPickerService _folderPickerService;
- private readonly IFileViewerService _fileViewerService;
- private readonly IDbContextFactory _dbContextFactory;
- private CancellationTokenSource? _cts;
- private bool _suppressPageReload;
-
- public static IReadOnlyList PageSizes { get; } = [25, 50, 75, 100, 125, 150, 175, 200];
-
- [ObservableProperty]
- [NotifyPropertyChangedFor(nameof(CanScan))]
- [NotifyCanExecuteChangedFor(nameof(StartScanCommand))]
- [NotifyCanExecuteChangedFor(nameof(LoadFromDatabaseCommand))]
- private string _selectedFolderPath = string.Empty;
-
- [ObservableProperty]
- [NotifyPropertyChangedFor(nameof(CanScan))]
- [NotifyCanExecuteChangedFor(nameof(StartScanCommand))]
- [NotifyCanExecuteChangedFor(nameof(SelectFolderCommand))]
- [NotifyCanExecuteChangedFor(nameof(CancelCommand))]
- private bool _isScanning;
-
- [ObservableProperty]
- private string _currentScanFolder = string.Empty;
-
- [ObservableProperty]
- private int _totalFilesProcessed;
-
- [ObservableProperty]
- [NotifyPropertyChangedFor(nameof(TotalPages), nameof(PagingInfo))]
- [NotifyCanExecuteChangedFor(nameof(FirstPageCommand))]
- [NotifyCanExecuteChangedFor(nameof(PreviousPageCommand))]
- [NotifyCanExecuteChangedFor(nameof(NextPageCommand))]
- [NotifyCanExecuteChangedFor(nameof(LastPageCommand))]
- private int _pageSize = 50;
-
- [ObservableProperty]
- [NotifyPropertyChangedFor(nameof(PagingInfo))]
- [NotifyCanExecuteChangedFor(nameof(FirstPageCommand))]
- [NotifyCanExecuteChangedFor(nameof(PreviousPageCommand))]
- [NotifyCanExecuteChangedFor(nameof(NextPageCommand))]
- [NotifyCanExecuteChangedFor(nameof(LastPageCommand))]
- private int _currentPage = 1;
-
- [ObservableProperty]
- [NotifyPropertyChangedFor(nameof(TotalPages), nameof(PagingInfo))]
- [NotifyCanExecuteChangedFor(nameof(NextPageCommand))]
- [NotifyCanExecuteChangedFor(nameof(LastPageCommand))]
- private int _totalFileCount;
-
- private bool _showDuplicatesOnly;
-
- public bool ShowDuplicatesOnly
- {
- get => _showDuplicatesOnly;
- set
- {
- if (SetProperty(ref _showDuplicatesOnly, value))
- {
- _suppressPageReload = true;
- CurrentPage = 1;
- _suppressPageReload = false;
- _ = LoadScannedFilesAsync();
- }
- }
- }
-
- [RelayCommand]
- private void ToggleDuplicatesOnly()
- {
- ShowDuplicatesOnly = !ShowDuplicatesOnly;
- }
-
- public int TotalPages => TotalFileCount == 0 ? 1 : (int)Math.Ceiling((double)TotalFileCount / PageSize);
-
- public string PagingInfo => $"PAGE {CurrentPage} OF {TotalPages} [{TotalFileCount} FILES]";
-
- public bool CanScan => !IsScanning && !string.IsNullOrWhiteSpace(SelectedFolderPath);
-
- public ObservableCollection StatusMessages { get; } = [];
- public ObservableCollection ScannedFiles { get; } = [];
-
- public event Action? ViewFileRequested;
- public event Action? OpenDeleteWindowRequested;
-
- public MainWindowViewModel(
- IFileScannerService fileScannerService,
- IFolderPickerService folderPickerService,
- IFileViewerService fileViewerService,
- IDbContextFactory dbContextFactory)
- {
- _fileScannerService = fileScannerService;
- _folderPickerService = folderPickerService;
- _fileViewerService = fileViewerService;
+using AStar.Dev.File.App.Data;
+using AStar.Dev.File.App.Models;
+using AStar.Dev.File.App.Services;
+using Microsoft.EntityFrameworkCore;
+using ReactiveUI;
+using ReactiveUI.Fody.Helpers;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Reactive;
+using System.Reactive.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AStar.Dev.File.App.ViewModels;
+
+public class MainWindowViewModel : ViewModelBase
+{
+ private readonly IFileScannerService _fileScannerService;
+ private readonly IFolderPickerService _folderPickerService;
+ private readonly IFileViewerService _fileViewerService;
+ private readonly IDbContextFactory _dbContextFactory;
+ private CancellationTokenSource? _cts;
+ private bool _suppressPageReload;
+
+ public static IReadOnlyList PageSizes { get; } = [25, 50, 75, 100, 125, 150, 175, 200];
+
+ [Reactive] public string SelectedFolderPath { get; set; } = string.Empty;
+ [Reactive] public bool IsScanning { get; set; }
+ [Reactive] public string CurrentScanFolder { get; set; } = string.Empty;
+ [Reactive] public int TotalFilesProcessed { get; set; }
+ [Reactive] public int PageSize { get; set; } = 50;
+ [Reactive] public int CurrentPage { get; set; } = 1;
+ [Reactive] public int TotalFileCount { get; set; }
+ [Reactive] public bool ShowDuplicatesOnly { get; set; }
+
+ private readonly ObservableAsPropertyHelper _totalPages;
+ public int TotalPages => _totalPages.Value;
+
+ private readonly ObservableAsPropertyHelper _pagingInfo;
+ public string PagingInfo => _pagingInfo.Value;
+
+ public ObservableCollection StatusMessages { get; } = [];
+ public ObservableCollection ScannedFiles { get; } = [];
+
+ public event Action? ViewFileRequested;
+ public event Action? OpenDeleteWindowRequested;
+
+ public ReactiveCommand SelectFolderCommand { get; }
+ public ReactiveCommand StartScanCommand { get; }
+ public ReactiveCommand CancelCommand { get; }
+ public ReactiveCommand LoadFromDatabaseCommand { get; }
+ public ReactiveCommand OpenDeleteWindowCommand { get; }
+ public ReactiveCommand ToggleDuplicatesOnlyCommand { get; }
+ public ReactiveCommand TogglePendingDeleteCommand { get; }
+ public ReactiveCommand ViewFileCommand { get; }
+ public ReactiveCommand FirstPageCommand { get; }
+ public ReactiveCommand PreviousPageCommand { get; }
+ public ReactiveCommand NextPageCommand { get; }
+ public ReactiveCommand LastPageCommand { get; }
+
+ public MainWindowViewModel(
+ IFileScannerService fileScannerService,
+ IFolderPickerService folderPickerService,
+ IFileViewerService fileViewerService,
+ IDbContextFactory dbContextFactory)
+ {
+ _fileScannerService = fileScannerService;
+ _folderPickerService = folderPickerService;
+ _fileViewerService = fileViewerService;
_dbContextFactory = dbContextFactory;
- // Subscribe to file viewer service events
+ // Computed properties
+ _totalPages = this.WhenAnyValue(x => x.TotalFileCount, x => x.PageSize,
+ (count, size) => count == 0 ? 1 : (int)Math.Ceiling((double)count / size))
+ .ToProperty(this, x => x.TotalPages, initialValue: 1);
+
+ _pagingInfo = this.WhenAnyValue(x => x.CurrentPage, x => x.TotalFileCount, x => x.PageSize,
+ (page, count, size) =>
+ {
+ int total = count == 0 ? 1 : (int)Math.Ceiling((double)count / size);
+ return $"PAGE {page} OF {total} [{count} FILES]";
+ })
+ .ToProperty(this, x => x.PagingInfo, initialValue: "PAGE 1 OF 1 [0 FILES]");
+
+ // Commands
+ var canSelectFolder = this.WhenAnyValue(x => x.IsScanning, scanning => !scanning);
+ SelectFolderCommand = ReactiveCommand.CreateFromTask(SelectFolder, canSelectFolder);
+
+ var canScan = this.WhenAnyValue(x => x.IsScanning, x => x.SelectedFolderPath,
+ (scanning, path) => !scanning && !string.IsNullOrWhiteSpace(path));
+ StartScanCommand = ReactiveCommand.CreateFromTask(StartScan, canScan);
+
+ var canCancel = this.WhenAnyValue(x => x.IsScanning);
+ CancelCommand = ReactiveCommand.Create(Cancel, canCancel);
+
+ var canLoad = this.WhenAnyValue(x => x.SelectedFolderPath, path => !string.IsNullOrWhiteSpace(path));
+ LoadFromDatabaseCommand = ReactiveCommand.CreateFromTask(LoadFromDatabase, canLoad);
+
+ OpenDeleteWindowCommand = ReactiveCommand.Create(() => OpenDeleteWindowRequested?.Invoke());
+ ToggleDuplicatesOnlyCommand = ReactiveCommand.Create(() => { ShowDuplicatesOnly = !ShowDuplicatesOnly; });
+
+ TogglePendingDeleteCommand = ReactiveCommand.CreateFromTask(TogglePendingDelete);
+ ViewFileCommand = ReactiveCommand.CreateFromTask(ViewFile);
+
+ var canGoFirst = this.WhenAnyValue(x => x.CurrentPage, page => page > 1);
+ FirstPageCommand = ReactiveCommand.Create(() => { CurrentPage = 1; }, canGoFirst);
+
+ var canGoPrevious = this.WhenAnyValue(x => x.CurrentPage, page => page > 1);
+ PreviousPageCommand = ReactiveCommand.Create(() => { CurrentPage--; }, canGoPrevious);
+
+ var canGoNext = this.WhenAnyValue(x => x.CurrentPage, x => x.TotalPages,
+ (page, total) => page < total);
+ NextPageCommand = ReactiveCommand.Create(() => { CurrentPage++; }, canGoNext);
+
+ var canGoLast = this.WhenAnyValue(x => x.CurrentPage, x => x.TotalPages,
+ (page, total) => page < total);
+ LastPageCommand = ReactiveCommand.Create(() => { CurrentPage = TotalPages; }, canGoLast);
+
+ // Side-effect subscriptions (replacing partial void OnXChanged hooks)
+ this.WhenAnyValue(x => x.CurrentPage)
+ .Skip(1)
+ .Subscribe(__ =>
+ {
+ if (!_suppressPageReload)
+ _ = LoadScannedFilesAsync();
+ });
+
+ this.WhenAnyValue(x => x.PageSize)
+ .Skip(1)
+ .Subscribe(__ =>
+ {
+ _suppressPageReload = true;
+ CurrentPage = 1;
+ _suppressPageReload = false;
+ _ = LoadScannedFilesAsync();
+ });
+
+ this.WhenAnyValue(x => x.ShowDuplicatesOnly)
+ .Skip(1)
+ .Subscribe(__ =>
+ {
+ _suppressPageReload = true;
+ CurrentPage = 1;
+ _suppressPageReload = false;
+ _ = LoadScannedFilesAsync();
+ });
+
_fileViewerService.FileViewRequested += item => ViewFileRequested?.Invoke(item);
- _ = InitializeAsync();
- }
-
- private async Task InitializeAsync()
- {
- await LoadSelectedFolderPathAsync();
- }
-
- private async Task LoadSelectedFolderPathAsync()
- {
- await using var db = await _dbContextFactory.CreateDbContextAsync();
- var setting = await db.AppSettings.FirstOrDefaultAsync(s => s.Key == "SelectedFolderPath");
-
- if (setting is not null && !string.IsNullOrWhiteSpace(setting.Value) && Directory.Exists(setting.Value))
- {
- SelectedFolderPath = setting.Value;
- }
- else
- {
- var defaultPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments));
- SelectedFolderPath = defaultPath;
- }
- }
-
- [RelayCommand(CanExecute = nameof(CanSelectFolder))]
- private async Task SelectFolder()
- {
- var path = await _folderPickerService.OpenFolderPickerAsync();
- if (!string.IsNullOrEmpty(path))
- {
- SelectedFolderPath = path;
- await SaveSelectedFolderPathAsync(path);
- }
- }
-
- private async Task SaveSelectedFolderPathAsync(string path)
- {
- await using var db = await _dbContextFactory.CreateDbContextAsync();
- var setting = await db.AppSettings.FirstOrDefaultAsync(s => s.Key == "SelectedFolderPath");
-
- if (setting is not null)
- {
- setting.Value = path;
- }
- else
- {
- db.AppSettings.Add(new Models.AppSetting { Key = "SelectedFolderPath", Value = path });
- }
-
- await db.SaveChangesAsync();
- }
-
- private bool CanSelectFolder() => !IsScanning;
-
- [RelayCommand(CanExecute = nameof(CanLoadFromDatabase))]
- private async Task LoadFromDatabase()
- {
- _suppressPageReload = true;
- CurrentPage = 1;
- _suppressPageReload = false;
- await LoadScannedFilesAsync();
- }
-
- private bool CanLoadFromDatabase() => !string.IsNullOrWhiteSpace(SelectedFolderPath);
-
- [RelayCommand]
- private void OpenDeleteWindow()
- {
- OpenDeleteWindowRequested?.Invoke();
- }
-
- [RelayCommand(CanExecute = nameof(CanScan))]
- private async Task StartScan()
- {
- if (string.IsNullOrWhiteSpace(SelectedFolderPath) || IsScanning)
- return;
-
- IsScanning = true;
- StatusMessages.Clear();
- ScannedFiles.Clear();
- TotalFilesProcessed = 0;
- TotalFileCount = 0;
- CurrentScanFolder = string.Empty;
- _suppressPageReload = true;
- CurrentPage = 1;
- _suppressPageReload = false;
-
- _cts = new CancellationTokenSource();
-
- var progress = new Progress(update =>
- {
- CurrentScanFolder = update.CurrentFolder;
- TotalFilesProcessed = update.TotalFilesProcessed;
- if (!string.IsNullOrEmpty(update.StatusMessage))
- StatusMessages.Add(update.StatusMessage);
- });
-
- try
- {
- await Task.Run(() => _fileScannerService.ScanAsync(SelectedFolderPath, progress, _cts.Token), _cts.Token);
- await LoadScannedFilesAsync();
- }
- catch (OperationCanceledException)
- {
- var time = DateTime.Now.ToString("HH:mm:ss");
- StatusMessages.Add($"[{time}] [CANCELLED] Scan cancelled by user.");
- }
- finally
- {
- IsScanning = false;
- _cts.Dispose();
- _cts = null;
- }
- }
-
- [RelayCommand]
- private async Task TogglePendingDelete(ScannedFileDisplayItem? item)
- {
- if (item is null) return;
-
- await using var db = await _dbContextFactory.CreateDbContextAsync();
- var file = await db.ScannedFiles.FindAsync(item.Id);
- if (file is not null)
- {
- file.PendingDelete = !file.PendingDelete;
- await db.SaveChangesAsync();
- item.PendingDelete = file.PendingDelete;
- }
- }
-
- [RelayCommand]
- private async Task ViewFile(ScannedFileDisplayItem? item)
- {
- await _fileViewerService.ViewFileAsync(item);
- }
-
- [RelayCommand(CanExecute = nameof(IsScanning))]
- private void Cancel()
- {
- _cts?.Cancel();
- }
-
- [RelayCommand(CanExecute = nameof(CanGoFirst))]
- private void FirstPage() { CurrentPage = 1; }
- private bool CanGoFirst() => CurrentPage > 1;
-
- [RelayCommand(CanExecute = nameof(CanGoPrevious))]
- private void PreviousPage() { CurrentPage--; }
- private bool CanGoPrevious() => CurrentPage > 1;
-
- [RelayCommand(CanExecute = nameof(CanGoNext))]
- private void NextPage() { CurrentPage++; }
- private bool CanGoNext() => CurrentPage < TotalPages;
-
- [RelayCommand(CanExecute = nameof(CanGoLast))]
- private void LastPage() { CurrentPage = TotalPages; }
- private bool CanGoLast() => CurrentPage < TotalPages;
-
- partial void OnCurrentPageChanged(int value)
- {
- if (!_suppressPageReload)
- _ = LoadScannedFilesAsync();
- }
-
- partial void OnPageSizeChanged(int value)
- {
- _suppressPageReload = true;
- CurrentPage = 1;
- _suppressPageReload = false;
- _ = LoadScannedFilesAsync();
- }
-
- private async Task LoadScannedFilesAsync()
- {
- try
- {
- if (string.IsNullOrWhiteSpace(SelectedFolderPath)) return;
-
- var prefix = SelectedFolderPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
-
- await using var db = await _dbContextFactory.CreateDbContextAsync();
-
- var baseQuery = db.ScannedFiles.Where(f => f.FullPath.StartsWith(prefix));
-
- IQueryable query;
- if (ShowDuplicatesOnly)
- {
- var duplicateSizeSubquery = baseQuery
- .GroupBy(f => f.SizeInBytes)
- .Where(g => g.Count() > 1)
- .Select(g => g.Key);
-
- query = baseQuery
- .Where(f => duplicateSizeSubquery.Contains(f.SizeInBytes))
- .OrderBy(f => f.SizeInBytes)
- .ThenBy(f => f.FolderPath)
- .ThenBy(f => f.FileName);
- }
- else
- {
- query = baseQuery
- .OrderBy(f => f.FolderPath)
- .ThenBy(f => f.FileName);
- }
-
- TotalFileCount = await query.CountAsync();
-
- ClampCurrentPage();
-
- var files = await query
- .Skip((CurrentPage - 1) * PageSize)
- .Take(PageSize)
- .ToListAsync();
-
- ScannedFiles.Clear();
- files.ForEach(file => ScannedFiles.Add(new ScannedFileDisplayItem(file)));
- }
- catch (Exception ex)
- {
- StatusMessages.Add($"Error loading files: {ex.Message}");
+ _ = InitializeAsync();
+ }
+
+ private async Task InitializeAsync()
+ {
+ await LoadSelectedFolderPathAsync();
+ }
+
+ private async Task LoadSelectedFolderPathAsync()
+ {
+ await using var db = await _dbContextFactory.CreateDbContextAsync();
+ var setting = await db.AppSettings.FirstOrDefaultAsync(s => s.Key == "SelectedFolderPath");
+
+ if (setting is not null && !string.IsNullOrWhiteSpace(setting.Value) && Directory.Exists(setting.Value))
+ {
+ SelectedFolderPath = setting.Value;
+ }
+ else
+ {
+ SelectedFolderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments));
+ }
+ }
+
+ private async Task SelectFolder()
+ {
+ var path = await _folderPickerService.OpenFolderPickerAsync();
+ if (!string.IsNullOrEmpty(path))
+ {
+ SelectedFolderPath = path;
+ await SaveSelectedFolderPathAsync(path);
+ }
+ }
+
+ private async Task SaveSelectedFolderPathAsync(string path)
+ {
+ await using var db = await _dbContextFactory.CreateDbContextAsync();
+ var setting = await db.AppSettings.FirstOrDefaultAsync(s => s.Key == "SelectedFolderPath");
+
+ if (setting is not null)
+ {
+ setting.Value = path;
+ }
+ else
+ {
+ db.AppSettings.Add(new AppSetting { Key = "SelectedFolderPath", Value = path });
+ }
+
+ await db.SaveChangesAsync();
+ }
+
+ private async Task LoadFromDatabase()
+ {
+ _suppressPageReload = true;
+ CurrentPage = 1;
+ _suppressPageReload = false;
+ await LoadScannedFilesAsync();
+ }
+
+ private async Task StartScan()
+ {
+ if (string.IsNullOrWhiteSpace(SelectedFolderPath) || IsScanning)
+ return;
+
+ IsScanning = true;
+ StatusMessages.Clear();
+ ScannedFiles.Clear();
+ TotalFilesProcessed = 0;
+ TotalFileCount = 0;
+ CurrentScanFolder = string.Empty;
+ _suppressPageReload = true;
+ CurrentPage = 1;
+ _suppressPageReload = false;
+
+ _cts = new CancellationTokenSource();
+
+ var progress = new Progress(update =>
+ {
+ CurrentScanFolder = update.CurrentFolder;
+ TotalFilesProcessed = update.TotalFilesProcessed;
+ if (!string.IsNullOrEmpty(update.StatusMessage))
+ StatusMessages.Add(update.StatusMessage);
+ });
+
+ try
+ {
+ await Task.Run(() => _fileScannerService.ScanAsync(SelectedFolderPath, progress, _cts.Token), _cts.Token);
+ await LoadScannedFilesAsync();
+ }
+ catch (OperationCanceledException)
+ {
+ var time = DateTime.Now.ToString("HH:mm:ss");
+ StatusMessages.Add($"[{time}] [CANCELLED] Scan cancelled by user.");
+ }
+ finally
+ {
+ IsScanning = false;
+ _cts.Dispose();
+ _cts = null;
+ }
+ }
+
+ private async Task TogglePendingDelete(ScannedFileDisplayItem? item)
+ {
+ if (item is null) return;
+
+ await using var db = await _dbContextFactory.CreateDbContextAsync();
+ var file = await db.ScannedFiles.FindAsync(item.Id);
+ if (file is not null)
+ {
+ file.PendingDelete = !file.PendingDelete;
+ await db.SaveChangesAsync();
+ item.PendingDelete = file.PendingDelete;
}
- }
-
- private void ClampCurrentPage()
- {
- if (CurrentPage <= TotalPages) return;
-
- _suppressPageReload = true;
- CurrentPage = TotalPages;
- _suppressPageReload = false;
- }
-}
+ }
+
+ private async Task ViewFile(ScannedFileDisplayItem? item)
+ {
+ await _fileViewerService.ViewFileAsync(item);
+ }
+
+ private void Cancel()
+ {
+ _cts?.Cancel();
+ }
+
+ private async Task LoadScannedFilesAsync()
+ {
+ try
+ {
+ if (string.IsNullOrWhiteSpace(SelectedFolderPath)) return;
+
+ var prefix = SelectedFolderPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
+
+ await using var db = await _dbContextFactory.CreateDbContextAsync();
+
+ var baseQuery = db.ScannedFiles.Where(f => f.FullPath.StartsWith(prefix));
+
+ IQueryable query;
+ if (ShowDuplicatesOnly)
+ {
+ var duplicateSizeSubquery = baseQuery
+ .GroupBy(f => f.SizeInBytes)
+ .Where(g => g.Count() > 1)
+ .Select(g => g.Key);
+
+ query = baseQuery
+ .Where(f => duplicateSizeSubquery.Contains(f.SizeInBytes))
+ .OrderBy(f => f.SizeInBytes)
+ .ThenBy(f => f.FolderPath)
+ .ThenBy(f => f.FileName);
+ }
+ else
+ {
+ query = baseQuery
+ .OrderBy(f => f.FolderPath)
+ .ThenBy(f => f.FileName);
+ }
+
+ TotalFileCount = await query.CountAsync();
+
+ ClampCurrentPage();
+
+ var files = await query
+ .Skip((CurrentPage - 1) * PageSize)
+ .Take(PageSize)
+ .ToListAsync();
+
+ ScannedFiles.Clear();
+ files.ForEach(file => ScannedFiles.Add(new ScannedFileDisplayItem(file)));
+ }
+ catch (Exception ex)
+ {
+ StatusMessages.Add($"Error loading files: {ex.Message}");
+ }
+ }
+
+ private void ClampCurrentPage()
+ {
+ if (CurrentPage <= TotalPages) return;
+
+ _suppressPageReload = true;
+ CurrentPage = TotalPages;
+ _suppressPageReload = false;
+ }
+}
diff --git a/src/AStar.Dev.File.App/ViewModels/ScannedFileDisplayItem.cs b/src/AStar.Dev.File.App/ViewModels/ScannedFileDisplayItem.cs
index 617b565..259b0a2 100644
--- a/src/AStar.Dev.File.App/ViewModels/ScannedFileDisplayItem.cs
+++ b/src/AStar.Dev.File.App/ViewModels/ScannedFileDisplayItem.cs
@@ -1,11 +1,11 @@
using AStar.Dev.File.App.Models;
+using ReactiveUI;
+using ReactiveUI.Fody.Helpers;
using System.IO;
namespace AStar.Dev.File.App.ViewModels;
-using System.ComponentModel;
-
-public class ScannedFileDisplayItem : INotifyPropertyChanged
+public class ScannedFileDisplayItem : ReactiveObject
{
public int Id { get; }
public string FullPath { get; }
@@ -19,19 +19,7 @@ public class ScannedFileDisplayItem : INotifyPropertyChanged
public string LastModified { get; }
public string LastViewed { get; }
- private bool _pendingDelete;
- public bool PendingDelete
- {
- get => _pendingDelete;
- set
- {
- if (_pendingDelete != value)
- {
- _pendingDelete = value;
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PendingDelete)));
- }
- }
- }
+ [Reactive] public bool PendingDelete { get; set; }
public ScannedFileDisplayItem(ScannedFile file)
{
@@ -48,11 +36,9 @@ public ScannedFileDisplayItem(ScannedFile file)
LastViewed = file.LastViewed.HasValue
? file.LastViewed.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss")
: "—";
- _pendingDelete = file.PendingDelete;
+ PendingDelete = file.PendingDelete;
}
- public event PropertyChangedEventHandler? PropertyChanged;
-
public static string FormatSize(long bytes)
{
if (bytes >= 1_073_741_824L)
diff --git a/src/AStar.Dev.File.App/ViewModels/ViewModelBase.cs b/src/AStar.Dev.File.App/ViewModels/ViewModelBase.cs
index 54056cf..7782c2e 100644
--- a/src/AStar.Dev.File.App/ViewModels/ViewModelBase.cs
+++ b/src/AStar.Dev.File.App/ViewModels/ViewModelBase.cs
@@ -1,7 +1,7 @@
-using CommunityToolkit.Mvvm.ComponentModel;
-
-namespace AStar.Dev.File.App.ViewModels;
-
-public abstract class ViewModelBase : ObservableObject
-{
-}
+using ReactiveUI;
+
+namespace AStar.Dev.File.App.ViewModels;
+
+public abstract class ViewModelBase : ReactiveObject
+{
+}
diff --git a/tests/AStar.Dev.File.App.Tests.Unit/obj/AStar.Dev.File.App.Tests.Unit.csproj.nuget.dgspec.json b/tests/AStar.Dev.File.App.Tests.Unit/obj/AStar.Dev.File.App.Tests.Unit.csproj.nuget.dgspec.json
index 493b7a5..29e906d 100644
--- a/tests/AStar.Dev.File.App.Tests.Unit/obj/AStar.Dev.File.App.Tests.Unit.csproj.nuget.dgspec.json
+++ b/tests/AStar.Dev.File.App.Tests.Unit/obj/AStar.Dev.File.App.Tests.Unit.csproj.nuget.dgspec.json
@@ -65,13 +65,13 @@
"target": "Package",
"version": "[11.3.13, )"
},
- "Avalonia.Themes.Fluent": {
+ "Avalonia.ReactiveUI": {
"target": "Package",
- "version": "[11.3.13, )"
+ "version": "[11.3.9, )"
},
- "CommunityToolkit.Mvvm": {
+ "Avalonia.Themes.Fluent": {
"target": "Package",
- "version": "[8.4.2, )"
+ "version": "[11.3.13, )"
},
"Microsoft.EntityFrameworkCore.Design": {
"include": "Runtime, Build, Native, ContentFiles, Analyzers, BuildTransitive",
@@ -91,6 +91,10 @@
"target": "Package",
"version": "[10.0.5, )"
},
+ "ReactiveUI.Fody": {
+ "target": "Package",
+ "version": "[19.5.41, )"
+ },
"Serilog": {
"target": "Package",
"version": "[4.3.1, )"
diff --git a/tests/AStar.Dev.File.App.Tests.Unit/obj/AStar.Dev.File.App.Tests.Unit.csproj.nuget.g.targets b/tests/AStar.Dev.File.App.Tests.Unit/obj/AStar.Dev.File.App.Tests.Unit.csproj.nuget.g.targets
index df41d60..b3e7286 100644
--- a/tests/AStar.Dev.File.App.Tests.Unit/obj/AStar.Dev.File.App.Tests.Unit.csproj.nuget.g.targets
+++ b/tests/AStar.Dev.File.App.Tests.Unit/obj/AStar.Dev.File.App.Tests.Unit.csproj.nuget.g.targets
@@ -12,7 +12,6 @@
-
diff --git a/tests/AStar.Dev.File.App.Tests.Unit/obj/project.assets.json b/tests/AStar.Dev.File.App.Tests.Unit/obj/project.assets.json
index 6fecc84..15347b4 100644
--- a/tests/AStar.Dev.File.App.Tests.Unit/obj/project.assets.json
+++ b/tests/AStar.Dev.File.App.Tests.Unit/obj/project.assets.json
@@ -233,6 +233,24 @@
}
}
},
+ "Avalonia.ReactiveUI/11.3.9": {
+ "type": "package",
+ "dependencies": {
+ "Avalonia": "11.3.9",
+ "ReactiveUI": "20.1.1",
+ "System.Reactive": "6.0.1"
+ },
+ "compile": {
+ "lib/net8.0/Avalonia.ReactiveUI.dll": {
+ "related": ".xml"
+ }
+ },
+ "runtime": {
+ "lib/net8.0/Avalonia.ReactiveUI.dll": {
+ "related": ".xml"
+ }
+ }
+ },
"Avalonia.Remote.Protocol/11.3.13": {
"type": "package",
"compile": {
@@ -357,35 +375,35 @@
}
}
},
- "CommunityToolkit.Mvvm/8.4.2": {
+ "DiffEngine/11.3.0": {
"type": "package",
+ "dependencies": {
+ "EmptyFiles": "4.4.0",
+ "System.Management": "6.0.1"
+ },
"compile": {
- "lib/net8.0/CommunityToolkit.Mvvm.dll": {
+ "lib/net7.0/DiffEngine.dll": {
"related": ".xml"
}
},
"runtime": {
- "lib/net8.0/CommunityToolkit.Mvvm.dll": {
+ "lib/net7.0/DiffEngine.dll": {
"related": ".xml"
}
- },
- "build": {
- "buildTransitive/CommunityToolkit.Mvvm.targets": {}
}
},
- "DiffEngine/11.3.0": {
+ "DynamicData/8.4.1": {
"type": "package",
"dependencies": {
- "EmptyFiles": "4.4.0",
- "System.Management": "6.0.1"
+ "System.Reactive": "6.0.0"
},
"compile": {
- "lib/net7.0/DiffEngine.dll": {
+ "lib/net8.0/DynamicData.dll": {
"related": ".xml"
}
},
"runtime": {
- "lib/net7.0/DiffEngine.dll": {
+ "lib/net8.0/DynamicData.dll": {
"related": ".xml"
}
}
@@ -406,6 +424,12 @@
"buildTransitive/EmptyFiles.targets": {}
}
},
+ "Fody/6.8.0": {
+ "type": "package",
+ "build": {
+ "build/_._": {}
+ }
+ },
"HarfBuzzSharp/8.3.1.1": {
"type": "package",
"dependencies": {
@@ -1125,6 +1149,44 @@
}
}
},
+ "ReactiveUI/20.1.1": {
+ "type": "package",
+ "dependencies": {
+ "DynamicData": "8.4.1",
+ "Splat": "15.1.1",
+ "System.ComponentModel.Annotations": "5.0.0"
+ },
+ "compile": {
+ "lib/net8.0/ReactiveUI.dll": {
+ "related": ".xml"
+ }
+ },
+ "runtime": {
+ "lib/net8.0/ReactiveUI.dll": {
+ "related": ".xml"
+ }
+ }
+ },
+ "ReactiveUI.Fody/19.5.41": {
+ "type": "package",
+ "dependencies": {
+ "Fody": "6.8.0",
+ "ReactiveUI": "19.5.41"
+ },
+ "compile": {
+ "lib/net8.0/ReactiveUI.Fody.Helpers.dll": {
+ "related": ".xml"
+ }
+ },
+ "runtime": {
+ "lib/net8.0/ReactiveUI.Fody.Helpers.dll": {
+ "related": ".xml"
+ }
+ },
+ "build": {
+ "build/_._": {}
+ }
+ },
"Serilog/4.3.1": {
"type": "package",
"compile": {
@@ -1308,6 +1370,19 @@
}
}
},
+ "Splat/15.1.1": {
+ "type": "package",
+ "compile": {
+ "lib/net8.0/Splat.dll": {
+ "related": ".xml"
+ }
+ },
+ "runtime": {
+ "lib/net8.0/Splat.dll": {
+ "related": ".xml"
+ }
+ }
+ },
"SQLitePCLRaw.bundle_e_sqlite3/2.1.11": {
"type": "package",
"dependencies": {
@@ -1464,6 +1539,19 @@
"buildTransitive/netcoreapp3.1/_._": {}
}
},
+ "System.ComponentModel.Annotations/5.0.0": {
+ "type": "package",
+ "compile": {
+ "ref/netstandard2.1/System.ComponentModel.Annotations.dll": {
+ "related": ".xml"
+ }
+ },
+ "runtime": {
+ "lib/netstandard2.1/System.ComponentModel.Annotations.dll": {
+ "related": ".xml"
+ }
+ }
+ },
"System.Diagnostics.EventLog/6.0.0": {
"type": "package",
"compile": {
@@ -1515,6 +1603,22 @@
}
}
},
+ "System.Reactive/6.0.1": {
+ "type": "package",
+ "compile": {
+ "lib/net6.0/System.Reactive.dll": {
+ "related": ".xml"
+ }
+ },
+ "runtime": {
+ "lib/net6.0/System.Reactive.dll": {
+ "related": ".xml"
+ }
+ },
+ "build": {
+ "buildTransitive/net6.0/_._": {}
+ }
+ },
"Tmds.DBus.Protocol/0.21.2": {
"type": "package",
"compile": {
@@ -1650,11 +1754,12 @@
"Avalonia.Desktop": "11.3.13",
"Avalonia.Diagnostics": "11.3.13",
"Avalonia.Fonts.Inter": "11.3.13",
+ "Avalonia.ReactiveUI": "11.3.9",
"Avalonia.Themes.Fluent": "11.3.13",
- "CommunityToolkit.Mvvm": "8.4.2",
"Microsoft.EntityFrameworkCore.Sqlite": "10.0.5",
"Microsoft.Extensions.DependencyInjection": "10.0.5",
"Microsoft.Extensions.Logging": "10.0.5",
+ "ReactiveUI.Fody": "19.5.41",
"Serilog": "4.3.1",
"Serilog.Extensions.Logging": "10.0.0",
"Serilog.Sinks.Console": "6.1.1",
@@ -2002,6 +2107,24 @@
"runtimes/osx/native/libAvaloniaNative.dylib"
]
},
+ "Avalonia.ReactiveUI/11.3.9": {
+ "sha512": "Butgdd/wqpZ9QFHAaoOC8qOMTryYoIPeCXrXrV6qf5At4lCEkqcmAe1pRmdqpgeA4e4t19tjbXMElKj28jmhPg==",
+ "type": "package",
+ "path": "avalonia.reactiveui/11.3.9",
+ "files": [
+ ".nupkg.metadata",
+ ".signature.p7s",
+ "Icon.png",
+ "avalonia.reactiveui.11.3.9.nupkg.sha512",
+ "avalonia.reactiveui.nuspec",
+ "lib/net6.0/Avalonia.ReactiveUI.dll",
+ "lib/net6.0/Avalonia.ReactiveUI.xml",
+ "lib/net8.0/Avalonia.ReactiveUI.dll",
+ "lib/net8.0/Avalonia.ReactiveUI.xml",
+ "lib/netstandard2.0/Avalonia.ReactiveUI.dll",
+ "lib/netstandard2.0/Avalonia.ReactiveUI.xml"
+ ]
+ },
"Avalonia.Remote.Protocol/11.3.13": {
"sha512": "ATDouTcCn484dUxmvzmyuOfNpwWxOKTz8LbBNnHpMZFCtyzZ0b4gvfZXh/d5szyRIF+kaTPK97LMQ5UH8ZQNhg==",
"type": "package",
@@ -2140,46 +2263,6 @@
"readme.txt"
]
},
- "CommunityToolkit.Mvvm/8.4.2": {
- "sha512": "WadCzGEc2U+3e20avRLng4qNtt4zoOGWrdUISqJWrHe3/FSnrYjuM5Sb4yQb09LhkBXrrI4Zt3dLKgRMbItsrg==",
- "type": "package",
- "path": "communitytoolkit.mvvm/8.4.2",
- "files": [
- ".nupkg.metadata",
- ".signature.p7s",
- "Icon.png",
- "License.md",
- "ThirdPartyNotices.txt",
- "analyzers/dotnet/roslyn4.0/cs/CommunityToolkit.Mvvm.CodeFixers.dll",
- "analyzers/dotnet/roslyn4.0/cs/CommunityToolkit.Mvvm.SourceGenerators.dll",
- "analyzers/dotnet/roslyn4.12/cs/CommunityToolkit.Mvvm.CodeFixers.dll",
- "analyzers/dotnet/roslyn4.12/cs/CommunityToolkit.Mvvm.SourceGenerators.dll",
- "analyzers/dotnet/roslyn4.3/cs/CommunityToolkit.Mvvm.CodeFixers.dll",
- "analyzers/dotnet/roslyn4.3/cs/CommunityToolkit.Mvvm.SourceGenerators.dll",
- "analyzers/dotnet/roslyn5.0/cs/CommunityToolkit.Mvvm.CodeFixers.dll",
- "analyzers/dotnet/roslyn5.0/cs/CommunityToolkit.Mvvm.SourceGenerators.dll",
- "build/CommunityToolkit.Mvvm.FeatureSwitches.targets",
- "build/CommunityToolkit.Mvvm.SourceGenerators.targets",
- "build/CommunityToolkit.Mvvm.Windows.targets",
- "build/CommunityToolkit.Mvvm.WindowsSdk.targets",
- "build/CommunityToolkit.Mvvm.targets",
- "buildTransitive/CommunityToolkit.Mvvm.FeatureSwitches.targets",
- "buildTransitive/CommunityToolkit.Mvvm.SourceGenerators.targets",
- "buildTransitive/CommunityToolkit.Mvvm.Windows.targets",
- "buildTransitive/CommunityToolkit.Mvvm.WindowsSdk.targets",
- "buildTransitive/CommunityToolkit.Mvvm.targets",
- "communitytoolkit.mvvm.8.4.2.nupkg.sha512",
- "communitytoolkit.mvvm.nuspec",
- "lib/net8.0-windows10.0.17763/CommunityToolkit.Mvvm.dll",
- "lib/net8.0-windows10.0.17763/CommunityToolkit.Mvvm.xml",
- "lib/net8.0/CommunityToolkit.Mvvm.dll",
- "lib/net8.0/CommunityToolkit.Mvvm.xml",
- "lib/netstandard2.0/CommunityToolkit.Mvvm.dll",
- "lib/netstandard2.0/CommunityToolkit.Mvvm.xml",
- "lib/netstandard2.1/CommunityToolkit.Mvvm.dll",
- "lib/netstandard2.1/CommunityToolkit.Mvvm.xml"
- ]
- },
"DiffEngine/11.3.0": {
"sha512": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==",
"type": "package",
@@ -2214,6 +2297,30 @@
"lib/netstandard2.1/DiffEngine.xml"
]
},
+ "DynamicData/8.4.1": {
+ "sha512": "Mn1+fU/jqxgONEJq8KLQPGWEi7g/hUVTbjZyn4QM0sWWDAVOHPO9WjXWORSykwdfg/6S3GM15qsfz+2EvO+QAQ==",
+ "type": "package",
+ "path": "dynamicdata/8.4.1",
+ "files": [
+ ".nupkg.metadata",
+ ".signature.p7s",
+ "LICENSE/LICENSE",
+ "README.md",
+ "dynamicdata.8.4.1.nupkg.sha512",
+ "dynamicdata.nuspec",
+ "lib/net462/DynamicData.dll",
+ "lib/net462/DynamicData.xml",
+ "lib/net6.0/DynamicData.dll",
+ "lib/net6.0/DynamicData.xml",
+ "lib/net7.0/DynamicData.dll",
+ "lib/net7.0/DynamicData.xml",
+ "lib/net8.0/DynamicData.dll",
+ "lib/net8.0/DynamicData.xml",
+ "lib/netstandard2.0/DynamicData.dll",
+ "lib/netstandard2.0/DynamicData.xml",
+ "logo.png"
+ ]
+ },
"EmptyFiles/4.4.0": {
"sha512": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==",
"type": "package",
@@ -2282,6 +2389,39 @@
"lib/netstandard2.1/EmptyFiles.xml"
]
},
+ "Fody/6.8.0": {
+ "sha512": "hfZ/f8Mezt8aTkgv9nsvFdYoQ809/AqwsJlOGOPYIfBcG2aAIG3v3ex9d8ZqQuFYyMoucjRg4eKy3VleeGodKQ==",
+ "type": "package",
+ "path": "fody/6.8.0",
+ "files": [
+ ".nupkg.metadata",
+ ".signature.p7s",
+ "License.txt",
+ "build/Fody.targets",
+ "fody.6.8.0.nupkg.sha512",
+ "fody.nuspec",
+ "netclassictask/Fody.dll",
+ "netclassictask/FodyCommon.dll",
+ "netclassictask/FodyHelpers.dll",
+ "netclassictask/FodyIsolated.dll",
+ "netclassictask/Mono.Cecil.Pdb.dll",
+ "netclassictask/Mono.Cecil.Pdb.pdb",
+ "netclassictask/Mono.Cecil.Rocks.dll",
+ "netclassictask/Mono.Cecil.Rocks.pdb",
+ "netclassictask/Mono.Cecil.dll",
+ "netclassictask/Mono.Cecil.pdb",
+ "netstandardtask/Fody.dll",
+ "netstandardtask/FodyCommon.dll",
+ "netstandardtask/FodyHelpers.dll",
+ "netstandardtask/FodyIsolated.dll",
+ "netstandardtask/Mono.Cecil.Pdb.dll",
+ "netstandardtask/Mono.Cecil.Pdb.pdb",
+ "netstandardtask/Mono.Cecil.Rocks.dll",
+ "netstandardtask/Mono.Cecil.Rocks.pdb",
+ "netstandardtask/Mono.Cecil.dll",
+ "netstandardtask/Mono.Cecil.pdb"
+ ]
+ },
"HarfBuzzSharp/8.3.1.1": {
"sha512": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
"type": "package",
@@ -3289,6 +3429,85 @@
"nsubstitute.nuspec"
]
},
+ "ReactiveUI/20.1.1": {
+ "sha512": "9hNPknWjijnaSWs6auypoXqUptPZcRpUypF+cf1zD50fgW+SEoQda502N3fVZ2eWPcaiUad+z6GaLwOWmUVHNw==",
+ "type": "package",
+ "path": "reactiveui/20.1.1",
+ "files": [
+ ".nupkg.metadata",
+ ".signature.p7s",
+ "LICENSE/LICENSE",
+ "README.md",
+ "lib/net462/ReactiveUI.dll",
+ "lib/net462/ReactiveUI.xml",
+ "lib/net472/ReactiveUI.dll",
+ "lib/net472/ReactiveUI.xml",
+ "lib/net6.0-windows10.0.17763/ReactiveUI.dll",
+ "lib/net6.0-windows10.0.17763/ReactiveUI.xml",
+ "lib/net6.0-windows10.0.19041/ReactiveUI.dll",
+ "lib/net6.0-windows10.0.19041/ReactiveUI.xml",
+ "lib/net6.0/ReactiveUI.dll",
+ "lib/net6.0/ReactiveUI.xml",
+ "lib/net8.0-android34.0/ReactiveUI.dll",
+ "lib/net8.0-android34.0/ReactiveUI.xml",
+ "lib/net8.0-ios17.2/ReactiveUI.dll",
+ "lib/net8.0-ios17.2/ReactiveUI.xml",
+ "lib/net8.0-maccatalyst17.2/ReactiveUI.dll",
+ "lib/net8.0-maccatalyst17.2/ReactiveUI.xml",
+ "lib/net8.0-macos14.2/ReactiveUI.dll",
+ "lib/net8.0-macos14.2/ReactiveUI.xml",
+ "lib/net8.0-tvos17.2/ReactiveUI.dll",
+ "lib/net8.0-tvos17.2/ReactiveUI.xml",
+ "lib/net8.0-windows10.0.17763/ReactiveUI.dll",
+ "lib/net8.0-windows10.0.17763/ReactiveUI.xml",
+ "lib/net8.0-windows10.0.19041/ReactiveUI.dll",
+ "lib/net8.0-windows10.0.19041/ReactiveUI.xml",
+ "lib/net8.0/ReactiveUI.dll",
+ "lib/net8.0/ReactiveUI.xml",
+ "lib/netstandard2.0/ReactiveUI.dll",
+ "lib/netstandard2.0/ReactiveUI.xml",
+ "logo.png",
+ "reactiveui.20.1.1.nupkg.sha512",
+ "reactiveui.nuspec"
+ ]
+ },
+ "ReactiveUI.Fody/19.5.41": {
+ "sha512": "bh6OH1WsSHf/ryUVaPnI6LWl1+lrCvq4L/AoHpzPXCKxtR4p0ogPVONnkq0jYgSQ55x7YNCVkthBKeb6uUSbpg==",
+ "type": "package",
+ "path": "reactiveui.fody/19.5.41",
+ "files": [
+ ".nupkg.metadata",
+ ".signature.p7s",
+ "LICENSE/LICENSE",
+ "README.md",
+ "build/ReactiveUI.Fody.props",
+ "lib/monoandroid13.0/ReactiveUI.Fody.Helpers.dll",
+ "lib/monoandroid13.0/ReactiveUI.Fody.Helpers.pdb",
+ "lib/monoandroid13.0/ReactiveUI.Fody.Helpers.xml",
+ "lib/net462/ReactiveUI.Fody.Helpers.dll",
+ "lib/net462/ReactiveUI.Fody.Helpers.xml",
+ "lib/net472/ReactiveUI.Fody.Helpers.dll",
+ "lib/net472/ReactiveUI.Fody.Helpers.xml",
+ "lib/net6.0/ReactiveUI.Fody.Helpers.dll",
+ "lib/net6.0/ReactiveUI.Fody.Helpers.xml",
+ "lib/net7.0/ReactiveUI.Fody.Helpers.dll",
+ "lib/net7.0/ReactiveUI.Fody.Helpers.xml",
+ "lib/net8.0/ReactiveUI.Fody.Helpers.dll",
+ "lib/net8.0/ReactiveUI.Fody.Helpers.xml",
+ "lib/netstandard2.0/ReactiveUI.Fody.Helpers.dll",
+ "lib/netstandard2.0/ReactiveUI.Fody.Helpers.xml",
+ "lib/xamarinios10/ReactiveUI.Fody.Helpers.dll",
+ "lib/xamarinios10/ReactiveUI.Fody.Helpers.xml",
+ "lib/xamarinmac20/ReactiveUI.Fody.Helpers.dll",
+ "lib/xamarinmac20/ReactiveUI.Fody.Helpers.xml",
+ "lib/xamarintvos10/ReactiveUI.Fody.Helpers.dll",
+ "lib/xamarintvos10/ReactiveUI.Fody.Helpers.xml",
+ "logo.png",
+ "reactiveui.fody.19.5.41.nupkg.sha512",
+ "reactiveui.fody.nuspec",
+ "weaver/ReactiveUI.Fody.dll"
+ ]
+ },
"Serilog/4.3.1": {
"sha512": "savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ==",
"type": "package",
@@ -3588,6 +3807,24 @@
"skiasharp.nativeassets.win32.nuspec"
]
},
+ "Splat/15.1.1": {
+ "sha512": "RHDTdF90FwVbRia2cmuIzkiVoETqnXSB2dDBBi/I35HWXqv4OKGqoMcfcd6obMvO2OmmY5PjU1M62K8LkJafAA==",
+ "type": "package",
+ "path": "splat/15.1.1",
+ "files": [
+ ".nupkg.metadata",
+ ".signature.p7s",
+ "LICENSE/LICENSE",
+ "lib/net6.0/Splat.dll",
+ "lib/net6.0/Splat.xml",
+ "lib/net8.0/Splat.dll",
+ "lib/net8.0/Splat.xml",
+ "lib/netstandard2.0/Splat.dll",
+ "lib/netstandard2.0/Splat.xml",
+ "splat.15.1.1.nupkg.sha512",
+ "splat.nuspec"
+ ]
+ },
"SQLitePCLRaw.bundle_e_sqlite3/2.1.11": {
"sha512": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==",
"type": "package",
@@ -3705,6 +3942,96 @@
"useSharedDesignerContext.txt"
]
},
+ "System.ComponentModel.Annotations/5.0.0": {
+ "sha512": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==",
+ "type": "package",
+ "path": "system.componentmodel.annotations/5.0.0",
+ "files": [
+ ".nupkg.metadata",
+ ".signature.p7s",
+ "Icon.png",
+ "LICENSE.TXT",
+ "THIRD-PARTY-NOTICES.TXT",
+ "lib/MonoAndroid10/_._",
+ "lib/MonoTouch10/_._",
+ "lib/net45/_._",
+ "lib/net461/System.ComponentModel.Annotations.dll",
+ "lib/netcore50/System.ComponentModel.Annotations.dll",
+ "lib/netstandard1.4/System.ComponentModel.Annotations.dll",
+ "lib/netstandard2.0/System.ComponentModel.Annotations.dll",
+ "lib/netstandard2.1/System.ComponentModel.Annotations.dll",
+ "lib/netstandard2.1/System.ComponentModel.Annotations.xml",
+ "lib/portable-net45+win8/_._",
+ "lib/win8/_._",
+ "lib/xamarinios10/_._",
+ "lib/xamarinmac20/_._",
+ "lib/xamarintvos10/_._",
+ "lib/xamarinwatchos10/_._",
+ "ref/MonoAndroid10/_._",
+ "ref/MonoTouch10/_._",
+ "ref/net45/_._",
+ "ref/net461/System.ComponentModel.Annotations.dll",
+ "ref/net461/System.ComponentModel.Annotations.xml",
+ "ref/netcore50/System.ComponentModel.Annotations.dll",
+ "ref/netcore50/System.ComponentModel.Annotations.xml",
+ "ref/netcore50/de/System.ComponentModel.Annotations.xml",
+ "ref/netcore50/es/System.ComponentModel.Annotations.xml",
+ "ref/netcore50/fr/System.ComponentModel.Annotations.xml",
+ "ref/netcore50/it/System.ComponentModel.Annotations.xml",
+ "ref/netcore50/ja/System.ComponentModel.Annotations.xml",
+ "ref/netcore50/ko/System.ComponentModel.Annotations.xml",
+ "ref/netcore50/ru/System.ComponentModel.Annotations.xml",
+ "ref/netcore50/zh-hans/System.ComponentModel.Annotations.xml",
+ "ref/netcore50/zh-hant/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.1/System.ComponentModel.Annotations.dll",
+ "ref/netstandard1.1/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.1/de/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.1/es/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.1/fr/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.1/it/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.1/ja/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.1/ko/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.1/ru/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.1/zh-hans/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.1/zh-hant/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.3/System.ComponentModel.Annotations.dll",
+ "ref/netstandard1.3/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.3/de/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.3/es/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.3/fr/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.3/it/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.3/ja/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.3/ko/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.3/ru/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.3/zh-hans/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.3/zh-hant/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.4/System.ComponentModel.Annotations.dll",
+ "ref/netstandard1.4/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.4/de/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.4/es/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.4/fr/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.4/it/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.4/ja/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.4/ko/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.4/ru/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.4/zh-hans/System.ComponentModel.Annotations.xml",
+ "ref/netstandard1.4/zh-hant/System.ComponentModel.Annotations.xml",
+ "ref/netstandard2.0/System.ComponentModel.Annotations.dll",
+ "ref/netstandard2.0/System.ComponentModel.Annotations.xml",
+ "ref/netstandard2.1/System.ComponentModel.Annotations.dll",
+ "ref/netstandard2.1/System.ComponentModel.Annotations.xml",
+ "ref/portable-net45+win8/_._",
+ "ref/win8/_._",
+ "ref/xamarinios10/_._",
+ "ref/xamarinmac20/_._",
+ "ref/xamarintvos10/_._",
+ "ref/xamarinwatchos10/_._",
+ "system.componentmodel.annotations.5.0.0.nupkg.sha512",
+ "system.componentmodel.annotations.nuspec",
+ "useSharedDesignerContext.txt",
+ "version.txt"
+ ]
+ },
"System.Diagnostics.EventLog/6.0.0": {
"sha512": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==",
"type": "package",
@@ -3764,6 +4091,34 @@
"useSharedDesignerContext.txt"
]
},
+ "System.Reactive/6.0.1": {
+ "sha512": "rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg==",
+ "type": "package",
+ "path": "system.reactive/6.0.1",
+ "files": [
+ ".nupkg.metadata",
+ ".signature.p7s",
+ "build/net6.0-windows10.0.19041/_._",
+ "build/net6.0/_._",
+ "buildTransitive/net6.0-windows10.0.19041/_._",
+ "buildTransitive/net6.0/_._",
+ "icon.png",
+ "lib/net472/System.Reactive.dll",
+ "lib/net472/System.Reactive.xml",
+ "lib/net6.0-windows10.0.19041/System.Reactive.dll",
+ "lib/net6.0-windows10.0.19041/System.Reactive.xml",
+ "lib/net6.0/System.Reactive.dll",
+ "lib/net6.0/System.Reactive.xml",
+ "lib/netstandard2.0/System.Reactive.dll",
+ "lib/netstandard2.0/System.Reactive.xml",
+ "lib/uap10.0.18362/System.Reactive.dll",
+ "lib/uap10.0.18362/System.Reactive.pri",
+ "lib/uap10.0.18362/System.Reactive.xml",
+ "readme.md",
+ "system.reactive.6.0.1.nupkg.sha512",
+ "system.reactive.nuspec"
+ ]
+ },
"Tmds.DBus.Protocol/0.21.2": {
"sha512": "ScSMrUrrw8px4kK1Glh0fZv/HQUlg1078bNXNPfRPKQ3WbRzV9HpsydYEOgSoMK5LWICMf2bMwIFH0pGjxjcMA==",
"type": "package",
diff --git a/tests/AStar.Dev.File.App.Tests.Unit/obj/project.nuget.cache b/tests/AStar.Dev.File.App.Tests.Unit/obj/project.nuget.cache
index 52daf34..6eddc42 100644
--- a/tests/AStar.Dev.File.App.Tests.Unit/obj/project.nuget.cache
+++ b/tests/AStar.Dev.File.App.Tests.Unit/obj/project.nuget.cache
@@ -1,6 +1,6 @@
{
"version": 2,
- "dgSpecHash": "h9CpUWNnkPE=",
+ "dgSpecHash": "svp4DAH3jrI=",
"success": true,
"projectFilePath": "/home/jbarden/repos/astar-dev-file-app/tests/AStar.Dev.File.App.Tests.Unit/AStar.Dev.File.App.Tests.Unit.csproj",
"expectedPackageFiles": [
@@ -14,6 +14,7 @@
"/home/jbarden/.nuget/packages/avalonia.fonts.inter/11.3.13/avalonia.fonts.inter.11.3.13.nupkg.sha512",
"/home/jbarden/.nuget/packages/avalonia.freedesktop/11.3.13/avalonia.freedesktop.11.3.13.nupkg.sha512",
"/home/jbarden/.nuget/packages/avalonia.native/11.3.13/avalonia.native.11.3.13.nupkg.sha512",
+ "/home/jbarden/.nuget/packages/avalonia.reactiveui/11.3.9/avalonia.reactiveui.11.3.9.nupkg.sha512",
"/home/jbarden/.nuget/packages/avalonia.remote.protocol/11.3.13/avalonia.remote.protocol.11.3.13.nupkg.sha512",
"/home/jbarden/.nuget/packages/avalonia.skia/11.3.13/avalonia.skia.11.3.13.nupkg.sha512",
"/home/jbarden/.nuget/packages/avalonia.themes.fluent/11.3.13/avalonia.themes.fluent.11.3.13.nupkg.sha512",
@@ -21,9 +22,10 @@
"/home/jbarden/.nuget/packages/avalonia.win32/11.3.13/avalonia.win32.11.3.13.nupkg.sha512",
"/home/jbarden/.nuget/packages/avalonia.x11/11.3.13/avalonia.x11.11.3.13.nupkg.sha512",
"/home/jbarden/.nuget/packages/castle.core/5.1.1/castle.core.5.1.1.nupkg.sha512",
- "/home/jbarden/.nuget/packages/communitytoolkit.mvvm/8.4.2/communitytoolkit.mvvm.8.4.2.nupkg.sha512",
"/home/jbarden/.nuget/packages/diffengine/11.3.0/diffengine.11.3.0.nupkg.sha512",
+ "/home/jbarden/.nuget/packages/dynamicdata/8.4.1/dynamicdata.8.4.1.nupkg.sha512",
"/home/jbarden/.nuget/packages/emptyfiles/4.4.0/emptyfiles.4.4.0.nupkg.sha512",
+ "/home/jbarden/.nuget/packages/fody/6.8.0/fody.6.8.0.nupkg.sha512",
"/home/jbarden/.nuget/packages/harfbuzzsharp/8.3.1.1/harfbuzzsharp.8.3.1.1.nupkg.sha512",
"/home/jbarden/.nuget/packages/harfbuzzsharp.nativeassets.linux/8.3.1.1/harfbuzzsharp.nativeassets.linux.8.3.1.1.nupkg.sha512",
"/home/jbarden/.nuget/packages/harfbuzzsharp.nativeassets.macos/8.3.1.1/harfbuzzsharp.nativeassets.macos.8.3.1.1.nupkg.sha512",
@@ -56,6 +58,8 @@
"/home/jbarden/.nuget/packages/microsoft.testing.platform.msbuild/2.0.2/microsoft.testing.platform.msbuild.2.0.2.nupkg.sha512",
"/home/jbarden/.nuget/packages/microsoft.win32.registry/5.0.0/microsoft.win32.registry.5.0.0.nupkg.sha512",
"/home/jbarden/.nuget/packages/nsubstitute/5.3.0/nsubstitute.5.3.0.nupkg.sha512",
+ "/home/jbarden/.nuget/packages/reactiveui/20.1.1/reactiveui.20.1.1.nupkg.sha512",
+ "/home/jbarden/.nuget/packages/reactiveui.fody/19.5.41/reactiveui.fody.19.5.41.nupkg.sha512",
"/home/jbarden/.nuget/packages/serilog/4.3.1/serilog.4.3.1.nupkg.sha512",
"/home/jbarden/.nuget/packages/serilog.extensions.logging/10.0.0/serilog.extensions.logging.10.0.0.nupkg.sha512",
"/home/jbarden/.nuget/packages/serilog.sinks.console/6.1.1/serilog.sinks.console.6.1.1.nupkg.sha512",
@@ -66,13 +70,16 @@
"/home/jbarden/.nuget/packages/skiasharp.nativeassets.macos/2.88.9/skiasharp.nativeassets.macos.2.88.9.nupkg.sha512",
"/home/jbarden/.nuget/packages/skiasharp.nativeassets.webassembly/2.88.9/skiasharp.nativeassets.webassembly.2.88.9.nupkg.sha512",
"/home/jbarden/.nuget/packages/skiasharp.nativeassets.win32/2.88.9/skiasharp.nativeassets.win32.2.88.9.nupkg.sha512",
+ "/home/jbarden/.nuget/packages/splat/15.1.1/splat.15.1.1.nupkg.sha512",
"/home/jbarden/.nuget/packages/sqlitepclraw.bundle_e_sqlite3/2.1.11/sqlitepclraw.bundle_e_sqlite3.2.1.11.nupkg.sha512",
"/home/jbarden/.nuget/packages/sqlitepclraw.core/2.1.11/sqlitepclraw.core.2.1.11.nupkg.sha512",
"/home/jbarden/.nuget/packages/sqlitepclraw.lib.e_sqlite3/2.1.11/sqlitepclraw.lib.e_sqlite3.2.1.11.nupkg.sha512",
"/home/jbarden/.nuget/packages/sqlitepclraw.provider.e_sqlite3/2.1.11/sqlitepclraw.provider.e_sqlite3.2.1.11.nupkg.sha512",
"/home/jbarden/.nuget/packages/system.codedom/6.0.0/system.codedom.6.0.0.nupkg.sha512",
+ "/home/jbarden/.nuget/packages/system.componentmodel.annotations/5.0.0/system.componentmodel.annotations.5.0.0.nupkg.sha512",
"/home/jbarden/.nuget/packages/system.diagnostics.eventlog/6.0.0/system.diagnostics.eventlog.6.0.0.nupkg.sha512",
"/home/jbarden/.nuget/packages/system.management/6.0.1/system.management.6.0.1.nupkg.sha512",
+ "/home/jbarden/.nuget/packages/system.reactive/6.0.1/system.reactive.6.0.1.nupkg.sha512",
"/home/jbarden/.nuget/packages/tmds.dbus.protocol/0.21.2/tmds.dbus.protocol.0.21.2.nupkg.sha512",
"/home/jbarden/.nuget/packages/xunit.analyzers/1.27.0/xunit.analyzers.1.27.0.nupkg.sha512",
"/home/jbarden/.nuget/packages/xunit.v3.assert/3.2.2/xunit.v3.assert.3.2.2.nupkg.sha512",