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
18 changes: 18 additions & 0 deletions .github/workflows/dotnet-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,21 @@ jobs:
env:
GITHUB_TOKEN: ${{ github.token }}
run: dotnet test UniGetUI.Windows.slnx --no-restore --verbosity q --nologo /p:Platform=x64 /p:UseSharedCompilation=false /m:1

- name: Report full-trim publish warnings
working-directory: src
shell: pwsh
continue-on-error: true
run: |
dotnet build-server shutdown
dotnet publish UniGetUI.Avalonia/UniGetUI.Avalonia.csproj `
--no-restore `
--configuration Release `
--runtime win-x64 `
--self-contained true `
--output "${{ runner.temp }}\unigetui-full-trim-publish" `
/p:Platform=x64 `
/p:TrimMode=full `
/p:TrimmerSingleWarn=false `
/p:UseSharedCompilation=false `
--verbosity minimal
12 changes: 2 additions & 10 deletions src/UniGetUI.Avalonia/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#if AVALONIA_DIAGNOSTICS_ENABLED
using Avalonia.Diagnostics;
#endif
using UniGetUI.Avalonia.Assets.Styles;
using UniGetUI.Avalonia.Infrastructure;
using UniGetUI.Avalonia.Views;
using UniGetUI.Avalonia.Views.DialogPages;
Expand All @@ -37,18 +38,9 @@ public override void Initialize()
#endif
}

// ResourceInclude is flagged with RequiresUnreferencedCode because, in general, it can load
// resources from other assemblies that trimming might remove. Styles.WindowsMica.axaml is an
// avares resource embedded in THIS assembly, so it is never trimmed — the warning is safe to
// suppress here. (It can't be declared in XAML because the merge is conditional at runtime.)
[UnconditionalSuppressMessage("Trimming", "IL2026",
Justification = "Styles.WindowsMica.axaml is an avares resource in this assembly and is not trimmed.")]
private void ApplyWindowsMicaStyling()
{
Resources.MergedDictionaries.Add(new ResourceInclude((Uri?)null)
{
Source = new Uri("avares://UniGetUI/Assets/Styles/Styles.WindowsMica.axaml")
});
Resources.MergedDictionaries.Add(new WindowsMicaStyles());
// Give flyouts/menus/tooltips a native acrylic backdrop (DWM) so they blur + tint
// from behind and adapt to the theme.
MicaWindowHelper.EnableAcrylicPopups();
Expand Down
3 changes: 2 additions & 1 deletion src/UniGetUI.Avalonia/Assets/Styles/Styles.WindowsMica.axaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="UniGetUI.Avalonia.Assets.Styles.WindowsMicaStyles">
<!--
Windows 11 Mica overrides. Merged into Application.Resources ONLY when Mica is
actually active (Windows 11 + "Transparency effects" on) — see App.Initialize().
Expand Down
12 changes: 12 additions & 0 deletions src/UniGetUI.Avalonia/Assets/Styles/Styles.WindowsMica.axaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;

namespace UniGetUI.Avalonia.Assets.Styles;

public partial class WindowsMicaStyles : ResourceDictionary
{
public WindowsMicaStyles()
{
AvaloniaXamlLoader.Load(this);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#if WINDOWS
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices;
using UniGetUI.Core.Logging;
Expand Down Expand Up @@ -61,6 +62,10 @@ public static void EnsureStamped()
return File.Exists(commonPath) ? commonPath : null;
}

[UnconditionalSuppressMessage(
"Trimming",
"IL2072",
Justification = "ShellLink COM activation uses the shell-registered CLSID and COM interfaces declared below; it does not depend on app-owned constructors trimmed from this assembly.")]
private static void StampIfMissing(string shortcutPath)
{
// ShellLink (CLSID 00021401-0000-0000-C000-000000000046) implements IPersistFile and
Expand Down
46 changes: 18 additions & 28 deletions src/UniGetUI.Avalonia/Infrastructure/GitHubAuthService.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using Octokit;
using UniGetUI.Core.Data;
using UniGetUI.Core.Logging;
using UniGetUI.Core.SecureSettings;
using UniGetUI.Core.Tools;
using UniGetUI.Interface;
using CoreSettings = UniGetUI.Core.SettingsEngine.Settings;

namespace UniGetUI.Avalonia.Infrastructure;
Expand All @@ -15,25 +15,18 @@ internal sealed class GitHubAuthService
private readonly string _gitHubClientId = Secrets.GetGitHubClientId();
private readonly string _gitHubClientSecret = Secrets.GetGitHubClientSecret();
private const string RedirectUri = "http://127.0.0.1:58642/";
private readonly GitHubClient _client;

public static event EventHandler<EventArgs>? AuthStatusChanged;

public GitHubAuthService()
{
_client = new GitHubClient(new ProductHeaderValue("UniGetUI", CoreData.VersionName));
}
public GitHubAuthService() { }

public static GitHubClient? CreateGitHubClient()
public static GitHubApiClient? CreateGitHubClient()
{
var token = SecureGHTokenManager.GetToken();
if (string.IsNullOrEmpty(token))
return null;

return new GitHubClient(new ProductHeaderValue("UniGetUI", CoreData.VersionName))
{
Credentials = new Credentials(token),
};
return new GitHubApiClient(token);
}

private GHAuthApiRunner? _loginBackend;
Expand All @@ -53,13 +46,11 @@ public async Task<bool> SignInAsync()

Logger.Info("Initiating GitHub sign-in process using loopback redirect...");

var request = new OauthLoginRequest(_gitHubClientId)
{
Scopes = { "read:user", "gist" },
RedirectUri = new Uri(RedirectUri),
};

var oauthLoginUrl = _client.Oauth.GetGitHubLoginUrl(request);
var oauthLoginUrl = GitHubApiClient.GetOAuthLoginUrl(
_gitHubClientId,
new Uri(RedirectUri),
["read:user", "gist"]
);

_codeFromApi = null;
_loginWasCancelled = false;
Expand Down Expand Up @@ -141,11 +132,13 @@ private async Task<bool> CompleteSignInAsync(string code)
{
try
{
var tokenRequest = new OauthTokenRequest(_gitHubClientId, _gitHubClientSecret, code)
{
RedirectUri = new Uri(RedirectUri), // The same redirect_uri must be sent
};
var token = await _client.Oauth.CreateAccessToken(tokenRequest);
using var client = new GitHubApiClient();
var token = await client.CreateAccessTokenAsync(
_gitHubClientId,
_gitHubClientSecret,
code,
new Uri(RedirectUri)
);

if (string.IsNullOrEmpty(token.AccessToken))
{
Expand All @@ -157,11 +150,8 @@ private async Task<bool> CompleteSignInAsync(string code)
Logger.Info("GitHub login successful. Storing access token.");
SecureGHTokenManager.StoreToken(token.AccessToken);

var userClient = new GitHubClient(new ProductHeaderValue("UniGetUI"))
{
Credentials = new Credentials(token.AccessToken),
};
var user = await userClient.User.Current();
using var userClient = new GitHubApiClient(token.AccessToken);
var user = await userClient.GetCurrentUserAsync();
if (user is not null)
{
CoreSettings.SetValue(CoreSettings.K.GitHubUserLogin, user.Login);
Expand Down
59 changes: 27 additions & 32 deletions src/UniGetUI.Avalonia/Infrastructure/GitHubCloudBackupService.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Octokit;
using UniGetUI.Core.Data;
using UniGetUI.Core.SecureSettings;
using UniGetUI.Core.Tools;
using UniGetUI.Interface;

namespace UniGetUI.Avalonia.Infrastructure;

Expand All @@ -20,56 +19,48 @@ internal sealed class CloudBackupEntry
public required string Display { get; init; }
}

public static GitHubClient? CreateGitHubClient()
public static GitHubApiClient? CreateGitHubClient()
{
string? token = SecureGHTokenManager.GetToken();
if (string.IsNullOrWhiteSpace(token))
return null;

return new GitHubClient(new ProductHeaderValue("UniGetUI", CoreData.VersionName))
{
Credentials = new Credentials(token),
};
return new GitHubApiClient(token);
}

public static async Task<(string Login, string DisplayName)> GetCurrentUserAsync()
{
var client = CreateGitHubClient()
using var client = CreateGitHubClient()
?? throw new InvalidOperationException(CoreTools.Translate("Log in to enable cloud backup"));

var user = await client.User.Current();
var user = await client.GetCurrentUserAsync();
string login = user.Login ?? string.Empty;
string displayName = string.IsNullOrWhiteSpace(user.Name) ? login : user.Name;
return (login, displayName);
}

public static async Task UploadPackageBundleAsync(string bundleContents)
{
var client = CreateGitHubClient()
using var client = CreateGitHubClient()
?? throw new InvalidOperationException(CoreTools.Translate("Log in to enable cloud backup"));

var user = await client.User.Current();
var backupGist = await GetBackupGistAsync(client, user.Login, createIfMissing: true)
var backupGist = await GetBackupGistAsync(client, createIfMissing: true)
?? throw new InvalidOperationException(CoreTools.Translate("Backup Failed"));

string fileKey = BuildGistFileKey();
var update = new GistUpdate { Description = GistDescription };

if (backupGist.Files.ContainsKey(fileKey))
update.Files[fileKey] = new GistFileUpdate { Content = bundleContents };
else
update.Files.Add(fileKey, new GistFileUpdate { Content = bundleContents });

await client.Gist.Edit(backupGist.Id, update);
await client.EditGistAsync(
backupGist.Id,
GistDescription,
new Dictionary<string, string> { [fileKey] = bundleContents }
);
}

public static async Task<IReadOnlyList<CloudBackupEntry>> GetAvailableBackupsAsync()
{
var client = CreateGitHubClient()
using var client = CreateGitHubClient()
?? throw new InvalidOperationException(CoreTools.Translate("Log in to enable cloud backup"));

var user = await client.User.Current();
var backupGist = await GetBackupGistAsync(client, user.Login, createIfMissing: false);
var backupGist = await GetBackupGistAsync(client, createIfMissing: false);
if (backupGist is null)
return [];

Expand All @@ -85,16 +76,15 @@ public static async Task<IReadOnlyList<CloudBackupEntry>> GetAvailableBackupsAsy

public static async Task<string> GetBackupContentsAsync(string backupKey)
{
var client = CreateGitHubClient()
using var client = CreateGitHubClient()
?? throw new InvalidOperationException(CoreTools.Translate("Log in to enable cloud backup"));

var user = await client.User.Current();
var backupGist = await GetBackupGistAsync(client, user.Login, createIfMissing: false);
var backupGist = await GetBackupGistAsync(client, createIfMissing: false);

if (backupGist is null)
throw new KeyNotFoundException(CoreTools.Translate("Log in to enable cloud backup"));

var fullGist = await client.Gist.Get(backupGist.Id);
var fullGist = await client.GetGistAsync(backupGist.Id);
var file = fullGist.Files.FirstOrDefault(f =>
f.Key.StartsWith(PackageBackupStartingKey, StringComparison.Ordinal)
&& f.Key.EndsWith(backupKey, StringComparison.Ordinal));
Expand All @@ -105,18 +95,23 @@ public static async Task<string> GetBackupContentsAsync(string backupKey)
return file.Value.Content;
}

private static async Task<Gist?> GetBackupGistAsync(GitHubClient client, string userLogin, bool createIfMissing)
private static async Task<GitHubGist?> GetBackupGistAsync(
GitHubApiClient client,
bool createIfMissing
)
{
var candidates = await client.Gist.GetAllForUser(userLogin);
var candidates = await client.GetCurrentUserGistsAsync();
var backupGist = candidates.FirstOrDefault(g =>
g.Description?.EndsWith(GistDescriptionEndingKey, StringComparison.Ordinal) == true);

if (backupGist is not null || !createIfMissing)
return backupGist;

var newGist = new NewGist { Description = GistDescription, Public = false };
newGist.Files.Add("- UniGetUI Package Backups", ReadMeContents);
return await client.Gist.Create(newGist);
return await client.CreateGistAsync(
GistDescription,
isPublic: false,
new Dictionary<string, string> { ["- UniGetUI Package Backups"] = ReadMeContents }
);
}

private static string BuildGistFileKey()
Expand Down
6 changes: 3 additions & 3 deletions src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<ApplicationManifest Condition="$([MSBuild]::IsOSPlatform('Windows'))">app.manifest</ApplicationManifest>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>partial</TrimMode>
<TrimMode>full</TrimMode>
<PublishSingleFile>false</PublishSingleFile>
<IncludeAvaloniaPublishSymbols>false</IncludeAvaloniaPublishSymbols>
</PropertyGroup>

<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
<NoWarn>$(NoWarn);MVVMTK0045</NoWarn>
<!-- Parameterized windows/base views are directly constructed in code, not through the runtime XAML loader. -->
<NoWarn>$(NoWarn);MVVMTK0045;AVLN3001</NoWarn>
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<RuntimeIdentifier Condition="'$(RuntimeIdentifier)' == '' and '$(Platform)' == 'arm64'">win-arm64</RuntimeIdentifier>
<RuntimeIdentifier Condition="'$(RuntimeIdentifier)' == '' and ('$(Platform)' == 'x64' or '$(Platform)' == 'x86')">win-$(Platform)</RuntimeIdentifier>
Expand Down Expand Up @@ -147,7 +148,6 @@
<PackageReference Include="Devolutions.AvaloniaTheme.Linux" Version="2026.3.11" Condition="$([MSBuild]::IsOSPlatform('Linux'))" />
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.0-beta3" Condition="'$(EnableAvaloniaDiagnostics)' == 'true'" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.*" />
<PackageReference Include="Octokit" Version="14.0.0" />
<PackageReference Include="Avalonia.Controls.WebView" Version="12.0.0" />
<PackageReference Include="Tmds.DBus.Protocol" Version="0.92.0" />
<PackageReference Include="Devolutions.UniGetUI.Elevator" Version="2.6.1.3" GeneratePathProperty="true" ExcludeAssets="build;buildTransitive;native" Condition="$([MSBuild]::IsOSPlatform('Windows'))" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.Input;
using Octokit;
using UniGetUI.Avalonia.Infrastructure;
using UniGetUI.Core.Logging;
using UniGetUI.Core.Tools;
using UniGetUI.Interface;
using MvvmRelayCommand = CommunityToolkit.Mvvm.Input.RelayCommand;

namespace UniGetUI.Avalonia.ViewModels.Controls;
Expand Down Expand Up @@ -57,10 +57,10 @@ private async Task RefreshAsync()
{
try
{
var client = GitHubAuthService.CreateGitHubClient();
using var client = GitHubAuthService.CreateGitHubClient();
if (client is not null)
{
User user = await client.User.Current();
GitHubUser user = await client.GetCurrentUserAsync();
displayName = string.IsNullOrEmpty(user.Name)
? $"@{user.Login}"
: $"{user.Name} (@{user.Login})";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,20 +156,24 @@ private void GenerateLoginState()

private async Task GenerateLogoutState()
{
var client = GitHubAuthService.CreateGitHubClient()
using var client = GitHubAuthService.CreateGitHubClient()
?? throw new InvalidOperationException("Authenticated but cannot create GitHub client.");
var user = await client.User.Current();
var user = await client.GetCurrentUserAsync();

IsLoggedIn = true;
GitHubUserTitle = CoreTools.Translate("You are logged in as {0} (@{1})", user.Name, user.Login);
string displayName = string.IsNullOrWhiteSpace(user.Name) ? user.Login : user.Name;
GitHubUserTitle = CoreTools.Translate("You are logged in as {0} (@{1})", displayName, user.Login);
GitHubUserSubtitle = CoreTools.Translate("Nice! Backups will be uploaded to a private gist on your account");

try
{
using var http = new HttpClient(CoreTools.GenericHttpClientParameters);
var bytes = await http.GetByteArrayAsync(user.AvatarUrl);
using var ms = new MemoryStream(bytes);
GitHubAvatarBitmap = new Bitmap(ms);
if (!string.IsNullOrWhiteSpace(user.AvatarUrl))
{
var bytes = await http.GetByteArrayAsync(user.AvatarUrl);
using var ms = new MemoryStream(bytes);
GitHubAvatarBitmap = new Bitmap(ms);
}
}
catch (Exception ex)
{
Expand Down
Loading
Loading