diff --git a/IrdLibraryClient/ApiConfig.cs b/IrdLibraryClient/ApiConfig.cs index 55e3d30..9590229 100644 --- a/IrdLibraryClient/ApiConfig.cs +++ b/IrdLibraryClient/ApiConfig.cs @@ -8,11 +8,4 @@ namespace IrdLibraryClient; public static class ApiConfig { public static readonly CancellationTokenSource Cts = new(); - public static readonly string IrdCachePath = "./ird/"; - - static ApiConfig() - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - IrdCachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ,"ps3-iso-dumper/ird/"); - } } \ No newline at end of file diff --git a/IrdLibraryClient/Log.cs b/IrdLibraryClient/Log.cs index f6079fa..b48d44e 100644 --- a/IrdLibraryClient/Log.cs +++ b/IrdLibraryClient/Log.cs @@ -18,11 +18,16 @@ static Log() { var path = Path.Combine("logs", DateTime.Now.ToString("yyyy-MM-dd") + ".log"); if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ps3-iso-dumper", path); + path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "ps3-iso-dumper", path); var folder = Path.GetDirectoryName(path) ?? "."; if (!Directory.Exists(folder)) Directory.CreateDirectory(folder); - var filestream = File.Open(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read); + var filestream = File.Open(path, new FileStreamOptions + { + Mode = FileMode.OpenOrCreate, + Access = FileAccess.Write, + Options = FileOptions.Asynchronous | FileOptions.SequentialScan, + }); filestream.Seek(0, SeekOrigin.End); FileLog = new(filestream, new UTF8Encoding(false)); LogPath = path; @@ -61,7 +66,7 @@ private static void LogInternal(Exception? e, string message, LogLevel level, Co try { #if DEBUG - const LogLevel minLevel = LogLevel.TRACE; + const LogLevel minLevel = LogLevel.TRACE; #else const LogLevel minLevel = LogLevel.DEBUG; #endif diff --git a/Ps3DiscDumper.sln b/Ps3DiscDumper.sln index 7d2c7d2..4e081fc 100644 --- a/Ps3DiscDumper.sln +++ b/Ps3DiscDumper.sln @@ -17,34 +17,53 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Deploy", "Deploy", "{BA0C92 ProjectSection(SolutionItems) = preProject azure-pipelines.yml = azure-pipelines.yml publish.ps1 = publish.ps1 + README.md = README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UI.Avalonia", "UI.Avalonia\UI.Avalonia.csproj", "{EBFEB92B-CF70-48C2-B1B4-80CC21EFA072}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU + Linux|Any CPU = Linux|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {F58970E2-7861-4635-BBAA-2E081D1B68F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F58970E2-7861-4635-BBAA-2E081D1B68F0}.Debug|Any CPU.Build.0 = Debug|Any CPU {F58970E2-7861-4635-BBAA-2E081D1B68F0}.Release|Any CPU.ActiveCfg = Release|Any CPU {F58970E2-7861-4635-BBAA-2E081D1B68F0}.Release|Any CPU.Build.0 = Release|Any CPU + {F58970E2-7861-4635-BBAA-2E081D1B68F0}.Linux|Any CPU.ActiveCfg = Debug|Any CPU + {F58970E2-7861-4635-BBAA-2E081D1B68F0}.Linux|Any CPU.Build.0 = Debug|Any CPU {EC25C188-51BB-4665-B79B-2AE17CABB01E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EC25C188-51BB-4665-B79B-2AE17CABB01E}.Debug|Any CPU.Build.0 = Debug|Any CPU {EC25C188-51BB-4665-B79B-2AE17CABB01E}.Release|Any CPU.ActiveCfg = Release|Any CPU {EC25C188-51BB-4665-B79B-2AE17CABB01E}.Release|Any CPU.Build.0 = Release|Any CPU + {EC25C188-51BB-4665-B79B-2AE17CABB01E}.Linux|Any CPU.ActiveCfg = Debug|Any CPU + {EC25C188-51BB-4665-B79B-2AE17CABB01E}.Linux|Any CPU.Build.0 = Debug|Any CPU {C8C058F0-01DA-41B9-BBAA-6787C5C755F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C8C058F0-01DA-41B9-BBAA-6787C5C755F1}.Debug|Any CPU.Build.0 = Debug|Any CPU {C8C058F0-01DA-41B9-BBAA-6787C5C755F1}.Release|Any CPU.ActiveCfg = Release|Any CPU {C8C058F0-01DA-41B9-BBAA-6787C5C755F1}.Release|Any CPU.Build.0 = Release|Any CPU + {C8C058F0-01DA-41B9-BBAA-6787C5C755F1}.Linux|Any CPU.ActiveCfg = Debug|Any CPU + {C8C058F0-01DA-41B9-BBAA-6787C5C755F1}.Linux|Any CPU.Build.0 = Debug|Any CPU {F5041E4B-77E7-4783-8A74-41BFD49FB9E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F5041E4B-77E7-4783-8A74-41BFD49FB9E8}.Debug|Any CPU.Build.0 = Debug|Any CPU {F5041E4B-77E7-4783-8A74-41BFD49FB9E8}.Release|Any CPU.ActiveCfg = Release|Any CPU {F5041E4B-77E7-4783-8A74-41BFD49FB9E8}.Release|Any CPU.Build.0 = Release|Any CPU + {F5041E4B-77E7-4783-8A74-41BFD49FB9E8}.Linux|Any CPU.ActiveCfg = Debug|Any CPU + {F5041E4B-77E7-4783-8A74-41BFD49FB9E8}.Linux|Any CPU.Build.0 = Debug|Any CPU {874BD35B-DFDF-40BE-BB6F-EC36059E40EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {874BD35B-DFDF-40BE-BB6F-EC36059E40EB}.Debug|Any CPU.Build.0 = Debug|Any CPU {874BD35B-DFDF-40BE-BB6F-EC36059E40EB}.Release|Any CPU.ActiveCfg = Release|Any CPU {874BD35B-DFDF-40BE-BB6F-EC36059E40EB}.Release|Any CPU.Build.0 = Release|Any CPU + {874BD35B-DFDF-40BE-BB6F-EC36059E40EB}.Linux|Any CPU.ActiveCfg = Debug|Any CPU + {EBFEB92B-CF70-48C2-B1B4-80CC21EFA072}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBFEB92B-CF70-48C2-B1B4-80CC21EFA072}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBFEB92B-CF70-48C2-B1B4-80CC21EFA072}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBFEB92B-CF70-48C2-B1B4-80CC21EFA072}.Release|Any CPU.Build.0 = Release|Any CPU + {EBFEB92B-CF70-48C2-B1B4-80CC21EFA072}.Linux|Any CPU.ActiveCfg = Linux|Any CPU + {EBFEB92B-CF70-48C2-B1B4-80CC21EFA072}.Linux|Any CPU.Build.0 = Linux|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Ps3DiscDumper/Dumper.cs b/Ps3DiscDumper/Dumper.cs index f2cdcfb..a04a59d 100644 --- a/Ps3DiscDumper/Dumper.cs +++ b/Ps3DiscDumper/Dumper.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Collections.Specialized; using System.IO; using System.Linq; using System.Management; using System.Net.Http; -using System.Net.Http.Headers; using System.Runtime.InteropServices; -using System.Text; +using System.Runtime.Versioning; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; @@ -27,9 +27,13 @@ namespace Ps3DiscDumper; public class Dumper: IDisposable { - public const string Version = "3.3.6"; + public const string Version = "4.0.0"; + + static Dumper() => Log.Info("PS3 Disc Dumper v" + Version); private static readonly Regex VersionParts = new(@"(?\d+(\.\d+){0,2})[ \-]*(?
.*)", RegexOptions.Singleline | RegexOptions.ExplicitCapture);
+    private static readonly Regex ScsiInfoParts = new(@"Host: .+$\s*Vendor: (?.+?)\s* Model: (?.+?)\s* Rev: (?.+)$\s*Type: \s*(?.+?)\s* ANSI  ?SCSI revision: (?.+?)\s*$",
+        RegexOptions.Multiline | RegexOptions.ExplicitCapture);
     private static readonly HashSet InvalidChars = new(Path.GetInvalidFileNameChars().Concat(Path.GetInvalidPathChars()));
     private static readonly char[] MultilineSplit = {'\r', '\n'};
     private long currentSector;
@@ -47,12 +51,22 @@ public class Dumper: IDisposable
     private byte[] sectorIV;
     private Stream driveStream;
     private static readonly byte[] Iso9660PrimaryVolumeDescriptorHeader = {0x01, 0x43, 0x44, 0x30, 0x30, 0x31, 0x01, 0x00};
-
     private static readonly JsonSerializerOptions JsonOptions = new()
     {
         PropertyNamingPolicy = new SnakeCasePolicy(),
         WriteIndented = true,
     };
+    public static readonly NameValueCollection RegionMapping = new()
+    {
+        ["A"] = "ASIA",
+        ["E"] = "EU",
+        ["H"] = "HK",
+        ["J"] = "JP",
+        ["K"] = "KR",
+        ["P"] = "JP",
+        ["T"] = "JP",
+        ["U"] = "US",
+    };
 
     public ParamSfo ParamSfo { get; private set; }
     public string ProductCode { get; private set; }
@@ -60,7 +74,8 @@ public class Dumper: IDisposable
     public string Title { get; private set; }
     public string OutputDir { get; private set; }
     public char Drive { get; set; }
-    private string input;
+    public string SelectedPhysicalDevice { get; private set; }
+    public string InputDevicePath { get; private set; }
     private List filesystemStructure;
     private List emptyDirStructure;
     private CDReader discReader;
@@ -96,6 +111,7 @@ public long CurrentSector
         }
     }
 
+    [SupportedOSPlatform("Windows")]
     private List EnumeratePhysicalDrivesWindows()
     {
         if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
@@ -105,14 +121,21 @@ private List EnumeratePhysicalDrivesWindows()
 #if !NATIVE
         try
         {
-            using var mgmtObjSearcher = new ManagementObjectSearcher("SELECT * FROM Win32_PhysicalMedia");
-            var drives = mgmtObjSearcher.Get();
+            using var physicalMediaQuery = new ManagementObjectSearcher("SELECT * FROM Win32_PhysicalMedia");
+            var drives = physicalMediaQuery.Get();
             foreach (var drive in drives)
             {
-                var tag = drive.Properties["Tag"].Value as string;
-                if (tag?.IndexOf("CDROM", StringComparison.InvariantCultureIgnoreCase) > -1)
+                if (drive.Properties["Tag"].Value is string tag
+                    && tag.StartsWith(@"\\.\CDROM"))
                     physicalDrives.Add(tag);
             }
+            using var cdromQuery = new ManagementObjectSearcher("SELECT * FROM Win32_CDROMDrive");
+            drives = cdromQuery.Get();
+            foreach (var drive in drives)
+            {
+                // Name and Caption are the same, so idk if they can be different
+                Log.Info($"Found optical media drive {drive.Properties["Name"].Value} ({drive.Properties["Drive"].Value})");
+            }
         }
         catch (Exception e)
         {
@@ -127,12 +150,27 @@ private List EnumeratePhysicalDrivesWindows()
         return physicalDrives;
     }
 
+    [SupportedOSPlatform("Linux")]
     private List EnumeratePhysicalDrivesLinux()
     {
         var cdInfo = "";
         try
         {
             cdInfo = File.ReadAllText("/proc/sys/dev/cdrom/info");
+            if (File.Exists("/proc/scsi/scsi"))
+            {
+                var scsiInfo = File.ReadAllText("/proc/scsi/scsi");
+                if (scsiInfo is { Length: > 0 })
+                {
+                    foreach (Match m in ScsiInfoParts.Matches(scsiInfo))
+                    {
+                        if (m.Groups["type"].Value is not "CD-ROM")
+                            continue;
+
+                        Log.Info($"Found optical media drive {m.Groups["vendor"].Value} {m.Groups["model"].Value}");
+                    }
+                }
+            }
         }
         catch (Exception e)
         {
@@ -176,10 +214,13 @@ private void CheckParamSfo(ParamSfo sfo)
 
     public void DetectDisc(string inDir = "", Func outputDirFormatter = null)
     {
-        outputDirFormatter ??= d => PatternFormatter.Format($"%{Patterns.Title}% [%{Patterns.ProductCode}%]", new()
+        outputDirFormatter ??= d => PatternFormatter.Format(SettingsProvider.Settings.DumpNameTemplate, new()
         {
             [Patterns.ProductCode] = d.ProductCode,
+            [Patterns.ProductCodeLetters] = d.ProductCode?[..4],
+            [Patterns.ProductCodeNumbers] = d.ProductCode?[4..],
             [Patterns.Title] = d.Title,
+            [Patterns.Region] = RegionMapping[d.ProductCode?[2..3] ?? ""],
         });
         string discSfbPath = null;
         if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
@@ -193,7 +234,7 @@ public void DetectDisc(string inDir = "", Func outputDirFormatte
                     if (!File.Exists(discSfbPath))
                         continue;
 
-                    input = drive.Name;
+                    InputDevicePath = drive.Name;
                     Drive = drive.Name[0];
                     break;
                 }
@@ -203,7 +244,7 @@ public void DetectDisc(string inDir = "", Func outputDirFormatte
                 discSfbPath = Path.Combine(inDir, "PS3_DISC.SFB");
                 if (File.Exists(discSfbPath))
                 {
-                    input = Path.GetPathRoot(discSfbPath);
+                    InputDevicePath = Path.GetPathRoot(discSfbPath);
                     Drive = discSfbPath[0];
                 }
             }
@@ -215,20 +256,20 @@ public void DetectDisc(string inDir = "", Func outputDirFormatte
                 : DriveInfo.GetDrives().Where(d => d.DriveType == DriveType.CDRom).Select(d => d.RootDirectory.FullName); 
             discSfbPath = mountList.SelectMany(mp => IOEx.GetFilepaths(mp, "PS3_DISC.SFB", 2)) .FirstOrDefault();
             if (!string.IsNullOrEmpty(discSfbPath))
-                input = Path.GetDirectoryName(discSfbPath)!;
+                InputDevicePath = Path.GetDirectoryName(discSfbPath)!;
         }
         else
             throw new NotImplementedException("Current OS is not supported");
 
-        if (string.IsNullOrEmpty(input) || string.IsNullOrEmpty(discSfbPath))
+        if (string.IsNullOrEmpty(InputDevicePath) || string.IsNullOrEmpty(discSfbPath))
             throw new DriveNotFoundException("No valid PS3 disc was detected. Disc must be detected and mounted.");
 
-        Log.Info("Selected disc: " + input);
+        Log.Info("Selected disc: " + InputDevicePath);
         discSfbData = File.ReadAllBytes(discSfbPath);
         var titleId = CheckDiscSfb(discSfbData);
-        var paramSfoPath = Path.Combine(input, "PS3_GAME", "PARAM.SFO");
+        var paramSfoPath = Path.Combine(InputDevicePath, "PS3_GAME", "PARAM.SFO");
         if (!File.Exists(paramSfoPath))
-            throw new InvalidOperationException($"Specified folder is not a valid PS3 disc root (param.sfo is missing): {input}");
+            throw new InvalidOperationException($"Specified folder is not a valid PS3 disc root (param.sfo is missing): {InputDevicePath}");
 
         using (var stream = File.Open(paramSfoPath, FileMode.Open, FileAccess.Read, FileShare.Read))
             ParamSfo = ParamSfo.ReadFrom(stream);
@@ -237,10 +278,10 @@ public void DetectDisc(string inDir = "", Func outputDirFormatte
             Log.Warn($"Product codes in ps3_disc.sfb ({titleId}) and in param.sfo ({ProductCode}) do not match");
 
         // todo: maybe use discutils instead to read TOC as one block
-        var files = IOEx.GetFilepaths(input, "*", SearchOption.AllDirectories);
+        var files = IOEx.GetFilepaths(InputDevicePath, "*", SearchOption.AllDirectories);
         DiscFilenames = new();
         var totalFilesize = 0L;
-        var rootLength = input.Length;
+        var rootLength = InputDevicePath.Length;
         foreach (var f in files)
         {
             try { totalFilesize += new FileInfo(f).Length; } catch { }
@@ -296,8 +337,6 @@ public async Task FindDiscKeyAsync(string discKeyCachePath)
         if (untestedKeys.Count == 0)
             throw new KeyNotFoundException("No valid disc decryption key was found");
 
-        // select physical device
-        string physicalDevice = null;
         List physicalDrives = new List();
         Log.Trace("Trying to enumerate physical drives...");
         try
@@ -324,7 +363,12 @@ public async Task FindDiscKeyAsync(string discKeyCachePath)
             try
             {
                 Log.Trace($"Checking physical drive {drive}...");
-                await using var discStream = File.Open(drive, FileMode.Open, FileAccess.Read, FileShare.Read);
+                await using var discStream = File.Open(drive, new FileStreamOptions
+                {
+                    Mode = FileMode.Open,
+                    Access = FileAccess.Read,
+                    Options = FileOptions.Asynchronous | FileOptions.SequentialScan
+                });
                 var tmpDiscReader = new CDReader(discStream, true, true);
                 if (tmpDiscReader.FileExists("PS3_DISC.SFB"))
                 {
@@ -339,7 +383,7 @@ public async Task FindDiscKeyAsync(string discKeyCachePath)
                         discStream.ReadExact(buf, 0, buf.Length);
                         if (buf.SequenceEqual(discSfbData))
                         {
-                            physicalDevice = drive;
+                            SelectedPhysicalDevice = drive;
                             break;
                         }
                         Log.Trace("SFB content check failed, skipping the drive");
@@ -351,11 +395,11 @@ public async Task FindDiscKeyAsync(string discKeyCachePath)
                 Log.Debug($"Skipping drive {drive}: {e.Message}");
             }
         }
-        if (physicalDevice == null)
-            throw new AccessViolationException("Couldn't get physical access to the drive");
+        if (SelectedPhysicalDevice == null)
+            throw new AccessViolationException("Direct disk access to the drive was denied");
 
-        Log.Debug($"Selected physical drive {physicalDevice}");
-        driveStream = File.Open(physicalDevice, FileMode.Open, FileAccess.Read, FileShare.Read);
+        Log.Debug($"Selected physical drive {SelectedPhysicalDevice}");
+        driveStream = File.Open(SelectedPhysicalDevice, FileMode.Open, FileAccess.Read, FileShare.Read);
 
         // find disc license file
         discReader = new(driveStream, true, true);
@@ -416,7 +460,9 @@ public async Task FindDiscKeyAsync(string discKeyCachePath)
 
         lock (AllKnownDiscKeys)
             AllKnownDiscKeys.TryGetValue(discKey, out allMatchingKeys);
-        var discKeyInfo = allMatchingKeys?.First();
+        var discKeyInfo = allMatchingKeys?.FirstOrDefault(k => k.FullPath.Contains(ProductCode, StringComparison.OrdinalIgnoreCase) && k.FullPath.EndsWith(".ird", StringComparison.OrdinalIgnoreCase))
+                          ?? allMatchingKeys?.FirstOrDefault(k => k.FullPath.EndsWith(".ird", StringComparison.OrdinalIgnoreCase))
+                          ?? allMatchingKeys?.First();
         DiscKeyFilename = Path.GetFileName(discKeyInfo?.FullPath);
         DiscKeyType = discKeyInfo?.KeyType ?? default;
     }
@@ -504,7 +550,7 @@ public async Task DumpAsync(string output)
                 Log.Info($"Reading {file.TargetFileName} ({file.Length.AsStorageUnit()})");
                 CurrentFileNumber++;
                 var convertedFilename = Path.DirectorySeparatorChar == '\\' ? file.TargetFileName : file.TargetFileName.Replace('\\', Path.DirectorySeparatorChar);
-                var inputFilename = Path.Combine(input, convertedFilename);
+                var inputFilename = Path.Combine(InputDevicePath, convertedFilename);
 
                 if (!File.Exists(inputFilename))
                 {
@@ -646,8 +692,8 @@ select v.Files[file.TargetFileName].Hashes
                 return (latestVer, latest);
             }
                 
-            if (latestBetaVer > latestVer
-                || (latestVer == latestBetaVer
+            if (latestBetaVer > curVer
+                || (latestBetaVer == curVer
                     && curVerPre is {Length: >0}
                     && (latestBetaVerPre is {Length: >0} && StringComparer.OrdinalIgnoreCase.Compare(latestBetaVerPre, curVerPre) > 0
                         || latestBetaVerStr is null or "")))
diff --git a/Ps3DiscDumper/Ps3DiscDumper.csproj b/Ps3DiscDumper/Ps3DiscDumper.csproj
index c5f8215..26f365e 100644
--- a/Ps3DiscDumper/Ps3DiscDumper.csproj
+++ b/Ps3DiscDumper/Ps3DiscDumper.csproj
@@ -10,7 +10,7 @@
   
 
   
-    
+    
   
 
   
diff --git a/Ps3DiscDumper/Settings.cs b/Ps3DiscDumper/Settings.cs
new file mode 100644
index 0000000..33472de
--- /dev/null
+++ b/Ps3DiscDumper/Settings.cs
@@ -0,0 +1,49 @@
+using System;
+using System.IO;
+using System.Text.Json.Serialization;
+using Ps3DiscDumper.Utils;
+
+using SpecialFolder = System.Environment.SpecialFolder;
+
+namespace Ps3DiscDumper;
+
+public struct Settings
+{
+    public Settings() { }
+
+    [JsonIgnore]
+    public const string DefaultPattern = $"%{Patterns.Title}% [%{Patterns.ProductCode}%]";
+
+    public string OutputDir { get; set; } = ".";
+    public string IrdDir { get; set; } = Path.Combine(Environment.GetFolderPath(SpecialFolder.ApplicationData) ,"ps3-iso-dumper", "ird");
+    public string DumpNameTemplate { get; set; } = DefaultPattern;
+    public bool ShowDetails { get; set; } = true;
+    public bool EnableTransparency { get; set; } = true;
+    public bool StayOnTop { get; set; } = false;
+
+    private static StringComparison Comparison => OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
+    private static StringComparer Comparer => OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
+
+    public override bool Equals(object obj)
+        => obj is Settings other && Equals(other);
+
+    public bool Equals(Settings other)
+        => string.Equals(OutputDir, other.OutputDir, Comparison)
+           && string.Equals(IrdDir, other.IrdDir, Comparison)
+           && string.Equals(DumpNameTemplate, other.DumpNameTemplate, Comparison)
+           && ShowDetails == other.ShowDetails
+           && EnableTransparency == other.EnableTransparency
+           && StayOnTop == other.StayOnTop;
+
+    public override int GetHashCode()
+    {
+        var hashCode = new HashCode();
+        hashCode.Add(OutputDir, Comparer);
+        hashCode.Add(IrdDir, Comparer);
+        hashCode.Add(DumpNameTemplate, Comparer);
+        hashCode.Add(ShowDetails);
+        hashCode.Add(EnableTransparency);
+        hashCode.Add(StayOnTop);
+        return hashCode.ToHashCode();
+    }
+}
\ No newline at end of file
diff --git a/Ps3DiscDumper/SettingsProvider.cs b/Ps3DiscDumper/SettingsProvider.cs
new file mode 100644
index 0000000..f5ebf2c
--- /dev/null
+++ b/Ps3DiscDumper/SettingsProvider.cs
@@ -0,0 +1,133 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Xml.Linq;
+using IrdLibraryClient;
+using SpecialFolder = System.Environment.SpecialFolder;
+
+namespace Ps3DiscDumper;
+
+public static class SettingsProvider
+{
+    private static readonly string settingsFolder;
+    private static readonly string settingsPath;
+    private static readonly JsonSerializerOptions serializerOptions = new() { WriteIndented = true, };
+    
+    static SettingsProvider()
+    {
+        try
+        {
+            Log.Info("Loading settings…");
+            settingsFolder = Path.Combine(Environment.GetFolderPath(SpecialFolder.LocalApplicationData), "ps3-disc-dumper");
+            settingsPath = Path.Combine(settingsFolder, "settings.json");
+            if (File.Exists(settingsPath))
+                savedSettings = Read() ?? savedSettings;
+            else
+                savedSettings = Import() ?? savedSettings;
+            Settings = savedSettings;
+        }
+        catch (Exception e)
+        {
+            Log.Error(e, "Failed to initialize settings");
+        }
+    }
+
+    private static Settings? Read()
+    {
+        try
+        {
+            using var file = File.Open(settingsPath, FileMode.Open, FileAccess.Read, FileShare.Read);
+            using var reader = new StreamReader(file);
+            var settingsContent = reader.ReadToEnd();
+            Log.Info($"Current settings: {settingsContent}");
+            return JsonSerializer.Deserialize(settingsContent);
+        }
+        catch (Exception e)
+        {
+            Log.Error(e, "Failed to initialize settings");
+            return null;
+        }
+    }
+
+    private static Settings? Import()
+    {
+        try
+        {
+            if (!OperatingSystem.IsWindows())
+                return null;
+
+            var localAppDataPath = Environment.GetFolderPath(SpecialFolder.LocalApplicationData);
+            var oldRootDir = Path.Combine(localAppDataPath, "UI");
+            if (!Directory.Exists(oldRootDir))
+                return null;
+
+            var lastUsedConfigPath = Directory
+                .GetDirectories(oldRootDir, "ps3-disc-dumper*", SearchOption.TopDirectoryOnly)
+                .Select(d => Path.Combine(d,"1.0.0.0", "user.config"))
+                .Where(File.Exists)
+                .MaxBy(f => new FileInfo(f).LastWriteTime);
+            if (lastUsedConfigPath is null)
+                return null;
+            
+            Log.Info($@"Importing old config from %LocalAppData%\{Path.GetRelativePath(localAppDataPath, lastUsedConfigPath)}");
+            var xml = XDocument.Load(lastUsedConfigPath);
+            if (xml.Root?.Element("userSettings")?.FirstNode is not XElement settingsNode)
+                return null;
+
+            var template = (string)settingsNode.Descendants("setting")
+                .FirstOrDefault(el => (string)el.Attribute("name") == "DumpNameTemplate")?
+                .Element("value");
+            var output = (string)settingsNode.Descendants("setting")
+                .FirstOrDefault(el => (string)el.Attribute("name") == "OutputDir")?
+                .Element("value");
+            var ird = (string)settingsNode.Descendants("setting")
+                .FirstOrDefault(el => (string)el.Attribute("name") == "IrdDir")?
+                .Element("value");
+            var result = new Settings();
+            if (template is { Length: > 0 })
+                result = result with { DumpNameTemplate = template };
+            if (output is { Length: > 0 } && Directory.Exists(output))
+                result = result with { OutputDir = output };
+            if (ird is { Length: > 0 } && Directory.Exists(ird))
+                result = result with { IrdDir = ird };
+            var newSettingsContent = JsonSerializer.Serialize(result, serializerOptions);
+            Log.Info($"Imported settings: {newSettingsContent}");
+            return result;
+        }
+        catch (Exception e)
+        {
+            Log.Error(e, "Failed to import old settings file");
+            return null;
+        }
+    }
+    
+    public static void Save()
+    {
+        var tmp = Settings;
+        if (tmp.Equals(savedSettings))
+            return;
+
+        try
+        {
+            if (!Directory.Exists(settingsFolder))
+                Directory.CreateDirectory(settingsFolder);
+
+            var settingsContent = JsonSerializer.Serialize(tmp, serializerOptions);
+            Log.Info($"Updated settings: {settingsContent}");
+            using var file = File.Open(settingsPath, FileMode.Create, FileAccess.Write, FileShare.Read);
+            using var writer = new StreamWriter(file);
+            writer.Write(settingsContent);
+            writer.Flush();
+            file.Flush();
+            savedSettings = tmp;
+        }
+        catch (Exception e)
+        {
+            Log.Error(e, "Failed to save settings");
+        }
+    }
+
+    private static Settings savedSettings = new();
+    public static Settings Settings { get; set; } = new();
+}
\ No newline at end of file
diff --git a/Ps3DiscDumper/Utils/SecurityEx.cs b/Ps3DiscDumper/Utils/SecurityEx.cs
new file mode 100644
index 0000000..f0365e7
--- /dev/null
+++ b/Ps3DiscDumper/Utils/SecurityEx.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Linq;
+using System.Security.Principal;
+
+namespace Ps3DiscDumper.Utils;
+
+public static class SecurityEx
+{
+    public static bool IsSafe(params string[] args)
+    {
+        if (OperatingSystem.IsWindows())
+        {
+            using var identity = WindowsIdentity.GetCurrent();
+            var principal = new WindowsPrincipal(identity);
+            if (principal.IsInRole(WindowsBuiltInRole.Administrator)
+                && !args.Any(p => p.Equals("/IUnderstandThatRunningSoftwareAsAdministratorIsDangerousAndNotRecommendedForAnyone")))
+            {
+                return false;
+            }
+        }
+        return true;
+    }
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 4a17b97..cfdd090 100644
--- a/README.md
+++ b/README.md
@@ -10,32 +10,42 @@ Requirements
 * Disc must have decryption key, either in redump database or in the IRD Library
 * For binary release you might need to install .NET prerequisites
   * See `Supported OS versions` and `Dependencies` sections in [documentation](https://learn.microsoft.com/en-us/dotnet/core/install/windows?tabs=net60#dependencies)
-* For source release you will need to have [.NET 7.0 SDK](https://www.microsoft.com/net/download) installed on your machine
 
 How to use
 ==========
-1. Put `ps3-disc-dumper` executable in the folder you want your dumps to be in (GUI version has configurable settings)
-2. Insert a PS3 disc in the compatible drive
-3. On Linux open terminal in the folder where you put the binary
-    1. `$ chmod +x ps3-disc-dumper` to make it executable
-    2. Mount the disc (either through file manager or manually `$ mount` it to `/media/...`)
-4. Start the dumper
-5. Wait for it to complete
+1. Put `ps3-disc-dumper` executable in any folder
+   * On Linux make the file executable (via `Properties > Permissions` or via console command `$ chmod +x ./ps3-disc-dumper`)
+2. Start the dumper
+3. Follow the instructions
+    * On Linux you may need to confirm auto-mount or to mount the disc manually (either through file manager or via console command `$ mount`)
 
-By default all files will be copied in the folder where the dumper was started from (`.\[BLUS12345] Game Title\`).
+By default all files will be copied in the folder where the dumper is, you can select different location in `Settings`.
 
-You can pass an optional parameter with the path if you want to dump in a custom location. This is mostly useful when ran from sources with `$ dotnet run`.
+If you have custom key or IRD file, you can put it in local cache (see `Settings` for exact location).
 
-If you have custom key or IRD file, you can put it in local cache (`.\ird\` on Windows, `~/.config/ps3-disc-dumper/ird/` on Linux).
+Logs can be found in `Settings` at the bottom.
 
-Logs can be found in `/logs/` on Windows or in `~/.config/ps3-disc-dumper/logs/` on Linux.
+Screenshots
+===========
+Windows 11
 
-Versions
+
+  
+  
+  Screenshot running on Windows 11
+
+
+Ubuntu 22.04
+
+
+  
+  
+  Screenshot running on Ubuntu 22.04
+
+
+Building
 ========
-There are different ways to get the dumper work:
-* precompiled binaries
-  * make it executable on Linux, then simply run it
-* running from sources:
-  * `$ git clone https://github.com/13xforever/ps3-disc-dumper.git`
-  * `$ cd UI.Console`
-  * `$ dotnet run`
+* To compile the solution, you will need to have [.NET 7.0 SDK](https://www.microsoft.com/net/download) installed on your machine.
+* It is recommended to use [JetBrains Raider](https://www.jetbrains.com/rider/), [Visual Studio](https://visualstudio.microsoft.com/), or [VS Code](https://code.visualstudio.com/) for development.
+  * On Linux select the Linux configuration to prevent various issues due to Windows-specific dependencies.
+  * You can build for both Windows and Linux on Windows (you can test Linux build with WSL2 or with a Linux VM).
diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj
index e7d8f7b..0c35b71 100644
--- a/Tests/Tests.csproj
+++ b/Tests/Tests.csproj
@@ -7,7 +7,7 @@
   
 
   
-    
+    
     
     
       all
diff --git a/UI.Avalonia/Animations/CustomPageSlide.cs b/UI.Avalonia/Animations/CustomPageSlide.cs
new file mode 100644
index 0000000..9872252
--- /dev/null
+++ b/UI.Avalonia/Animations/CustomPageSlide.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Animation;
+
+namespace UI.Avalonia.Animations;
+
+public class CustomPageSlide : PageSlide
+{
+    /// 
+    /// Initializes a new instance of the  class.
+    /// 
+    public CustomPageSlide()
+    {
+    }
+
+    /// 
+    /// Initializes a new instance of the  class.
+    /// 
+    /// The duration of the animation.
+    /// The axis on which the animation should occur
+    /// Whether to reverse the animation direction
+    public CustomPageSlide(TimeSpan duration, SlideAxis orientation = SlideAxis.Horizontal, bool reverseAnimation = false)
+    {
+        Duration = duration;
+        Orientation = orientation;
+        ReverseAnimation = reverseAnimation;
+    }
+    
+    public bool ReverseAnimation { get; set; }
+
+    /// 
+    public override Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
+    {
+        if (ReverseAnimation)
+            forward = !forward;
+
+        return base.Start(from, to, forward, cancellationToken);
+    }
+}
diff --git a/UI.Avalonia/App.axaml b/UI.Avalonia/App.axaml
new file mode 100644
index 0000000..85936e0
--- /dev/null
+++ b/UI.Avalonia/App.axaml
@@ -0,0 +1,28 @@
+
+             
+
+    
+        
+    
+  
+    
+        
+    
+             
+    
+        avares://ps3-disc-dumper/Assets/Fonts#Font Awesome 6 Free
+        
+        
+        
+        
+        
+        
+        
+    
+             
+
\ No newline at end of file
diff --git a/UI.Avalonia/App.axaml.cs b/UI.Avalonia/App.axaml.cs
new file mode 100644
index 0000000..0b48903
--- /dev/null
+++ b/UI.Avalonia/App.axaml.cs
@@ -0,0 +1,149 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.Styling;
+using Ps3DiscDumper;
+using Ps3DiscDumper.Utils;
+using UI.Avalonia.Utils.ColorPalette;
+using UI.Avalonia.ViewModels;
+using UI.Avalonia.Views;
+
+namespace UI.Avalonia;
+
+public partial class App : Application
+{
+    private static readonly WindowTransparencyLevel[] DesiredTransparencyHints =
+    {
+        WindowTransparencyLevel.Mica,
+        WindowTransparencyLevel.AcrylicBlur,
+        WindowTransparencyLevel.None,
+    };
+
+    private readonly Lazy isMicaCapable = new(() =>
+        Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: Window w }
+        && w.ActualTransparencyLevel == WindowTransparencyLevel.Mica
+    );
+
+    public override void Initialize() => AvaloniaXamlLoader.Load(this);
+
+    public override void OnFrameworkInitializationCompleted()
+    {
+        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+        {
+            var safeToRun = SecurityEx.IsSafe(desktop.Args);
+            Window w;
+            ViewModelBase vm;
+            if (safeToRun)
+            {
+                var mainViewModel = new MainWindowViewModel();
+                vm = mainViewModel.CurrentPage;
+                SetSymbolFont(vm);
+                w = new MainWindow { DataContext = mainViewModel };
+            }
+            else
+            {
+                vm = new ErrorStubViewModel();
+                SetSymbolFont(vm);
+                w = new ErrorStub { DataContext = vm };
+            }
+            desktop.MainWindow = w;
+            desktop.MainWindow.Activated += OnActivated;
+            desktop.MainWindow.Deactivated += OnDeactivated;
+            desktop.MainWindow.ActualThemeVariantChanged += OnThemeChanged;
+            if (w.PlatformSettings is { } ps)
+                ps.ColorValuesChanged += OnPlatformColorsChanged;
+
+            vm.MicaEnabled = isMicaCapable.Value;
+            vm.AcrylicEnabled = w.ActualTransparencyLevel == WindowTransparencyLevel.AcrylicBlur;
+
+            var systemFonts = FontManager.Current.SystemFonts;
+            if (systemFonts.TryGetGlyphTypeface("Segoe UI Variable Text", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out _))
+                w.FontFamily = new("Segoe UI Variable Text");
+            else if (systemFonts.TryGetGlyphTypeface("Segoe UI", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out _))
+                w.FontFamily = new("Segoe UI");
+        }
+        base.OnFrameworkInitializationCompleted();
+    }
+
+    private static void SetSymbolFont(ViewModelBase vm)
+    {
+        var systemFonts = FontManager.Current.SystemFonts;
+        if (systemFonts.TryGetGlyphTypeface("Segoe Fluent Icons", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out _))
+            vm.SymbolFontFamily = new("Segoe Fluent Icons");
+        if (systemFonts.TryGetGlyphTypeface("Segoe UI Variable Small", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out _))
+            vm.SmallFontFamily = new("Segoe UI Variable Small");
+        if (systemFonts.TryGetGlyphTypeface("Segoe UI Variable Display", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out _))
+            vm.LargeFontFamily = new("Segoe UI Variable Display");
+    }
+
+    private void OnActivated(object? sender, EventArgs e)
+    {
+        if (sender is not Window w)
+            return;
+
+        if (isMicaCapable.Value && SettingsProvider.Settings.EnableTransparency)
+            w.TransparencyLevelHint = DesiredTransparencyHints;
+    }
+
+    private void OnDeactivated(object? sender, EventArgs e)
+    {
+        if (sender is not Window { DataContext: MainWindowViewModel vm } w)
+            return;
+
+        if (isMicaCapable.Value)
+            w.TransparencyLevelHint = Array.Empty();
+        if (w.ActualThemeVariant == ThemeVariant.Light)
+            vm.CurrentPage.TintColor = ThemeConsts.LightThemeTintColor;
+        else if (w.ActualThemeVariant == ThemeVariant.Dark)
+            vm.CurrentPage.TintColor = ThemeConsts.DarkThemeTintColor;
+    }
+
+    internal static void OnThemeChanged(object? sender, EventArgs e)
+    {
+        Window w;
+        ViewModelBase vm;
+        if (sender is Window { DataContext: MainWindowViewModel { CurrentPage: {} vm1 } } w1)
+            (w, vm) = (w1, vm1);
+        else if (sender is Window { DataContext: ViewModelBase vm2 } w2)
+            (w, vm) = (w2, vm2);
+        else
+            return;
+
+        if (w.ActualThemeVariant == ThemeVariant.Light)
+        {
+            vm.TintColor = ThemeConsts.LightThemeTintColor;
+            vm.TintOpacity = ThemeConsts.LightThemeTintOpacity;
+            vm.DimTextColor = ThemeConsts.LightThemeDimGray;
+            vm.Layer2BackgroundColor = ThemeConsts.LightThemeLayer2Background;
+            vm.Layer2GroundedColor = ThemeConsts.LightThemeLayer2Grounded;
+            vm.ColorPalette = ThemeConsts.Light;
+        }
+        else if (w.ActualThemeVariant == ThemeVariant.Dark)
+        {
+            vm.TintColor = ThemeConsts.DarkThemeTintColor;
+            vm.TintOpacity = ThemeConsts.DarkThemeTintOpacity;
+            vm.DimTextColor = ThemeConsts.DarkThemeDimGray;
+            vm.Layer2BackgroundColor = ThemeConsts.DarkThemeLayer2Background;
+            vm.Layer2GroundedColor = ThemeConsts.DarkThemeLayer2Grounded;
+            vm.ColorPalette = ThemeConsts.Dark;
+        }
+    }
+
+
+    internal static void OnPlatformColorsChanged(object? sender, PlatformColorValues e)
+    {
+        if (Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime
+            {
+                MainWindow.DataContext: MainWindowViewModel { CurrentPage: ViewModelBase vm }
+            })
+            return;
+        
+        vm.SystemAccentColor1 = e.AccentColor1.ToString();
+        vm.SystemAccentColor2 = e.AccentColor2.ToString();
+        vm.SystemAccentColor3 = e.AccentColor3.ToString();
+    }
+}
\ No newline at end of file
diff --git a/UI.Avalonia/Assets/Fonts/Font Awesome 6 Free-Solid-900.otf b/UI.Avalonia/Assets/Fonts/Font Awesome 6 Free-Solid-900.otf
new file mode 100644
index 0000000..a95c71e
Binary files /dev/null and b/UI.Avalonia/Assets/Fonts/Font Awesome 6 Free-Solid-900.otf differ
diff --git a/UI.Avalonia/Assets/icon.ico b/UI.Avalonia/Assets/icon.ico
new file mode 100644
index 0000000..f4e8289
Binary files /dev/null and b/UI.Avalonia/Assets/icon.ico differ
diff --git a/UI.Avalonia/Converters/BrushConverter.cs b/UI.Avalonia/Converters/BrushConverter.cs
new file mode 100644
index 0000000..2adc4f7
--- /dev/null
+++ b/UI.Avalonia/Converters/BrushConverter.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Concurrent;
+using System.Globalization;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+
+namespace UI.Avalonia.Converters;
+
+public class BrushConverter: IValueConverter
+{
+    private static readonly ConcurrentDictionary KnownBrushes = new();
+
+    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+    {
+        if (value is string { Length: > 0 } s)
+            return Parse(s);
+        return default(Brush);
+    }
+
+    public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+        => throw new NotImplementedException();
+
+    internal static IBrush Parse(string color)
+    {
+        if (KnownBrushes.TryGetValue(color, out var result))
+            return result;
+
+        var c = Color.Parse(color);
+        result = new SolidColorBrush(c, c.A / 255.0);
+        KnownBrushes.TryAdd(color, result);
+        return result;
+    }
+}
\ No newline at end of file
diff --git a/UI.Avalonia/Converters/ColorConverter.cs b/UI.Avalonia/Converters/ColorConverter.cs
new file mode 100644
index 0000000..0ef6fa6
--- /dev/null
+++ b/UI.Avalonia/Converters/ColorConverter.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+
+namespace UI.Avalonia.Converters;
+
+public class ColorConverter: IValueConverter
+{
+    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+    {
+        if (value is string { Length: > 0 } s)
+            return Color.Parse(s);
+        return default(Color);
+    }
+
+    public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+    {
+        if (value is Color c)
+            return c.ToString();
+        return default(Color).ToString();
+    }
+}
\ No newline at end of file
diff --git a/UI.Avalonia/Converters/OnOffConverter.cs b/UI.Avalonia/Converters/OnOffConverter.cs
new file mode 100644
index 0000000..c5d2e14
--- /dev/null
+++ b/UI.Avalonia/Converters/OnOffConverter.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace UI.Avalonia.Converters;
+
+public class OnOffConverter: IValueConverter
+{
+    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+        => value is true ? "On" : "Off";
+
+    public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+        => throw new NotImplementedException();
+}
\ No newline at end of file
diff --git a/UI.Avalonia/Converters/PageTransitionConverter.cs b/UI.Avalonia/Converters/PageTransitionConverter.cs
new file mode 100644
index 0000000..093b2f3
--- /dev/null
+++ b/UI.Avalonia/Converters/PageTransitionConverter.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Globalization;
+using Avalonia.Animation;
+using Avalonia.Data.Converters;
+using UI.Avalonia.Animations;
+
+namespace UI.Avalonia.Converters;
+
+public class PageTransitionConverter: IValueConverter
+{
+    // https://learn.microsoft.com/en-us/windows/apps/design/motion/timing-and-easing
+    private static readonly TimeSpan AnimationTime = TimeSpan.FromMilliseconds(167);
+    private static readonly IPageTransition Normal = new CustomPageSlide(AnimationTime, PageSlide.SlideAxis.Horizontal, false);
+    private static readonly IPageTransition Reversed = new CustomPageSlide(AnimationTime, PageSlide.SlideAxis.Horizontal, true);
+    
+    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+        => value is true ? Normal : Reversed;
+
+    public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+        => throw new NotImplementedException();
+}
\ No newline at end of file
diff --git a/UI.Avalonia/Converters/SymbolConverterBase.cs b/UI.Avalonia/Converters/SymbolConverterBase.cs
new file mode 100644
index 0000000..b43cac7
--- /dev/null
+++ b/UI.Avalonia/Converters/SymbolConverterBase.cs
@@ -0,0 +1,10 @@
+using System;
+using System.Linq;
+using Avalonia.Media;
+
+namespace UI.Avalonia.Converters;
+
+public class SymbolConverterBase
+{
+    protected static readonly Lazy HasFluentIcons = new(() => FontManager.Current.SystemFonts.Any(f => f.Name is "Segoe Fluent Icons"));
+}
\ No newline at end of file
diff --git a/UI.Avalonia/Converters/TextRenderingModeConverter.cs b/UI.Avalonia/Converters/TextRenderingModeConverter.cs
new file mode 100644
index 0000000..64ef987
--- /dev/null
+++ b/UI.Avalonia/Converters/TextRenderingModeConverter.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Globalization;
+using Avalonia.Controls;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+
+namespace UI.Avalonia.Converters;
+
+public class TextRenderingModeConverter: IValueConverter
+{
+    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+        => value is WindowTransparencyLevel wtl
+            ? wtl == WindowTransparencyLevel.Mica || wtl == WindowTransparencyLevel.AcrylicBlur
+                ? TextRenderingMode.Antialias
+                : TextRenderingMode.SubpixelAntialias
+            : null;
+
+    public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+        => throw new NotImplementedException();
+}
\ No newline at end of file
diff --git a/UI.Avalonia/Converters/UpdateColorConverter.cs b/UI.Avalonia/Converters/UpdateColorConverter.cs
new file mode 100644
index 0000000..4986150
--- /dev/null
+++ b/UI.Avalonia/Converters/UpdateColorConverter.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Globalization;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Data;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+using Avalonia.Styling;
+using UI.Avalonia.Utils;
+using UI.Avalonia.Utils.ColorPalette;
+
+namespace UI.Avalonia.Converters;
+
+public class UpdateColorConverter: IValueConverter
+{
+    private static readonly IBrush AccentBrush = BrushConverter.Parse(ThemeConsts.AccentColor);
+    
+    public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+    {
+        IBrush dimGrey = Brushes.Red;
+        if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime { MainWindow: Window w })
+            return dimGrey;
+
+        if (w.ActualThemeVariant == ThemeVariant.Light)
+            dimGrey = BrushConverter.Parse(ThemeConsts.LightThemeDimGray);
+        else if (w.ActualThemeVariant == ThemeVariant.Dark)
+            dimGrey = BrushConverter.Parse(ThemeConsts.DarkThemeDimGray);
+        return value is true ? dimGrey : AccentBrush;
+    }
+
+    public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+        => throw new NotImplementedException();
+}
\ No newline at end of file
diff --git a/UI.Avalonia/Converters/ValidationColorConverter.cs b/UI.Avalonia/Converters/ValidationColorConverter.cs
new file mode 100644
index 0000000..874e635
--- /dev/null
+++ b/UI.Avalonia/Converters/ValidationColorConverter.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Globalization;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Data.Converters;
+using Avalonia.Styling;
+using UI.Avalonia.Utils.ColorPalette;
+
+namespace UI.Avalonia.Converters;
+
+public class ValidationColorConverter: IValueConverter
+{
+    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+    {
+        if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime { MainWindow: Window w })
+            return default;
+        
+        IPalette palette = ThemeConsts.Debug;
+        if (w.ActualThemeVariant == ThemeVariant.Light)
+            palette = ThemeConsts.Light;
+        else if (w.ActualThemeVariant == ThemeVariant.Dark)
+            palette = ThemeConsts.Dark;
+        return value is bool b 
+            ? b
+                ? BrushConverter.Parse(palette.StatusSuccessForeground1)
+                : BrushConverter.Parse(palette.StatusDangerForeground1)
+            : default;
+    }
+
+    public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+        => throw new NotImplementedException();
+}
\ No newline at end of file
diff --git a/UI.Avalonia/Converters/ValidationSymbolConverter.cs b/UI.Avalonia/Converters/ValidationSymbolConverter.cs
new file mode 100644
index 0000000..1e540fc
--- /dev/null
+++ b/UI.Avalonia/Converters/ValidationSymbolConverter.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace UI.Avalonia.Converters;
+
+public class ValidationSymbolConverter: SymbolConverterBase, IValueConverter
+{
+    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+        => value is bool b
+            ? HasFluentIcons.Value
+                ? b ? "\ue73e" : "\ueb90"
+                : b ? "\uf058" : "\uf06a"
+            : null;
+
+    public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+        => throw new NotImplementedException();
+}
\ No newline at end of file
diff --git a/UI.Avalonia/Program.cs b/UI.Avalonia/Program.cs
new file mode 100644
index 0000000..7739829
--- /dev/null
+++ b/UI.Avalonia/Program.cs
@@ -0,0 +1,24 @@
+using Avalonia;
+using Avalonia.ReactiveUI;
+using System;
+
+namespace UI.Avalonia;
+
+class Program
+{
+    // Initialization code. Don't use any Avalonia, third-party APIs or any
+    // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
+    // yet and stuff might break.
+    [STAThread]
+    public static void Main(string[] args)
+        => BuildAvaloniaApp()
+            .StartWithClassicDesktopLifetime(args);
+
+    // Avalonia configuration, don't remove; also used by visual designer.
+    public static AppBuilder BuildAvaloniaApp()
+        => AppBuilder.Configure()
+            .UsePlatformDetect()
+            .WithInterFont()
+            .LogToTrace()
+            .UseReactiveUI();
+}
\ No newline at end of file
diff --git a/UI.Avalonia/UI.Avalonia.csproj b/UI.Avalonia/UI.Avalonia.csproj
new file mode 100644
index 0000000..546ee20
--- /dev/null
+++ b/UI.Avalonia/UI.Avalonia.csproj
@@ -0,0 +1,62 @@
+
+    
+        WinExe
+        enable
+        true
+        app.manifest
+        true
+        ps3-disc-dumper
+        latest
+        Assets\icon.ico
+        true
+        Debug;Release;Debug Linux
+        AnyCPU
+    
+
+    
+        net7.0-windows;net7.0
+    
+    
+    
+        net7.0
+        true
+        false
+    
+    
+    
+        WINDOWS
+    
+
+    
+        true
+        false
+        false
+        true
+    
+
+    
+        
+        
+    
+
+    
+        
+        
+        
+        
+        
+        
+        
+        
+        
+    
+    
+        
+        
+    
+
+    
+      
+      
+    
+
diff --git a/UI.Avalonia/Utils/ColorPalette/DebugPalette.cs b/UI.Avalonia/Utils/ColorPalette/DebugPalette.cs
new file mode 100644
index 0000000..bc1e64a
--- /dev/null
+++ b/UI.Avalonia/Utils/ColorPalette/DebugPalette.cs
@@ -0,0 +1,344 @@
+namespace UI.Avalonia.Utils.ColorPalette;
+
+public class DebugPalette: IPalette
+{
+    public string NeutralForeground1 => "#ff0000";
+    public string NeutralForeground1Hover => "#ff0000";
+    public string NeutralForeground1Pressed => "#ff0000";
+    public string NeutralForeground1Selected => "#ff0000";
+    public string NeutralForeground2 => "#ff0000";
+    public string NeutralForeground2Hover => "#ff0000";
+    public string NeutralForeground2Pressed => "#ff0000";
+    public string NeutralForeground2Selected => "#ff0000";
+    public string NeutralForeground2BrandHover => "#ff0000";
+    public string NeutralForeground2BrandPressed => "#ff0000";
+    public string NeutralForeground2BrandSelected => "#ff0000";
+    public string NeutralForeground3 => "#ff0000";
+    public string NeutralForeground3Hover => "#ff0000";
+    public string NeutralForeground3Pressed => "#ff0000";
+    public string NeutralForeground3Selected => "#ff0000";
+    public string NeutralForeground3BrandHover => "#ff0000";
+    public string NeutralForeground3BrandPressed => "#ff0000";
+    public string NeutralForeground3BrandSelected => "#ff0000";
+    public string NeutralForeground4 => "#ff0000";
+    public string NeutralForegroundDisabled => "#ff0000";
+    public string NeutralForegroundInvertedDisabled => "#ff0000";
+    public string BrandForegroundLink => "#ff0000";
+    public string BrandForegroundLinkHover => "#ff0000";
+    public string BrandForegroundLinkPressed => "#ff0000";
+    public string BrandForegroundLinkSelected => "#ff0000";
+    public string NeutralForeground2Link => "#ff0000";
+    public string NeutralForeground2LinkHover => "#ff0000";
+    public string NeutralForeground2LinkPressed => "#ff0000";
+    public string NeutralForeground2LinkSelected => "#ff0000";
+    public string CompoundBrandForeground1 => "#ff0000";
+    public string CompoundBrandForeground1Hover => "#ff0000";
+    public string CompoundBrandForeground1Pressed => "#ff0000";
+    public string BrandForeground1 => "#ff0000";
+    public string BrandForeground2 => "#ff0000";
+    public string BrandForeground2Hover => "#ff0000";
+    public string BrandForeground2Pressed => "#ff0000";
+    public string NeutralForeground1Static => "#ff0000";
+    public string NeutralForegroundStaticInverted => "#ff0000";
+    public string NeutralForegroundInverted => "#ff0000";
+    public string NeutralForegroundInvertedHover => "#ff0000";
+    public string NeutralForegroundInvertedPressed => "#ff0000";
+    public string NeutralForegroundInvertedSelected => "#ff0000";
+    public string NeutralForegroundInverted2 => "#ff0000";
+    public string NeutralForegroundOnBrand => "#ff0000";
+    public string NeutralForegroundInvertedLink => "#ff0000";
+    public string NeutralForegroundInvertedLinkHover => "#ff0000";
+    public string NeutralForegroundInvertedLinkPressed => "#ff0000";
+    public string NeutralForegroundInvertedLinkSelected => "#ff0000";
+    public string BrandForegroundInverted => "#ff0000";
+    public string BrandForegroundInvertedHover => "#ff0000";
+    public string BrandForegroundInvertedPressed => "#ff0000";
+    public string BrandForegroundOnLight => "#ff0000";
+    public string BrandForegroundOnLightHover => "#ff0000";
+    public string BrandForegroundOnLightPressed => "#ff0000";
+    public string BrandForegroundOnLightSelected => "#ff0000";
+    public string NeutralBackground1 => "#ff0000";
+    public string NeutralBackground1Hover => "#ff0000";
+    public string NeutralBackground1Pressed => "#ff0000";
+    public string NeutralBackground1Selected => "#ff0000";
+    public string NeutralBackground2 => "#ff0000";
+    public string NeutralBackground2Hover => "#ff0000";
+    public string NeutralBackground2Pressed => "#ff0000";
+    public string NeutralBackground2Selected => "#ff0000";
+    public string NeutralBackground3 => "#ff0000";
+    public string NeutralBackground3Hover => "#ff0000";
+    public string NeutralBackground3Pressed => "#ff0000";
+    public string NeutralBackground3Selected => "#ff0000";
+    public string NeutralBackground4 => "#ff0000";
+    public string NeutralBackground4Hover => "#ff0000";
+    public string NeutralBackground4Pressed => "#ff0000";
+    public string NeutralBackground4Selected => "#ff0000";
+    public string NeutralBackground5 => "#ff0000";
+    public string NeutralBackground5Hover => "#ff0000";
+    public string NeutralBackground5Pressed => "#ff0000";
+    public string NeutralBackground5Selected => "#ff0000";
+    public string NeutralBackground6 => "#ff0000";
+    public string NeutralBackgroundInverted => "#ff0000";
+    public string NeutralBackgroundStatic => "#ff0000";
+    public string NeutralBackgroundAlpha => "#ff0000";
+    public string NeutralBackgroundAlpha2 => "#ff0000";
+    public string SubtleBackground => "#ff0000";
+    public string SubtleBackgroundHover => "#ff0000";
+    public string SubtleBackgroundPressed => "#ff0000";
+    public string SubtleBackgroundSelected => "#ff0000";
+    public string SubtleBackgroundLightAlphaHover => "#ff0000";
+    public string SubtleBackgroundLightAlphaPressed => "#ff0000";
+    public string SubtleBackgroundLightAlphaSelected => "#ff0000";
+    public string SubtleBackgroundInverted => "#ff0000";
+    public string SubtleBackgroundInvertedHover => "#ff0000";
+    public string SubtleBackgroundInvertedPressed => "#ff0000";
+    public string SubtleBackgroundInvertedSelected => "#ff0000";
+    public string TransparentBackground => "#ff0000";
+    public string TransparentBackgroundHover => "#ff0000";
+    public string TransparentBackgroundPressed => "#ff0000";
+    public string TransparentBackgroundSelected => "#ff0000";
+    public string NeutralBackgroundDisabled => "#ff0000";
+    public string NeutralBackgroundInvertedDisabled => "#ff0000";
+    public string NeutralStencil1 => "#ff0000";
+    public string NeutralStencil2 => "#ff0000";
+    public string NeutralStencil1Alpha => "#ff0000";
+    public string NeutralStencil2Alpha => "#ff0000";
+    public string BackgroundOverlay => "#ff0000";
+    public string ScrollbarOverlay => "#ff0000";
+    public string BrandBackground => "#ff0000";
+    public string BrandBackgroundHover => "#ff0000";
+    public string BrandBackgroundPressed => "#ff0000";
+    public string BrandBackgroundSelected => "#ff0000";
+    public string CompoundBrandBackground => "#ff0000";
+    public string CompoundBrandBackgroundHover => "#ff0000";
+    public string CompoundBrandBackgroundPressed => "#ff0000";
+    public string BrandBackgroundStatic => "#ff0000";
+    public string BrandBackground2 => "#ff0000";
+    public string BrandBackground2Hover => "#ff0000";
+    public string BrandBackground2Pressed => "#ff0000";
+    public string BrandBackgroundInverted => "#ff0000";
+    public string BrandBackgroundInvertedHover => "#ff0000";
+    public string BrandBackgroundInvertedPressed => "#ff0000";
+    public string BrandBackgroundInvertedSelected => "#ff0000";
+    public string NeutralStrokeAccessible => "#ff0000";
+    public string NeutralStrokeAccessibleHover => "#ff0000";
+    public string NeutralStrokeAccessiblePressed => "#ff0000";
+    public string NeutralStrokeAccessibleSelected => "#ff0000";
+    public string NeutralStroke1 => "#ff0000";
+    public string NeutralStroke1Hover => "#ff0000";
+    public string NeutralStroke1Pressed => "#ff0000";
+    public string NeutralStroke1Selected => "#ff0000";
+    public string NeutralStroke2 => "#ff0000";
+    public string NeutralStroke3 => "#ff0000";
+    public string NeutralStrokeSubtle => "#ff0000";
+    public string NeutralStrokeOnBrand => "#ff0000";
+    public string NeutralStrokeOnBrand2 => "#ff0000";
+    public string NeutralStrokeOnBrand2Hover => "#ff0000";
+    public string NeutralStrokeOnBrand2Pressed => "#ff0000";
+    public string NeutralStrokeOnBrand2Selected => "#ff0000";
+    public string BrandStroke1 => "#ff0000";
+    public string BrandStroke2 => "#ff0000";
+    public string BrandStroke2Hover => "#ff0000";
+    public string BrandStroke2Pressed => "#ff0000";
+    public string BrandStroke2Contrast => "#ff0000";
+    public string CompoundBrandStroke => "#ff0000";
+    public string CompoundBrandStrokeHover => "#ff0000";
+    public string CompoundBrandStrokePressed => "#ff0000";
+    public string NeutralStrokeDisabled => "#ff0000";
+    public string NeutralStrokeInvertedDisabled => "#ff0000";
+    public string TransparentStroke => "#ff0000";
+    public string TransparentStrokeInteractive => "#ff0000";
+    public string TransparentStrokeDisabled => "#ff0000";
+    public string NeutralStrokeAlpha => "#ff0000";
+    public string NeutralStrokeAlpha2 => "#ff0000";
+    public string StrokeFocus1 => "#ff0000";
+    public string StrokeFocus2 => "#ff0000";
+    public string NeutralShadowAmbient => "#ff0000";
+    public string NeutralShadowKey => "#ff0000";
+    public string NeutralShadowAmbientLighter => "#ff0000";
+    public string NeutralShadowKeyLighter => "#ff0000";
+    public string NeutralShadowAmbientDarker => "#ff0000";
+    public string NeutralShadowKeyDarker => "#ff0000";
+    public string BrandShadowAmbient => "#ff0000";
+    public string BrandShadowKey => "#ff0000";
+    public string PaletteRedBackground1 => "#ff0000";
+    public string PaletteRedBackground2 => "#ff0000";
+    public string PaletteRedBackground3 => "#ff0000";
+    public string PaletteRedForeground1 => "#ff0000";
+    public string PaletteRedForeground2 => "#ff0000";
+    public string PaletteRedForeground3 => "#ff0000";
+    public string PaletteRedBorderActive => "#ff0000";
+    public string PaletteRedBorder1 => "#ff0000";
+    public string PaletteRedBorder2 => "#ff0000";
+    public string PaletteGreenBackground1 => "#ff0000";
+    public string PaletteGreenBackground2 => "#ff0000";
+    public string PaletteGreenBackground3 => "#ff0000";
+    public string PaletteGreenForeground1 => "#ff0000";
+    public string PaletteGreenForeground2 => "#ff0000";
+    public string PaletteGreenForeground3 => "#ff0000";
+    public string PaletteGreenBorderActive => "#ff0000";
+    public string PaletteGreenBorder1 => "#ff0000";
+    public string PaletteGreenBorder2 => "#ff0000";
+    public string PaletteDarkOrangeBackground1 => "#ff0000";
+    public string PaletteDarkOrangeBackground2 => "#ff0000";
+    public string PaletteDarkOrangeBackground3 => "#ff0000";
+    public string PaletteDarkOrangeForeground1 => "#ff0000";
+    public string PaletteDarkOrangeForeground2 => "#ff0000";
+    public string PaletteDarkOrangeForeground3 => "#ff0000";
+    public string PaletteDarkOrangeBorderActive => "#ff0000";
+    public string PaletteDarkOrangeBorder1 => "#ff0000";
+    public string PaletteDarkOrangeBorder2 => "#ff0000";
+    public string PaletteYellowBackground1 => "#ff0000";
+    public string PaletteYellowBackground2 => "#ff0000";
+    public string PaletteYellowBackground3 => "#ff0000";
+    public string PaletteYellowForeground1 => "#ff0000";
+    public string PaletteYellowForeground2 => "#ff0000";
+    public string PaletteYellowForeground3 => "#ff0000";
+    public string PaletteYellowBorderActive => "#ff0000";
+    public string PaletteYellowBorder1 => "#ff0000";
+    public string PaletteYellowBorder2 => "#ff0000";
+    public string PaletteBerryBackground1 => "#ff0000";
+    public string PaletteBerryBackground2 => "#ff0000";
+    public string PaletteBerryBackground3 => "#ff0000";
+    public string PaletteBerryForeground1 => "#ff0000";
+    public string PaletteBerryForeground2 => "#ff0000";
+    public string PaletteBerryForeground3 => "#ff0000";
+    public string PaletteBerryBorderActive => "#ff0000";
+    public string PaletteBerryBorder1 => "#ff0000";
+    public string PaletteBerryBorder2 => "#ff0000";
+    public string PaletteLightGreenBackground1 => "#ff0000";
+    public string PaletteLightGreenBackground2 => "#ff0000";
+    public string PaletteLightGreenBackground3 => "#ff0000";
+    public string PaletteLightGreenForeground1 => "#ff0000";
+    public string PaletteLightGreenForeground2 => "#ff0000";
+    public string PaletteLightGreenForeground3 => "#ff0000";
+    public string PaletteLightGreenBorderActive => "#ff0000";
+    public string PaletteLightGreenBorder1 => "#ff0000";
+    public string PaletteLightGreenBorder2 => "#ff0000";
+    public string PaletteMarigoldBackground1 => "#ff0000";
+    public string PaletteMarigoldBackground2 => "#ff0000";
+    public string PaletteMarigoldBackground3 => "#ff0000";
+    public string PaletteMarigoldForeground1 => "#ff0000";
+    public string PaletteMarigoldForeground2 => "#ff0000";
+    public string PaletteMarigoldForeground3 => "#ff0000";
+    public string PaletteMarigoldBorderActive => "#ff0000";
+    public string PaletteMarigoldBorder1 => "#ff0000";
+    public string PaletteMarigoldBorder2 => "#ff0000";
+    public string PaletteRedForegroundInverted => "#ff0000";
+    public string PaletteGreenForegroundInverted => "#ff0000";
+    public string PaletteYellowForegroundInverted => "#ff0000";
+    public string PaletteDarkRedBackground2 => "#ff0000";
+    public string PaletteDarkRedForeground2 => "#ff0000";
+    public string PaletteDarkRedBorderActive => "#ff0000";
+    public string PaletteCranberryBackground2 => "#ff0000";
+    public string PaletteCranberryForeground2 => "#ff0000";
+    public string PaletteCranberryBorderActive => "#ff0000";
+    public string PalettePumpkinBackground2 => "#ff0000";
+    public string PalettePumpkinForeground2 => "#ff0000";
+    public string PalettePumpkinBorderActive => "#ff0000";
+    public string PalettePeachBackground2 => "#ff0000";
+    public string PalettePeachForeground2 => "#ff0000";
+    public string PalettePeachBorderActive => "#ff0000";
+    public string PaletteGoldBackground2 => "#ff0000";
+    public string PaletteGoldForeground2 => "#ff0000";
+    public string PaletteGoldBorderActive => "#ff0000";
+    public string PaletteBrassBackground2 => "#ff0000";
+    public string PaletteBrassForeground2 => "#ff0000";
+    public string PaletteBrassBorderActive => "#ff0000";
+    public string PaletteBrownBackground2 => "#ff0000";
+    public string PaletteBrownForeground2 => "#ff0000";
+    public string PaletteBrownBorderActive => "#ff0000";
+    public string PaletteForestBackground2 => "#ff0000";
+    public string PaletteForestForeground2 => "#ff0000";
+    public string PaletteForestBorderActive => "#ff0000";
+    public string PaletteSeafoamBackground2 => "#ff0000";
+    public string PaletteSeafoamForeground2 => "#ff0000";
+    public string PaletteSeafoamBorderActive => "#ff0000";
+    public string PaletteDarkGreenBackground2 => "#ff0000";
+    public string PaletteDarkGreenForeground2 => "#ff0000";
+    public string PaletteDarkGreenBorderActive => "#ff0000";
+    public string PaletteLightTealBackground2 => "#ff0000";
+    public string PaletteLightTealForeground2 => "#ff0000";
+    public string PaletteLightTealBorderActive => "#ff0000";
+    public string PaletteTealBackground2 => "#ff0000";
+    public string PaletteTealForeground2 => "#ff0000";
+    public string PaletteTealBorderActive => "#ff0000";
+    public string PaletteSteelBackground2 => "#ff0000";
+    public string PaletteSteelForeground2 => "#ff0000";
+    public string PaletteSteelBorderActive => "#ff0000";
+    public string PaletteBlueBackground2 => "#ff0000";
+    public string PaletteBlueForeground2 => "#ff0000";
+    public string PaletteBlueBorderActive => "#ff0000";
+    public string PaletteRoyalBlueBackground2 => "#ff0000";
+    public string PaletteRoyalBlueForeground2 => "#ff0000";
+    public string PaletteRoyalBlueBorderActive => "#ff0000";
+    public string PaletteCornflowerBackground2 => "#ff0000";
+    public string PaletteCornflowerForeground2 => "#ff0000";
+    public string PaletteCornflowerBorderActive => "#ff0000";
+    public string PaletteNavyBackground2 => "#ff0000";
+    public string PaletteNavyForeground2 => "#ff0000";
+    public string PaletteNavyBorderActive => "#ff0000";
+    public string PaletteLavenderBackground2 => "#ff0000";
+    public string PaletteLavenderForeground2 => "#ff0000";
+    public string PaletteLavenderBorderActive => "#ff0000";
+    public string PalettePurpleBackground2 => "#ff0000";
+    public string PalettePurpleForeground2 => "#ff0000";
+    public string PalettePurpleBorderActive => "#ff0000";
+    public string PaletteGrapeBackground2 => "#ff0000";
+    public string PaletteGrapeForeground2 => "#ff0000";
+    public string PaletteGrapeBorderActive => "#ff0000";
+    public string PaletteLilacBackground2 => "#ff0000";
+    public string PaletteLilacForeground2 => "#ff0000";
+    public string PaletteLilacBorderActive => "#ff0000";
+    public string PalettePinkBackground2 => "#ff0000";
+    public string PalettePinkForeground2 => "#ff0000";
+    public string PalettePinkBorderActive => "#ff0000";
+    public string PaletteMagentaBackground2 => "#ff0000";
+    public string PaletteMagentaForeground2 => "#ff0000";
+    public string PaletteMagentaBorderActive => "#ff0000";
+    public string PalettePlumBackground2 => "#ff0000";
+    public string PalettePlumForeground2 => "#ff0000";
+    public string PalettePlumBorderActive => "#ff0000";
+    public string PaletteBeigeBackground2 => "#ff0000";
+    public string PaletteBeigeForeground2 => "#ff0000";
+    public string PaletteBeigeBorderActive => "#ff0000";
+    public string PaletteMinkBackground2 => "#ff0000";
+    public string PaletteMinkForeground2 => "#ff0000";
+    public string PaletteMinkBorderActive => "#ff0000";
+    public string PalettePlatinumBackground2 => "#ff0000";
+    public string PalettePlatinumForeground2 => "#ff0000";
+    public string PalettePlatinumBorderActive => "#ff0000";
+    public string PaletteAnchorBackground2 => "#ff0000";
+    public string PaletteAnchorForeground2 => "#ff0000";
+    public string PaletteAnchorBorderActive => "#ff0000";
+    public string StatusSuccessBackground1 => "#ff0000";
+    public string StatusSuccessBackground2 => "#ff0000";
+    public string StatusSuccessBackground3 => "#ff0000";
+    public string StatusSuccessForeground1 => "#ff0000";
+    public string StatusSuccessForeground2 => "#ff0000";
+    public string StatusSuccessForeground3 => "#ff0000";
+    public string StatusSuccessForegroundInverted => "#ff0000";
+    public string StatusSuccessBorderActive => "#ff0000";
+    public string StatusSuccessBorder1 => "#ff0000";
+    public string StatusSuccessBorder2 => "#ff0000";
+    public string StatusWarningBackground1 => "#ff0000";
+    public string StatusWarningBackground2 => "#ff0000";
+    public string StatusWarningBackground3 => "#ff0000";
+    public string StatusWarningForeground1 => "#ff0000";
+    public string StatusWarningForeground2 => "#ff0000";
+    public string StatusWarningForeground3 => "#ff0000";
+    public string StatusWarningForegroundInverted => "#ff0000";
+    public string StatusWarningBorderActive => "#ff0000";
+    public string StatusWarningBorder1 => "#ff0000";
+    public string StatusWarningBorder2 => "#ff0000";
+    public string StatusDangerBackground1 => "#ff0000";
+    public string StatusDangerBackground2 => "#ff0000";
+    public string StatusDangerBackground3 => "#ff0000";
+    public string StatusDangerForeground1 => "#ff0000";
+    public string StatusDangerForeground2 => "#ff0000";
+    public string StatusDangerForeground3 => "#ff0000";
+    public string StatusDangerForegroundInverted => "#ff0000";
+    public string StatusDangerBorderActive => "#ff0000";
+    public string StatusDangerBorder1 => "#ff0000";
+    public string StatusDangerBorder2 => "#ff0000";
+}
\ No newline at end of file
diff --git a/UI.Avalonia/Utils/ColorPalette/FluentPaletteDark.cs b/UI.Avalonia/Utils/ColorPalette/FluentPaletteDark.cs
new file mode 100644
index 0000000..19cad28
--- /dev/null
+++ b/UI.Avalonia/Utils/ColorPalette/FluentPaletteDark.cs
@@ -0,0 +1,344 @@
+namespace UI.Avalonia.Utils.ColorPalette;
+
+public class FluentPaletteDark: IPalette
+{
+    public string NeutralForeground1 => "#ffffff";
+    public string NeutralForeground1Hover => "#ffffff";
+    public string NeutralForeground1Pressed => "#ffffff";
+    public string NeutralForeground1Selected => "#ffffff";
+    public string NeutralForeground2 => "#d6d6d6";
+    public string NeutralForeground2Hover => "#ffffff";
+    public string NeutralForeground2Pressed => "#ffffff";
+    public string NeutralForeground2Selected => "#ffffff";
+    public string NeutralForeground2BrandHover => "#479ef5";
+    public string NeutralForeground2BrandPressed => "#2886de";
+    public string NeutralForeground2BrandSelected => "#479ef5";
+    public string NeutralForeground3 => "#adadad";
+    public string NeutralForeground3Hover => "#d6d6d6";
+    public string NeutralForeground3Pressed => "#d6d6d6";
+    public string NeutralForeground3Selected => "#d6d6d6";
+    public string NeutralForeground3BrandHover => "#479ef5";
+    public string NeutralForeground3BrandPressed => "#2886de";
+    public string NeutralForeground3BrandSelected => "#479ef5";
+    public string NeutralForeground4 => "#999999";
+    public string NeutralForegroundDisabled => "#5c5c5c";
+    public string NeutralForegroundInvertedDisabled => "rgba(255, 255, 255, 0.4)";
+    public string BrandForegroundLink => "#479ef5";
+    public string BrandForegroundLinkHover => "#62abf5";
+    public string BrandForegroundLinkPressed => "#2886de";
+    public string BrandForegroundLinkSelected => "#479ef5";
+    public string NeutralForeground2Link => "#d6d6d6";
+    public string NeutralForeground2LinkHover => "#ffffff";
+    public string NeutralForeground2LinkPressed => "#ffffff";
+    public string NeutralForeground2LinkSelected => "#ffffff";
+    public string CompoundBrandForeground1 => "#479ef5";
+    public string CompoundBrandForeground1Hover => "#62abf5";
+    public string CompoundBrandForeground1Pressed => "#2886de";
+    public string BrandForeground1 => "#479ef5";
+    public string BrandForeground2 => "#62abf5";
+    public string BrandForeground2Hover => "#96c6fa";
+    public string BrandForeground2Pressed => "#ebf3fc";
+    public string NeutralForeground1Static => "#242424";
+    public string NeutralForegroundStaticInverted => "#ffffff";
+    public string NeutralForegroundInverted => "#242424";
+    public string NeutralForegroundInvertedHover => "#242424";
+    public string NeutralForegroundInvertedPressed => "#242424";
+    public string NeutralForegroundInvertedSelected => "#242424";
+    public string NeutralForegroundInverted2 => "#242424";
+    public string NeutralForegroundOnBrand => "#ffffff";
+    public string NeutralForegroundInvertedLink => "#ffffff";
+    public string NeutralForegroundInvertedLinkHover => "#ffffff";
+    public string NeutralForegroundInvertedLinkPressed => "#ffffff";
+    public string NeutralForegroundInvertedLinkSelected => "#ffffff";
+    public string BrandForegroundInverted => "#0f6cbd";
+    public string BrandForegroundInvertedHover => "#115ea3";
+    public string BrandForegroundInvertedPressed => "#0f548c";
+    public string BrandForegroundOnLight => "#0f6cbd";
+    public string BrandForegroundOnLightHover => "#115ea3";
+    public string BrandForegroundOnLightPressed => "#0e4775";
+    public string BrandForegroundOnLightSelected => "#0f548c";
+    public string NeutralBackground1 => "#292929";
+    public string NeutralBackground1Hover => "#3d3d3d";
+    public string NeutralBackground1Pressed => "#1f1f1f";
+    public string NeutralBackground1Selected => "#383838";
+    public string NeutralBackground2 => "#1f1f1f";
+    public string NeutralBackground2Hover => "#333333";
+    public string NeutralBackground2Pressed => "#141414";
+    public string NeutralBackground2Selected => "#2e2e2e";
+    public string NeutralBackground3 => "#141414";
+    public string NeutralBackground3Hover => "#292929";
+    public string NeutralBackground3Pressed => "#0a0a0a";
+    public string NeutralBackground3Selected => "#242424";
+    public string NeutralBackground4 => "#0a0a0a";
+    public string NeutralBackground4Hover => "#1f1f1f";
+    public string NeutralBackground4Pressed => "#000000";
+    public string NeutralBackground4Selected => "#1a1a1a";
+    public string NeutralBackground5 => "#000000";
+    public string NeutralBackground5Hover => "#141414";
+    public string NeutralBackground5Pressed => "#050505";
+    public string NeutralBackground5Selected => "#0f0f0f";
+    public string NeutralBackground6 => "#333333";
+    public string NeutralBackgroundInverted => "#ffffff";
+    public string NeutralBackgroundStatic => "#3d3d3d";
+    public string NeutralBackgroundAlpha => "rgba(26, 26, 26, 0.5)";
+    public string NeutralBackgroundAlpha2 => "rgba(31, 31, 31, 0.7)";
+    public string SubtleBackground => "transparent";
+    public string SubtleBackgroundHover => "#383838";
+    public string SubtleBackgroundPressed => "#2e2e2e";
+    public string SubtleBackgroundSelected => "#333333";
+    public string SubtleBackgroundLightAlphaHover => "rgba(36, 36, 36, 0.8)";
+    public string SubtleBackgroundLightAlphaPressed => "rgba(36, 36, 36, 0.5)";
+    public string SubtleBackgroundLightAlphaSelected => "transparent";
+    public string SubtleBackgroundInverted => "transparent";
+    public string SubtleBackgroundInvertedHover => "rgba(0, 0, 0, 0.1)";
+    public string SubtleBackgroundInvertedPressed => "rgba(0, 0, 0, 0.3)";
+    public string SubtleBackgroundInvertedSelected => "rgba(0, 0, 0, 0.2)";
+    public string TransparentBackground => "transparent";
+    public string TransparentBackgroundHover => "transparent";
+    public string TransparentBackgroundPressed => "transparent";
+    public string TransparentBackgroundSelected => "transparent";
+    public string NeutralBackgroundDisabled => "#141414";
+    public string NeutralBackgroundInvertedDisabled => "rgba(255, 255, 255, 0.1)";
+    public string NeutralStencil1 => "#575757";
+    public string NeutralStencil2 => "#333333";
+    public string NeutralStencil1Alpha => "rgba(255, 255, 255, 0.1)";
+    public string NeutralStencil2Alpha => "rgba(255, 255, 255, 0.05)";
+    public string BackgroundOverlay => "rgba(0, 0, 0, 0.5)";
+    public string ScrollbarOverlay => "rgba(255, 255, 255, 0.6)";
+    public string BrandBackground => "#115ea3";
+    public string BrandBackgroundHover => "#0f6cbd";
+    public string BrandBackgroundPressed => "#0c3b5e";
+    public string BrandBackgroundSelected => "#0f548c";
+    public string CompoundBrandBackground => "#479ef5";
+    public string CompoundBrandBackgroundHover => "#62abf5";
+    public string CompoundBrandBackgroundPressed => "#2886de";
+    public string BrandBackgroundStatic => "#0f6cbd";
+    public string BrandBackground2 => "#082338";
+    public string BrandBackground2Hover => "#0c3b5e";
+    public string BrandBackground2Pressed => "#061724";
+    public string BrandBackgroundInverted => "#ffffff";
+    public string BrandBackgroundInvertedHover => "#ebf3fc";
+    public string BrandBackgroundInvertedPressed => "#b4d6fa";
+    public string BrandBackgroundInvertedSelected => "#cfe4fa";
+    public string NeutralStrokeAccessible => "#adadad";
+    public string NeutralStrokeAccessibleHover => "#bdbdbd";
+    public string NeutralStrokeAccessiblePressed => "#b3b3b3";
+    public string NeutralStrokeAccessibleSelected => "#479ef5";
+    public string NeutralStroke1 => "#666666";
+    public string NeutralStroke1Hover => "#757575";
+    public string NeutralStroke1Pressed => "#6b6b6b";
+    public string NeutralStroke1Selected => "#707070";
+    public string NeutralStroke2 => "#525252";
+    public string NeutralStroke3 => "#3d3d3d";
+    public string NeutralStrokeSubtle => "#0a0a0a";
+    public string NeutralStrokeOnBrand => "#292929";
+    public string NeutralStrokeOnBrand2 => "#ffffff";
+    public string NeutralStrokeOnBrand2Hover => "#ffffff";
+    public string NeutralStrokeOnBrand2Pressed => "#ffffff";
+    public string NeutralStrokeOnBrand2Selected => "#ffffff";
+    public string BrandStroke1 => "#479ef5";
+    public string BrandStroke2 => "#0e4775";
+    public string BrandStroke2Hover => "#0e4775";
+    public string BrandStroke2Pressed => "#0a2e4a";
+    public string BrandStroke2Contrast => "#0e4775";
+    public string CompoundBrandStroke => "#479ef5";
+    public string CompoundBrandStrokeHover => "#62abf5";
+    public string CompoundBrandStrokePressed => "#2886de";
+    public string NeutralStrokeDisabled => "#424242";
+    public string NeutralStrokeInvertedDisabled => "rgba(255, 255, 255, 0.4)";
+    public string TransparentStroke => "transparent";
+    public string TransparentStrokeInteractive => "transparent";
+    public string TransparentStrokeDisabled => "transparent";
+    public string NeutralStrokeAlpha => "rgba(255, 255, 255, 0.1)";
+    public string NeutralStrokeAlpha2 => "rgba(255, 255, 255, 0.2)";
+    public string StrokeFocus1 => "#000000";
+    public string StrokeFocus2 => "#ffffff";
+    public string NeutralShadowAmbient => "rgba(0,0,0,0.24)";
+    public string NeutralShadowKey => "rgba(0,0,0,0.28)";
+    public string NeutralShadowAmbientLighter => "rgba(0,0,0,0.12)";
+    public string NeutralShadowKeyLighter => "rgba(0,0,0,0.14)";
+    public string NeutralShadowAmbientDarker => "rgba(0,0,0,0.40)";
+    public string NeutralShadowKeyDarker => "rgba(0,0,0,0.48)";
+    public string BrandShadowAmbient => "rgba(0,0,0,0.30)";
+    public string BrandShadowKey => "rgba(0,0,0,0.25)";
+    public string PaletteRedBackground1 => "#3f1011";
+    public string PaletteRedBackground2 => "#751d1f";
+    public string PaletteRedBackground3 => "#d13438";
+    public string PaletteRedForeground1 => "#e37d80";
+    public string PaletteRedForeground2 => "#f1bbbc";
+    public string PaletteRedForeground3 => "#e37d80";
+    public string PaletteRedBorderActive => "#e37d80";
+    public string PaletteRedBorder1 => "#d13438";
+    public string PaletteRedBorder2 => "#e37d80";
+    public string PaletteGreenBackground1 => "#052505";
+    public string PaletteGreenBackground2 => "#094509";
+    public string PaletteGreenBackground3 => "#107c10";
+    public string PaletteGreenForeground1 => "#54b054";
+    public string PaletteGreenForeground2 => "#9fd89f";
+    public string PaletteGreenForeground3 => "#9fd89f";
+    public string PaletteGreenBorderActive => "#54b054";
+    public string PaletteGreenBorder1 => "#107c10";
+    public string PaletteGreenBorder2 => "#9fd89f";
+    public string PaletteDarkOrangeBackground1 => "#411200";
+    public string PaletteDarkOrangeBackground2 => "#7a2101";
+    public string PaletteDarkOrangeBackground3 => "#da3b01";
+    public string PaletteDarkOrangeForeground1 => "#e9835e";
+    public string PaletteDarkOrangeForeground2 => "#f4bfab";
+    public string PaletteDarkOrangeForeground3 => "#e9835e";
+    public string PaletteDarkOrangeBorderActive => "#e9835e";
+    public string PaletteDarkOrangeBorder1 => "#da3b01";
+    public string PaletteDarkOrangeBorder2 => "#e9835e";
+    public string PaletteYellowBackground1 => "#4c4400";
+    public string PaletteYellowBackground2 => "#817400";
+    public string PaletteYellowBackground3 => "#fde300";
+    public string PaletteYellowForeground1 => "#feee66";
+    public string PaletteYellowForeground2 => "#fef7b2";
+    public string PaletteYellowForeground3 => "#fdea3d";
+    public string PaletteYellowBorderActive => "#feee66";
+    public string PaletteYellowBorder1 => "#fde300";
+    public string PaletteYellowBorder2 => "#fdea3d";
+    public string PaletteBerryBackground1 => "#3a1136";
+    public string PaletteBerryBackground2 => "#6d2064";
+    public string PaletteBerryBackground3 => "#c239b3";
+    public string PaletteBerryForeground1 => "#da7ed0";
+    public string PaletteBerryForeground2 => "#edbbe7";
+    public string PaletteBerryForeground3 => "#d161c4";
+    public string PaletteBerryBorderActive => "#da7ed0";
+    public string PaletteBerryBorder1 => "#c239b3";
+    public string PaletteBerryBorder2 => "#d161c4";
+    public string PaletteLightGreenBackground1 => "#063004";
+    public string PaletteLightGreenBackground2 => "#0b5a08";
+    public string PaletteLightGreenBackground3 => "#13a10e";
+    public string PaletteLightGreenForeground1 => "#5ec75a";
+    public string PaletteLightGreenForeground2 => "#a7e3a5";
+    public string PaletteLightGreenForeground3 => "#3db838";
+    public string PaletteLightGreenBorderActive => "#5ec75a";
+    public string PaletteLightGreenBorder1 => "#13a10e";
+    public string PaletteLightGreenBorder2 => "#3db838";
+    public string PaletteMarigoldBackground1 => "#463100";
+    public string PaletteMarigoldBackground2 => "#835b00";
+    public string PaletteMarigoldBackground3 => "#eaa300";
+    public string PaletteMarigoldForeground1 => "#f2c661";
+    public string PaletteMarigoldForeground2 => "#f9e2ae";
+    public string PaletteMarigoldForeground3 => "#efb839";
+    public string PaletteMarigoldBorderActive => "#f2c661";
+    public string PaletteMarigoldBorder1 => "#eaa300";
+    public string PaletteMarigoldBorder2 => "#efb839";
+    public string PaletteRedForegroundInverted => "#d13438";
+    public string PaletteGreenForegroundInverted => "#107c10";
+    public string PaletteYellowForegroundInverted => "#817400";
+    public string PaletteDarkRedBackground2 => "#590815";
+    public string PaletteDarkRedForeground2 => "#d69ca5";
+    public string PaletteDarkRedBorderActive => "#ac4f5e";
+    public string PaletteCranberryBackground2 => "#6e0811";
+    public string PaletteCranberryForeground2 => "#eeacb2";
+    public string PaletteCranberryBorderActive => "#dc626d";
+    public string PalettePumpkinBackground2 => "#712d09";
+    public string PalettePumpkinForeground2 => "#efc4ad";
+    public string PalettePumpkinBorderActive => "#df8e64";
+    public string PalettePeachBackground2 => "#8f4e00";
+    public string PalettePeachForeground2 => "#ffddb3";
+    public string PalettePeachBorderActive => "#ffba66";
+    public string PaletteGoldBackground2 => "#6c5700";
+    public string PaletteGoldForeground2 => "#ecdfa5";
+    public string PaletteGoldBorderActive => "#dac157";
+    public string PaletteBrassBackground2 => "#553e06";
+    public string PaletteBrassForeground2 => "#e0cea2";
+    public string PaletteBrassBorderActive => "#c1a256";
+    public string PaletteBrownBackground2 => "#50301a";
+    public string PaletteBrownForeground2 => "#ddc3b0";
+    public string PaletteBrownBorderActive => "#bb8f6f";
+    public string PaletteForestBackground2 => "#294903";
+    public string PaletteForestForeground2 => "#bdd99b";
+    public string PaletteForestBorderActive => "#85b44c";
+    public string PaletteSeafoamBackground2 => "#00723b";
+    public string PaletteSeafoamForeground2 => "#a8f0cd";
+    public string PaletteSeafoamBorderActive => "#5ae0a0";
+    public string PaletteDarkGreenBackground2 => "#063b06";
+    public string PaletteDarkGreenForeground2 => "#9ad29a";
+    public string PaletteDarkGreenBorderActive => "#4da64d";
+    public string PaletteLightTealBackground2 => "#00666d";
+    public string PaletteLightTealForeground2 => "#a6e9ed";
+    public string PaletteLightTealBorderActive => "#58d3db";
+    public string PaletteTealBackground2 => "#02494c";
+    public string PaletteTealForeground2 => "#9bd9db";
+    public string PaletteTealBorderActive => "#4cb4b7";
+    public string PaletteSteelBackground2 => "#00333f";
+    public string PaletteSteelForeground2 => "#94c8d4";
+    public string PaletteSteelBorderActive => "#4496a9";
+    public string PaletteBlueBackground2 => "#004377";
+    public string PaletteBlueForeground2 => "#a9d3f2";
+    public string PaletteBlueBorderActive => "#5caae5";
+    public string PaletteRoyalBlueBackground2 => "#002c4e";
+    public string PaletteRoyalBlueForeground2 => "#9abfdc";
+    public string PaletteRoyalBlueBorderActive => "#4a89ba";
+    public string PaletteCornflowerBackground2 => "#2c3c85";
+    public string PaletteCornflowerForeground2 => "#c8d1fa";
+    public string PaletteCornflowerBorderActive => "#93a4f4";
+    public string PaletteNavyBackground2 => "#001665";
+    public string PaletteNavyForeground2 => "#a3b2e8";
+    public string PaletteNavyBorderActive => "#546fd2";
+    public string PaletteLavenderBackground2 => "#3f3682";
+    public string PaletteLavenderForeground2 => "#d2ccf8";
+    public string PaletteLavenderBorderActive => "#a79cf1";
+    public string PalettePurpleBackground2 => "#341a51";
+    public string PalettePurpleForeground2 => "#c6b1de";
+    public string PalettePurpleBorderActive => "#9470bd";
+    public string PaletteGrapeBackground2 => "#4c0d55";
+    public string PaletteGrapeForeground2 => "#d9a7e0";
+    public string PaletteGrapeBorderActive => "#b55fc1";
+    public string PaletteLilacBackground2 => "#63276d";
+    public string PaletteLilacForeground2 => "#e6bfed";
+    public string PaletteLilacBorderActive => "#cf87da";
+    public string PalettePinkBackground2 => "#80215d";
+    public string PalettePinkForeground2 => "#f7c0e3";
+    public string PalettePinkBorderActive => "#ef85c8";
+    public string PaletteMagentaBackground2 => "#6b0043";
+    public string PaletteMagentaForeground2 => "#eca5d1";
+    public string PaletteMagentaBorderActive => "#d957a8";
+    public string PalettePlumBackground2 => "#5a003b";
+    public string PalettePlumForeground2 => "#d696c0";
+    public string PalettePlumBorderActive => "#ad4589";
+    public string PaletteBeigeBackground2 => "#444241";
+    public string PaletteBeigeForeground2 => "#d7d4d4";
+    public string PaletteBeigeBorderActive => "#afabaa";
+    public string PaletteMinkBackground2 => "#343231";
+    public string PaletteMinkForeground2 => "#cecccb";
+    public string PaletteMinkBorderActive => "#9e9b99";
+    public string PalettePlatinumBackground2 => "#3b4447";
+    public string PalettePlatinumForeground2 => "#cdd6d8";
+    public string PalettePlatinumBorderActive => "#a0adb2";
+    public string PaletteAnchorBackground2 => "#202427";
+    public string PaletteAnchorForeground2 => "#bcc3c7";
+    public string PaletteAnchorBorderActive => "#808a90";
+    public string StatusSuccessBackground1 => "#052505";
+    public string StatusSuccessBackground2 => "#094509";
+    public string StatusSuccessBackground3 => "#107c10";
+    public string StatusSuccessForeground1 => "#54b054";
+    public string StatusSuccessForeground2 => "#9fd89f";
+    public string StatusSuccessForeground3 => "#9fd89f";
+    public string StatusSuccessForegroundInverted => "#0e700e";
+    public string StatusSuccessBorderActive => "#54b054";
+    public string StatusSuccessBorder1 => "#107c10";
+    public string StatusSuccessBorder2 => "#9fd89f";
+    public string StatusWarningBackground1 => "#4a1e04";
+    public string StatusWarningBackground2 => "#8a3707";
+    public string StatusWarningBackground3 => "#f7630c";
+    public string StatusWarningForeground1 => "#faa06b";
+    public string StatusWarningForeground2 => "#fdcfb4";
+    public string StatusWarningForeground3 => "#f98845";
+    public string StatusWarningForegroundInverted => "#bc4b09";
+    public string StatusWarningBorderActive => "#faa06b";
+    public string StatusWarningBorder1 => "#f7630c";
+    public string StatusWarningBorder2 => "#f98845";
+    public string StatusDangerBackground1 => "#3b0509";
+    public string StatusDangerBackground2 => "#6e0811";
+    public string StatusDangerBackground3 => "#c50f1f";
+    public string StatusDangerForeground1 => "#dc626d";
+    public string StatusDangerForeground2 => "#eeacb2";
+    public string StatusDangerForeground3 => "#dc626d";
+    public string StatusDangerForegroundInverted => "#b10e1c";
+    public string StatusDangerBorderActive => "#dc626d";
+    public string StatusDangerBorder1 => "#c50f1f";
+    public string StatusDangerBorder2 => "#dc626d";
+}
\ No newline at end of file
diff --git a/UI.Avalonia/Utils/ColorPalette/FluentPaletteLight.cs b/UI.Avalonia/Utils/ColorPalette/FluentPaletteLight.cs
new file mode 100644
index 0000000..d611197
--- /dev/null
+++ b/UI.Avalonia/Utils/ColorPalette/FluentPaletteLight.cs
@@ -0,0 +1,344 @@
+namespace UI.Avalonia.Utils.ColorPalette;
+
+public class FluentPaletteLight: IPalette
+{
+    public string NeutralForeground1 => "#242424";
+    public string NeutralForeground1Hover => "#242424";
+    public string NeutralForeground1Pressed => "#242424";
+    public string NeutralForeground1Selected => "#242424";
+    public string NeutralForeground2 => "#424242";
+    public string NeutralForeground2Hover => "#242424";
+    public string NeutralForeground2Pressed => "#242424";
+    public string NeutralForeground2Selected => "#242424";
+    public string NeutralForeground2BrandHover => "#0f6cbd";
+    public string NeutralForeground2BrandPressed => "#115ea3";
+    public string NeutralForeground2BrandSelected => "#0f6cbd";
+    public string NeutralForeground3 => "#616161";
+    public string NeutralForeground3Hover => "#424242";
+    public string NeutralForeground3Pressed => "#424242";
+    public string NeutralForeground3Selected => "#424242";
+    public string NeutralForeground3BrandHover => "#0f6cbd";
+    public string NeutralForeground3BrandPressed => "#115ea3";
+    public string NeutralForeground3BrandSelected => "#0f6cbd";
+    public string NeutralForeground4 => "#707070";
+    public string NeutralForegroundDisabled => "#bdbdbd";
+    public string NeutralForegroundInvertedDisabled => "rgba(255, 255, 255, 0.4)";
+    public string BrandForegroundLink => "#115ea3";
+    public string BrandForegroundLinkHover => "#0f548c";
+    public string BrandForegroundLinkPressed => "#0c3b5e";
+    public string BrandForegroundLinkSelected => "#115ea3";
+    public string NeutralForeground2Link => "#424242";
+    public string NeutralForeground2LinkHover => "#242424";
+    public string NeutralForeground2LinkPressed => "#242424";
+    public string NeutralForeground2LinkSelected => "#242424";
+    public string CompoundBrandForeground1 => "#0f6cbd";
+    public string CompoundBrandForeground1Hover => "#115ea3";
+    public string CompoundBrandForeground1Pressed => "#0f548c";
+    public string BrandForeground1 => "#0f6cbd";
+    public string BrandForeground2 => "#115ea3";
+    public string BrandForeground2Hover => "#0f548c";
+    public string BrandForeground2Pressed => "#0a2e4a";
+    public string NeutralForeground1Static => "#242424";
+    public string NeutralForegroundStaticInverted => "#ffffff";
+    public string NeutralForegroundInverted => "#ffffff";
+    public string NeutralForegroundInvertedHover => "#ffffff";
+    public string NeutralForegroundInvertedPressed => "#ffffff";
+    public string NeutralForegroundInvertedSelected => "#ffffff";
+    public string NeutralForegroundInverted2 => "#ffffff";
+    public string NeutralForegroundOnBrand => "#ffffff";
+    public string NeutralForegroundInvertedLink => "#ffffff";
+    public string NeutralForegroundInvertedLinkHover => "#ffffff";
+    public string NeutralForegroundInvertedLinkPressed => "#ffffff";
+    public string NeutralForegroundInvertedLinkSelected => "#ffffff";
+    public string BrandForegroundInverted => "#479ef5";
+    public string BrandForegroundInvertedHover => "#62abf5";
+    public string BrandForegroundInvertedPressed => "#479ef5";
+    public string BrandForegroundOnLight => "#0f6cbd";
+    public string BrandForegroundOnLightHover => "#115ea3";
+    public string BrandForegroundOnLightPressed => "#0e4775";
+    public string BrandForegroundOnLightSelected => "#0f548c";
+    public string NeutralBackground1 => "#ffffff";
+    public string NeutralBackground1Hover => "#f5f5f5";
+    public string NeutralBackground1Pressed => "#e0e0e0";
+    public string NeutralBackground1Selected => "#ebebeb";
+    public string NeutralBackground2 => "#fafafa";
+    public string NeutralBackground2Hover => "#f0f0f0";
+    public string NeutralBackground2Pressed => "#dbdbdb";
+    public string NeutralBackground2Selected => "#e6e6e6";
+    public string NeutralBackground3 => "#f5f5f5";
+    public string NeutralBackground3Hover => "#ebebeb";
+    public string NeutralBackground3Pressed => "#d6d6d6";
+    public string NeutralBackground3Selected => "#e0e0e0";
+    public string NeutralBackground4 => "#f0f0f0";
+    public string NeutralBackground4Hover => "#fafafa";
+    public string NeutralBackground4Pressed => "#f5f5f5";
+    public string NeutralBackground4Selected => "#ffffff";
+    public string NeutralBackground5 => "#ebebeb";
+    public string NeutralBackground5Hover => "#f5f5f5";
+    public string NeutralBackground5Pressed => "#f0f0f0";
+    public string NeutralBackground5Selected => "#fafafa";
+    public string NeutralBackground6 => "#e6e6e6";
+    public string NeutralBackgroundInverted => "#292929";
+    public string NeutralBackgroundStatic => "#333333";
+    public string NeutralBackgroundAlpha => "rgba(255, 255, 255, 0.5)";
+    public string NeutralBackgroundAlpha2 => "rgba(255, 255, 255, 0.8)";
+    public string SubtleBackground => "transparent";
+    public string SubtleBackgroundHover => "#f5f5f5";
+    public string SubtleBackgroundPressed => "#e0e0e0";
+    public string SubtleBackgroundSelected => "#ebebeb";
+    public string SubtleBackgroundLightAlphaHover => "rgba(255, 255, 255, 0.7)";
+    public string SubtleBackgroundLightAlphaPressed => "rgba(255, 255, 255, 0.5)";
+    public string SubtleBackgroundLightAlphaSelected => "transparent";
+    public string SubtleBackgroundInverted => "transparent";
+    public string SubtleBackgroundInvertedHover => "rgba(0, 0, 0, 0.1)";
+    public string SubtleBackgroundInvertedPressed => "rgba(0, 0, 0, 0.3)";
+    public string SubtleBackgroundInvertedSelected => "rgba(0, 0, 0, 0.2)";
+    public string TransparentBackground => "transparent";
+    public string TransparentBackgroundHover => "transparent";
+    public string TransparentBackgroundPressed => "transparent";
+    public string TransparentBackgroundSelected => "transparent";
+    public string NeutralBackgroundDisabled => "#f0f0f0";
+    public string NeutralBackgroundInvertedDisabled => "rgba(255, 255, 255, 0.1)";
+    public string NeutralStencil1 => "#e6e6e6";
+    public string NeutralStencil2 => "#fafafa";
+    public string NeutralStencil1Alpha => "rgba(0, 0, 0, 0.1)";
+    public string NeutralStencil2Alpha => "rgba(0, 0, 0, 0.05)";
+    public string BackgroundOverlay => "rgba(0, 0, 0, 0.4)";
+    public string ScrollbarOverlay => "rgba(0, 0, 0, 0.5)";
+    public string BrandBackground => "#0f6cbd";
+    public string BrandBackgroundHover => "#115ea3";
+    public string BrandBackgroundPressed => "#0c3b5e";
+    public string BrandBackgroundSelected => "#0f548c";
+    public string CompoundBrandBackground => "#0f6cbd";
+    public string CompoundBrandBackgroundHover => "#115ea3";
+    public string CompoundBrandBackgroundPressed => "#0f548c";
+    public string BrandBackgroundStatic => "#0f6cbd";
+    public string BrandBackground2 => "#ebf3fc";
+    public string BrandBackground2Hover => "#cfe4fa";
+    public string BrandBackground2Pressed => "#96c6fa";
+    public string BrandBackgroundInverted => "#ffffff";
+    public string BrandBackgroundInvertedHover => "#ebf3fc";
+    public string BrandBackgroundInvertedPressed => "#b4d6fa";
+    public string BrandBackgroundInvertedSelected => "#cfe4fa";
+    public string NeutralStrokeAccessible => "#616161";
+    public string NeutralStrokeAccessibleHover => "#575757";
+    public string NeutralStrokeAccessiblePressed => "#4d4d4d";
+    public string NeutralStrokeAccessibleSelected => "#0f6cbd";
+    public string NeutralStroke1 => "#d1d1d1";
+    public string NeutralStroke1Hover => "#c7c7c7";
+    public string NeutralStroke1Pressed => "#b3b3b3";
+    public string NeutralStroke1Selected => "#bdbdbd";
+    public string NeutralStroke2 => "#e0e0e0";
+    public string NeutralStroke3 => "#f0f0f0";
+    public string NeutralStrokeSubtle => "#e0e0e0";
+    public string NeutralStrokeOnBrand => "#ffffff";
+    public string NeutralStrokeOnBrand2 => "#ffffff";
+    public string NeutralStrokeOnBrand2Hover => "#ffffff";
+    public string NeutralStrokeOnBrand2Pressed => "#ffffff";
+    public string NeutralStrokeOnBrand2Selected => "#ffffff";
+    public string BrandStroke1 => "#0f6cbd";
+    public string BrandStroke2 => "#b4d6fa";
+    public string BrandStroke2Hover => "#77b7f7";
+    public string BrandStroke2Pressed => "#0f6cbd";
+    public string BrandStroke2Contrast => "#b4d6fa";
+    public string CompoundBrandStroke => "#0f6cbd";
+    public string CompoundBrandStrokeHover => "#115ea3";
+    public string CompoundBrandStrokePressed => "#0f548c";
+    public string NeutralStrokeDisabled => "#e0e0e0";
+    public string NeutralStrokeInvertedDisabled => "rgba(255, 255, 255, 0.4)";
+    public string TransparentStroke => "transparent";
+    public string TransparentStrokeInteractive => "transparent";
+    public string TransparentStrokeDisabled => "transparent";
+    public string NeutralStrokeAlpha => "rgba(0, 0, 0, 0.05)";
+    public string NeutralStrokeAlpha2 => "rgba(255, 255, 255, 0.2)";
+    public string StrokeFocus1 => "#ffffff";
+    public string StrokeFocus2 => "#000000";
+    public string NeutralShadowAmbient => "rgba(0,0,0,0.12)";
+    public string NeutralShadowKey => "rgba(0,0,0,0.14)";
+    public string NeutralShadowAmbientLighter => "rgba(0,0,0,0.06)";
+    public string NeutralShadowKeyLighter => "rgba(0,0,0,0.07)";
+    public string NeutralShadowAmbientDarker => "rgba(0,0,0,0.20)";
+    public string NeutralShadowKeyDarker => "rgba(0,0,0,0.24)";
+    public string BrandShadowAmbient => "rgba(0,0,0,0.30)";
+    public string BrandShadowKey => "rgba(0,0,0,0.25)";
+    public string PaletteRedBackground1 => "#fdf6f6";
+    public string PaletteRedBackground2 => "#f1bbbc";
+    public string PaletteRedBackground3 => "#d13438";
+    public string PaletteRedForeground1 => "#bc2f32";
+    public string PaletteRedForeground2 => "#751d1f";
+    public string PaletteRedForeground3 => "#d13438";
+    public string PaletteRedBorderActive => "#d13438";
+    public string PaletteRedBorder1 => "#f1bbbc";
+    public string PaletteRedBorder2 => "#d13438";
+    public string PaletteGreenBackground1 => "#f1faf1";
+    public string PaletteGreenBackground2 => "#9fd89f";
+    public string PaletteGreenBackground3 => "#107c10";
+    public string PaletteGreenForeground1 => "#0e700e";
+    public string PaletteGreenForeground2 => "#094509";
+    public string PaletteGreenForeground3 => "#107c10";
+    public string PaletteGreenBorderActive => "#107c10";
+    public string PaletteGreenBorder1 => "#9fd89f";
+    public string PaletteGreenBorder2 => "#107c10";
+    public string PaletteDarkOrangeBackground1 => "#fdf6f3";
+    public string PaletteDarkOrangeBackground2 => "#f4bfab";
+    public string PaletteDarkOrangeBackground3 => "#da3b01";
+    public string PaletteDarkOrangeForeground1 => "#c43501";
+    public string PaletteDarkOrangeForeground2 => "#7a2101";
+    public string PaletteDarkOrangeForeground3 => "#da3b01";
+    public string PaletteDarkOrangeBorderActive => "#da3b01";
+    public string PaletteDarkOrangeBorder1 => "#f4bfab";
+    public string PaletteDarkOrangeBorder2 => "#da3b01";
+    public string PaletteYellowBackground1 => "#fffef5";
+    public string PaletteYellowBackground2 => "#fef7b2";
+    public string PaletteYellowBackground3 => "#fde300";
+    public string PaletteYellowForeground1 => "#817400";
+    public string PaletteYellowForeground2 => "#817400";
+    public string PaletteYellowForeground3 => "#fde300";
+    public string PaletteYellowBorderActive => "#fde300";
+    public string PaletteYellowBorder1 => "#fef7b2";
+    public string PaletteYellowBorder2 => "#fde300";
+    public string PaletteBerryBackground1 => "#fdf5fc";
+    public string PaletteBerryBackground2 => "#edbbe7";
+    public string PaletteBerryBackground3 => "#c239b3";
+    public string PaletteBerryForeground1 => "#af33a1";
+    public string PaletteBerryForeground2 => "#6d2064";
+    public string PaletteBerryForeground3 => "#c239b3";
+    public string PaletteBerryBorderActive => "#c239b3";
+    public string PaletteBerryBorder1 => "#edbbe7";
+    public string PaletteBerryBorder2 => "#c239b3";
+    public string PaletteLightGreenBackground1 => "#f2fbf2";
+    public string PaletteLightGreenBackground2 => "#a7e3a5";
+    public string PaletteLightGreenBackground3 => "#13a10e";
+    public string PaletteLightGreenForeground1 => "#11910d";
+    public string PaletteLightGreenForeground2 => "#0b5a08";
+    public string PaletteLightGreenForeground3 => "#13a10e";
+    public string PaletteLightGreenBorderActive => "#13a10e";
+    public string PaletteLightGreenBorder1 => "#a7e3a5";
+    public string PaletteLightGreenBorder2 => "#13a10e";
+    public string PaletteMarigoldBackground1 => "#fefbf4";
+    public string PaletteMarigoldBackground2 => "#f9e2ae";
+    public string PaletteMarigoldBackground3 => "#eaa300";
+    public string PaletteMarigoldForeground1 => "#d39300";
+    public string PaletteMarigoldForeground2 => "#835b00";
+    public string PaletteMarigoldForeground3 => "#eaa300";
+    public string PaletteMarigoldBorderActive => "#eaa300";
+    public string PaletteMarigoldBorder1 => "#f9e2ae";
+    public string PaletteMarigoldBorder2 => "#eaa300";
+    public string PaletteRedForegroundInverted => "#dc5e62";
+    public string PaletteGreenForegroundInverted => "#359b35";
+    public string PaletteYellowForegroundInverted => "#fef7b2";
+    public string PaletteDarkRedBackground2 => "#d69ca5";
+    public string PaletteDarkRedForeground2 => "#420610";
+    public string PaletteDarkRedBorderActive => "#750b1c";
+    public string PaletteCranberryBackground2 => "#eeacb2";
+    public string PaletteCranberryForeground2 => "#6e0811";
+    public string PaletteCranberryBorderActive => "#c50f1f";
+    public string PalettePumpkinBackground2 => "#efc4ad";
+    public string PalettePumpkinForeground2 => "#712d09";
+    public string PalettePumpkinBorderActive => "#ca5010";
+    public string PalettePeachBackground2 => "#ffddb3";
+    public string PalettePeachForeground2 => "#8f4e00";
+    public string PalettePeachBorderActive => "#ff8c00";
+    public string PaletteGoldBackground2 => "#ecdfa5";
+    public string PaletteGoldForeground2 => "#6c5700";
+    public string PaletteGoldBorderActive => "#c19c00";
+    public string PaletteBrassBackground2 => "#e0cea2";
+    public string PaletteBrassForeground2 => "#553e06";
+    public string PaletteBrassBorderActive => "#986f0b";
+    public string PaletteBrownBackground2 => "#ddc3b0";
+    public string PaletteBrownForeground2 => "#50301a";
+    public string PaletteBrownBorderActive => "#8e562e";
+    public string PaletteForestBackground2 => "#bdd99b";
+    public string PaletteForestForeground2 => "#294903";
+    public string PaletteForestBorderActive => "#498205";
+    public string PaletteSeafoamBackground2 => "#a8f0cd";
+    public string PaletteSeafoamForeground2 => "#00723b";
+    public string PaletteSeafoamBorderActive => "#00cc6a";
+    public string PaletteDarkGreenBackground2 => "#9ad29a";
+    public string PaletteDarkGreenForeground2 => "#063b06";
+    public string PaletteDarkGreenBorderActive => "#0b6a0b";
+    public string PaletteLightTealBackground2 => "#a6e9ed";
+    public string PaletteLightTealForeground2 => "#00666d";
+    public string PaletteLightTealBorderActive => "#00b7c3";
+    public string PaletteTealBackground2 => "#9bd9db";
+    public string PaletteTealForeground2 => "#02494c";
+    public string PaletteTealBorderActive => "#038387";
+    public string PaletteSteelBackground2 => "#94c8d4";
+    public string PaletteSteelForeground2 => "#00333f";
+    public string PaletteSteelBorderActive => "#005b70";
+    public string PaletteBlueBackground2 => "#a9d3f2";
+    public string PaletteBlueForeground2 => "#004377";
+    public string PaletteBlueBorderActive => "#0078d4";
+    public string PaletteRoyalBlueBackground2 => "#9abfdc";
+    public string PaletteRoyalBlueForeground2 => "#002c4e";
+    public string PaletteRoyalBlueBorderActive => "#004e8c";
+    public string PaletteCornflowerBackground2 => "#c8d1fa";
+    public string PaletteCornflowerForeground2 => "#2c3c85";
+    public string PaletteCornflowerBorderActive => "#4f6bed";
+    public string PaletteNavyBackground2 => "#a3b2e8";
+    public string PaletteNavyForeground2 => "#001665";
+    public string PaletteNavyBorderActive => "#0027b4";
+    public string PaletteLavenderBackground2 => "#d2ccf8";
+    public string PaletteLavenderForeground2 => "#3f3682";
+    public string PaletteLavenderBorderActive => "#7160e8";
+    public string PalettePurpleBackground2 => "#c6b1de";
+    public string PalettePurpleForeground2 => "#341a51";
+    public string PalettePurpleBorderActive => "#5c2e91";
+    public string PaletteGrapeBackground2 => "#d9a7e0";
+    public string PaletteGrapeForeground2 => "#4c0d55";
+    public string PaletteGrapeBorderActive => "#881798";
+    public string PaletteLilacBackground2 => "#e6bfed";
+    public string PaletteLilacForeground2 => "#63276d";
+    public string PaletteLilacBorderActive => "#b146c2";
+    public string PalettePinkBackground2 => "#f7c0e3";
+    public string PalettePinkForeground2 => "#80215d";
+    public string PalettePinkBorderActive => "#e43ba6";
+    public string PaletteMagentaBackground2 => "#eca5d1";
+    public string PaletteMagentaForeground2 => "#6b0043";
+    public string PaletteMagentaBorderActive => "#bf0077";
+    public string PalettePlumBackground2 => "#d696c0";
+    public string PalettePlumForeground2 => "#43002b";
+    public string PalettePlumBorderActive => "#77004d";
+    public string PaletteBeigeBackground2 => "#d7d4d4";
+    public string PaletteBeigeForeground2 => "#444241";
+    public string PaletteBeigeBorderActive => "#7a7574";
+    public string PaletteMinkBackground2 => "#cecccb";
+    public string PaletteMinkForeground2 => "#343231";
+    public string PaletteMinkBorderActive => "#5d5a58";
+    public string PalettePlatinumBackground2 => "#cdd6d8";
+    public string PalettePlatinumForeground2 => "#3b4447";
+    public string PalettePlatinumBorderActive => "#69797e";
+    public string PaletteAnchorBackground2 => "#bcc3c7";
+    public string PaletteAnchorForeground2 => "#202427";
+    public string PaletteAnchorBorderActive => "#394146";
+    public string StatusSuccessBackground1 => "#f1faf1";
+    public string StatusSuccessBackground2 => "#9fd89f";
+    public string StatusSuccessBackground3 => "#107c10";
+    public string StatusSuccessForeground1 => "#0e700e";
+    public string StatusSuccessForeground2 => "#094509";
+    public string StatusSuccessForeground3 => "#107c10";
+    public string StatusSuccessForegroundInverted => "#54b054";
+    public string StatusSuccessBorderActive => "#107c10";
+    public string StatusSuccessBorder1 => "#9fd89f";
+    public string StatusSuccessBorder2 => "#107c10";
+    public string StatusWarningBackground1 => "#fff9f5";
+    public string StatusWarningBackground2 => "#fdcfb4";
+    public string StatusWarningBackground3 => "#f7630c";
+    public string StatusWarningForeground1 => "#bc4b09";
+    public string StatusWarningForeground2 => "#8a3707";
+    public string StatusWarningForeground3 => "#bc4b09";
+    public string StatusWarningForegroundInverted => "#faa06b";
+    public string StatusWarningBorderActive => "#f7630c";
+    public string StatusWarningBorder1 => "#fdcfb4";
+    public string StatusWarningBorder2 => "#bc4b09";
+    public string StatusDangerBackground1 => "#fdf3f4";
+    public string StatusDangerBackground2 => "#eeacb2";
+    public string StatusDangerBackground3 => "#c50f1f";
+    public string StatusDangerForeground1 => "#b10e1c";
+    public string StatusDangerForeground2 => "#6e0811";
+    public string StatusDangerForeground3 => "#c50f1f";
+    public string StatusDangerForegroundInverted => "#dc626d";
+    public string StatusDangerBorderActive => "#c50f1f";
+    public string StatusDangerBorder1 => "#eeacb2";
+    public string StatusDangerBorder2 => "#c50f1f";
+}
\ No newline at end of file
diff --git a/UI.Avalonia/Utils/ColorPalette/IPalette.cs b/UI.Avalonia/Utils/ColorPalette/IPalette.cs
new file mode 100644
index 0000000..0574dd9
--- /dev/null
+++ b/UI.Avalonia/Utils/ColorPalette/IPalette.cs
@@ -0,0 +1,345 @@
+namespace UI.Avalonia.Utils.ColorPalette;
+
+// generated from https://react.fluentui.dev/?path=/docs/theme-colors--page
+public interface IPalette
+{
+    string NeutralForeground1 { get; }
+    string NeutralForeground1Hover { get; }
+    string NeutralForeground1Pressed { get; }
+    string NeutralForeground1Selected { get; }
+    string NeutralForeground2 { get; }
+    string NeutralForeground2Hover { get; }
+    string NeutralForeground2Pressed { get; }
+    string NeutralForeground2Selected { get; }
+    string NeutralForeground2BrandHover { get; }
+    string NeutralForeground2BrandPressed { get; }
+    string NeutralForeground2BrandSelected { get; }
+    string NeutralForeground3 { get; }
+    string NeutralForeground3Hover { get; }
+    string NeutralForeground3Pressed { get; }
+    string NeutralForeground3Selected { get; }
+    string NeutralForeground3BrandHover { get; }
+    string NeutralForeground3BrandPressed { get; }
+    string NeutralForeground3BrandSelected { get; }
+    string NeutralForeground4 { get; }
+    string NeutralForegroundDisabled { get; }
+    string NeutralForegroundInvertedDisabled { get; }
+    string BrandForegroundLink { get; }
+    string BrandForegroundLinkHover { get; }
+    string BrandForegroundLinkPressed { get; }
+    string BrandForegroundLinkSelected { get; }
+    string NeutralForeground2Link { get; }
+    string NeutralForeground2LinkHover { get; }
+    string NeutralForeground2LinkPressed { get; }
+    string NeutralForeground2LinkSelected { get; }
+    string CompoundBrandForeground1 { get; }
+    string CompoundBrandForeground1Hover { get; }
+    string CompoundBrandForeground1Pressed { get; }
+    string BrandForeground1 { get; }
+    string BrandForeground2 { get; }
+    string BrandForeground2Hover { get; }
+    string BrandForeground2Pressed { get; }
+    string NeutralForeground1Static { get; }
+    string NeutralForegroundStaticInverted { get; }
+    string NeutralForegroundInverted { get; }
+    string NeutralForegroundInvertedHover { get; }
+    string NeutralForegroundInvertedPressed { get; }
+    string NeutralForegroundInvertedSelected { get; }
+    string NeutralForegroundInverted2 { get; }
+    string NeutralForegroundOnBrand { get; }
+    string NeutralForegroundInvertedLink { get; }
+    string NeutralForegroundInvertedLinkHover { get; }
+    string NeutralForegroundInvertedLinkPressed { get; }
+    string NeutralForegroundInvertedLinkSelected { get; }
+    string BrandForegroundInverted { get; }
+    string BrandForegroundInvertedHover { get; }
+    string BrandForegroundInvertedPressed { get; }
+    string BrandForegroundOnLight { get; }
+    string BrandForegroundOnLightHover { get; }
+    string BrandForegroundOnLightPressed { get; }
+    string BrandForegroundOnLightSelected { get; }
+    string NeutralBackground1 { get; }
+    string NeutralBackground1Hover { get; }
+    string NeutralBackground1Pressed { get; }
+    string NeutralBackground1Selected { get; }
+    string NeutralBackground2 { get; }
+    string NeutralBackground2Hover { get; }
+    string NeutralBackground2Pressed { get; }
+    string NeutralBackground2Selected { get; }
+    string NeutralBackground3 { get; }
+    string NeutralBackground3Hover { get; }
+    string NeutralBackground3Pressed { get; }
+    string NeutralBackground3Selected { get; }
+    string NeutralBackground4 { get; }
+    string NeutralBackground4Hover { get; }
+    string NeutralBackground4Pressed { get; }
+    string NeutralBackground4Selected { get; }
+    string NeutralBackground5 { get; }
+    string NeutralBackground5Hover { get; }
+    string NeutralBackground5Pressed { get; }
+    string NeutralBackground5Selected { get; }
+    string NeutralBackground6 { get; }
+    string NeutralBackgroundInverted { get; }
+    string NeutralBackgroundStatic { get; }
+    string NeutralBackgroundAlpha { get; }
+    string NeutralBackgroundAlpha2 { get; }
+    string SubtleBackground { get; }
+    string SubtleBackgroundHover { get; }
+    string SubtleBackgroundPressed { get; }
+    string SubtleBackgroundSelected { get; }
+    string SubtleBackgroundLightAlphaHover { get; }
+    string SubtleBackgroundLightAlphaPressed { get; }
+    string SubtleBackgroundLightAlphaSelected { get; }
+    string SubtleBackgroundInverted { get; }
+    string SubtleBackgroundInvertedHover { get; }
+    string SubtleBackgroundInvertedPressed { get; }
+    string SubtleBackgroundInvertedSelected { get; }
+    string TransparentBackground { get; }
+    string TransparentBackgroundHover { get; }
+    string TransparentBackgroundPressed { get; }
+    string TransparentBackgroundSelected { get; }
+    string NeutralBackgroundDisabled { get; }
+    string NeutralBackgroundInvertedDisabled { get; }
+    string NeutralStencil1 { get; }
+    string NeutralStencil2 { get; }
+    string NeutralStencil1Alpha { get; }
+    string NeutralStencil2Alpha { get; }
+    string BackgroundOverlay { get; }
+    string ScrollbarOverlay { get; }
+    string BrandBackground { get; }
+    string BrandBackgroundHover { get; }
+    string BrandBackgroundPressed { get; }
+    string BrandBackgroundSelected { get; }
+    string CompoundBrandBackground { get; }
+    string CompoundBrandBackgroundHover { get; }
+    string CompoundBrandBackgroundPressed { get; }
+    string BrandBackgroundStatic { get; }
+    string BrandBackground2 { get; }
+    string BrandBackground2Hover { get; }
+    string BrandBackground2Pressed { get; }
+    string BrandBackgroundInverted { get; }
+    string BrandBackgroundInvertedHover { get; }
+    string BrandBackgroundInvertedPressed { get; }
+    string BrandBackgroundInvertedSelected { get; }
+    string NeutralStrokeAccessible { get; }
+    string NeutralStrokeAccessibleHover { get; }
+    string NeutralStrokeAccessiblePressed { get; }
+    string NeutralStrokeAccessibleSelected { get; }
+    string NeutralStroke1 { get; }
+    string NeutralStroke1Hover { get; }
+    string NeutralStroke1Pressed { get; }
+    string NeutralStroke1Selected { get; }
+    string NeutralStroke2 { get; }
+    string NeutralStroke3 { get; }
+    string NeutralStrokeSubtle { get; }
+    string NeutralStrokeOnBrand { get; }
+    string NeutralStrokeOnBrand2 { get; }
+    string NeutralStrokeOnBrand2Hover { get; }
+    string NeutralStrokeOnBrand2Pressed { get; }
+    string NeutralStrokeOnBrand2Selected { get; }
+    string BrandStroke1 { get; }
+    string BrandStroke2 { get; }
+    string BrandStroke2Hover { get; }
+    string BrandStroke2Pressed { get; }
+    string BrandStroke2Contrast { get; }
+    string CompoundBrandStroke { get; }
+    string CompoundBrandStrokeHover { get; }
+    string CompoundBrandStrokePressed { get; }
+    string NeutralStrokeDisabled { get; }
+    string NeutralStrokeInvertedDisabled { get; }
+    string TransparentStroke { get; }
+    string TransparentStrokeInteractive { get; }
+    string TransparentStrokeDisabled { get; }
+    string NeutralStrokeAlpha { get; }
+    string NeutralStrokeAlpha2 { get; }
+    string StrokeFocus1 { get; }
+    string StrokeFocus2 { get; }
+    string NeutralShadowAmbient { get; }
+    string NeutralShadowKey { get; }
+    string NeutralShadowAmbientLighter { get; }
+    string NeutralShadowKeyLighter { get; }
+    string NeutralShadowAmbientDarker { get; }
+    string NeutralShadowKeyDarker { get; }
+    string BrandShadowAmbient { get; }
+    string BrandShadowKey { get; }
+    string PaletteRedBackground1 { get; }
+    string PaletteRedBackground2 { get; }
+    string PaletteRedBackground3 { get; }
+    string PaletteRedForeground1 { get; }
+    string PaletteRedForeground2 { get; }
+    string PaletteRedForeground3 { get; }
+    string PaletteRedBorderActive { get; }
+    string PaletteRedBorder1 { get; }
+    string PaletteRedBorder2 { get; }
+    string PaletteGreenBackground1 { get; }
+    string PaletteGreenBackground2 { get; }
+    string PaletteGreenBackground3 { get; }
+    string PaletteGreenForeground1 { get; }
+    string PaletteGreenForeground2 { get; }
+    string PaletteGreenForeground3 { get; }
+    string PaletteGreenBorderActive { get; }
+    string PaletteGreenBorder1 { get; }
+    string PaletteGreenBorder2 { get; }
+    string PaletteDarkOrangeBackground1 { get; }
+    string PaletteDarkOrangeBackground2 { get; }
+    string PaletteDarkOrangeBackground3 { get; }
+    string PaletteDarkOrangeForeground1 { get; }
+    string PaletteDarkOrangeForeground2 { get; }
+    string PaletteDarkOrangeForeground3 { get; }
+    string PaletteDarkOrangeBorderActive { get; }
+    string PaletteDarkOrangeBorder1 { get; }
+    string PaletteDarkOrangeBorder2 { get; }
+    string PaletteYellowBackground1 { get; }
+    string PaletteYellowBackground2 { get; }
+    string PaletteYellowBackground3 { get; }
+    string PaletteYellowForeground1 { get; }
+    string PaletteYellowForeground2 { get; }
+    string PaletteYellowForeground3 { get; }
+    string PaletteYellowBorderActive { get; }
+    string PaletteYellowBorder1 { get; }
+    string PaletteYellowBorder2 { get; }
+    string PaletteBerryBackground1 { get; }
+    string PaletteBerryBackground2 { get; }
+    string PaletteBerryBackground3 { get; }
+    string PaletteBerryForeground1 { get; }
+    string PaletteBerryForeground2 { get; }
+    string PaletteBerryForeground3 { get; }
+    string PaletteBerryBorderActive { get; }
+    string PaletteBerryBorder1 { get; }
+    string PaletteBerryBorder2 { get; }
+    string PaletteLightGreenBackground1 { get; }
+    string PaletteLightGreenBackground2 { get; }
+    string PaletteLightGreenBackground3 { get; }
+    string PaletteLightGreenForeground1 { get; }
+    string PaletteLightGreenForeground2 { get; }
+    string PaletteLightGreenForeground3 { get; }
+    string PaletteLightGreenBorderActive { get; }
+    string PaletteLightGreenBorder1 { get; }
+    string PaletteLightGreenBorder2 { get; }
+    string PaletteMarigoldBackground1 { get; }
+    string PaletteMarigoldBackground2 { get; }
+    string PaletteMarigoldBackground3 { get; }
+    string PaletteMarigoldForeground1 { get; }
+    string PaletteMarigoldForeground2 { get; }
+    string PaletteMarigoldForeground3 { get; }
+    string PaletteMarigoldBorderActive { get; }
+    string PaletteMarigoldBorder1 { get; }
+    string PaletteMarigoldBorder2 { get; }
+    string PaletteRedForegroundInverted { get; }
+    string PaletteGreenForegroundInverted { get; }
+    string PaletteYellowForegroundInverted { get; }
+    string PaletteDarkRedBackground2 { get; }
+    string PaletteDarkRedForeground2 { get; }
+    string PaletteDarkRedBorderActive { get; }
+    string PaletteCranberryBackground2 { get; }
+    string PaletteCranberryForeground2 { get; }
+    string PaletteCranberryBorderActive { get; }
+    string PalettePumpkinBackground2 { get; }
+    string PalettePumpkinForeground2 { get; }
+    string PalettePumpkinBorderActive { get; }
+    string PalettePeachBackground2 { get; }
+    string PalettePeachForeground2 { get; }
+    string PalettePeachBorderActive { get; }
+    string PaletteGoldBackground2 { get; }
+    string PaletteGoldForeground2 { get; }
+    string PaletteGoldBorderActive { get; }
+    string PaletteBrassBackground2 { get; }
+    string PaletteBrassForeground2 { get; }
+    string PaletteBrassBorderActive { get; }
+    string PaletteBrownBackground2 { get; }
+    string PaletteBrownForeground2 { get; }
+    string PaletteBrownBorderActive { get; }
+    string PaletteForestBackground2 { get; }
+    string PaletteForestForeground2 { get; }
+    string PaletteForestBorderActive { get; }
+    string PaletteSeafoamBackground2 { get; }
+    string PaletteSeafoamForeground2 { get; }
+    string PaletteSeafoamBorderActive { get; }
+    string PaletteDarkGreenBackground2 { get; }
+    string PaletteDarkGreenForeground2 { get; }
+    string PaletteDarkGreenBorderActive { get; }
+    string PaletteLightTealBackground2 { get; }
+    string PaletteLightTealForeground2 { get; }
+    string PaletteLightTealBorderActive { get; }
+    string PaletteTealBackground2 { get; }
+    string PaletteTealForeground2 { get; }
+    string PaletteTealBorderActive { get; }
+    string PaletteSteelBackground2 { get; }
+    string PaletteSteelForeground2 { get; }
+    string PaletteSteelBorderActive { get; }
+    string PaletteBlueBackground2 { get; }
+    string PaletteBlueForeground2 { get; }
+    string PaletteBlueBorderActive { get; }
+    string PaletteRoyalBlueBackground2 { get; }
+    string PaletteRoyalBlueForeground2 { get; }
+    string PaletteRoyalBlueBorderActive { get; }
+    string PaletteCornflowerBackground2 { get; }
+    string PaletteCornflowerForeground2 { get; }
+    string PaletteCornflowerBorderActive { get; }
+    string PaletteNavyBackground2 { get; }
+    string PaletteNavyForeground2 { get; }
+    string PaletteNavyBorderActive { get; }
+    string PaletteLavenderBackground2 { get; }
+    string PaletteLavenderForeground2 { get; }
+    string PaletteLavenderBorderActive { get; }
+    string PalettePurpleBackground2 { get; }
+    string PalettePurpleForeground2 { get; }
+    string PalettePurpleBorderActive { get; }
+    string PaletteGrapeBackground2 { get; }
+    string PaletteGrapeForeground2 { get; }
+    string PaletteGrapeBorderActive { get; }
+    string PaletteLilacBackground2 { get; }
+    string PaletteLilacForeground2 { get; }
+    string PaletteLilacBorderActive { get; }
+    string PalettePinkBackground2 { get; }
+    string PalettePinkForeground2 { get; }
+    string PalettePinkBorderActive { get; }
+    string PaletteMagentaBackground2 { get; }
+    string PaletteMagentaForeground2 { get; }
+    string PaletteMagentaBorderActive { get; }
+    string PalettePlumBackground2 { get; }
+    string PalettePlumForeground2 { get; }
+    string PalettePlumBorderActive { get; }
+    string PaletteBeigeBackground2 { get; }
+    string PaletteBeigeForeground2 { get; }
+    string PaletteBeigeBorderActive { get; }
+    string PaletteMinkBackground2 { get; }
+    string PaletteMinkForeground2 { get; }
+    string PaletteMinkBorderActive { get; }
+    string PalettePlatinumBackground2 { get; }
+    string PalettePlatinumForeground2 { get; }
+    string PalettePlatinumBorderActive { get; }
+    string PaletteAnchorBackground2 { get; }
+    string PaletteAnchorForeground2 { get; }
+    string PaletteAnchorBorderActive { get; }
+    string StatusSuccessBackground1 { get; }
+    string StatusSuccessBackground2 { get; }
+    string StatusSuccessBackground3 { get; }
+    string StatusSuccessForeground1 { get; }
+    string StatusSuccessForeground2 { get; }
+    string StatusSuccessForeground3 { get; }
+    string StatusSuccessForegroundInverted { get; }
+    string StatusSuccessBorderActive { get; }
+    string StatusSuccessBorder1 { get; }
+    string StatusSuccessBorder2 { get; }
+    string StatusWarningBackground1 { get; }
+    string StatusWarningBackground2 { get; }
+    string StatusWarningBackground3 { get; }
+    string StatusWarningForeground1 { get; }
+    string StatusWarningForeground2 { get; }
+    string StatusWarningForeground3 { get; }
+    string StatusWarningForegroundInverted { get; }
+    string StatusWarningBorderActive { get; }
+    string StatusWarningBorder1 { get; }
+    string StatusWarningBorder2 { get; }
+    string StatusDangerBackground1 { get; }
+    string StatusDangerBackground2 { get; }
+    string StatusDangerBackground3 { get; }
+    string StatusDangerForeground1 { get; }
+    string StatusDangerForeground2 { get; }
+    string StatusDangerForeground3 { get; }
+    string StatusDangerForegroundInverted { get; }
+    string StatusDangerBorderActive { get; }
+    string StatusDangerBorder1 { get; }
+    string StatusDangerBorder2 { get; }
+}
\ No newline at end of file
diff --git a/UI.Avalonia/Utils/ColorPalette/ThemeConsts.cs b/UI.Avalonia/Utils/ColorPalette/ThemeConsts.cs
new file mode 100644
index 0000000..b870c1e
--- /dev/null
+++ b/UI.Avalonia/Utils/ColorPalette/ThemeConsts.cs
@@ -0,0 +1,31 @@
+namespace UI.Avalonia.Utils.ColorPalette;
+
+public static class ThemeConsts
+{
+    // ref https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/Materials/Backdrop/MicaController.h#L34C61-L34C64
+    public const string LightThemeTintColor = "#f3f3f3";
+    public const double LightThemeTintOpacity = 0.5;
+    public const double LightThemeMaterialOpacity = 0.8;
+    public const double LightThemeLuminosityOpacity = 0;
+
+    public const string DarkThemeTintColor = "#202020";
+    public const double DarkThemeTintOpacity = 0.8;
+    public const double DarkThemeMaterialOpacity = 0.8;
+    public const double DarkThemeLuminosityOpacity = 0;
+
+    public const string LightThemeDimGray = "#646464";
+    public const string DarkThemeDimGray = "#a9aba9";
+
+    // idk man, can't find dark const anywhere
+    public const string LightThemeLayer2Background = "#ccffffff";
+    public const string DarkThemeLayer2Background = "#b32e2e2e";
+
+    public const string LightThemeLayer2Grounded = "ccf4f4f4";
+    public const string DarkThemeLayer2Grounded = "#b3272727";
+
+    public static readonly IPalette Debug = new DebugPalette();
+    public static readonly IPalette Dark = new FluentPaletteDark();
+    public static readonly IPalette Light = new FluentPaletteLight();
+
+    public const string AccentColor = "#0094ff";
+}
\ No newline at end of file
diff --git a/UI.Avalonia/Utils/DEV_BROADCAST_HDR.cs b/UI.Avalonia/Utils/DEV_BROADCAST_HDR.cs
new file mode 100644
index 0000000..1ea744c
--- /dev/null
+++ b/UI.Avalonia/Utils/DEV_BROADCAST_HDR.cs
@@ -0,0 +1,8 @@
+namespace UI.Avalonia.Utils;
+
+public struct DEV_BROADCAST_HDR
+{
+    public int dbch_size;
+    public int dbch_devicetype;
+    public int dbch_reserved;
+}
\ No newline at end of file
diff --git a/UI.Avalonia/Utils/DEV_BROADCAST_VOLUME.cs b/UI.Avalonia/Utils/DEV_BROADCAST_VOLUME.cs
new file mode 100644
index 0000000..153748e
--- /dev/null
+++ b/UI.Avalonia/Utils/DEV_BROADCAST_VOLUME.cs
@@ -0,0 +1,10 @@
+namespace UI.Avalonia.Utils;
+
+public struct DEV_BROADCAST_VOLUME
+{
+    public int dbcv_size;
+    public int dbcv_devicetype;
+    public int dbcv_reserved;
+    public int dbcv_unitmask;
+    public int dbcv_flags;
+}
\ No newline at end of file
diff --git a/UI.Avalonia/Utils/DriveLetterToIdConverter.cs b/UI.Avalonia/Utils/DriveLetterToIdConverter.cs
new file mode 100644
index 0000000..887eab9
--- /dev/null
+++ b/UI.Avalonia/Utils/DriveLetterToIdConverter.cs
@@ -0,0 +1,13 @@
+namespace UI.Avalonia.Utils;
+
+internal static class DriveLetterToIdConverter
+{
+    public static int ToDriveId(this char driveLetter)
+    {
+        driveLetter = char.ToUpperInvariant(driveLetter);
+        if (driveLetter < 'A' || driveLetter > 'Z')
+            return 0;
+
+        return 1 << (driveLetter - 'A');
+    }
+}
\ No newline at end of file
diff --git a/UI.Avalonia/Utils/OS.cs b/UI.Avalonia/Utils/OS.cs
new file mode 100644
index 0000000..82b33a3
--- /dev/null
+++ b/UI.Avalonia/Utils/OS.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace UI.Avalonia.Utils;
+
+public static class OS
+{
+    public static bool IsWin11 => OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000);
+    public static bool IsWin10 => OperatingSystem.IsWindowsVersionAtLeast(10);
+    public static bool IsWin81 => OperatingSystem.IsWindowsVersionAtLeast(8, 1);
+    public static bool IsWin80 => OperatingSystem.IsWindowsVersionAtLeast(8);
+    public static bool IsWin7 => OperatingSystem.IsWindowsVersionAtLeast(6, 1);
+    public static bool IsWinVista => OperatingSystem.IsWindowsVersionAtLeast(6);
+}
\ No newline at end of file
diff --git a/UI.Avalonia/Utils/WndProcHelper.cs b/UI.Avalonia/Utils/WndProcHelper.cs
new file mode 100644
index 0000000..03f4388
--- /dev/null
+++ b/UI.Avalonia/Utils/WndProcHelper.cs
@@ -0,0 +1,55 @@
+#if WINDOWS
+using System;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+using Avalonia.Controls;
+using IrdLibraryClient;
+using TerraFX.Interop.Windows;
+
+namespace UI.Avalonia.Utils;
+
+[SupportedOSPlatform("windows")]
+public static unsafe class WndProcHelper
+{
+    public delegate void OnWndProc(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam);
+
+    private static delegate* unmanaged wrappedWndProc;
+    private static OnWndProc userFunc = null!;
+
+    public static bool Register(Window window, OnWndProc onWndProc)
+    {
+        try
+        {
+            userFunc = onWndProc;
+            var windowImpl = window.PlatformImpl!;
+            //var platformHandle = windowImpl.GetType().GetProperty("Handle", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(windowImpl);
+            var windowHandle = (IntPtr)windowImpl.GetType().GetProperty("Hwnd", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(windowImpl)!;
+            var hwnd = (HWND)windowHandle;
+            wrappedWndProc = (delegate* unmanaged)Windows.GetWindowLongPtr(hwnd, GWLP.GWLP_WNDPROC);
+            delegate* unmanaged wrapperWinProc = &WndProcHook;
+            Windows.SetWindowLongPtr(hwnd, GWLP.GWLP_WNDPROC, (nint)wrapperWinProc);
+            return true;
+        }
+        catch (Exception e)
+        {
+            Log.Error(e, "Failed to hook the WndProc event loop");
+            return false;
+        }
+    }
+
+    [UnmanagedCallersOnly]
+    public static LRESULT WndProcHook(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam)
+    {
+        try
+        {
+            userFunc(hwnd, msg, wParam, lParam);
+        }
+        catch (Exception e)
+        {
+            Log.Error(e, "Exception in user-defined event loop hook");
+        }
+        return wrappedWndProc(hwnd, msg, wParam, lParam);
+    }
+}
+#endif
\ No newline at end of file
diff --git a/UI.Avalonia/ViewLocator.cs b/UI.Avalonia/ViewLocator.cs
new file mode 100644
index 0000000..0b87bf5
--- /dev/null
+++ b/UI.Avalonia/ViewLocator.cs
@@ -0,0 +1,27 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Controls.Templates;
+using UI.Avalonia.ViewModels;
+
+namespace UI.Avalonia;
+
+public class ViewLocator : IDataTemplate
+{
+    public Control Build(object? data)
+    {
+        var vmName = data?.GetType().FullName!;
+        if (!vmName.EndsWith("ViewModel"))
+            return new TextBlock { Text = "Not a ViewModel: " + vmName };
+            
+        var name = vmName.Replace("ViewModel", "View");
+        if (name is {Length: >0} && Type.GetType(name) is Type viewType)
+            return (Control)Activator.CreateInstance(viewType)!;
+        
+        name = name[..^4];
+        if (name is {Length: >0} && Type.GetType(name) is Type type)
+            return (Control)Activator.CreateInstance(type)!;
+        return new TextBlock { Text = "Not Found: " + name };
+    }
+
+    public bool Match(object? data) => data is ViewModelBase;
+}
\ No newline at end of file
diff --git a/UI.Avalonia/ViewModels/ErrorStubViewModel.cs b/UI.Avalonia/ViewModels/ErrorStubViewModel.cs
new file mode 100644
index 0000000..0808761
--- /dev/null
+++ b/UI.Avalonia/ViewModels/ErrorStubViewModel.cs
@@ -0,0 +1,5 @@
+namespace UI.Avalonia.ViewModels;
+
+public partial class ErrorStubViewModel: ViewModelBase
+{
+}
\ No newline at end of file
diff --git a/UI.Avalonia/ViewModels/MainViewModel.cs b/UI.Avalonia/ViewModels/MainViewModel.cs
new file mode 100644
index 0000000..e1969d4
--- /dev/null
+++ b/UI.Avalonia/ViewModels/MainViewModel.cs
@@ -0,0 +1,277 @@
+using System;
+using System.Collections.Specialized;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia.Threading;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using IrdLibraryClient;
+using Ps3DiscDumper;
+using Ps3DiscDumper.Utils;
+
+namespace UI.Avalonia.ViewModels;
+
+public partial class MainViewModel : ViewModelBase, IDisposable
+{
+    private readonly SettingsViewModel settings;
+    
+    public MainViewModel(): this(new()){}
+    
+    public MainViewModel(SettingsViewModel settings) => this.settings = settings;
+
+    [ObservableProperty] private string stepTitle = "Please insert a PS3 game disc";
+    [ObservableProperty] private string stepSubtitle = "";
+    [ObservableProperty] private bool lastOperationSuccess = true;
+    [ObservableProperty] private bool lastOperationWarning = false;
+    [ObservableProperty] private string startButtonCaption = "Start";
+
+    [ObservableProperty] private bool foundDisc;
+    [ObservableProperty] private bool dumperIsReady;
+    [ObservableProperty] private bool dumpingInProgress;
+
+    [ObservableProperty] private bool discInfoExpanded = SettingsProvider.Settings.ShowDetails;
+    [ObservableProperty] private string productCode = "";
+    [ObservableProperty] private string gameTitle = "";
+    [ObservableProperty] private string discSizeInfo = "";
+    [ObservableProperty] private string discKeyName = "";
+    [ObservableProperty] private int progress;
+    [ObservableProperty] private int progressMax = 10_000;
+    [ObservableProperty] private string progressInfo = "";
+    [ObservableProperty] private bool? success;
+    [ObservableProperty] private bool? validated;
+
+    internal Dumper? dumper;
+
+    partial void OnDiscInfoExpandedChanged(bool value)
+    {
+        SettingsProvider.Settings = SettingsProvider.Settings with { ShowDetails = value };
+    }
+
+    partial void OnProgressChanged(int value)
+    {
+        if (OperatingSystem.IsWindowsVersionAtLeast(6, 1))
+            SetTaskbarProgress(value);
+    }
+    
+    [RelayCommand]
+    private void ResetViewModel()
+    {
+        StepTitle = "Please insert a PS3 game disc";
+        StepSubtitle = "";
+        LastOperationSuccess = true;
+        LastOperationWarning = false;
+        StartButtonCaption = "Start";
+        FoundDisc = false;
+        DumperIsReady = false;
+        DumpingInProgress = false;
+        CanEditSettings = true;
+        ProductCode = "";
+        GameTitle = "";
+        DiscSizeInfo = "";
+        DiscKeyName = "";
+        Progress = 0;
+        ProgressInfo = "";
+        Success = null;
+        Validated = null;
+        ResetTaskbarProgress();
+    }
+
+    [RelayCommand]
+    private void ScanDiscs() => Dispatcher.UIThread.Post(ScanDiscsAsync, DispatcherPriority.Background);
+        /*
+         => Dispatcher.UIThread.Post(() =>
+        {
+            var thread = new Thread(async () => ScanDiscsAsync());
+            thread.Start();
+            while (!thread.Join(15))
+                Thread.Yield();
+        }, DispatcherPriority.Background);
+        */
+
+    [RelayCommand]
+    private void DumpDisc() => Dispatcher.UIThread.Post(DumpDiscAsync, DispatcherPriority.Background);
+
+    [RelayCommand]
+    private void CancelDump()
+    {
+        dumper?.Cts.Cancel();
+    }
+    
+    private async void ScanDiscsAsync()
+    {
+        ResetViewModel();
+        
+        FoundDisc = true;
+        StepTitle = "Scanning disc drives";
+        StepSubtitle = "Checking the inserted disc…";
+        dumper?.Dispose();
+        dumper = new(new());
+        try
+        {
+            dumper.DetectDisc("",
+                d =>
+                {
+                    var items = new NameValueCollection
+                    {
+                        [Patterns.ProductCode] = d.ProductCode,
+                        [Patterns.ProductCodeLetters] = d.ProductCode?[..4],
+                        [Patterns.ProductCodeNumbers] = d.ProductCode?[4..],
+                        [Patterns.Title] = d.Title,
+                        [Patterns.Region] = Dumper.RegionMapping[d.ProductCode?[2..3] ?? ""],
+                    };
+                    settings.TestItems = items;
+                    return PatternFormatter.Format(SettingsProvider.Settings.DumpNameTemplate, items);
+                });
+        } catch {}
+        if (dumper.ProductCode is not { Length: > 0 } || dumper.Cts.IsCancellationRequested)
+        {
+            ResetViewModel();
+            return;
+        }
+
+        Success = null;
+        Validated = null;
+        ProductCode = dumper.ProductCode;
+        GameTitle = dumper.Title;
+        DiscSizeInfo = $"{dumper.TotalFileSize.AsStorageUnit()} ({dumper.TotalFileCount} files)";
+
+        StepTitle = "Looking for a disc key";
+        StepSubtitle = "Checking IRD and Redump data sets…";
+        try
+        {
+            await dumper.FindDiscKeyAsync(settings.IrdDir).WaitAsync(dumper.Cts.Token).ConfigureAwait(false);
+        }
+        catch (Exception e)
+        {
+            Log.Error(e, "Failed to find a matching key");
+            dumper.Cts.Cancel();
+            FoundDisc = false;
+            StepTitle = "Disc check error";
+            StepSubtitle = e.Message;
+            LastOperationSuccess = false;
+            return;
+        }
+        StepSubtitle = "";
+        if (dumper.DiscKeyFilename is {Length: >0})
+            DiscKeyName = Path.GetFileName(dumper.DiscKeyFilename);
+        else
+            DiscKeyName = "No match found";
+
+        var destination = Path.Combine(settings.OutputDir, dumper.OutputDir);
+        if (Directory.Exists(destination))
+        {
+            StepTitle = "Dump already exists";
+            StepSubtitle = "Existing dump files will be overwritten";
+            StartButtonCaption = "Overwrite";
+            LastOperationWarning = true;
+        }
+        else
+        {
+            StepTitle = "Ready to dump";
+        }
+        DumperIsReady = true;
+    }
+    
+    private async void DumpDiscAsync()
+    {
+        if (dumper is null)
+        {
+            StepTitle = "Unexpected error occured";
+            StepSubtitle = "Please restart the application";
+            LastOperationSuccess = false;
+            return;
+        }
+
+        StepTitle = "Dumping the disc";
+        StepSubtitle = "Decrypting and copying the data…";
+        LastOperationSuccess = true;
+        LastOperationWarning = false;
+        DumpingInProgress = true;
+        CanEditSettings = false;
+        if (OperatingSystem.IsWindowsVersionAtLeast(6, 1))
+            EnableTaskbarProgress();
+
+        try
+        {
+            var threadCts = new CancellationTokenSource();
+            var combinedToken = CancellationTokenSource.CreateLinkedTokenSource(threadCts.Token, dumper.Cts.Token);
+            var monitor = new Thread(() =>
+            {
+                try
+                {
+                    do
+                    {
+                        if (dumper.TotalSectors > 0 && !dumper.Cts.IsCancellationRequested)
+                        {
+                            Progress = (int)(dumper.CurrentSector * 10000L / dumper.TotalSectors);
+                            ProgressInfo =
+                                $"Sector data {(dumper.CurrentSector * dumper.SectorSize).AsStorageUnit()} of {(dumper.TotalSectors * dumper.SectorSize).AsStorageUnit()} / File {dumper.CurrentFileNumber} of {dumper.TotalFileCount}";
+                        }
+                        Task.Delay(200, combinedToken.Token).GetAwaiter().GetResult();
+                    } while (!combinedToken.Token.IsCancellationRequested);
+                }
+                catch (TaskCanceledException)
+                {
+                }
+            });
+            monitor.Start();
+            await dumper.DumpAsync(settings.OutputDir).WaitAsync(dumper.Cts.Token);
+            threadCts.Cancel();
+            monitor.Join(100);
+        }
+        catch (OperationCanceledException e)
+        {
+            Log.Warn(e, "Cancellation requested");
+            StepTitle = "Dump is not valid";
+            StepSubtitle = "Operation was cancelled";
+            Success = false;
+        }
+        catch (Exception e)
+        {
+            Log.Error(e, "Failed to dump the disc");
+            StepTitle = "Failed to dump the disc";
+            StepSubtitle = e.Message;
+            LastOperationSuccess = false;
+            dumper.Cts.Cancel();
+        }
+
+        if (OperatingSystem.IsWindowsVersionAtLeast(6, 1))
+            ResetTaskbarProgress();
+        DumpingInProgress = false;
+        CanEditSettings = true;
+        DumperIsReady = false;
+        FoundDisc = false;
+        if (dumper.Cts.IsCancellationRequested
+            || LastOperationSuccess is false
+            || LastOperationWarning is true)
+            return;
+
+        Success = dumper.ValidationStatus is not false;
+        if (Success == false)
+        {
+            StepTitle = "Dump is not valid";
+            if (dumper.BrokenFiles.Count > 0)
+                StepSubtitle =
+                    $"{dumper.BrokenFiles.Count} invalid file{(dumper.BrokenFiles.Count == 1 ? "" : "s")}";
+        }
+        else
+        {
+            StepTitle = "Files are decrypted and copied";
+            if (Success == true)
+                StepSubtitle = "All files match a verified copy";
+        }
+    }
+
+    partial void ResetTaskbarProgress();
+    partial void EnableTaskbarProgress();
+    partial void SetTaskbarProgress(int position);
+    
+    public void Dispose()
+    {
+        dumper?.Dispose();
+        OnDisposePlatform();
+    }
+
+    partial void OnDisposePlatform();
+}
\ No newline at end of file
diff --git a/UI.Avalonia/ViewModels/MainViewModel.windows.cs b/UI.Avalonia/ViewModels/MainViewModel.windows.cs
new file mode 100644
index 0000000..f8cb786
--- /dev/null
+++ b/UI.Avalonia/ViewModels/MainViewModel.windows.cs
@@ -0,0 +1,25 @@
+#if WINDOWS
+using System.Runtime.Versioning;
+using Microsoft.WindowsAPICodePack.Taskbar;
+
+namespace UI.Avalonia.ViewModels;
+
+public partial class MainViewModel
+{
+    partial void ResetTaskbarProgress()
+    {
+        TaskbarManager.Instance.SetProgressState(TaskbarProgressBarState.NoProgress);
+        TaskbarManager.Instance.SetProgressValue(0, ProgressMax);
+    }
+
+    partial void EnableTaskbarProgress()
+    {
+        TaskbarManager.Instance.SetProgressState(TaskbarProgressBarState.Normal);
+    }
+
+    partial void SetTaskbarProgress(int position)
+    {
+        TaskbarManager.Instance.SetProgressValue(position, ProgressMax);
+    }
+}
+#endif
\ No newline at end of file
diff --git a/UI.Avalonia/ViewModels/MainWindowViewModel.cs b/UI.Avalonia/ViewModels/MainWindowViewModel.cs
new file mode 100644
index 0000000..788a8fa
--- /dev/null
+++ b/UI.Avalonia/ViewModels/MainWindowViewModel.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Diagnostics;
+using System.Linq;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Ps3DiscDumper;
+using Ps3DiscDumper.POCOs;
+
+namespace UI.Avalonia.ViewModels;
+
+public partial class MainWindowViewModel : ObservableObject, IDisposable
+{
+    private readonly SettingsViewModel settingsPage = new();
+    private readonly MainViewModel mainPage;
+
+    public MainWindowViewModel() => CurrentPage = mainPage = new(settingsPage);
+
+    [NotifyPropertyChangedFor(nameof(InSettings))]
+    [ObservableProperty] private ViewModelBase currentPage;
+    public bool InSettings => CurrentPage is SettingsViewModel;
+ 
+    [ObservableProperty] private GitHubReleaseInfo? updateInfo;
+    [ObservableProperty] private bool? updateIsPrerelease;
+    [ObservableProperty] private string formattedUpdateInfoHeader = "";
+    [ObservableProperty] private string formattedUpdateInfoBody = "";
+    [ObservableProperty] private string formattedUpdateInfoUrl = $"{SettingsViewModel.ProjectUrl}/releases/latest";
+    [ObservableProperty] private string formattedUpdateInfoVersion = "";
+
+    [RelayCommand]
+    private void ToggleSettingsPage()
+    {
+        if (CurrentPage is MainViewModel)
+            CurrentPage = settingsPage;
+        else
+        {
+            SettingsProvider.Save();
+            CurrentPage = mainPage;
+        }
+    }
+
+    internal async void CheckUpdatesAsync()
+    {
+        var (ver, rel) = await Dumper.CheckUpdatesAsync().ConfigureAwait(false);
+        if (ver is null || rel is null)
+            return;
+
+        UpdateInfo = rel;
+        UpdateIsPrerelease = rel.Prerelease;
+        FormattedUpdateInfoHeader = rel.Name;
+        FormattedUpdateInfoVersion = $"Download v{rel.TagName.TrimStart('v')}";
+        FormattedUpdateInfoBody = rel.Body;
+    }
+    
+    public void Dispose()
+    {
+        mainPage.Dispose();
+        OnDisposePlatform();
+    }
+
+    partial void OnDisposePlatform();
+}
\ No newline at end of file
diff --git a/UI.Avalonia/ViewModels/SettingsViewModel.cs b/UI.Avalonia/ViewModels/SettingsViewModel.cs
new file mode 100644
index 0000000..8292525
--- /dev/null
+++ b/UI.Avalonia/ViewModels/SettingsViewModel.cs
@@ -0,0 +1,164 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Diagnostics;
+using System.IO;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Platform.Storage;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using IrdLibraryClient;
+using Ps3DiscDumper;
+using Ps3DiscDumper.Utils;
+
+using SpecialFolder = System.Environment.SpecialFolder;
+
+namespace UI.Avalonia.ViewModels;
+
+public partial class SettingsViewModel: ViewModelBase
+{
+    public SettingsViewModel() => pageTitle = "Settings";
+
+    [NotifyPropertyChangedFor(nameof(OutputDirPreview))]
+    [ObservableProperty] private string outputDir = SettingsProvider.Settings.OutputDir;
+    [NotifyPropertyChangedFor(nameof(IrdDirPreview))]
+    [ObservableProperty] private string irdDir = SettingsProvider.Settings.IrdDir;
+
+    public string OutputDirPreview => FormatPathPreview(OutputDir);
+    public string IrdDirPreview => FormatPathPreview(IrdDir);
+    
+    [ObservableProperty] private string templatePreview = FormatPreview(SettingsProvider.Settings.OutputDir, SettingsProvider.Settings.DumpNameTemplate, testItems);
+    [ObservableProperty] private string dumpNameTemplate = SettingsProvider.Settings.DumpNameTemplate;
+    [NotifyPropertyChangedFor(nameof(TestProductCode))]
+    [NotifyPropertyChangedFor(nameof(TestProductCodeLetters))]
+    [NotifyPropertyChangedFor(nameof(TestProductCodeNumbers))]
+    [NotifyPropertyChangedFor(nameof(TestTitle))]
+    [NotifyPropertyChangedFor(nameof(TestRegion))]
+    [ObservableProperty]
+    private static NameValueCollection testItems = new()
+    {
+        [Patterns.ProductCode] = "BLES02127",
+        [Patterns.ProductCodeLetters] = "BLES",
+        [Patterns.ProductCodeNumbers] = "02127",
+        [Patterns.Title] = "Under Night In-Birth Exe:Late",
+        [Patterns.Region] = "EU",
+    };
+    public string TestProductCode => testItems[Patterns.ProductCode]!;
+    public string TestProductCodeLetters => testItems[Patterns.ProductCodeLetters]!;
+    public string TestProductCodeNumbers => testItems[Patterns.ProductCodeNumbers]!;
+    public string TestTitle => testItems[Patterns.Title]!;
+    public string TestRegion => testItems[Patterns.Region]!;
+
+    public bool LogAvailable => File.Exists(Log.LogPath); 
+    public string LogPath => FormatPathPreview(Log.LogPath);
+    public string DumperVersion => Dumper.Version;
+    public string CurrentYear => DateTime.Now.Year.ToString();
+    public static string ProjectUrl => "https://github.com/13xforever/ps3-disc-dumper";
+    public static string SubmitIssueUrl => $"{ProjectUrl}/issues/new/choose";
+
+    private static string FormatPathPreview(string path)
+    {
+        if (path is "")
+            return "Couldn't create file due to permission or other issues";
+
+        if (OperatingSystem.IsLinux())
+        {
+            var homePath = Environment.GetFolderPath(SpecialFolder.UserProfile);
+            if (path.StartsWith(homePath))
+                return Path.Combine("~", Path.GetRelativePath(homePath, path));
+        }
+
+        if (path is "."
+            || path.StartsWith("./")
+            || OperatingSystem.IsWindows() && path.StartsWith(@".\"))
+            return "" + path[1..];
+        
+        return Path.IsPathRooted(path)
+            ? path
+            : Path.Combine("", path);
+    }
+
+    private static string FormatPreview(string outDir, string template, NameValueCollection items)
+        => PatternFormatter.Format(template.Trim(), items);
+
+    partial void OnOutputDirChanged(string value)
+    {
+        TemplatePreview = FormatPreview(value, DumpNameTemplate, TestItems);
+        SettingsProvider.Settings = SettingsProvider.Settings with { OutputDir = value};
+    }
+
+    partial void OnIrdDirChanged(string value)
+    {
+        SettingsProvider.Settings = SettingsProvider.Settings with { IrdDir = value };
+    }
+
+    partial void OnDumpNameTemplateChanged(string value)
+    {
+        TemplatePreview = FormatPreview(OutputDir, value, TestItems);
+        SettingsProvider.Settings = SettingsProvider.Settings with { DumpNameTemplate = value };
+    }
+
+    partial void OnTestItemsChanged(NameValueCollection value)
+        => TemplatePreview = FormatPreview(OutputDir, DumpNameTemplate, value);
+
+    [RelayCommand]
+    private void ResetTemplate() => DumpNameTemplate = Settings.DefaultPattern;
+
+    [RelayCommand]
+    private async Task SelectFolderAsync(string purpose)
+    {
+        var curSelectedPath = purpose switch
+        {
+            "output" => OutputDir,
+            "ird" => IrdDir,
+            _ => throw new NotImplementedException()
+        };
+        var itemsType = purpose switch
+        {
+            "output" => "disc dumps",
+            "ird" => "cached disc keys",
+            _ => throw new NotImplementedException()
+        };
+        if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime
+            {
+                MainWindow.StorageProvider: { } sp
+            })
+            return;
+        
+        var curDir = await sp.TryGetFolderFromPathAsync(curSelectedPath).ConfigureAwait(false);
+        var result = await sp.OpenFolderPickerAsync(new()
+        {
+            SuggestedStartLocation = curDir,
+            Title = "Please select a folder to save " + itemsType,
+        });
+        if (result is [var newDir])
+        {
+            var newPath = newDir.Path.IsFile
+                ? newDir.Path.LocalPath
+                : newDir.Path.ToString();
+            if (purpose is "output")
+                OutputDir = newPath;
+            else if (purpose is "ird")
+                IrdDir = newPath;
+        }
+    }
+
+    [RelayCommand]
+    private void OpenLogs()
+    {
+        var folder = $"\"{Path.GetDirectoryName(Log.LogPath)}\"";
+        ProcessStartInfo psi = OperatingSystem.IsWindows()
+            ? new() { Verb = "open", FileName = folder, UseShellExecute = true, }
+            : new () { FileName = "open", Arguments = folder, };
+        try
+        {
+            Process.Start(psi);
+        }
+        catch (Exception e)
+        {
+            Log.Error(e, "Failed to open log folder");
+        }
+    }
+}
\ No newline at end of file
diff --git a/UI.Avalonia/ViewModels/ViewModelBase.cs b/UI.Avalonia/ViewModels/ViewModelBase.cs
new file mode 100644
index 0000000..9a2b63f
--- /dev/null
+++ b/UI.Avalonia/ViewModels/ViewModelBase.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Diagnostics;
+using Avalonia.Media;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using IrdLibraryClient;
+using Ps3DiscDumper;
+using UI.Avalonia.Utils.ColorPalette;
+
+namespace UI.Avalonia.ViewModels;
+
+public partial class ViewModelBase: ObservableObject
+{
+    [ObservableProperty] private static string tintColor = "#ffffff";
+    [ObservableProperty] private static double tintOpacity = 1.0;
+    [ObservableProperty] private static double materialOpacity = 0.69;
+    [ObservableProperty] private static double luminosityOpacity = 1.0;
+    [ObservableProperty] private static string accentColor = ThemeConsts.AccentColor;
+    [ObservableProperty] private static string systemAccentColor1 = ThemeConsts.AccentColor;
+    [ObservableProperty] private static string systemAccentColor2 = ThemeConsts.AccentColor;
+    [ObservableProperty] private static string systemAccentColor3 = ThemeConsts.AccentColor;
+    [ObservableProperty] private static bool micaEnabled = true;
+    [ObservableProperty] private static bool acrylicEnabled = false;
+    [ObservableProperty] private static bool enableTransparency = SettingsProvider.Settings.EnableTransparency;
+    [ObservableProperty] private static bool stayOnTop = SettingsProvider.Settings.StayOnTop;
+    
+    [ObservableProperty] private static string dimTextColor = "#00ff00"; //ThemeConsts.LightThemeDimGray;
+    [ObservableProperty] private static IPalette colorPalette = ThemeConsts.Debug;
+    [ObservableProperty] private static string layer2BackgroundColor = "#ff0000"; //ThemeConsts.LightThemeLayerHover;
+    [ObservableProperty] private static string layer2GroundedColor = "#7f0000"; //ThemeConsts.LightThemeLayerHover;
+    [NotifyPropertyChangedFor(nameof(SettingsSymbol))]
+    [ObservableProperty] private static FontFamily symbolFontFamily = new("avares://ps3-disc-dumper/Assets/Fonts#Font Awesome 6 Free");
+    [ObservableProperty] private static FontFamily largeFontFamily = FontManager.Current.DefaultFontFamily;
+    [ObservableProperty] private static FontFamily smallFontFamily = FontManager.Current.DefaultFontFamily;
+
+    private bool UseSegoeIcons => SymbolFontFamily.Name is "Segoe Fluent Icons";
+    public string SettingsSymbol => UseSegoeIcons ? "\ue713" : "\uf013";
+    public string UpdateSymbol => UseSegoeIcons ? "\ue946" : "\uf05a"; //"\uf06a" // exclamation mark in circle
+    public string BackSymbol => UseSegoeIcons ? "\ue72b" : "\uf060";
+    public string FolderSymbol => UseSegoeIcons ? "\ue8b7" : "\uf07b";
+    public string RenameSymbol => UseSegoeIcons ? "\ue8ac" : "\uf573";
+    public string HelpSymbol => UseSegoeIcons ? "\ue8ac" : "\uf573";
+    public string HomeSymbol => UseSegoeIcons ? "\ue80f" : "\uf015";
+    public string FeedbackSymbol => UseSegoeIcons ? "\ue939" : "\uf086";
+    public string TransparencySymbol => UseSegoeIcons ? "\uef20" : "\uf853"; //e8b3
+    public string PinSymbol => UseSegoeIcons ? "\ue718" : "\uf08d";
+    public string ValidationErrorSymbol => UseSegoeIcons ? "\ue783" : "\uf06a";
+    public string ValidationWarningSymbol => UseSegoeIcons ? "\ue7ba" : "\uf071";
+    public string DiagnosticsSymbol => UseSegoeIcons ? "\ue9d9" : "\uf478";
+
+    [ObservableProperty] protected string pageTitle = "PS3 Disc Dumper";// + Dumper.Version;
+    [ObservableProperty] private bool canEditSettings = true;
+    
+    [RelayCommand]
+    private static void OpenUrl(string url)
+    {
+        ProcessStartInfo psi = OperatingSystem.IsWindows()
+            ? new() { FileName = url, UseShellExecute = true, }
+            : new() { FileName = "open", Arguments = url, };
+        psi.CreateNoWindow = true;
+        try
+        {
+            using var _ = Process.Start(psi);
+        }
+        catch (Exception e)
+        {
+            Log.Warn(e, "Failed to open web URL");
+        }
+    }
+
+    partial void OnEnableTransparencyChanged(bool value)
+    {
+        SettingsProvider.Settings = SettingsProvider.Settings with { EnableTransparency = value };
+        /* // seems to be broken atm, only works at the time the window is created
+        if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: Window w })
+            RenderOptions.SetTextRenderingMode(w, value ? TextRenderingMode.Antialias : TextRenderingMode.SubpixelAntialias);
+        */
+    }
+
+    partial void OnStayOnTopChanged(bool value)
+        => SettingsProvider.Settings = SettingsProvider.Settings with { StayOnTop = value };
+}
\ No newline at end of file
diff --git a/UI.Avalonia/Views/ErrorStub.axaml b/UI.Avalonia/Views/ErrorStub.axaml
new file mode 100644
index 0000000..a4f34a0
--- /dev/null
+++ b/UI.Avalonia/Views/ErrorStub.axaml
@@ -0,0 +1,92 @@
+
+        
+        
+                
+                
+        
+    
+        
+            
+                
+                    
+                        
+                        
+                        
+                            
+                            
+                        
+                    
+                
+            
+            
+                
+                    
+                        
+                        
+                        
+                    
+                
+                
+                    
+                
+            
+
+            
+            
+            
+            
+            
+                
+                    
+                    
+                        Please do not run software as Administrator unless application was designed to properly handle it, and it is explicitly required.
+                    
+                
+                
+                
+            
+
+        
+
+
diff --git a/UI.Avalonia/Views/ErrorStub.axaml.cs b/UI.Avalonia/Views/ErrorStub.axaml.cs
new file mode 100644
index 0000000..d3d336e
--- /dev/null
+++ b/UI.Avalonia/Views/ErrorStub.axaml.cs
@@ -0,0 +1,33 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+using Avalonia.Platform;
+using UI.Avalonia.Utils.ColorPalette;
+
+namespace UI.Avalonia.Views;
+
+public partial class ErrorStub : Window
+{
+    public ErrorStub()
+    {
+        InitializeComponent();
+    }
+    
+    public override void Show()
+    {
+
+        base.Show();
+        App.OnThemeChanged(this, EventArgs.Empty);
+        App.OnPlatformColorsChanged(this, PlatformSettings?.GetColorValues() ?? new PlatformColorValues
+        {
+            AccentColor1 = Color.Parse(ThemeConsts.AccentColor),
+            AccentColor2 = Color.Parse(ThemeConsts.AccentColor),
+            AccentColor3 = Color.Parse(ThemeConsts.AccentColor),
+        });
+    }
+
+    private void Exit(object? sender, RoutedEventArgs e) => Close();
+}
\ No newline at end of file
diff --git a/UI.Avalonia/Views/Main.axaml b/UI.Avalonia/Views/Main.axaml
new file mode 100644
index 0000000..f89a8f3
--- /dev/null
+++ b/UI.Avalonia/Views/Main.axaml
@@ -0,0 +1,105 @@
+
+
+	
+		
+		
+	
+
+    
+        
+        
+        
+	        
+	        
+	        
+		        
+			        
+				        
+				        
+			        
+		        
+	        
+        
+        
+        
+        
+	        
+		        
+		        
+			        
+			        
+			        
+		        
+	        
+	        
+		        Product Code
+		        
+		        Dump Size
+		        
+		        Disc Key
+		        
+	        
+        
+        
+        
+        
+        
+        
+        
+	        
+		        
+		        
+		        
+	        
+	        
+        
+    
+
diff --git a/UI.Avalonia/Views/Main.axaml.cs b/UI.Avalonia/Views/Main.axaml.cs
new file mode 100644
index 0000000..f094284
--- /dev/null
+++ b/UI.Avalonia/Views/Main.axaml.cs
@@ -0,0 +1,19 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+
+namespace UI.Avalonia.Views;
+
+public partial class Main : UserControl
+{
+    public Main() => InitializeComponent();
+    
+    private void OnLoaded(object? sender, RoutedEventArgs e)
+    {
+    }
+
+    private void OnResized(object? sender, SizeChangedEventArgs e)
+    {
+        if (e.WidthChanged)
+            ProgressBar.Width = 500 - ProgressBar.Margin.Right - e.NewSize.Width;
+    }
+}
\ No newline at end of file
diff --git a/UI.Avalonia/Views/MainWindow.axaml b/UI.Avalonia/Views/MainWindow.axaml
new file mode 100644
index 0000000..ed87e2f
--- /dev/null
+++ b/UI.Avalonia/Views/MainWindow.axaml
@@ -0,0 +1,137 @@
+
+
+    
+        
+        
+    
+ 
+    
+	    
+		    
+			    
+				    
+				    
+				    
+					    
+					    
+				    
+			    
+		    
+	    
+	    
+		    
+			    
+				    
+				    
+				    
+			    
+		    
+		    
+			    
+		    
+	    
+	    
+	    
+	    
+		    
+		    
+		    
+			
+	    
+	    
+	    
+		    
+		    
+						
+						
+					
+				
+			
+		    
+		    
+	    
+
+	    
+	    
+    
+
diff --git a/UI.Avalonia/Views/MainWindow.axaml.cs b/UI.Avalonia/Views/MainWindow.axaml.cs
new file mode 100644
index 0000000..4976a1f
--- /dev/null
+++ b/UI.Avalonia/Views/MainWindow.axaml.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Linq;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.LogicalTree;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+using UI.Avalonia.Utils;
+using UI.Avalonia.Utils.ColorPalette;
+using UI.Avalonia.ViewModels;
+
+namespace UI.Avalonia.Views;
+
+public partial class MainWindow : Window
+{
+    public MainWindow()
+    {
+        InitializeComponent();
+#if DEBUG
+        this.AttachDevTools();
+#endif
+    }
+
+    public override void Show()
+    {
+        base.Show();
+        App.OnThemeChanged(this, EventArgs.Empty);
+        App.OnPlatformColorsChanged(this, PlatformSettings?.GetColorValues() ?? new PlatformColorValues
+        {
+            AccentColor1 = Color.Parse(ThemeConsts.AccentColor),
+            AccentColor2 = Color.Parse(ThemeConsts.AccentColor),
+            AccentColor3 = Color.Parse(ThemeConsts.AccentColor),
+        });
+    }
+
+    private void OnLoaded(object? sender, RoutedEventArgs e)
+    {
+        if (!IsExtendedIntoWindowDecorations)
+        {
+            TitleButtons.Margin = new(0, 6, 4, 0);
+            var buttons = TitleButtons.GetSelfAndVisualDescendants().OfType
+            
+        
+        
+        
+            
+                
+                
+                    Disc key copies will save to
+                    
+                
+                
+            
+        
+        
+        
+        Naming
+        
+            
+                
+                
+                    Dump name template
+                    
+                        Disc metadata will be used to substitute the values
+                    
+                
+                
+            
+        
+        
+            
+                
+                    Live Preview
+                    
+                
+                
+            
+        
+        
+            
+                
+                    %product_code%
+                    %product_code_letters%
+                    %product_code_numbers%
+                    %title%
+                    %region%
+                
+                
+                    
+                    
+                    
+                    
+                    
+                
+            
+        
+
+        
+        Personalization
+        
+        
+            
+                
+                
+                    Stay on top
+                    
+                        Window will stay always visible on top of normal windows
+                    
+                
+                
+                    
+                    
+                
+            
+        
+        
+        
+            
+                
+                    
+                    
+                
+            
+            
+                
+                
+                    Transparency effects
+                    
+                        Windows and surfaces appear translucent
+                    
+                
+                
+                    
+                    
+                
+            
+        
+
+        
+        Diagnostics
+        
+            
+                
+                
+                    Diagnostics will save to
+                    
+                
+                
+            
+        
+        
+        
+        About this app
+        
+        
+        
+            
+            
+        
+        
+            
+            
+        
+    
+    
+
diff --git a/UI.Avalonia/Views/Settings.axaml.cs b/UI.Avalonia/Views/Settings.axaml.cs
new file mode 100644
index 0000000..7a4157c
--- /dev/null
+++ b/UI.Avalonia/Views/Settings.axaml.cs
@@ -0,0 +1,18 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.VisualTree;
+
+namespace UI.Avalonia.Views;
+
+public partial class Settings : UserControl
+{
+    public Settings() => InitializeComponent();
+
+    private void OnLoad(object? sender, RoutedEventArgs e)
+    {
+        var m = SettingsPanel.Margin;
+        var w = this.FindAncestorOfType()!;
+        var additionalTop = w.IsExtendedIntoWindowDecorations ? 16 : 50;
+        SettingsPanel.Margin = new(m.Left, m.Top + additionalTop, m.Right, m.Bottom + 16);
+    }
+}
\ No newline at end of file
diff --git a/UI.Avalonia/app.manifest b/UI.Avalonia/app.manifest
new file mode 100644
index 0000000..f04c0a4
--- /dev/null
+++ b/UI.Avalonia/app.manifest
@@ -0,0 +1,30 @@
+
+
+  
+  
+
+  
+    
+      
+
+      
+      
+
+      
+      
+
+      
+      
+
+      
+      
+
+      
+      
+    
+  
+
diff --git a/UI.Console/Program.cs b/UI.Console/Program.cs
index 39a2797..4317f58 100644
--- a/UI.Console/Program.cs
+++ b/UI.Console/Program.cs
@@ -8,6 +8,7 @@
 using IrdLibraryClient;
 using Mono.Options;
 using Ps3DiscDumper;
+using Ps3DiscDumper.POCOs;
 
 namespace UIConsole;
 
@@ -15,12 +16,11 @@ internal static class Program
 {
     internal static async Task Main(string[] args)
     {
-        Log.Info("PS3 Disc Dumper v" + Dumper.Version);
         Log.Debug($"Log file location: {Log.LogPath}");
-
         await Dumper.CheckUpdatesAsync().ConfigureAwait(false);
 
-        if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && Console.WindowHeight < 1 && Console.WindowWidth < 1)
+        if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
+            && Console.WindowHeight < 1 && Console.WindowWidth < 1)
             try
             {
                 Log.Error("Looks like there's no console present, restarting...");
@@ -73,7 +73,7 @@ internal static async Task Main(string[] args)
         const string titleBase = "PS3 Disc Dumper";
         var title = titleBase;
         Console.Title = title;
-        var output = ".";
+        var output = SettingsProvider.Settings.OutputDir;
         var inDir = "";
         var showHelp = false;
         var interactive = true;
@@ -87,7 +87,7 @@ internal static async Task Main(string[] args)
                 }
             },
             {
-                "o|output=", "Path to the output folder. Subfolder for each disc will be created automatically", v =>
+                "o|output=", $"Path to the output folder. Subfolder for each disc will be created automatically. Default is {output}", v =>
                 {
                     if (v is string outd)
                         output = outd;
@@ -127,12 +127,13 @@ internal static async Task Main(string[] args)
 
             var dumper = new Dumper(ApiConfig.Cts);
             dumper.DetectDisc(inDir);
-            await dumper.FindDiscKeyAsync(ApiConfig.IrdCachePath).ConfigureAwait(false);
+            await dumper.FindDiscKeyAsync(SettingsProvider.Settings.IrdDir).ConfigureAwait(false);
             if (string.IsNullOrEmpty(dumper.OutputDir))
             {
                 Log.Info("No compatible disc was found, exiting");
                 return 2;
             }
+            
             if (lastDiscId == dumper.ProductCode)
             {
                 Console.ForegroundColor = ConsoleColor.Yellow;
diff --git a/UI.WinForms.Msil/Forms/MainForm.cs b/UI.WinForms.Msil/Forms/MainForm.cs
index 9bc4b70..5f097f7 100644
--- a/UI.WinForms.Msil/Forms/MainForm.cs
+++ b/UI.WinForms.Msil/Forms/MainForm.cs
@@ -5,6 +5,7 @@
 using System.Drawing;
 using System.IO;
 using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Windows.Forms;
@@ -18,9 +19,9 @@
 
 namespace UI.WinForms.Msil;
 
+[SupportedOSPlatform("Windows6.1")]
 public partial class MainForm : Form
 {
-    private readonly Settings settings = new();
     private BackgroundWorker discBackgroundWorker;
     private BackgroundWorker updateCheckWorker;
     private Dumper currentDumper;
@@ -30,24 +31,13 @@ public partial class MainForm : Form
 
     private const int DBT_DEVNODES_CHANGED = 0x0007;
     private const int DBT_DEVICEARRIVAL = 0x8000;
+    private const int DBT_DEVICEREMOVEPENDING = 0x8003;
     private const int DBT_DEVICEREMOVECOMPLETE = 0x8004;
 
     private const int DBT_DEVTYP_VOLUME = 0x2;
     private const int DBTF_MEDIA = 0x0001;
     private const int DBTF_MOUNT_ISO = 0x001b0000; // ???????
 
-    private static readonly NameValueCollection RegionMapping = new()
-    {
-        ["A"] = "ASIA",
-        ["E"] = "EU",
-        ["H"] = "HK",
-        ["J"] = "JP",
-        ["K"] = "KR",
-        ["P"] = "JP",
-        ["T"] = "JP",
-        ["U"] = "US",
-    };
-
     public MainForm()
     {
         InitializeComponent();
@@ -62,6 +52,7 @@ protected override void WndProc(ref Message msg)
             switch (msgType)
             {
                 case DBT_DEVICEARRIVAL:
+                case DBT_DEVICEREMOVEPENDING:
                 case DBT_DEVICEREMOVECOMPLETE:
                     var hdr = (DEV_BROADCAST_HDR)Marshal.PtrToStructure(msg.LParam, typeof(DEV_BROADCAST_HDR));
                     if (hdr.dbch_devicetype == DBT_DEVTYP_VOLUME)
@@ -94,13 +85,11 @@ protected override void WndProc(ref Message msg)
         }
     }
 
+    private static Settings Settings => SettingsProvider.Settings;
+    
     private void MainForm_Load(object sender, EventArgs e)
     {
         Text = "PS3 Disc Dumper v" + Dumper.Version;
-        Log.Info(Text);
-        settings.Reload();
-        Log.Info("User settings:");
-        LogCurrentSettings();
         ResetForm();
 
         updateCheckWorker = new() { WorkerSupportsCancellation = false, WorkerReportsProgress = false, };
@@ -108,21 +97,14 @@ private void MainForm_Load(object sender, EventArgs e)
         updateCheckWorker.RunWorkerCompleted += ShowUpdateCheckResults;
         updateCheckWorker.RunWorkerAsync();
 
-        if (string.IsNullOrEmpty(settings.OutputDir) || string.IsNullOrEmpty(settings.IrdDir) || string.IsNullOrWhiteSpace(settings.DumpNameTemplate))
+        if (string.IsNullOrEmpty(Settings.OutputDir)
+            || string.IsNullOrEmpty(Settings.IrdDir)
+            || string.IsNullOrWhiteSpace(Settings.DumpNameTemplate))
             settingsButton_Click(sender, e);
     }
 
     private void settingsButton_Click(object sender, EventArgs e)
-    {
-        var settingsForm = new SettingsForm();
-        var result = settingsForm.ShowDialog();
-        if (result == DialogResult.OK)
-        {
-            settings.Reload();
-            Log.Info("Changed user settings:");
-            LogCurrentSettings();
-        }
-    }
+        => new SettingsForm().ShowDialog();
 
     private void ResetForm()
     {
@@ -190,7 +172,7 @@ private void selectIrdButton_Click(object sender, EventArgs e)
             FilterIndex = 2,
             Title = "Select a disc key file",
             SupportMultiDottedExtensions = true,
-            InitialDirectory = settings.IrdDir,
+            InitialDirectory = Settings.IrdDir,
         };
         var dialogResult = dialog.ShowDialog();
         if (dialogResult != DialogResult.OK || string.IsNullOrEmpty(dialog.FileName) || !File.Exists(dialog.FileName))
@@ -209,12 +191,12 @@ private void selectIrdButton_Click(object sender, EventArgs e)
             else
                 keyInfo = new(null, discKey, discKeyPath, KeyType.Redump, discKey.ToHexString());
             var discKeyFilename = Path.GetFileName(discKeyPath);
-            var cacheFilename = Path.Combine(settings.IrdDir, discKeyFilename);
+            var cacheFilename = Path.Combine(Settings.IrdDir, discKeyFilename);
             if (!File.Exists(cacheFilename))
                 File.Copy(discKeyPath, cacheFilename);
 
             //todo: proper check
-            currentDumper.FindDiscKeyAsync(settings.IrdDir).GetAwaiter().GetResult();
+            currentDumper.FindDiscKeyAsync(Settings.IrdDir).GetAwaiter().GetResult();
             if (!currentDumper.IsValidDiscKey(discKey))
             {
                 MessageBox.Show("Selected disk key file contains incompatible file set, and cannot be used with the selected PS3 game disc.", "IRD file check", MessageBoxButtons.OK, MessageBoxIcon.Error);
@@ -234,7 +216,7 @@ private void selectIrdButton_Click(object sender, EventArgs e)
 
     private void startDumpingButton_Click(object sender, EventArgs e)
     {
-        var outputDir = Path.Combine(settings.OutputDir, currentDumper.OutputDir);
+        var outputDir = Path.Combine(SettingsProvider.Settings.OutputDir, currentDumper.OutputDir);
         if (Directory.Exists(outputDir))
         {
             var msgResult = MessageBox.Show(
@@ -290,19 +272,7 @@ private void DetectPs3DiscGame(object sender, DoWorkEventArgs doWorkEventArgs)
         var dumper = (Dumper)doWorkEventArgs.Argument;
         try
         {
-            dumper.DetectDisc("",
-                d =>
-                {
-                    var items = new NameValueCollection
-                    {
-                        [Patterns.ProductCode] = d.ProductCode,
-                        [Patterns.ProductCodeLetters] = d.ProductCode?.Substring(0, 4),
-                        [Patterns.ProductCodeNumbers] = d.ProductCode?.Substring(4),
-                        [Patterns.Title] = d.Title,
-                        [Patterns.Region] = RegionMapping[d.ProductCode?.Substring(2, 1) ?? ""],
-                    };
-                    return PatternFormatter.Format(settings.DumpNameTemplate, items);
-                });
+            dumper.DetectDisc();
         }
         catch { }
         doWorkEventArgs.Result = dumper;
@@ -344,11 +314,11 @@ private void FindMatchingIrd(object sender, DoWorkEventArgs doWorkEventArgs)
         var dumper = (Dumper)doWorkEventArgs.Argument;
         try
         {
-            dumper.FindDiscKeyAsync(settings.IrdDir).Wait(dumper.Cts.Token);
+            dumper.FindDiscKeyAsync(Settings.IrdDir).Wait(dumper.Cts.Token);
         }
         catch (Exception e)
         {
-            Log.Error(e, "Failed to find matching key");
+            Log.Error(e, "Failed to find a matching key");
             //                MessageBox.Show(e.Message, "Disc check error", MessageBoxButtons.OK, MessageBoxIcon.Error);
             dumper.Cts.Cancel();
         }
@@ -415,7 +385,7 @@ private void DumpDisc(object sender, DoWorkEventArgs doWorkEventArgs)
                 }
             });
             monitor.Start();
-            dumper.DumpAsync(settings.OutputDir).Wait(dumper.Cts.Token);
+            dumper.DumpAsync(Settings.OutputDir).Wait(dumper.Cts.Token);
             threadCts.Cancel();
             monitor.Join(100);
         }
@@ -521,12 +491,4 @@ private void updateButton_Click(object sender, EventArgs e)
         var psi = new ProcessStartInfo(UpdateUrl) { UseShellExecute = true };
         Process.Start(psi);
     }
-
-    private void LogCurrentSettings()
-    {
-        Log.Info($"\t{nameof(settings.Configured)}: {settings.Configured}");
-        Log.Info($"\t{nameof(settings.OutputDir)}: {settings.OutputDir}");
-        Log.Info($"\t{nameof(settings.IrdDir)}: {settings.IrdDir}");
-        Log.Info($"\t{nameof(settings.DumpNameTemplate)}: {settings.DumpNameTemplate}");
-    }
 }
\ No newline at end of file
diff --git a/UI.WinForms.Msil/Forms/SettingsForm.cs b/UI.WinForms.Msil/Forms/SettingsForm.cs
index fcc1b84..256a043 100644
--- a/UI.WinForms.Msil/Forms/SettingsForm.cs
+++ b/UI.WinForms.Msil/Forms/SettingsForm.cs
@@ -5,13 +5,13 @@
 using System.IO;
 using System.Linq;
 using System.Windows.Forms;
+using Ps3DiscDumper;
 using Ps3DiscDumper.Utils;
 
 namespace UI.WinForms.Msil;
 
 public partial class SettingsForm : Form
 {
-    private readonly Settings Settings = new();
     private readonly HashSet InvalidFileNameChars = new HashSet(Path.GetInvalidFileNameChars());
     private Color defaultTextBoxBackground;
 
@@ -32,10 +32,10 @@ public SettingsForm()
 
     private void SettingsForm_Load(object sender, EventArgs e)
     {
-        Settings.Reload();
-        outputTextBox.Text = Settings.OutputDir;
-        irdTextBox.Text = Settings.IrdDir;
-        namePatternTextBox.Text = Settings.DumpNameTemplate;
+        var settings = SettingsProvider.Settings;
+        outputTextBox.Text = settings.OutputDir;
+        irdTextBox.Text = settings.IrdDir;
+        namePatternTextBox.Text = settings.DumpNameTemplate;
         namePatternTextBox_TextChanged();
     }
 
@@ -46,11 +46,12 @@ private void cancelButton_Click(object sender, EventArgs e)
 
     private void saveButton_Click(object sender, EventArgs e)
     {
-        Settings.OutputDir = outputTextBox.Text;
-        Settings.IrdDir = irdTextBox.Text;
-        Settings.DumpNameTemplate = namePatternTextBox.Text.Trim();
-        Settings.Configured = true;
-        Settings.Save();
+        var settings = SettingsProvider.Settings;
+        settings.OutputDir = outputTextBox.Text;
+        settings.IrdDir = irdTextBox.Text;
+        settings.DumpNameTemplate = namePatternTextBox.Text.Trim();
+        SettingsProvider.Settings = settings;
+        SettingsProvider.Save();
         Close();
     }
 
@@ -148,9 +149,10 @@ private void outputTextBox_TextChanged(object sender, EventArgs e)
 
     private void defaultsButton_Click(object sender, EventArgs e)
     {
-        outputTextBox.Text = (string)Settings.Properties[nameof(Settings.OutputDir)].DefaultValue;
-        irdTextBox.Text = (string)Settings.Properties[nameof(Settings.IrdDir)].DefaultValue;
-        namePatternTextBox.Text = (string)Settings.Properties[nameof(Settings.DumpNameTemplate)].DefaultValue;
+        var defaultSettings = new Settings();
+        outputTextBox.Text = defaultSettings.OutputDir;
+        irdTextBox.Text = defaultSettings.IrdDir;
+        namePatternTextBox.Text = defaultSettings.DumpNameTemplate;
         namePatternTextBox_TextChanged();
     }
 }
\ No newline at end of file
diff --git a/UI.WinForms.Msil/Program.cs b/UI.WinForms.Msil/Program.cs
index dcb7af0..05bd66b 100644
--- a/UI.WinForms.Msil/Program.cs
+++ b/UI.WinForms.Msil/Program.cs
@@ -1,9 +1,6 @@
 using System;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Runtime.InteropServices;
 using System.Windows.Forms;
+using Ps3DiscDumper.Utils;
 
 namespace UI.WinForms.Msil;
 
@@ -18,17 +15,13 @@ static void Main(string[] args)
         Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);
         Application.EnableVisualStyles();
         Application.SetCompatibleTextRenderingDefault(false);
-        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+        if (!SecurityEx.IsSafe(args))
         {
-            using var identity = System.Security.Principal.WindowsIdentity.GetCurrent();
-            var principal = new System.Security.Principal.WindowsPrincipal(identity);
-            if (principal.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator)
-                && !args.Any(p => p.Equals("/IUnderstandThatRunningSoftwareAsAdministratorIsDangerousAndNotRecommendedForAnyone")))
-            {
-                MessageBox.Show("Please do not run software as Administrator unless application was designed to properly handle it, and it is explicitly required.", "", MessageBoxButtons.OK, MessageBoxIcon.Error);
-                Environment.Exit(-1);
-            }
+            MessageBox.Show("Please do not run software as Administrator unless application was designed to properly handle it, and it is explicitly required.", "", MessageBoxButtons.OK, MessageBoxIcon.Error);
+            Environment.Exit(-1);
         }
+#pragma warning disable CA1416
         Application.Run(new MainForm());
+#pragma warning restore CA1416
     }
 }
\ No newline at end of file
diff --git a/UI.WinForms.Msil/Settings.cs b/UI.WinForms.Msil/Settings.cs
deleted file mode 100644
index 5e945ea..0000000
--- a/UI.WinForms.Msil/Settings.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Configuration;
-using Ps3DiscDumper.Utils;
-
-namespace UI.WinForms.Msil;
-
-internal sealed class Settings : ApplicationSettingsBase
-{
-    [UserScopedSetting, DefaultSettingValue(@".\")]
-    public string OutputDir { get => (string)this[nameof(OutputDir)]; set => this[nameof(OutputDir)] = value; }
-
-    [UserScopedSetting, DefaultSettingValue(@".\ird")]
-    public string IrdDir { get => (string)this[nameof(IrdDir)]; set => this[nameof(IrdDir)] = value; }
-
-    [UserScopedSetting, DefaultSettingValue($"%{Patterns.Title}% [%{Patterns.ProductCode}%]")]
-    public string DumpNameTemplate { get => (string)this[nameof(DumpNameTemplate)]; set => this[nameof(DumpNameTemplate)] = value; }
-
-    [UserScopedSetting, DefaultSettingValue("false")]
-    public bool Configured { get => (bool)this[nameof(Configured)]; set => this[nameof(Configured)] = value; }
-}
\ No newline at end of file
diff --git a/UI.WinForms.Msil/UI.WinForms.Msil.csproj b/UI.WinForms.Msil/UI.WinForms.Msil.csproj
index cd7e584..55f9f77 100644
--- a/UI.WinForms.Msil/UI.WinForms.Msil.csproj
+++ b/UI.WinForms.Msil/UI.WinForms.Msil.csproj
@@ -25,7 +25,7 @@
     
       1.16.0
     
-    
+    
     
       5.2.9
     
diff --git a/publish.ps1 b/publish.ps1
index d9dc283..116b2ee 100644
--- a/publish.ps1
+++ b/publish.ps1
@@ -1,4 +1,8 @@
 #!/usr/bin/pwsh
+param (
+    [switch] $BuildLegacyVersions
+)
+
 Clear-Host
 
 if ($PSVersionTable.PSVersion.Major -lt 6)
@@ -11,33 +15,57 @@ if ($PSVersionTable.PSVersion.Major -lt 6)
 Write-Host 'Clearing bin/obj...' -ForegroundColor Cyan
 Remove-Item -LiteralPath UI.WinForms.Msil/bin -Recurse -Force -ErrorAction SilentlyContinue
 Remove-Item -LiteralPath UI.WinForms.Msil/obj -Recurse -Force -ErrorAction SilentlyContinue
+Remove-Item -LiteralPath UI.Avalonia/bin -Recurse -Force -ErrorAction SilentlyContinue
+Remove-Item -LiteralPath UI.Avalonia/obj -Recurse -Force -ErrorAction SilentlyContinue
 
 if (($PSVersionTable.Platform -eq 'Win32NT') -or $IsWindows)
 {
-    Write-Host 'Building Windows binary...' -ForegroundColor Cyan
-    dotnet build -v:q -r win-x64 --self-contained -c Release UI.WinForms.Msil/UI.WinForms.Msil.csproj
-    dotnet publish -v:q -r win-x64 --self-contained -c Release -o distrib/gui/win/ UI.WinForms.Msil/UI.WinForms.Msil.csproj /p:PublishTrimmed=false /p:PublishSingleFile=true
+    Write-Host 'Building Windows binaries...' -ForegroundColor Cyan
+    if ($BuildLegacyVersions)
+    {
+        dotnet build -v:q -r win-x64 --self-contained -c Release UI.WinForms.Msil/UI.WinForms.Msil.csproj
+        dotnet publish -v:q -r win-x64 --self-contained -c Release -o distrib/gui/win-legacy/ UI.WinForms.Msil/UI.WinForms.Msil.csproj /p:PublishTrimmed=False /p:PublishSingleFile=True
+    }
+    dotnet build -v:q -r win-x64 -f net7.0-windows --self-contained -c Release UI.Avalonia/UI.Avalonia.csproj
+    dotnet publish -v:q -r win-x64 -f net7.0-windows --self-contained -c Release -o distrib/gui/win/ UI.Avalonia/UI.Avalonia.csproj /p:PublishTrimmed=False /p:PublishSingleFile=True
 }
 
 Write-Host 'Building Linux binary...' -ForegroundColor Cyan
-dotnet build -v:q -r linux-x64 --self-contained -c Release UI.Console/UI.Console.csproj
-dotnet publish -v:q -r linux-x64 --self-contained -c Release -o distrib/cli/lin/ UI.Console/UI.Console.csproj /p:PublishTrimmed=false /p:PublishSingleFile=true
+if ($BuildLegacyVersions)
+{
+    dotnet build -v:q -r linux-x64 --self-contained -c Release UI.Console/UI.Console.csproj
+    dotnet publish -v:q -r linux-x64 --self-contained -c Release -o distrib/cli/lin/ UI.Console/UI.Console.csproj /p:PublishTrimmed=False /p:PublishSingleFile=True
+}
+dotnet build -v:q -r linux-x64 -f net7.0 --self-contained -c Release UI.Avalonia/UI.Avalonia.csproj
+dotnet publish -v:q -r linux-x64 -f net7.0 --self-contained -c Release -o distrib/gui/lin/ UI.Avalonia/UI.Avalonia.csproj /p:PublishTrimmed=False /p:PublishSingleFile=True
 if (($LASTEXITCODE -eq 0) -and (($PSVersionTable.Platform -eq 'Unix') -or $IsLinux))
 {
-    chmod +x distrib/cli/lin/ps3-disc-dumper
+    if ($BuildLegacyVersions)
+    {
+        chmod +x distrib/cli/lin/ps3-disc-dumper
+    }
+    chmod +x distrib/gui/lin/ps3-disc-dumper
 }
 
 Write-Host 'Clearing extra files in distrib...' -ForegroundColor Cyan
 Get-ChildItem -LiteralPath distrib -Include *.pdb,*.config -Recurse | Remove-Item
 
 Write-Host 'Zipping...' -ForegroundColor Cyan
+if (Test-Path -LiteralPath distrib/gui/win-legacy/ps3-disc-dumper.exe)
+{
+    Compress-Archive -LiteralPath distrib/gui/win-legacy/ps3-disc-dumper.exe -DestinationPath distrib/ps3-disc-dumper_windows_legacy_NEW.zip -CompressionLevel Optimal -Force
+}
 if (Test-Path -LiteralPath distrib/gui/win/ps3-disc-dumper.exe)
 {
-    Compress-Archive -LiteralPath distrib/gui/win/ps3-disc-dumper.exe -DestinationPath distrib/gui/win/ps3-disc-dumper_win64_NEW.zip -CompressionLevel Optimal -Force
+    Compress-Archive -LiteralPath distrib/gui/win/ps3-disc-dumper.exe -DestinationPath distrib/ps3-disc-dumper_windows_NEW.zip -CompressionLevel Optimal -Force
 }
 if (Test-Path -LiteralPath distrib/cli/lin/ps3-disc-dumper)
 {
-    Compress-Archive -LiteralPath distrib/cli/lin/ps3-disc-dumper -DestinationPath distrib/cli/lin/ps3-disc-dumper_lin64_NEW.zip -CompressionLevel Optimal -Force
+    Compress-Archive -LiteralPath distrib/cli/lin/ps3-disc-dumper -DestinationPath distrib/ps3-disc-dumper_linux_legacy_NEW.zip -CompressionLevel Optimal -Force
+}
+if (Test-Path -LiteralPath distrib/gui/lin/ps3-disc-dumper)
+{
+    Compress-Archive -LiteralPath distrib/gui/lin/ps3-disc-dumper -DestinationPath distrib/ps3-disc-dumper_linux_NEW.zip -CompressionLevel Optimal -Force
 }
 
 Write-Host 'Done' -ForegroundColor Cyan
diff --git a/screenshots/ubuntu-dark.png b/screenshots/ubuntu-dark.png
new file mode 100644
index 0000000..39be6a9
Binary files /dev/null and b/screenshots/ubuntu-dark.png differ
diff --git a/screenshots/ubuntu-light.png b/screenshots/ubuntu-light.png
new file mode 100644
index 0000000..a4aa87d
Binary files /dev/null and b/screenshots/ubuntu-light.png differ
diff --git a/screenshots/windows11-dark.png b/screenshots/windows11-dark.png
new file mode 100644
index 0000000..c95a76b
Binary files /dev/null and b/screenshots/windows11-dark.png differ
diff --git a/screenshots/windows11-light.png b/screenshots/windows11-light.png
new file mode 100644
index 0000000..fcba5d1
Binary files /dev/null and b/screenshots/windows11-light.png differ