Skip to content
This repository was archived by the owner on Apr 2, 2026. It is now read-only.

Commit 031fba6

Browse files
authored
Feature/delete UI updates (#4)
* refactor: remove unused using directives and simplify constructors * feat: add FileViewerService and integrate file viewing functionality * got a little carried away... cleanup, logging and delete ui updates...
1 parent 4d1d9c5 commit 031fba6

31 files changed

Lines changed: 1410 additions & 433 deletions

.vscode/settings.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"cSpell.words": [
3+
"ALLOWUNDO",
4+
"CONFIRMMOUSE",
5+
"FILESONLY",
6+
"lpsz",
7+
"MULTIDESTFILES",
8+
"NOCONFIRMATION",
9+
"NOCONFIRMMKDIR",
10+
"NOCOPYSECURITYATTRIBS",
11+
"NOERRORUI",
12+
"NORECURSION",
13+
"RENAMEONCOLLISION",
14+
"SHFILEOPSTRUCT",
15+
"SIMPLEPROGRESS",
16+
"SYSLIB",
17+
"WANTMAPPINGHANDLE"
18+
]
19+
}
Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<Project Sdk="Microsoft.NET.Sdk">
2+
23
<PropertyGroup>
34
<OutputType>WinExe</OutputType>
45
<TargetFramework>net10.0</TargetFramework>
@@ -9,27 +10,32 @@
910
</PropertyGroup>
1011

1112
<ItemGroup>
12-
1313
<AvaloniaResource Include="Assets\**" />
1414
</ItemGroup>
1515

1616
<ItemGroup>
17-
<PackageReference Include="Avalonia" Version="11.3.12" />
18-
<PackageReference Include="Avalonia.Desktop" Version="11.3.12" />
19-
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.12" />
20-
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.12" />
21-
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
22-
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.12">
17+
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.5" />
18+
<PackageReference Include="Serilog" Version="4.3.1" />
19+
<PackageReference Include="Serilog.Extensions.Logging" Version="10.0.0" />
20+
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
21+
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
22+
23+
<PackageReference Include="Avalonia" Version="11.3.13" />
24+
<PackageReference Include="Avalonia.Desktop" Version="11.3.13" />
25+
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.13" />
26+
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.13" />
27+
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.13">
2328
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
2429
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
2530
</PackageReference>
26-
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.12" />
27-
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
28-
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.4" />
29-
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.4">
31+
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.13" />
32+
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
33+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
34+
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">
3035
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
3136
<PrivateAssets>all</PrivateAssets>
3237
</PackageReference>
33-
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.4" />
38+
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" />
3439
</ItemGroup>
40+
3541
</Project>

src/AStar.Dev.File.App/App.axaml.cs

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,22 @@
77
using Avalonia.Markup.Xaml;
88
using Microsoft.EntityFrameworkCore;
99
using Microsoft.Extensions.DependencyInjection;
10+
using Serilog;
1011
using System;
1112
using System.IO;
13+
using Microsoft.Extensions.Logging;
14+
using Serilog.Events;
15+
16+
using MelILogger = Microsoft.Extensions.Logging.ILogger;
17+
using System.Globalization;
1218

1319
namespace AStar.Dev.File.App;
1420

1521
public partial class App : Application
1622
{
23+
private const string ApplicationName = "AStar.Dev.File.App";
24+
private static readonly string _appVersion = typeof(App).Assembly.GetName().Version?.ToString() ?? "unknown";
25+
private const int _logRetentionDays = 7;
1726
private IServiceProvider? _services;
1827

1928
public IServiceProvider? Services => _services;
@@ -27,9 +36,9 @@ public override void Initialize()
2736

2837
public override void OnFrameworkInitializationCompleted()
2938
{
39+
ConfigureSerilog();
3040
_services = BuildServices();
3141

32-
// Apply EF migrations on startup
3342
var factory = _services.GetRequiredService<IDbContextFactory<FileAppDbContext>>();
3443
using var ctx = factory.CreateDbContext();
3544
ctx.Database.Migrate();
@@ -47,24 +56,47 @@ public override void OnFrameworkInitializationCompleted()
4756

4857
private static IServiceProvider BuildServices()
4958
{
50-
var dbPath = Path.Combine(
51-
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
52-
"AStar.Dev.File.App",
53-
"files.db");
59+
var dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ApplicationName, "files.db");
5460
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
5561

5662
var services = new ServiceCollection();
5763

58-
services.AddDbContextFactory<FileAppDbContext>(options =>
59-
options.UseSqlite($"Data Source={dbPath}"));
64+
services.AddDbContextFactory<FileAppDbContext>(options => options.UseSqlite($"Data Source={dbPath}"));
6065

6166
services.AddSingleton<IFileTypeClassifier, FileTypeClassifier>();
6267
services.AddSingleton<IFolderPickerService, FolderPickerService>();
6368
services.AddSingleton<IFileDeleteService, FileDeleteService>();
6469
services.AddTransient<IFileScannerService, FileScannerService>();
70+
services.AddTransient<IFileViewerService, FileViewerService>();
6571
services.AddTransient<MainWindowViewModel>();
6672
services.AddTransient<DeletePendingViewModel>();
73+
_ = services.AddLogging(logging => logging.AddSerilog(dispose: true));
74+
75+
var serviceProvider = services.BuildServiceProvider();
76+
var logger = serviceProvider.GetRequiredService<ILogger<App>>();
77+
LogAppStarting(logger, _appVersion);
78+
79+
return serviceProvider;
80+
}
6781

68-
return services.BuildServiceProvider();
82+
private static void ConfigureSerilog()
83+
{
84+
string logDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ApplicationName, "logs");
85+
86+
_ = Directory.CreateDirectory(logDirectory);
87+
88+
Log.Logger = new LoggerConfiguration()
89+
.MinimumLevel.Information()
90+
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
91+
.WriteTo.Console(formatProvider: CultureInfo.InvariantCulture)
92+
.WriteTo.File(
93+
path: Path.Combine(logDirectory, "app.log"),
94+
formatProvider: CultureInfo.InvariantCulture,
95+
rollingInterval: RollingInterval.Day,
96+
retainedFileCountLimit: _logRetentionDays)
97+
.CreateLogger();
6998
}
99+
100+
[LoggerMessage(Level = LogLevel.Information, Message = "Application starting — version {AppVersion}")]
101+
private static partial void LogAppStarting(MelILogger logger, string appVersion);
70102
}

src/AStar.Dev.File.App/Services/FileDeleteService.cs

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics;
4-
using System.IO;
54
using System.Linq;
65
using System.Runtime.InteropServices;
76
using System.Threading.Tasks;
87

98
namespace AStar.Dev.File.App.Services;
109

11-
public interface IFileDeleteService
12-
{
13-
Task DeleteFileAsync(string filePath, bool moveToRecycleBin = true);
14-
Task DeleteFilesAsync(IEnumerable<string> filePaths, bool moveToRecycleBin = true);
15-
}
16-
1710
public class FileDeleteService : IFileDeleteService
1811
{
1912
public async Task DeleteFileAsync(string filePath, bool moveToRecycleBin = true)
@@ -34,15 +27,15 @@ await Task.Run(() =>
3427
{
3528
if (moveToRecycleBin)
3629
{
37-
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
30+
if (OperatingSystem.IsWindows())
3831
{
3932
MoveFilesToRecycleBinWindows(files);
4033
}
41-
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
34+
else if (OperatingSystem.IsLinux())
4235
{
4336
MoveFilesToTrashLinux(files);
4437
}
45-
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
38+
else if (OperatingSystem.IsMacOS())
4639
{
4740
MoveFilesToTrashMacOS(files);
4841
}
@@ -68,7 +61,7 @@ private void PermanentlyDeleteFiles(IEnumerable<string> filePaths)
6861
}
6962
catch (Exception ex)
7063
{
71-
System.Diagnostics.Debug.WriteLine($"Failed to delete {file}: {ex.Message}");
64+
Debug.WriteLine($"Failed to delete {file}: {ex.Message}");
7265
}
7366
}
7467
}
@@ -96,13 +89,13 @@ private void MoveFilesToTrashLinux(IEnumerable<string> filePaths)
9689

9790
if (process.ExitCode != 0)
9891
{
99-
System.Diagnostics.Debug.WriteLine("gio trash failed, falling back to permanent delete");
92+
Debug.WriteLine("gio trash failed, falling back to permanent delete");
10093
PermanentlyDeleteFiles(filePaths);
10194
}
10295
}
10396
catch (Exception ex)
10497
{
105-
System.Diagnostics.Debug.WriteLine($"gio trash not available: {ex.Message}. Falling back to permanent delete.");
98+
Debug.WriteLine($"gio trash not available: {ex.Message}. Falling back to permanent delete.");
10699
PermanentlyDeleteFiles(filePaths);
107100
}
108101
}
@@ -130,7 +123,7 @@ private void MoveFilesToTrashMacOS(IEnumerable<string> filePaths)
130123
}
131124
catch (Exception ex)
132125
{
133-
System.Diagnostics.Debug.WriteLine($"macOS trash failed: {ex.Message}");
126+
Debug.WriteLine($"macOS trash failed: {ex.Message}");
134127
PermanentlyDeleteFiles(filePaths);
135128
}
136129
}
@@ -147,17 +140,25 @@ private void MoveFilesToRecycleBinWindows(IEnumerable<string> filePaths)
147140

148141
try
149142
{
150-
SHFileOperation(ref fileOp);
143+
int result = SHFileOperation(ref fileOp);
144+
145+
if (result != 0)
146+
{
147+
Debug.WriteLine($"Shell delete failed with code {result}. Falling back to permanent delete.");
148+
PermanentlyDeleteFiles(filePaths);
149+
}
151150
}
152151
catch (Exception ex)
153152
{
154-
System.Diagnostics.Debug.WriteLine($"Shell delete failed: {ex.Message}. Falling back to permanent delete.");
153+
Debug.WriteLine($"Shell delete failed: {ex.Message}. Falling back to permanent delete.");
155154
PermanentlyDeleteFiles(filePaths);
156155
}
157156
}
158157

159158
[DllImport("shell32.dll", CharSet = CharSet.Auto)]
159+
#pragma warning disable SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time
160160
private static extern int SHFileOperation(ref SHFILEOPSTRUCT lpFileOp);
161+
#pragma warning restore SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time
161162

162163
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
163164
private struct SHFILEOPSTRUCT

src/AStar.Dev.File.App/Services/FileScannerService.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ public async Task ScanAsync(string rootPath, IProgress<ScanProgressUpdate> progr
2424

2525
await RecurseDirectory(rootPath, rootPath, progress, counter, ct);
2626

27-
// Only mark missing files when scan ran to completion (not cancelled)
2827
await using var db = await dbContextFactory.CreateDbContextAsync(ct);
2928
await db.ScannedFiles
3029
.Where(f => f.RootPath == rootPath && f.LastScannedAt < scanStartedAt)
@@ -46,11 +45,7 @@ private async Task RecurseDirectory(
4645
CancellationToken ct)
4746
{
4847
var time = DateTime.Now.ToString("HH:mm:ss");
49-
progress.Report(new ScanProgressUpdate(
50-
CurrentFolder: directory,
51-
TotalFilesProcessed: counter.Value,
52-
CurrentFileName: null,
53-
StatusMessage: $"[{time}] Scanning: {directory}"));
48+
progress.Report(new ScanProgressUpdate(CurrentFolder: directory, TotalFilesProcessed: counter.Value, CurrentFileName: null, StatusMessage: $"[{time}] Scanning: {directory}"));
5449

5550
await using var db = await dbContextFactory.CreateDbContextAsync(ct);
5651

@@ -93,7 +88,6 @@ private async Task RecurseDirectory(
9388

9489
counter.Value++;
9590

96-
// Report every N files to avoid flooding the UI thread
9791
if (counter.Value % ProgressReportInterval == 0)
9892
{
9993
time = DateTime.Now.ToString("HH:mm:ss");

0 commit comments

Comments
 (0)