diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..69c5913 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,19 @@ +{ + "cSpell.words": [ + "ALLOWUNDO", + "CONFIRMMOUSE", + "FILESONLY", + "lpsz", + "MULTIDESTFILES", + "NOCONFIRMATION", + "NOCONFIRMMKDIR", + "NOCOPYSECURITYATTRIBS", + "NOERRORUI", + "NORECURSION", + "RENAMEONCOLLISION", + "SHFILEOPSTRUCT", + "SIMPLEPROGRESS", + "SYSLIB", + "WANTMAPPINGHANDLE" + ] +} \ No newline at end of file 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 eeddeef..a0590be 100644 --- a/src/AStar.Dev.File.App/AStar.Dev.File.App.csproj +++ b/src/AStar.Dev.File.App/AStar.Dev.File.App.csproj @@ -1,4 +1,5 @@  + WinExe net10.0 @@ -9,27 +10,32 @@ - - - - - - - + + + + + + + + + + + None All - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + + diff --git a/src/AStar.Dev.File.App/App.axaml.cs b/src/AStar.Dev.File.App/App.axaml.cs index 44e4649..7b1962d 100644 --- a/src/AStar.Dev.File.App/App.axaml.cs +++ b/src/AStar.Dev.File.App/App.axaml.cs @@ -7,13 +7,22 @@ using Avalonia.Markup.Xaml; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Serilog; using System; using System.IO; +using Microsoft.Extensions.Logging; +using Serilog.Events; + +using MelILogger = Microsoft.Extensions.Logging.ILogger; +using System.Globalization; namespace AStar.Dev.File.App; public partial class App : Application { + private const string ApplicationName = "AStar.Dev.File.App"; + private static readonly string _appVersion = typeof(App).Assembly.GetName().Version?.ToString() ?? "unknown"; + private const int _logRetentionDays = 7; private IServiceProvider? _services; public IServiceProvider? Services => _services; @@ -27,9 +36,9 @@ public override void Initialize() public override void OnFrameworkInitializationCompleted() { + ConfigureSerilog(); _services = BuildServices(); - // Apply EF migrations on startup var factory = _services.GetRequiredService>(); using var ctx = factory.CreateDbContext(); ctx.Database.Migrate(); @@ -47,24 +56,47 @@ public override void OnFrameworkInitializationCompleted() private static IServiceProvider BuildServices() { - var dbPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "AStar.Dev.File.App", - "files.db"); + var dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ApplicationName, "files.db"); Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!); var services = new ServiceCollection(); - services.AddDbContextFactory(options => - options.UseSqlite($"Data Source={dbPath}")); + services.AddDbContextFactory(options => options.UseSqlite($"Data Source={dbPath}")); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); + _ = services.AddLogging(logging => logging.AddSerilog(dispose: true)); + + var serviceProvider = services.BuildServiceProvider(); + var logger = serviceProvider.GetRequiredService>(); + LogAppStarting(logger, _appVersion); + + return serviceProvider; + } - return services.BuildServiceProvider(); + private static void ConfigureSerilog() + { + string logDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ApplicationName, "logs"); + + _ = Directory.CreateDirectory(logDirectory); + + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning) + .WriteTo.Console(formatProvider: CultureInfo.InvariantCulture) + .WriteTo.File( + path: Path.Combine(logDirectory, "app.log"), + formatProvider: CultureInfo.InvariantCulture, + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: _logRetentionDays) + .CreateLogger(); } + + [LoggerMessage(Level = LogLevel.Information, Message = "Application starting — version {AppVersion}")] + private static partial void LogAppStarting(MelILogger logger, string appVersion); } \ No newline at end of file diff --git a/src/AStar.Dev.File.App/Services/FileDeleteService.cs b/src/AStar.Dev.File.App/Services/FileDeleteService.cs index a035a19..2c3f5e1 100644 --- a/src/AStar.Dev.File.App/Services/FileDeleteService.cs +++ b/src/AStar.Dev.File.App/Services/FileDeleteService.cs @@ -1,19 +1,12 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; namespace AStar.Dev.File.App.Services; -public interface IFileDeleteService -{ - Task DeleteFileAsync(string filePath, bool moveToRecycleBin = true); - Task DeleteFilesAsync(IEnumerable filePaths, bool moveToRecycleBin = true); -} - public class FileDeleteService : IFileDeleteService { public async Task DeleteFileAsync(string filePath, bool moveToRecycleBin = true) @@ -34,15 +27,15 @@ await Task.Run(() => { if (moveToRecycleBin) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (OperatingSystem.IsWindows()) { MoveFilesToRecycleBinWindows(files); } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + else if (OperatingSystem.IsLinux()) { MoveFilesToTrashLinux(files); } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + else if (OperatingSystem.IsMacOS()) { MoveFilesToTrashMacOS(files); } @@ -68,7 +61,7 @@ private void PermanentlyDeleteFiles(IEnumerable filePaths) } catch (Exception ex) { - System.Diagnostics.Debug.WriteLine($"Failed to delete {file}: {ex.Message}"); + Debug.WriteLine($"Failed to delete {file}: {ex.Message}"); } } } @@ -96,13 +89,13 @@ private void MoveFilesToTrashLinux(IEnumerable filePaths) if (process.ExitCode != 0) { - System.Diagnostics.Debug.WriteLine("gio trash failed, falling back to permanent delete"); + Debug.WriteLine("gio trash failed, falling back to permanent delete"); PermanentlyDeleteFiles(filePaths); } } catch (Exception ex) { - System.Diagnostics.Debug.WriteLine($"gio trash not available: {ex.Message}. Falling back to permanent delete."); + Debug.WriteLine($"gio trash not available: {ex.Message}. Falling back to permanent delete."); PermanentlyDeleteFiles(filePaths); } } @@ -130,7 +123,7 @@ private void MoveFilesToTrashMacOS(IEnumerable filePaths) } catch (Exception ex) { - System.Diagnostics.Debug.WriteLine($"macOS trash failed: {ex.Message}"); + Debug.WriteLine($"macOS trash failed: {ex.Message}"); PermanentlyDeleteFiles(filePaths); } } @@ -147,17 +140,25 @@ private void MoveFilesToRecycleBinWindows(IEnumerable filePaths) try { - SHFileOperation(ref fileOp); + int result = SHFileOperation(ref fileOp); + + if (result != 0) + { + Debug.WriteLine($"Shell delete failed with code {result}. Falling back to permanent delete."); + PermanentlyDeleteFiles(filePaths); + } } catch (Exception ex) { - System.Diagnostics.Debug.WriteLine($"Shell delete failed: {ex.Message}. Falling back to permanent delete."); + Debug.WriteLine($"Shell delete failed: {ex.Message}. Falling back to permanent delete."); PermanentlyDeleteFiles(filePaths); } } [DllImport("shell32.dll", CharSet = CharSet.Auto)] +#pragma warning disable SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time private static extern int SHFileOperation(ref SHFILEOPSTRUCT lpFileOp); +#pragma warning restore SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] private struct SHFILEOPSTRUCT diff --git a/src/AStar.Dev.File.App/Services/FileScannerService.cs b/src/AStar.Dev.File.App/Services/FileScannerService.cs index 379961b..22c2eba 100644 --- a/src/AStar.Dev.File.App/Services/FileScannerService.cs +++ b/src/AStar.Dev.File.App/Services/FileScannerService.cs @@ -24,7 +24,6 @@ public async Task ScanAsync(string rootPath, IProgress progr await RecurseDirectory(rootPath, rootPath, progress, counter, ct); - // Only mark missing files when scan ran to completion (not cancelled) await using var db = await dbContextFactory.CreateDbContextAsync(ct); await db.ScannedFiles .Where(f => f.RootPath == rootPath && f.LastScannedAt < scanStartedAt) @@ -46,11 +45,7 @@ private async Task RecurseDirectory( CancellationToken ct) { var time = DateTime.Now.ToString("HH:mm:ss"); - progress.Report(new ScanProgressUpdate( - CurrentFolder: directory, - TotalFilesProcessed: counter.Value, - CurrentFileName: null, - StatusMessage: $"[{time}] Scanning: {directory}")); + progress.Report(new ScanProgressUpdate(CurrentFolder: directory, TotalFilesProcessed: counter.Value, CurrentFileName: null, StatusMessage: $"[{time}] Scanning: {directory}")); await using var db = await dbContextFactory.CreateDbContextAsync(ct); @@ -93,7 +88,6 @@ private async Task RecurseDirectory( counter.Value++; - // Report every N files to avoid flooding the UI thread if (counter.Value % ProgressReportInterval == 0) { time = DateTime.Now.ToString("HH:mm:ss"); diff --git a/src/AStar.Dev.File.App/Services/FileTypeClassifier.cs b/src/AStar.Dev.File.App/Services/FileTypeClassifier.cs index 3f16773..79af9b2 100644 --- a/src/AStar.Dev.File.App/Services/FileTypeClassifier.cs +++ b/src/AStar.Dev.File.App/Services/FileTypeClassifier.cs @@ -10,69 +10,107 @@ public class FileTypeClassifier : IFileTypeClassifier new(StringComparer.OrdinalIgnoreCase) { // Image - [".jpg"] = FileType.Image, [".jpeg"] = FileType.Image, - [".png"] = FileType.Image, [".gif"] = FileType.Image, - [".bmp"] = FileType.Image, [".tiff"] = FileType.Image, - [".tif"] = FileType.Image, [".webp"] = FileType.Image, - [".svg"] = FileType.Image, [".heic"] = FileType.Image, - [".raw"] = FileType.Image, [".ico"] = FileType.Image, + [".jpg"] = FileType.Image, + [".jpeg"] = FileType.Image, + [".png"] = FileType.Image, + [".gif"] = FileType.Image, + [".bmp"] = FileType.Image, + [".tiff"] = FileType.Image, + [".tif"] = FileType.Image, + [".webp"] = FileType.Image, + [".svg"] = FileType.Image, + [".heic"] = FileType.Image, + [".raw"] = FileType.Image, + [".ico"] = FileType.Image, [".avif"] = FileType.Image, // Document - [".pdf"] = FileType.Document, [".doc"] = FileType.Document, - [".docx"] = FileType.Document, [".txt"] = FileType.Document, - [".rtf"] = FileType.Document, [".odt"] = FileType.Document, - [".md"] = FileType.Document, [".pages"] = FileType.Document, + [".pdf"] = FileType.Document, + [".doc"] = FileType.Document, + [".docx"] = FileType.Document, + [".txt"] = FileType.Document, + [".rtf"] = FileType.Document, + [".odt"] = FileType.Document, + [".md"] = FileType.Document, + [".pages"] = FileType.Document, [".epub"] = FileType.Document, // Spreadsheet - [".xls"] = FileType.Spreadsheet, [".xlsx"] = FileType.Spreadsheet, - [".csv"] = FileType.Spreadsheet, [".ods"] = FileType.Spreadsheet, + [".xls"] = FileType.Spreadsheet, + [".xlsx"] = FileType.Spreadsheet, + [".csv"] = FileType.Spreadsheet, + [".ods"] = FileType.Spreadsheet, [".numbers"] = FileType.Spreadsheet, // Presentation - [".ppt"] = FileType.Presentation, [".pptx"] = FileType.Presentation, - [".odp"] = FileType.Presentation, [".key"] = FileType.Presentation, + [".ppt"] = FileType.Presentation, + [".pptx"] = FileType.Presentation, + [".odp"] = FileType.Presentation, + [".key"] = FileType.Presentation, // Video - [".mp4"] = FileType.Video, [".avi"] = FileType.Video, - [".mov"] = FileType.Video, [".mkv"] = FileType.Video, - [".wmv"] = FileType.Video, [".flv"] = FileType.Video, - [".webm"] = FileType.Video, [".m4v"] = FileType.Video, + [".mp4"] = FileType.Video, + [".avi"] = FileType.Video, + [".mov"] = FileType.Video, + [".mkv"] = FileType.Video, + [".wmv"] = FileType.Video, + [".flv"] = FileType.Video, + [".webm"] = FileType.Video, + [".m4v"] = FileType.Video, // Audio - [".mp3"] = FileType.Audio, [".wav"] = FileType.Audio, - [".flac"] = FileType.Audio, [".aac"] = FileType.Audio, - [".ogg"] = FileType.Audio, [".m4a"] = FileType.Audio, + [".mp3"] = FileType.Audio, + [".wav"] = FileType.Audio, + [".flac"] = FileType.Audio, + [".aac"] = FileType.Audio, + [".ogg"] = FileType.Audio, + [".m4a"] = FileType.Audio, [".wma"] = FileType.Audio, // Archive - [".zip"] = FileType.Archive, [".rar"] = FileType.Archive, - [".7z"] = FileType.Archive, [".tar"] = FileType.Archive, - [".gz"] = FileType.Archive, [".bz2"] = FileType.Archive, + [".zip"] = FileType.Archive, + [".rar"] = FileType.Archive, + [".7z"] = FileType.Archive, + [".tar"] = FileType.Archive, + [".gz"] = FileType.Archive, + [".bz2"] = FileType.Archive, [".xz"] = FileType.Archive, // Code - [".cs"] = FileType.Code, [".py"] = FileType.Code, - [".js"] = FileType.Code, [".ts"] = FileType.Code, - [".java"] = FileType.Code, [".cpp"] = FileType.Code, - [".c"] = FileType.Code, [".h"] = FileType.Code, - [".go"] = FileType.Code, [".rs"] = FileType.Code, - [".rb"] = FileType.Code, [".php"] = FileType.Code, - [".html"] = FileType.Code, [".css"] = FileType.Code, - [".json"] = FileType.Code, [".xml"] = FileType.Code, - [".yaml"] = FileType.Code, [".yml"] = FileType.Code, - [".sh"] = FileType.Code, [".ps1"] = FileType.Code, + [".cs"] = FileType.Code, + [".py"] = FileType.Code, + [".js"] = FileType.Code, + [".ts"] = FileType.Code, + [".java"] = FileType.Code, + [".cpp"] = FileType.Code, + [".c"] = FileType.Code, + [".h"] = FileType.Code, + [".go"] = FileType.Code, + [".rs"] = FileType.Code, + [".rb"] = FileType.Code, + [".php"] = FileType.Code, + [".html"] = FileType.Code, + [".css"] = FileType.Code, + [".json"] = FileType.Code, + [".xml"] = FileType.Code, + [".yaml"] = FileType.Code, + [".yml"] = FileType.Code, + [".sh"] = FileType.Code, + [".ps1"] = FileType.Code, [".sql"] = FileType.Code, // Database - [".db"] = FileType.Database, [".sqlite"] = FileType.Database, - [".sqlite3"] = FileType.Database, [".mdb"] = FileType.Database, + [".db"] = FileType.Database, + [".sqlite"] = FileType.Database, + [".sqlite3"] = FileType.Database, + [".mdb"] = FileType.Database, [".accdb"] = FileType.Database, // Executable - [".exe"] = FileType.Executable, [".dll"] = FileType.Executable, - [".so"] = FileType.Executable, [".dylib"] = FileType.Executable, + [".exe"] = FileType.Executable, + [".dll"] = FileType.Executable, + [".so"] = FileType.Executable, + [".dylib"] = FileType.Executable, }; public FileType Classify(string fileExtension) diff --git a/src/AStar.Dev.File.App/Services/FileViewerService.cs b/src/AStar.Dev.File.App/Services/FileViewerService.cs new file mode 100644 index 0000000..86e5a68 --- /dev/null +++ b/src/AStar.Dev.File.App/Services/FileViewerService.cs @@ -0,0 +1,43 @@ +using AStar.Dev.File.App.Data; +using AStar.Dev.File.App.ViewModels; +using Microsoft.EntityFrameworkCore; +using System; +using System.Threading.Tasks; + +namespace AStar.Dev.File.App.Services; + +/// +/// Service responsible for file viewing operations, including updating view history. +/// +public class FileViewerService : IFileViewerService +{ + private readonly IDbContextFactory _dbContextFactory; + + public event Action? FileViewRequested; + + public FileViewerService(IDbContextFactory dbContextFactory) + { + _dbContextFactory = dbContextFactory; + } + + /// + /// Processes a file view request by updating the last viewed timestamp in the database + /// and raising the FileViewRequested event. + /// + /// The file to view. If null, this method returns without action. + public async Task ViewFileAsync(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.LastViewed = DateTime.UtcNow; + await db.SaveChangesAsync(); + } + + FileViewRequested?.Invoke(item); + } +} diff --git a/src/AStar.Dev.File.App/Services/IFileDeleteService.cs b/src/AStar.Dev.File.App/Services/IFileDeleteService.cs new file mode 100644 index 0000000..6498226 --- /dev/null +++ b/src/AStar.Dev.File.App/Services/IFileDeleteService.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace AStar.Dev.File.App.Services; + +public interface IFileDeleteService +{ + Task DeleteFileAsync(string filePath, bool moveToRecycleBin = true); + Task DeleteFilesAsync(IEnumerable filePaths, bool moveToRecycleBin = true); +} diff --git a/src/AStar.Dev.File.App/Services/IFileViewerService.cs b/src/AStar.Dev.File.App/Services/IFileViewerService.cs new file mode 100644 index 0000000..274b137 --- /dev/null +++ b/src/AStar.Dev.File.App/Services/IFileViewerService.cs @@ -0,0 +1,22 @@ +using AStar.Dev.File.App.ViewModels; +using System.Threading.Tasks; + +namespace AStar.Dev.File.App.Services; + +/// +/// Service for handling file viewing operations including updating view history. +/// +public interface IFileViewerService +{ + /// + /// Raised when a file is requested to be viewed. + /// + event System.Action? FileViewRequested; + + /// + /// Processes a file view request, updating the last viewed timestamp and raising the view event. + /// + /// The file to view + /// A task representing the asynchronous operation + Task ViewFileAsync(ScannedFileDisplayItem? item); +} diff --git a/src/AStar.Dev.File.App/Services/ScanProgressUpdate.cs b/src/AStar.Dev.File.App/Services/ScanProgressUpdate.cs index 6b480b6..f18d1c6 100644 --- a/src/AStar.Dev.File.App/Services/ScanProgressUpdate.cs +++ b/src/AStar.Dev.File.App/Services/ScanProgressUpdate.cs @@ -1,7 +1,3 @@ namespace AStar.Dev.File.App.Services; -public record ScanProgressUpdate( - string CurrentFolder, - int TotalFilesProcessed, - string? CurrentFileName, - string StatusMessage); +public record ScanProgressUpdate(string CurrentFolder, int TotalFilesProcessed, string? CurrentFileName, string StatusMessage); diff --git a/src/AStar.Dev.File.App/ViewLocator.cs b/src/AStar.Dev.File.App/ViewLocator.cs index a1b1627..153acdb 100644 --- a/src/AStar.Dev.File.App/ViewLocator.cs +++ b/src/AStar.Dev.File.App/ViewLocator.cs @@ -17,16 +17,16 @@ public class ViewLocator : IDataTemplate public Control? Build(object? param) { if (param is null) - return null; - + return null; + var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); var type = Type.GetType(name); if (type != null) { return (Control)Activator.CreateInstance(type)!; - } - + } + return new TextBlock { Text = "Not Found: " + name }; } diff --git a/src/AStar.Dev.File.App/ViewModels/DeletePendingViewModel.cs b/src/AStar.Dev.File.App/ViewModels/DeletePendingViewModel.cs index f4bbd2b..e509385 100644 --- a/src/AStar.Dev.File.App/ViewModels/DeletePendingViewModel.cs +++ b/src/AStar.Dev.File.App/ViewModels/DeletePendingViewModel.cs @@ -1,11 +1,9 @@ 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.Linq; using System.Threading.Tasks; @@ -16,6 +14,7 @@ public partial class DeletePendingViewModel : ViewModelBase { private readonly IDbContextFactory _dbContextFactory; private readonly IFileDeleteService _fileDeleteService; + private readonly IFileViewerService _fileViewerService; [ObservableProperty] private bool _isDeleting; @@ -28,12 +27,18 @@ public partial class DeletePendingViewModel : ViewModelBase public ObservableCollection PendingDeleteFiles { get; } = []; + public event Action? ViewFileRequested; + public DeletePendingViewModel( IDbContextFactory dbContextFactory, - IFileDeleteService fileDeleteService) + IFileDeleteService fileDeleteService, + IFileViewerService fileViewerService) { _dbContextFactory = dbContextFactory; _fileDeleteService = fileDeleteService; + _fileViewerService = fileViewerService; + _fileViewerService.FileViewRequested += item => ViewFileRequested?.Invoke(item); + _ = LoadPendingFilesAsync(); } @@ -111,6 +116,12 @@ private async Task ClearMarkings() await LoadPendingFilesAsync(); } + [RelayCommand] + private async Task ViewFile(ScannedFileDisplayItem? item) + { + await _fileViewerService.ViewFileAsync(item); + } + private async Task LoadPendingFilesAsync() { try diff --git a/src/AStar.Dev.File.App/ViewModels/MainWindowViewModel.cs b/src/AStar.Dev.File.App/ViewModels/MainWindowViewModel.cs index 8df6c80..6decac8 100644 --- a/src/AStar.Dev.File.App/ViewModels/MainWindowViewModel.cs +++ b/src/AStar.Dev.File.App/ViewModels/MainWindowViewModel.cs @@ -18,9 +18,9 @@ public partial class MainWindowViewModel : ViewModelBase { private readonly IFileScannerService _fileScannerService; private readonly IFolderPickerService _folderPickerService; + private readonly IFileViewerService _fileViewerService; private readonly IDbContextFactory _dbContextFactory; private CancellationTokenSource? _cts; - // Guards against cascading reloads when programmatically resetting CurrentPage private bool _suppressPageReload; public static IReadOnlyList PageSizes { get; } = [25, 50, 75, 100, 125, 150, 175, 200]; @@ -104,11 +104,17 @@ private void ToggleDuplicatesOnly() public MainWindowViewModel( IFileScannerService fileScannerService, IFolderPickerService folderPickerService, + IFileViewerService fileViewerService, IDbContextFactory dbContextFactory) { _fileScannerService = fileScannerService; _folderPickerService = folderPickerService; - _dbContextFactory = dbContextFactory; + _fileViewerService = fileViewerService; + _dbContextFactory = dbContextFactory; + + // Subscribe to file viewer service events + _fileViewerService.FileViewRequested += item => ViewFileRequested?.Invoke(item); + _ = InitializeAsync(); } @@ -235,25 +241,14 @@ private async Task TogglePendingDelete(ScannedFileDisplayItem? item) { file.PendingDelete = !file.PendingDelete; await db.SaveChangesAsync(); + item.PendingDelete = file.PendingDelete; } - - await LoadScannedFilesAsync(); } [RelayCommand] private async Task ViewFile(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.LastViewed = DateTime.UtcNow; - await db.SaveChangesAsync(); - } - - ViewFileRequested?.Invoke(item); + await _fileViewerService.ViewFileAsync(item); } [RelayCommand(CanExecute = nameof(IsScanning))] @@ -340,7 +335,7 @@ private async Task LoadScannedFilesAsync() catch (Exception ex) { StatusMessages.Add($"Error loading files: {ex.Message}"); - } + } } private void ClampCurrentPage() diff --git a/src/AStar.Dev.File.App/ViewModels/ScannedFileDisplayItem.cs b/src/AStar.Dev.File.App/ViewModels/ScannedFileDisplayItem.cs index 9784ae4..617b565 100644 --- a/src/AStar.Dev.File.App/ViewModels/ScannedFileDisplayItem.cs +++ b/src/AStar.Dev.File.App/ViewModels/ScannedFileDisplayItem.cs @@ -1,10 +1,11 @@ using AStar.Dev.File.App.Models; -using System; using System.IO; namespace AStar.Dev.File.App.ViewModels; -public class ScannedFileDisplayItem +using System.ComponentModel; + +public class ScannedFileDisplayItem : INotifyPropertyChanged { public int Id { get; } public string FullPath { get; } @@ -17,7 +18,20 @@ public class ScannedFileDisplayItem public string FileType { get; } public string LastModified { get; } public string LastViewed { get; } - public bool PendingDelete { get; } + + private bool _pendingDelete; + public bool PendingDelete + { + get => _pendingDelete; + set + { + if (_pendingDelete != value) + { + _pendingDelete = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PendingDelete))); + } + } + } public ScannedFileDisplayItem(ScannedFile file) { @@ -34,9 +48,11 @@ 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/Views/DeletePendingWindow.axaml b/src/AStar.Dev.File.App/Views/DeletePendingWindow.axaml index 505c89c..e73f514 100644 --- a/src/AStar.Dev.File.App/Views/DeletePendingWindow.axaml +++ b/src/AStar.Dev.File.App/Views/DeletePendingWindow.axaml @@ -72,21 +72,27 @@ - - - - - - + @@ -106,15 +112,23 @@ Width="110" /> - + -