Skip to content
This repository was archived by the owner on Apr 2, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"cSpell.words": [
"ALLOWUNDO",
"CONFIRMMOUSE",
"FILESONLY",
"lpsz",
"MULTIDESTFILES",
"NOCONFIRMATION",
"NOCONFIRMMKDIR",
"NOCOPYSECURITYATTRIBS",
"NOERRORUI",
"NORECURSION",
"RENAMEONCOLLISION",
"SHFILEOPSTRUCT",
"SIMPLEPROGRESS",
"SYSLIB",
"WANTMAPPINGHANDLE"
]
}
30 changes: 18 additions & 12 deletions src/AStar.Dev.File.App/AStar.Dev.File.App.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0</TargetFramework>
Expand All @@ -9,27 +10,32 @@
</PropertyGroup>

<ItemGroup>

<AvaloniaResource Include="Assets\**" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.12" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.12" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.12" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.12" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.12">
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.5" />
<PackageReference Include="Serilog" Version="4.3.1" />
<PackageReference Include="Serilog.Extensions.Logging" Version="10.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />

<PackageReference Include="Avalonia" Version="11.3.13" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.13" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.13" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.13" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.13">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.12" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.4">
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.13" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" />
</ItemGroup>

</Project>
48 changes: 40 additions & 8 deletions src/AStar.Dev.File.App/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,9 +36,9 @@ public override void Initialize()

public override void OnFrameworkInitializationCompleted()
{
ConfigureSerilog();
_services = BuildServices();

// Apply EF migrations on startup
var factory = _services.GetRequiredService<IDbContextFactory<FileAppDbContext>>();
using var ctx = factory.CreateDbContext();
ctx.Database.Migrate();
Expand All @@ -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<FileAppDbContext>(options =>
options.UseSqlite($"Data Source={dbPath}"));
services.AddDbContextFactory<FileAppDbContext>(options => options.UseSqlite($"Data Source={dbPath}"));

services.AddSingleton<IFileTypeClassifier, FileTypeClassifier>();
services.AddSingleton<IFolderPickerService, FolderPickerService>();
services.AddSingleton<IFileDeleteService, FileDeleteService>();
services.AddTransient<IFileScannerService, FileScannerService>();
services.AddTransient<IFileViewerService, FileViewerService>();
services.AddTransient<MainWindowViewModel>();
services.AddTransient<DeletePendingViewModel>();
_ = services.AddLogging(logging => logging.AddSerilog(dispose: true));

var serviceProvider = services.BuildServiceProvider();
var logger = serviceProvider.GetRequiredService<ILogger<App>>();
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);
}
33 changes: 17 additions & 16 deletions src/AStar.Dev.File.App/Services/FileDeleteService.cs
Original file line number Diff line number Diff line change
@@ -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<string> filePaths, bool moveToRecycleBin = true);
}

public class FileDeleteService : IFileDeleteService
{
public async Task DeleteFileAsync(string filePath, bool moveToRecycleBin = true)
Expand All @@ -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);
}
Expand All @@ -68,7 +61,7 @@ private void PermanentlyDeleteFiles(IEnumerable<string> filePaths)
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Failed to delete {file}: {ex.Message}");
Debug.WriteLine($"Failed to delete {file}: {ex.Message}");
}
}
}
Expand Down Expand Up @@ -96,13 +89,13 @@ private void MoveFilesToTrashLinux(IEnumerable<string> 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);
}
}
Expand Down Expand Up @@ -130,7 +123,7 @@ private void MoveFilesToTrashMacOS(IEnumerable<string> filePaths)
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"macOS trash failed: {ex.Message}");
Debug.WriteLine($"macOS trash failed: {ex.Message}");
PermanentlyDeleteFiles(filePaths);
}
}
Expand All @@ -147,17 +140,25 @@ private void MoveFilesToRecycleBinWindows(IEnumerable<string> 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
Expand Down
8 changes: 1 addition & 7 deletions src/AStar.Dev.File.App/Services/FileScannerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ public async Task ScanAsync(string rootPath, IProgress<ScanProgressUpdate> 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)
Expand All @@ -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);

Expand Down Expand Up @@ -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");
Expand Down
Loading
Loading