diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 505547d..28a29cb 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -73,12 +73,16 @@ jobs:
include:
- rid: win-x64
os: windows-latest
- tfm: net10.0-windows10.0.19041.0
artifact: gui-win-x64
- - rid: maccatalyst-x64
+ - rid: osx-x64
os: macos-latest
- tfm: net10.0-maccatalyst
artifact: gui-osx-x64
+ - rid: osx-arm64
+ os: macos-latest
+ artifact: gui-osx-arm64
+ - rid: linux-x64
+ os: ubuntu-latest
+ artifact: gui-linux-x64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
@@ -87,21 +91,9 @@ jobs:
with:
dotnet-version: '10.x'
- - name: Install required MAUI workloads
- run: dotnet workload restore src/AtsScanner.Gui/AtsScanner.Gui.csproj
-
- - name: Publish (Windows)
- if: matrix.os == 'windows-latest'
- run: >
- dotnet publish src/AtsScanner.Gui -f ${{ matrix.tfm }} -c Release -r ${{ matrix.rid }}
- --self-contained true
- -p:WindowsAppSDKSelfContained=true
- -o ./dist/${{ matrix.artifact }}
-
- - name: Publish (MacCatalyst)
- if: matrix.os == 'macos-latest'
+ - name: Publish
run: >
- dotnet publish src/AtsScanner.Gui -f ${{ matrix.tfm }} -c Release -r ${{ matrix.rid }}
+ dotnet publish src/AtsScanner.Gui -c Release -r ${{ matrix.rid }}
--self-contained true
-o ./dist/${{ matrix.artifact }}
@@ -111,7 +103,7 @@ jobs:
if [ "${{ matrix.os }}" = "windows-latest" ]; then
test -f "dist/${{ matrix.artifact }}/AtsScanner.Gui.exe"
else
- ls -d dist/${{ matrix.artifact }}/*.app >/dev/null 2>&1 || ls dist/${{ matrix.artifact }}/*.pkg >/dev/null 2>&1
+ test -f "dist/${{ matrix.artifact }}/AtsScanner.Gui"
fi
- name: Package (Windows)
@@ -122,8 +114,8 @@ jobs:
Compress-Archive -Path * -DestinationPath "../../ats-scanner-${{ matrix.artifact }}.zip" -Force
Pop-Location
- - name: Package (macOS)
- if: matrix.os == 'macos-latest'
+ - name: Package (Unix)
+ if: matrix.os != 'windows-latest'
shell: bash
run: |
cd dist/${{ matrix.artifact }}
diff --git a/README.md b/README.md
index b406959..a6c0c1a 100644
--- a/README.md
+++ b/README.md
@@ -48,13 +48,12 @@ dotnet build
## Desktop GUI
-Prefer a graphical interface? A .NET MAUI desktop app is available in `src/AtsScanner.Gui`, built on the same `AtsScanner.Core` engine as the CLI — all scanning still happens fully offline, on your machine.
+Prefer a graphical interface? An Avalonia UI desktop app is available in `src/AtsScanner.Gui`, built on the same `AtsScanner.Core` engine as the CLI — all scanning still happens fully offline, on your machine.
-Requires the MAUI workload: `dotnet workload install maui`.
+The GUI targets Windows, macOS, and Linux with the standard .NET SDK.
```bash
-dotnet run --project src/AtsScanner.Gui -f net10.0-windows10.0.19041.0 # Windows
-dotnet run --project src/AtsScanner.Gui -f net10.0-maccatalyst # macOS
+dotnet run --project src/AtsScanner.Gui
```
The app lets you pick a resume file, choose which ATS platforms to check against, and view the same scores, ratings, and issue breakdowns as the CLI in an expandable results list.
diff --git a/src/AtsScanner.Gui/App.axaml b/src/AtsScanner.Gui/App.axaml
new file mode 100644
index 0000000..079c65c
--- /dev/null
+++ b/src/AtsScanner.Gui/App.axaml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/src/AtsScanner.Gui/App.axaml.cs b/src/AtsScanner.Gui/App.axaml.cs
new file mode 100644
index 0000000..05a9af8
--- /dev/null
+++ b/src/AtsScanner.Gui/App.axaml.cs
@@ -0,0 +1,21 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+
+namespace AtsScanner.Gui;
+
+public partial class App : Application
+{
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ desktop.MainWindow = new MainWindow();
+
+ base.OnFrameworkInitializationCompleted();
+ }
+}
diff --git a/src/AtsScanner.Gui/App.xaml b/src/AtsScanner.Gui/App.xaml
deleted file mode 100644
index c54d413..0000000
--- a/src/AtsScanner.Gui/App.xaml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/AtsScanner.Gui/App.xaml.cs b/src/AtsScanner.Gui/App.xaml.cs
deleted file mode 100644
index 39500ac..0000000
--- a/src/AtsScanner.Gui/App.xaml.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace AtsScanner.Gui;
-
-public partial class App : Application
-{
- public App()
- {
- InitializeComponent();
- }
-
- protected override Window CreateWindow(IActivationState? activationState)
- {
- return new Window(new AppShell());
- }
-}
\ No newline at end of file
diff --git a/src/AtsScanner.Gui/AppShell.xaml b/src/AtsScanner.Gui/AppShell.xaml
deleted file mode 100644
index 09a6759..0000000
--- a/src/AtsScanner.Gui/AppShell.xaml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
diff --git a/src/AtsScanner.Gui/AppShell.xaml.cs b/src/AtsScanner.Gui/AppShell.xaml.cs
deleted file mode 100644
index d782363..0000000
--- a/src/AtsScanner.Gui/AppShell.xaml.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace AtsScanner.Gui;
-
-public partial class AppShell : Shell
-{
- public AppShell()
- {
- InitializeComponent();
- }
-}
diff --git a/src/AtsScanner.Gui/AtsScanner.Gui.csproj b/src/AtsScanner.Gui/AtsScanner.Gui.csproj
index cd5ff5d..2409dcf 100644
--- a/src/AtsScanner.Gui/AtsScanner.Gui.csproj
+++ b/src/AtsScanner.Gui/AtsScanner.Gui.csproj
@@ -1,76 +1,28 @@
-
-
- net10.0-windows10.0.19041.0
- net10.0-maccatalyst
-
-
-
-
- Exe
- AtsScanner.Gui
- true
- true
- enable
- enable
-
-
- SourceGen
-
-
- ATS Scanner
-
-
- com.atsscanner.gui
-
-
- 1.0
- 1
-
-
- None
-
- 15.0
- 15.0
- 21.0
- 10.0.17763.0
- 10.0.17763.0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ WinExe
+ net10.0
+ AtsScanner.Gui
+ enable
+ enable
+ true
+ ATS Scanner
+ com.atsscanner.gui
+ 1.0
+ 1
+ false
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AtsScanner.Gui/MainPage.xaml b/src/AtsScanner.Gui/MainPage.xaml
deleted file mode 100644
index 04739b7..0000000
--- a/src/AtsScanner.Gui/MainPage.xaml
+++ /dev/null
@@ -1,112 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/AtsScanner.Gui/MainPage.xaml.cs b/src/AtsScanner.Gui/MainPage.xaml.cs
deleted file mode 100644
index f8527f5..0000000
--- a/src/AtsScanner.Gui/MainPage.xaml.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-using AtsScanner.Gui.ViewModels;
-
-namespace AtsScanner.Gui;
-
-public partial class MainPage : ContentPage
-{
- private readonly MainViewModel _viewModel;
-
- public MainPage()
- {
- InitializeComponent();
- _viewModel = new MainViewModel();
- BindingContext = _viewModel;
- }
-
- private async void OnChooseFileClicked(object? sender, EventArgs e) =>
- await _viewModel.PickFileAsync();
-
- private void OnSelectAllClicked(object? sender, EventArgs e) =>
- _viewModel.SelectAllCommand.Execute(null);
-
- private void OnSelectNoneClicked(object? sender, EventArgs e) =>
- _viewModel.SelectNoneCommand.Execute(null);
-
- private void OnToggleExpandClicked(object? sender, EventArgs e)
- {
- if (sender is BindableObject { BindingContext: ScanResultViewModel result })
- result.IsExpanded = !result.IsExpanded;
- }
-
- private static async void OnSponsorLinkTapped(object? sender, TappedEventArgs e)
- {
- try
- {
- await Launcher.Default.OpenAsync(new Uri("https://github.com/sponsors/sassdawe"));
- }
- catch
- {
- // Ignore failures opening the default browser (e.g. no handler registered).
- }
- }
-}
-
diff --git a/src/AtsScanner.Gui/MainWindow.axaml b/src/AtsScanner.Gui/MainWindow.axaml
new file mode 100644
index 0000000..972cd0d
--- /dev/null
+++ b/src/AtsScanner.Gui/MainWindow.axaml
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AtsScanner.Gui/MainWindow.axaml.cs b/src/AtsScanner.Gui/MainWindow.axaml.cs
new file mode 100644
index 0000000..d6fb8cf
--- /dev/null
+++ b/src/AtsScanner.Gui/MainWindow.axaml.cs
@@ -0,0 +1,63 @@
+using System.Diagnostics;
+using AtsScanner.Gui.ViewModels;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Platform.Storage;
+
+namespace AtsScanner.Gui;
+
+public partial class MainWindow : Window
+{
+ private readonly MainViewModel _viewModel;
+
+ public MainWindow()
+ {
+ InitializeComponent();
+ _viewModel = new MainViewModel();
+ DataContext = _viewModel;
+ }
+
+ private async void OnChooseFileClicked(object? sender, RoutedEventArgs e)
+ {
+ var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
+ {
+ Title = "Select a resume",
+ AllowMultiple = false,
+ FileTypeFilter =
+ [
+ new FilePickerFileType("Resume files")
+ {
+ Patterns = ["*.pdf", "*.docx", "*.md", "*.markdown"]
+ }
+ ]
+ });
+
+ if (files.Count == 0)
+ return;
+
+ var localPath = files[0].TryGetLocalPath();
+ _viewModel.SelectFile(localPath ?? files[0].Path.LocalPath);
+ }
+
+ private void OnToggleExpandClicked(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Control { DataContext: ScanResultViewModel result })
+ result.IsExpanded = !result.IsExpanded;
+ }
+
+ private static void OnSponsorLinkPressed(object? sender, PointerPressedEventArgs e)
+ {
+ try
+ {
+ Process.Start(new ProcessStartInfo("https://github.com/sponsors/sassdawe")
+ {
+ UseShellExecute = true
+ });
+ }
+ catch
+ {
+ // Ignore failures opening the default browser (e.g. no handler registered).
+ }
+ }
+}
diff --git a/src/AtsScanner.Gui/MauiProgram.cs b/src/AtsScanner.Gui/MauiProgram.cs
deleted file mode 100644
index 086bbfd..0000000
--- a/src/AtsScanner.Gui/MauiProgram.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using Microsoft.Extensions.Logging;
-
-namespace AtsScanner.Gui;
-
-public static class MauiProgram
-{
- public static MauiApp CreateMauiApp()
- {
- var builder = MauiApp.CreateBuilder();
- builder
- .UseMauiApp()
- .ConfigureFonts(fonts =>
- {
- fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
- fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
- });
-
-#if DEBUG
- builder.Logging.AddDebug();
-#endif
-
- return builder.Build();
- }
-}
diff --git a/src/AtsScanner.Gui/Platforms/MacCatalyst/AppDelegate.cs b/src/AtsScanner.Gui/Platforms/MacCatalyst/AppDelegate.cs
deleted file mode 100644
index bbae4e4..0000000
--- a/src/AtsScanner.Gui/Platforms/MacCatalyst/AppDelegate.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using Foundation;
-
-namespace AtsScanner.Gui;
-
-[Register("AppDelegate")]
-public class AppDelegate : MauiUIApplicationDelegate
-{
- protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
-}
diff --git a/src/AtsScanner.Gui/Platforms/MacCatalyst/Entitlements.plist b/src/AtsScanner.Gui/Platforms/MacCatalyst/Entitlements.plist
deleted file mode 100644
index 85c6dce..0000000
--- a/src/AtsScanner.Gui/Platforms/MacCatalyst/Entitlements.plist
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
- com.apple.security.app-sandbox
-
-
-
-
diff --git a/src/AtsScanner.Gui/Platforms/MacCatalyst/Info.plist b/src/AtsScanner.Gui/Platforms/MacCatalyst/Info.plist
deleted file mode 100644
index 29e82b2..0000000
--- a/src/AtsScanner.Gui/Platforms/MacCatalyst/Info.plist
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
- UIDeviceFamily
-
- 2
-
- LSApplicationCategoryType
- public.app-category.lifestyle
-
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- XSAppIconAssets
- Assets.xcassets/appicon.appiconset
-
-
diff --git a/src/AtsScanner.Gui/Platforms/MacCatalyst/Program.cs b/src/AtsScanner.Gui/Platforms/MacCatalyst/Program.cs
deleted file mode 100644
index 3e4a790..0000000
--- a/src/AtsScanner.Gui/Platforms/MacCatalyst/Program.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using ObjCRuntime;
-using UIKit;
-
-namespace AtsScanner.Gui;
-
-public class Program
-{
- // This is the main entry point of the application.
- static void Main(string[] args)
- {
- // if you want to use a different Application Delegate class from "AppDelegate"
- // you can specify it here.
- UIApplication.Main(args, null, typeof(AppDelegate));
- }
-}
diff --git a/src/AtsScanner.Gui/Platforms/Windows/App.xaml b/src/AtsScanner.Gui/Platforms/Windows/App.xaml
deleted file mode 100644
index 2d83bff..0000000
--- a/src/AtsScanner.Gui/Platforms/Windows/App.xaml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
diff --git a/src/AtsScanner.Gui/Platforms/Windows/App.xaml.cs b/src/AtsScanner.Gui/Platforms/Windows/App.xaml.cs
deleted file mode 100644
index 452563d..0000000
--- a/src/AtsScanner.Gui/Platforms/Windows/App.xaml.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using Microsoft.UI.Xaml;
-
-// To learn more about WinUI, the WinUI project structure,
-// and more about our project templates, see: http://aka.ms/winui-project-info.
-
-namespace AtsScanner.Gui.WinUI;
-
-///
-/// Provides application-specific behavior to supplement the default Application class.
-///
-public partial class App : MauiWinUIApplication
-{
- ///
- /// Initializes the singleton application object. This is the first line of authored code
- /// executed, and as such is the logical equivalent of main() or WinMain().
- ///
- public App()
- {
- this.InitializeComponent();
- }
-
- protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
-}
-
diff --git a/src/AtsScanner.Gui/Platforms/Windows/Package.appxmanifest b/src/AtsScanner.Gui/Platforms/Windows/Package.appxmanifest
deleted file mode 100644
index c743123..0000000
--- a/src/AtsScanner.Gui/Platforms/Windows/Package.appxmanifest
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
-
-
-
-
- $placeholder$
- User Name
- $placeholder$.png
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/AtsScanner.Gui/Platforms/Windows/app.manifest b/src/AtsScanner.Gui/Platforms/Windows/app.manifest
deleted file mode 100644
index a71e579..0000000
--- a/src/AtsScanner.Gui/Platforms/Windows/app.manifest
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
- true/PM
- PerMonitorV2, PerMonitor
-
- true
-
-
-
diff --git a/src/AtsScanner.Gui/Program.cs b/src/AtsScanner.Gui/Program.cs
new file mode 100644
index 0000000..7caccca
--- /dev/null
+++ b/src/AtsScanner.Gui/Program.cs
@@ -0,0 +1,16 @@
+using Avalonia;
+using Avalonia.Fonts.Inter;
+
+namespace AtsScanner.Gui;
+
+internal static class Program
+{
+ [STAThread]
+ public static void Main(string[] args) => BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+
+ public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure()
+ .UsePlatformDetect()
+ .WithInterFont()
+ .LogToTrace();
+}
diff --git a/src/AtsScanner.Gui/Properties/launchSettings.json b/src/AtsScanner.Gui/Properties/launchSettings.json
index 4f85793..740605e 100644
--- a/src/AtsScanner.Gui/Properties/launchSettings.json
+++ b/src/AtsScanner.Gui/Properties/launchSettings.json
@@ -1,6 +1,6 @@
{
"profiles": {
- "Windows Machine": {
+ "ATS Scanner GUI": {
"commandName": "Project",
"nativeDebugging": false
}
diff --git a/src/AtsScanner.Gui/Resources/AppIcon/appicon.svg b/src/AtsScanner.Gui/Resources/AppIcon/appicon.svg
deleted file mode 100644
index 9d63b65..0000000
--- a/src/AtsScanner.Gui/Resources/AppIcon/appicon.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
\ No newline at end of file
diff --git a/src/AtsScanner.Gui/Resources/AppIcon/appiconfg.svg b/src/AtsScanner.Gui/Resources/AppIcon/appiconfg.svg
deleted file mode 100644
index 21dfb25..0000000
--- a/src/AtsScanner.Gui/Resources/AppIcon/appiconfg.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/src/AtsScanner.Gui/Resources/Fonts/OpenSans-Regular.ttf b/src/AtsScanner.Gui/Resources/Fonts/OpenSans-Regular.ttf
deleted file mode 100644
index 2cc82d2..0000000
Binary files a/src/AtsScanner.Gui/Resources/Fonts/OpenSans-Regular.ttf and /dev/null differ
diff --git a/src/AtsScanner.Gui/Resources/Fonts/OpenSans-Semibold.ttf b/src/AtsScanner.Gui/Resources/Fonts/OpenSans-Semibold.ttf
deleted file mode 100644
index fcb5284..0000000
Binary files a/src/AtsScanner.Gui/Resources/Fonts/OpenSans-Semibold.ttf and /dev/null differ
diff --git a/src/AtsScanner.Gui/Resources/Raw/AboutAssets.txt b/src/AtsScanner.Gui/Resources/Raw/AboutAssets.txt
deleted file mode 100644
index 89dc758..0000000
--- a/src/AtsScanner.Gui/Resources/Raw/AboutAssets.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-Any raw assets you want to be deployed with your application can be placed in
-this directory (and child directories). Deployment of the asset to your application
-is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
-
-
-
-These files will be deployed with your package and will be accessible using Essentials:
-
- async Task LoadMauiAsset()
- {
- using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
- using var reader = new StreamReader(stream);
-
- var contents = reader.ReadToEnd();
- }
diff --git a/src/AtsScanner.Gui/Resources/Splash/splash.svg b/src/AtsScanner.Gui/Resources/Splash/splash.svg
deleted file mode 100644
index 21dfb25..0000000
--- a/src/AtsScanner.Gui/Resources/Splash/splash.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/src/AtsScanner.Gui/Resources/Styles/Colors.xaml b/src/AtsScanner.Gui/Resources/Styles/Colors.xaml
deleted file mode 100644
index d57fcc6..0000000
--- a/src/AtsScanner.Gui/Resources/Styles/Colors.xaml
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
- #512BD4
- #ac99ea
- #242424
- #DFD8F7
- #9880e5
- #2B0B98
-
- White
- Black
- #D600AA
- #190649
- #1f1f1f
-
- #E1E1E1
- #C8C8C8
- #ACACAC
- #919191
- #6E6E6E
- #404040
- #212121
- #141414
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/AtsScanner.Gui/Resources/Styles/Styles.xaml b/src/AtsScanner.Gui/Resources/Styles/Styles.xaml
deleted file mode 100644
index 5fef12a..0000000
--- a/src/AtsScanner.Gui/Resources/Styles/Styles.xaml
+++ /dev/null
@@ -1,434 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/AtsScanner.Gui/ViewModels/AsyncRelayCommand.cs b/src/AtsScanner.Gui/ViewModels/AsyncRelayCommand.cs
new file mode 100644
index 0000000..c70c0da
--- /dev/null
+++ b/src/AtsScanner.Gui/ViewModels/AsyncRelayCommand.cs
@@ -0,0 +1,33 @@
+using System.Windows.Input;
+
+namespace AtsScanner.Gui.ViewModels;
+
+public sealed class AsyncRelayCommand(Func execute, Func? canExecute = null) : ICommand
+{
+ private bool _isExecuting;
+
+ public event EventHandler? CanExecuteChanged;
+
+ public bool CanExecute(object? parameter) => !_isExecuting && (canExecute?.Invoke() ?? true);
+
+ public async void Execute(object? parameter)
+ {
+ if (!CanExecute(parameter))
+ return;
+
+ _isExecuting = true;
+ NotifyCanExecuteChanged();
+
+ try
+ {
+ await execute();
+ }
+ finally
+ {
+ _isExecuting = false;
+ NotifyCanExecuteChanged();
+ }
+ }
+
+ public void NotifyCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
+}
diff --git a/src/AtsScanner.Gui/ViewModels/IssueDisplay.cs b/src/AtsScanner.Gui/ViewModels/IssueDisplay.cs
index bd4702d..26ef9c8 100644
--- a/src/AtsScanner.Gui/ViewModels/IssueDisplay.cs
+++ b/src/AtsScanner.Gui/ViewModels/IssueDisplay.cs
@@ -1,5 +1,7 @@
using AtsScanner.Core.Models;
+using Avalonia.Media;
+
namespace AtsScanner.Gui.ViewModels;
/// Display-ready wrapper around a single .
@@ -12,11 +14,11 @@ public sealed class IssueDisplay(ScanIssue issue)
_ => "ℹ"
};
- public Color IconColor => issue.Severity switch
+ public IBrush IconBrush => issue.Severity switch
{
- IssueSeverity.Critical => Colors.Crimson,
- IssueSeverity.Warning => Colors.Goldenrod,
- _ => Colors.Gray
+ IssueSeverity.Critical => Brushes.Crimson,
+ IssueSeverity.Warning => Brushes.Goldenrod,
+ _ => Brushes.Gray
};
public string Category => issue.Category;
diff --git a/src/AtsScanner.Gui/ViewModels/MainViewModel.cs b/src/AtsScanner.Gui/ViewModels/MainViewModel.cs
index 53cb65a..917f996 100644
--- a/src/AtsScanner.Gui/ViewModels/MainViewModel.cs
+++ b/src/AtsScanner.Gui/ViewModels/MainViewModel.cs
@@ -5,7 +5,7 @@
namespace AtsScanner.Gui.ViewModels;
-/// Backing view model for : file selection, platform toggles, and scan execution.
+/// Backing view model for : file selection, platform toggles, and scan execution.
public sealed class MainViewModel : ObservableObject
{
private readonly ResumeAnalyzer _analyzer = new();
@@ -19,11 +19,14 @@ public MainViewModel()
Platforms = new ObservableCollection(
ProfileRegistry.GetAll().Select(p => new PlatformOption(p.Platform, p.DisplayName)));
+ foreach (var platform in Platforms)
+ platform.PropertyChanged += (_, _) => NotifyCanScanChanged();
+
Results = [];
- ScanCommand = new Command(async () => await ScanAsync(), () => CanScan);
- SelectAllCommand = new Command(() => SetAllPlatforms(true));
- SelectNoneCommand = new Command(() => SetAllPlatforms(false));
+ ScanCommand = new AsyncRelayCommand(ScanAsync, () => CanScan);
+ SelectAllCommand = new RelayCommand(() => SetAllPlatforms(true));
+ SelectNoneCommand = new RelayCommand(() => SetAllPlatforms(false));
}
public ObservableCollection Platforms { get; }
@@ -45,7 +48,7 @@ private set
{
OnPropertyChanged(nameof(SelectedFileName));
OnPropertyChanged(nameof(HasSelectedFile));
- ((Command)ScanCommand).ChangeCanExecute();
+ NotifyCanScanChanged();
}
}
}
@@ -61,7 +64,7 @@ public bool IsBusy
private set
{
if (SetProperty(ref _isBusy, value))
- ((Command)ScanCommand).ChangeCanExecute();
+ NotifyCanScanChanged();
}
}
@@ -73,32 +76,11 @@ public string StatusMessage
private bool CanScan => !IsBusy && HasSelectedFile && Platforms.Any(p => p.IsSelected);
- public async Task PickFileAsync()
+ public void SelectFile(string selectedFilePath)
{
- try
- {
- var customFileType = new FilePickerFileType(new Dictionary>
- {
- { DevicePlatform.WinUI, [".pdf", ".docx", ".md", ".markdown"] },
- { DevicePlatform.MacCatalyst, ["pdf", "docx", "md", "markdown"] }
- });
-
- var result = await FilePicker.Default.PickAsync(new PickOptions
- {
- PickerTitle = "Select a resume",
- FileTypes = customFileType
- });
-
- if (result is null) return;
-
- SelectedFilePath = result.FullPath;
- Results.Clear();
- StatusMessage = "Ready to scan.";
- }
- catch (Exception ex)
- {
- StatusMessage = $"Could not open file picker: {ex.Message}";
- }
+ SelectedFilePath = selectedFilePath;
+ Results.Clear();
+ StatusMessage = "Ready to scan.";
}
private void SetAllPlatforms(bool selected)
@@ -106,7 +88,13 @@ private void SetAllPlatforms(bool selected)
foreach (var platform in Platforms)
platform.IsSelected = selected;
- ((Command)ScanCommand).ChangeCanExecute();
+ NotifyCanScanChanged();
+ }
+
+ private void NotifyCanScanChanged()
+ {
+ if (ScanCommand is AsyncRelayCommand scanCommand)
+ scanCommand.NotifyCanExecuteChanged();
}
private async Task ScanAsync()
diff --git a/src/AtsScanner.Gui/ViewModels/RelayCommand.cs b/src/AtsScanner.Gui/ViewModels/RelayCommand.cs
new file mode 100644
index 0000000..7d5de13
--- /dev/null
+++ b/src/AtsScanner.Gui/ViewModels/RelayCommand.cs
@@ -0,0 +1,14 @@
+using System.Windows.Input;
+
+namespace AtsScanner.Gui.ViewModels;
+
+public sealed class RelayCommand(Action execute, Func? canExecute = null) : ICommand
+{
+ public event EventHandler? CanExecuteChanged;
+
+ public bool CanExecute(object? parameter) => canExecute?.Invoke() ?? true;
+
+ public void Execute(object? parameter) => execute();
+
+ public void NotifyCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
+}
diff --git a/src/AtsScanner.Gui/ViewModels/ScanResultViewModel.cs b/src/AtsScanner.Gui/ViewModels/ScanResultViewModel.cs
index 3f6759e..22d45d6 100644
--- a/src/AtsScanner.Gui/ViewModels/ScanResultViewModel.cs
+++ b/src/AtsScanner.Gui/ViewModels/ScanResultViewModel.cs
@@ -1,5 +1,7 @@
using AtsScanner.Core.Models;
+using Avalonia.Media;
+
namespace AtsScanner.Gui.ViewModels;
/// Display-ready wrapper around a single platform's , with expand/collapse state for the details panel.
@@ -45,13 +47,13 @@ public ScanResultViewModel(ScanResult result, string displayName)
public string ScoreLabel => $"{Score}/100";
- public Color RatingColor => Score switch
+ public IBrush RatingBrush => Score switch
{
- >= 90 => Colors.SeaGreen,
- >= 75 => Colors.YellowGreen,
- >= 55 => Colors.Goldenrod,
- >= 35 => Colors.DarkOrange,
- _ => Colors.Crimson
+ >= 90 => Brushes.SeaGreen,
+ >= 75 => Brushes.YellowGreen,
+ >= 55 => Brushes.Goldenrod,
+ >= 35 => Brushes.DarkOrange,
+ _ => Brushes.Crimson
};
public string IssuesSummary { get; }