Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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