Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/UniGetUI.Avalonia/App.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
Expand All @@ -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);
Expand Down
37 changes: 25 additions & 12 deletions src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,20 @@
<ApplicationIcon>..\UniGetUI\icon.ico</ApplicationIcon>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<ApplicationManifest Condition="$([MSBuild]::IsOSPlatform('Windows'))">app.manifest</ApplicationManifest>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>partial</TrimMode>
<PublishSingleFile>false</PublishSingleFile>
<IncludeAvaloniaPublishSymbols>false</IncludeAvaloniaPublishSymbols>
<AppxGeneratePriEnabled>false</AppxGeneratePriEnabled>
<AppxGeneratePrisForPortableLibrariesEnabled>false</AppxGeneratePrisForPortableLibrariesEnabled>
</PropertyGroup>

<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
<PingetCliProject>$(MSBuildThisFileDirectory)..\UniGetUI.Pinget.Cli\UniGetUI.Pinget.Cli.csproj</PingetCliProject>
<PingetCliRuntimeIdentifier Condition="'$(RuntimeIdentifier)' != ''">$(RuntimeIdentifier)</PingetCliRuntimeIdentifier>
<PingetCliRuntimeIdentifier Condition="'$(PingetCliRuntimeIdentifier)' == '' and '$(Platform)' == 'arm64'">win-arm64</PingetCliRuntimeIdentifier>
<PingetCliRuntimeIdentifier Condition="'$(PingetCliRuntimeIdentifier)' == ''">win-x64</PingetCliRuntimeIdentifier>
<PingetCliPublishDir>$(MSBuildProjectDirectory)\obj\$(Platform)\$(Configuration)\BundledPinget\$(PingetCliRuntimeIdentifier)\</PingetCliPublishDir>
<PingetCliExecutablePath>$(PingetCliPublishDir)pinget.exe</PingetCliExecutablePath>
<PingetCliNativeSqlitePath>$(PingetCliPublishDir)e_sqlite3.dll</PingetCliNativeSqlitePath>
<PingetCliPackageNativePath>$(PkgDevolutions_Pinget_Cli_Rust)\runtimes\$(PingetCliRuntimeIdentifier)\native</PingetCliPackageNativePath>
<PingetCliExecutablePath>$(PingetCliPackageNativePath)\pinget.exe</PingetCliExecutablePath>
</PropertyGroup>

<PropertyGroup Condition="'$(EnableAvaloniaDiagnostics)' == 'true'">
Expand All @@ -42,18 +46,13 @@
AfterTargets="Build;Publish"
Condition="'$(DesignTimeBuild)' != 'true' and '$(SkipBundledPingetCli)' != 'true' and $([MSBuild]::IsOSPlatform('Windows'))"
>
<MSBuild
Projects="$(PingetCliProject)"
Targets="Restore;Publish"
Properties="Configuration=$(Configuration);Platform=$(Platform);TargetFramework=net10.0;RuntimeIdentifier=$(PingetCliRuntimeIdentifier);SelfContained=true;PublishSingleFile=true;PublishTrimmed=true;TrimMode=partial;JsonSerializerIsReflectionEnabledByDefault=true;PublishDir=$(PingetCliPublishDir);AppendRuntimeIdentifierToOutputPath=false"
<Error
Condition="!Exists('$(PingetCliExecutablePath)')"
Text="NuGet pinget executable not found at '$(PingetCliExecutablePath)'. Ensure package restore has completed for Devolutions.Pinget.Cli.Rust."
/>
<ItemGroup>
<PingetCliOutputFiles Include="$(PingetCliExecutablePath)" Condition="Exists('$(PingetCliExecutablePath)')" />
<PingetCliOutputFiles Include="$(PingetCliNativeSqlitePath)" Condition="Exists('$(PingetCliNativeSqlitePath)')" />
<PingetCliOutputFiles Include="$(TargetDir)runtimes\$(PingetCliRuntimeIdentifier)\native\e_sqlite3.dll" Condition="!Exists('$(PingetCliNativeSqlitePath)') and Exists('$(TargetDir)runtimes\$(PingetCliRuntimeIdentifier)\native\e_sqlite3.dll')" />
<PingetCliPublishOutputFiles Include="$(PingetCliExecutablePath)" Condition="Exists('$(PingetCliExecutablePath)')" />
<PingetCliPublishOutputFiles Include="$(PingetCliNativeSqlitePath)" Condition="Exists('$(PingetCliNativeSqlitePath)')" />
<PingetCliPublishOutputFiles Include="$(PublishDir)runtimes\$(PingetCliRuntimeIdentifier)\native\e_sqlite3.dll" Condition="!Exists('$(PingetCliNativeSqlitePath)') and Exists('$(PublishDir)runtimes\$(PingetCliRuntimeIdentifier)\native\e_sqlite3.dll')" />
</ItemGroup>
<Copy
SourceFiles="@(PingetCliOutputFiles)"
Expand All @@ -69,6 +68,19 @@
/>
</Target>

<Target
Name="RemoveAvaloniaPublishSymbols"
AfterTargets="ComputeFilesToPublish"
Condition="'$(IncludeAvaloniaPublishSymbols)' != 'true'"
>
<ItemGroup>
<ResolvedFileToPublish
Remove="@(ResolvedFileToPublish)"
Condition="'%(ResolvedFileToPublish.Extension)' == '.pdb'"
/>
</ItemGroup>
</Target>

<ItemGroup>
<PackageReference Include="Avalonia" Version="12.0.0-rc1" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="12.0.0-rc1" />
Expand All @@ -82,6 +94,7 @@
<PackageReference Include="Octokit" Version="14.0.0" />
<PackageReference Include="Avalonia.Controls.WebView" Version="12.0.0-rc1" />
<PackageReference Include="Tmds.DBus.Protocol" Version="0.92.0" />
<PackageReference Include="Devolutions.Pinget.Cli.Rust" Version="0.4.0" GeneratePathProperty="true" ExcludeAssets="build;buildTransitive;native" />
</ItemGroup>

<ItemGroup>
Expand Down
22 changes: 9 additions & 13 deletions src/UniGetUI.Avalonia/ViewLocator.cs
Original file line number Diff line number Diff line change
@@ -1,37 +1,33 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using UniGetUI.Avalonia.ViewModels;
using UniGetUI.Avalonia.Views;

namespace UniGetUI.Avalonia;

/// <summary>
/// Given a view model, returns the corresponding view if possible.
/// Given a view model, returns the corresponding view if possible without reflection.
/// </summary>
[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)
{
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;
}
}
4 changes: 2 additions & 2 deletions src/UniGetUI.Avalonia/Views/Controls/UserAvatarControl.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@
</Flyout>
</Button.Flyout>

<!-- Avatar / placeholder (reflection bindings: compiled bindings lose scope inside Button content) -->
<Panel Width="32" Height="32" ClipToBounds="True" x:CompileBindings="False">
<!-- Avatar / placeholder -->
<Panel Width="32" Height="32" ClipToBounds="True" x:DataType="vm:UserAvatarViewModel">
<Ellipse Fill="{DynamicResource SettingsCardBackground}"
Width="32" Height="32"/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -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)
Expand Down
30 changes: 30 additions & 0 deletions src/UniGetUI.Core.Data.Tests/CoreTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
72 changes: 60 additions & 12 deletions src/UniGetUI.Core.Data/CoreData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<string, bool>? fileExists = null,
Func<string, bool>? 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;
}

/// <summary>
/// A path pointing to the executable file of the app
/// </summary>
Expand Down Expand Up @@ -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 (
Expand All @@ -611,5 +646,18 @@ private static string NormalizeExecutablePath(string path)

return path;
}

private static bool IsInstallRoot(
string directory,
Func<string, bool> fileExists,
Func<string, bool> 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"));
}
}
}
5 changes: 2 additions & 3 deletions src/UniGetUI.Core.IconStore/IconDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,8 @@ public async Task LoadFromCacheAsync()
"Icon Database.json"
);
IconScreenshotDatabase_v2 JsonData =
JsonSerializer.Deserialize<IconScreenshotDatabase_v2>(
await File.ReadAllTextAsync(IconsAndScreenshotsFile),
SerializationHelpers.DefaultOptions
IconStoreJson.DeserializeIconDatabase(
await File.ReadAllTextAsync(IconsAndScreenshotsFile)
);
if (JsonData.icons_and_screenshots is not null)
{
Expand Down
25 changes: 25 additions & 0 deletions src/UniGetUI.Core.IconStore/IconStoreJson.cs
Original file line number Diff line number Diff line change
@@ -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<IconScreenshotDatabase_v2>());
}

private static JsonTypeInfo<T> GetTypeInfo<T>()
{
return (JsonTypeInfo<T>?)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;
Loading
Loading