diff --git a/src/UniGetUI.Avalonia/App.axaml.cs b/src/UniGetUI.Avalonia/App.axaml.cs
index 92a8beaea..052ee30bf 100644
--- a/src/UniGetUI.Avalonia/App.axaml.cs
+++ b/src/UniGetUI.Avalonia/App.axaml.cs
@@ -1,4 +1,5 @@
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
@@ -22,6 +23,10 @@ namespace UniGetUI.Avalonia;
public partial class App : Application
{
+ [UnconditionalSuppressMessage(
+ "Trimming",
+ "IL2026",
+ Justification = "Platform theme dictionaries are Avalonia resources included in the app package; only the resource URI is selected dynamically.")]
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
diff --git a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
index bc185e5bd..4abf35467 100644
--- a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
+++ b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
@@ -21,16 +21,20 @@
..\UniGetUI\icon.ico
true
app.manifest
+ true
+ partial
+ false
+ false
+ false
+ false
- $(MSBuildThisFileDirectory)..\UniGetUI.Pinget.Cli\UniGetUI.Pinget.Cli.csproj
$(RuntimeIdentifier)
win-arm64
win-x64
- $(MSBuildProjectDirectory)\obj\$(Platform)\$(Configuration)\BundledPinget\$(PingetCliRuntimeIdentifier)\
- $(PingetCliPublishDir)pinget.exe
- $(PingetCliPublishDir)e_sqlite3.dll
+ $(PkgDevolutions_Pinget_Cli_Rust)\runtimes\$(PingetCliRuntimeIdentifier)\native
+ $(PingetCliPackageNativePath)\pinget.exe
@@ -42,18 +46,13 @@
AfterTargets="Build;Publish"
Condition="'$(DesignTimeBuild)' != 'true' and '$(SkipBundledPingetCli)' != 'true' and $([MSBuild]::IsOSPlatform('Windows'))"
>
-
-
-
-
-
+
+
+
+
+
+
@@ -82,6 +94,7 @@
+
diff --git a/src/UniGetUI.Avalonia/ViewLocator.cs b/src/UniGetUI.Avalonia/ViewLocator.cs
index 33d60fa05..7d178d54b 100644
--- a/src/UniGetUI.Avalonia/ViewLocator.cs
+++ b/src/UniGetUI.Avalonia/ViewLocator.cs
@@ -1,17 +1,13 @@
-using System;
-using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using UniGetUI.Avalonia.ViewModels;
+using UniGetUI.Avalonia.Views;
namespace UniGetUI.Avalonia;
///
-/// Given a view model, returns the corresponding view if possible.
+/// Given a view model, returns the corresponding view if possible without reflection.
///
-[RequiresUnreferencedCode(
- "Default implementation of ViewLocator involves reflection which may be trimmed away.",
- Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")]
public class ViewLocator : IDataTemplate
{
public Control? Build(object? param)
@@ -19,19 +15,19 @@ public class ViewLocator : IDataTemplate
if (param is null)
return null;
- var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
- var type = Type.GetType(name);
-
- if (type != null)
+ if (param is SidebarViewModel sidebar)
{
- return (Control)Activator.CreateInstance(type)!;
+ return new SidebarView
+ {
+ DataContext = sidebar,
+ };
}
- return new TextBlock { Text = "Not Found: " + name };
+ return new TextBlock { Text = "Not Found: " + param.GetType().Name };
}
public bool Match(object? data)
{
- return data is ViewModelBase;
+ return data is SidebarViewModel;
}
}
diff --git a/src/UniGetUI.Avalonia/Views/Controls/UserAvatarControl.axaml b/src/UniGetUI.Avalonia/Views/Controls/UserAvatarControl.axaml
index f5783d2a7..ec2b11859 100644
--- a/src/UniGetUI.Avalonia/Views/Controls/UserAvatarControl.axaml
+++ b/src/UniGetUI.Avalonia/Views/Controls/UserAvatarControl.axaml
@@ -87,8 +87,8 @@
-
-
+
+
diff --git a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs
index 541d79ee9..40557e977 100644
--- a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs
+++ b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs
@@ -69,8 +69,7 @@ or nameof(PackagesPageViewModel.SortAscending))
{
var reloadBtn = ViewModel.AddToolbarButton("reload", CoreTools.Translate("Reload"),
ViewModel.TriggerReload);
- reloadBtn.Bind(ToolTip.TipProperty,
- new global::Avalonia.Data.Binding(nameof(PackagesPageViewModel.ReloadButtonTooltip)) { Source = ViewModel });
+ UpdateReloadButtonTooltip(reloadBtn);
ViewModel.AddToolbarSeparator();
}
@@ -136,6 +135,16 @@ or nameof(PackagesPageViewModel.SortAscending))
// ─── UI-only: focus the package list ─────────────────────────────────────
private void OnFocusListRequested() => PackageList.Focus();
+ private void UpdateReloadButtonTooltip(Button reloadButton)
+ {
+ ToolTip.SetTip(reloadButton, ViewModel.ReloadButtonTooltip);
+ ViewModel.PropertyChanged += (_, args) =>
+ {
+ if (args.PropertyName is nameof(PackagesPageViewModel.ReloadButtonTooltip))
+ ToolTip.SetTip(reloadButton, ViewModel.ReloadButtonTooltip);
+ };
+ }
+
public void FocusPackageList()
{
if (ViewModel.MegaQueryBoxEnabled)
diff --git a/src/UniGetUI.Core.Data.Tests/CoreTests.cs b/src/UniGetUI.Core.Data.Tests/CoreTests.cs
index 27ce43a34..c6029b255 100644
--- a/src/UniGetUI.Core.Data.Tests/CoreTests.cs
+++ b/src/UniGetUI.Core.Data.Tests/CoreTests.cs
@@ -39,6 +39,36 @@ public void CheckOtherAttributes()
);
}
+ [Fact]
+ public void ResolveInstallationDirectoryReturnsParentForBundledAvaloniaDirectory()
+ {
+ string installDirectory = Path.GetFullPath(Path.Join("install-root"));
+ string avaloniaDirectory = Path.Join(installDirectory, "Avalonia");
+ string classicExecutable = Path.Join(installDirectory, "UniGetUI.exe");
+
+ string resolvedDirectory = CoreData.ResolveInstallationDirectory(
+ avaloniaDirectory,
+ filePath => filePath == classicExecutable,
+ static _ => false
+ );
+
+ Assert.Equal(installDirectory, resolvedDirectory);
+ }
+
+ [Fact]
+ public void ResolveInstallationDirectoryKeepsStandaloneAvaloniaDirectory()
+ {
+ string avaloniaDirectory = Path.GetFullPath(Path.Join("standalone", "Avalonia"));
+
+ string resolvedDirectory = CoreData.ResolveInstallationDirectory(
+ avaloniaDirectory,
+ static _ => false,
+ static _ => false
+ );
+
+ Assert.Equal(avaloniaDirectory, resolvedDirectory);
+ }
+
[Theory]
[InlineData("3.3.7", "3.3.7")]
[InlineData("2026.1.2", "v2026.1.2")]
diff --git a/src/UniGetUI.Core.Data/CoreData.cs b/src/UniGetUI.Core.Data/CoreData.cs
index a5ca7f794..ffc2cb460 100644
--- a/src/UniGetUI.Core.Data/CoreData.cs
+++ b/src/UniGetUI.Core.Data/CoreData.cs
@@ -8,6 +8,9 @@ public static class CoreData
{
private const string GitHubReleasePageBaseUrl = "https://github.com/Devolutions/UniGetUI/releases/tag/";
private const string GitHubReleaseApiBaseUrl = "https://api.github.com/repos/Devolutions/UniGetUI/releases/tags/";
+ private const string BundledModernAppDirectoryName = "Avalonia";
+ private const string ClassicExecutableName = "UniGetUI.exe";
+ private const string BundledPingetExecutableName = "pinget.exe";
private static int? __code_page;
public static int CODE_PAGE
@@ -326,25 +329,49 @@ public static string UniGetUIExecutableDirectory
{
get
{
- string? dir = Path.GetDirectoryName(
- System.Reflection.Assembly.GetExecutingAssembly().Location
- );
- if (dir is not null)
+ string dir = NormalizeDirectoryPath(AppContext.BaseDirectory);
+ if (!string.IsNullOrEmpty(dir))
{
- return dir;
+ return ResolveInstallationDirectory(dir);
}
- Logger.Error(
- "System.Reflection.Assembly.GetExecutingAssembly().Location returned an empty path"
- );
+ Logger.Error("AppContext.BaseDirectory returned an empty path");
- return AppContext.BaseDirectory.TrimEnd(
- Path.DirectorySeparatorChar,
- Path.AltDirectorySeparatorChar
- );
+ return ResolveInstallationDirectory(NormalizeDirectoryPath(AppContext.BaseDirectory));
}
}
+ public static string ResolveInstallationDirectory(
+ string executableDirectory,
+ Func? fileExists = null,
+ Func? directoryExists = null
+ )
+ {
+ fileExists ??= File.Exists;
+ directoryExists ??= Directory.Exists;
+
+ string normalizedDirectory = NormalizeDirectoryPath(executableDirectory);
+ if (!string.Equals(
+ Path.GetFileName(normalizedDirectory),
+ BundledModernAppDirectoryName,
+ StringComparison.OrdinalIgnoreCase
+ ))
+ {
+ return normalizedDirectory;
+ }
+
+ string? parentDirectory = Path.GetDirectoryName(normalizedDirectory);
+ if (string.IsNullOrEmpty(parentDirectory))
+ {
+ return normalizedDirectory;
+ }
+
+ parentDirectory = NormalizeDirectoryPath(parentDirectory);
+ return IsInstallRoot(parentDirectory, fileExists, directoryExists)
+ ? parentDirectory
+ : normalizedDirectory;
+ }
+
///
/// A path pointing to the executable file of the app
///
@@ -599,6 +626,14 @@ private static string GetUserHomeDirectory()
return Environment.GetEnvironmentVariable("HOME") ?? AppContext.BaseDirectory;
}
+ private static string NormalizeDirectoryPath(string path)
+ {
+ return Path.GetFullPath(path).TrimEnd(
+ Path.DirectorySeparatorChar,
+ Path.AltDirectorySeparatorChar
+ );
+ }
+
private static string NormalizeExecutablePath(string path)
{
if (
@@ -611,5 +646,18 @@ private static string NormalizeExecutablePath(string path)
return path;
}
+
+ private static bool IsInstallRoot(
+ string directory,
+ Func fileExists,
+ Func directoryExists
+ )
+ {
+ return fileExists(Path.Join(directory, ClassicExecutableName))
+ || fileExists(Path.Join(directory, BundledPingetExecutableName))
+ || fileExists(Path.Join(directory, "IntegrityTree.json"))
+ || directoryExists(Path.Join(directory, "Assets", "Utilities"))
+ || directoryExists(Path.Join(directory, "Assets", "Data"));
+ }
}
}
diff --git a/src/UniGetUI.Core.IconStore/IconDatabase.cs b/src/UniGetUI.Core.IconStore/IconDatabase.cs
index 7a28119c0..cd9afb9c4 100644
--- a/src/UniGetUI.Core.IconStore/IconDatabase.cs
+++ b/src/UniGetUI.Core.IconStore/IconDatabase.cs
@@ -88,9 +88,8 @@ public async Task LoadFromCacheAsync()
"Icon Database.json"
);
IconScreenshotDatabase_v2 JsonData =
- JsonSerializer.Deserialize(
- await File.ReadAllTextAsync(IconsAndScreenshotsFile),
- SerializationHelpers.DefaultOptions
+ IconStoreJson.DeserializeIconDatabase(
+ await File.ReadAllTextAsync(IconsAndScreenshotsFile)
);
if (JsonData.icons_and_screenshots is not null)
{
diff --git a/src/UniGetUI.Core.IconStore/IconStoreJson.cs b/src/UniGetUI.Core.IconStore/IconStoreJson.cs
new file mode 100644
index 000000000..ed6c2433a
--- /dev/null
+++ b/src/UniGetUI.Core.IconStore/IconStoreJson.cs
@@ -0,0 +1,25 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+
+namespace UniGetUI.Core.IconEngine;
+
+internal static class IconStoreJson
+{
+ public static IconScreenshotDatabase_v2 DeserializeIconDatabase(string json)
+ {
+ return JsonSerializer.Deserialize(json, GetTypeInfo());
+ }
+
+ private static JsonTypeInfo GetTypeInfo()
+ {
+ return (JsonTypeInfo?)IconStoreJsonContext.Default.GetTypeInfo(typeof(T))
+ ?? throw new InvalidOperationException(
+ $"Icon store JSON metadata for {typeof(T).FullName} was not generated."
+ );
+ }
+}
+
+[JsonSourceGenerationOptions(AllowTrailingCommas = true, WriteIndented = true)]
+[JsonSerializable(typeof(IconScreenshotDatabase_v2))]
+internal sealed partial class IconStoreJsonContext : JsonSerializerContext;
diff --git a/src/UniGetUI.Core.Settings/SettingsEngine_Dictionaries.cs b/src/UniGetUI.Core.Settings/SettingsEngine_Dictionaries.cs
index a57da6c1e..19701e180 100644
--- a/src/UniGetUI.Core.Settings/SettingsEngine_Dictionaries.cs
+++ b/src/UniGetUI.Core.Settings/SettingsEngine_Dictionaries.cs
@@ -1,203 +1,202 @@
-using System.Collections.Concurrent;
-using System.Text.Json;
-using UniGetUI.Core.Data;
-using UniGetUI.Core.Logging;
-
-namespace UniGetUI.Core.SettingsEngine
-{
- public static partial class Settings
- {
- private static readonly ConcurrentDictionary<
- K,
- Dictionary