Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
54352d0
Avalonia: Load and display custom package icons in all list views
GabrielDuf Apr 29, 2026
8895cc0
Avalonia: Show package tag state badge (installed, upgradable, pinned…
GabrielDuf Apr 29, 2026
28e6148
Avalonia: Add bulk operations and expand/collapse to the operations p…
GabrielDuf Apr 29, 2026
98aff7d
Avalonia: Replace SplitView with resizable GridSplitter for filter pane
GabrielDuf Apr 29, 2026
902029c
Avalonia: Add clear-successful, cloud backup on load, metered-connect…
GabrielDuf Apr 29, 2026
ab20135
Avalonia: Type-to-search and global search box focus API
GabrielDuf Apr 29, 2026
84fa013
Avalonia: Per-line color coding in Operation Output window
GabrielDuf Apr 29, 2026
b3ae7de
Avalonia: Hierarchical sources tree in filter pane
GabrielDuf Apr 29, 2026
db41b77
Avalonia: Per-card overflow button in Grid/Icons views
GabrielDuf Apr 29, 2026
e5bf2e6
Avalonia: Installation options button in PackageDetails
GabrielDuf Apr 29, 2026
3126fb2
Fix build error
GabrielDuf Apr 29, 2026
29930b1
Avalonia: Auto-close filter pane when dragged below 100 px
GabrielDuf Apr 29, 2026
f9f28fc
Avalonia: Responsive filter pane overlay mode + per-page state persis…
GabrielDuf Apr 29, 2026
f7d33ca
Added installOptions to the packages details
GabrielDuf Apr 29, 2026
49716c9
fixed webview for windows
GabrielDuf Apr 30, 2026
1987ba1
Fix build on non-windows
GabrielDuf Apr 30, 2026
a91fffc
add a linux fallback for the webview
GabrielDuf May 1, 2026
677262b
fix code styling
GabrielDuf May 1, 2026
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
16 changes: 16 additions & 0 deletions src/UniGetUI.Avalonia/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.Threading;
using UniGetUI.Avalonia.Infrastructure;
using UniGetUI.Avalonia.Views;
using UniGetUI.Avalonia.Views.DialogPages;
Expand Down Expand Up @@ -35,6 +36,21 @@ public override void Initialize()

public override void OnFrameworkInitializationCompleted()
{
if (OperatingSystem.IsWindows())
{
// Safety net for NativeWebView (WebView2) initialization failures thrown
// asynchronously on the dispatcher. Without this the app crashes; with it
// the Help page shows a fallback "Open in browser" button.
Dispatcher.UIThread.UnhandledException += (_, e) =>
{
if (e.Exception is InvalidOperationException { Message: var msg }
&& msg.Contains("child window for native control host"))
{
e.Handled = true;
}
};
}

if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
if (OperatingSystem.IsMacOS())
Expand Down
37 changes: 37 additions & 0 deletions src/UniGetUI.Avalonia/Infrastructure/AvaloniaOperationRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,43 @@ public static void Add(AbstractOperation op)
};
}

public static void RetryFailed()
{
var failed = OperationViewModels
.Where(vm => vm.Operation.Status is OperationStatus.Failed)
.ToList();
foreach (var vm in failed)
vm.Operation.Retry(AbstractOperation.RetryMode.Retry);
}

public static void ClearSuccessful()
{
var succeeded = OperationViewModels
.Where(vm => vm.Operation.Status is OperationStatus.Succeeded)
.ToList();
foreach (var vm in succeeded)
Remove(vm);
}

public static void ClearFinished()
{
var finished = OperationViewModels
.Where(vm => vm.Operation.Status
is OperationStatus.Succeeded or OperationStatus.Failed or OperationStatus.Canceled)
.ToList();
foreach (var vm in finished)
Remove(vm);
}

public static void CancelAll()
{
var active = OperationViewModels
.Where(vm => vm.Operation.Status is OperationStatus.Running or OperationStatus.InQueue)
.ToList();
foreach (var vm in active)
vm.Operation.Cancel();
}

/// <summary>Remove a view-model (and its backing operation) from the panel. Called by the Close button.</summary>
public static void Remove(OperationViewModel vm)
{
Expand Down
81 changes: 81 additions & 0 deletions src/UniGetUI.Avalonia/Models/PackageCollections.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Net.Http;
using Avalonia.Collections;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using UniGetUI.Avalonia.ViewModels.Pages;
using UniGetUI.Core.SettingsEngine;
using UniGetUI.Core.Tools;
using UniGetUI.Interface.Enums;
using UniGetUI.PackageEngine.Interfaces;

Expand All @@ -13,6 +19,12 @@ namespace UniGetUI.PackageEngine.PackageClasses;
/// </summary>
public sealed class PackageWrapper : INotifyPropertyChanged, IDisposable
{
private static readonly HttpClient _iconHttpClient = new(CoreTools.GenericHttpClientParameters)
{
Timeout = TimeSpan.FromSeconds(8),
};
private static readonly ConcurrentDictionary<long, Bitmap?> _iconCache = new();

public IPackage Package { get; }
public PackageWrapper Self => this;
public int Index { get; set; }
Expand All @@ -21,6 +33,19 @@ public sealed class PackageWrapper : INotifyPropertyChanged, IDisposable

private readonly PackagesPageViewModel _page;

private Bitmap? _iconBitmap;
public Bitmap? IconBitmap
{
get => _iconBitmap;
private set
{
_iconBitmap = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IconBitmap)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasCustomIcon)));
}
}
public bool HasCustomIcon => _iconBitmap is not null;

public bool IsChecked
{
get => Package.IsChecked;
Expand All @@ -35,6 +60,8 @@ public bool IsChecked
public string VersionComboString { get; }
public string ListedNameTooltip { get; private set; } = "";
public float ListedOpacity { get; private set; } = 1.0f;
public string TagIconPath { get; private set; } = "";
public bool TagIconVisible { get; private set; }

public string SourceIconPath => IconTypeToSvgPath(Package.Source.IconId);

Expand Down Expand Up @@ -63,6 +90,43 @@ public PackageWrapper(IPackage package, PackagesPageViewModel page)

Package.PropertyChanged += Package_PropertyChanged;
UpdateDisplayState();

if (!Settings.Get(Settings.K.DisableIconsOnPackageLists))
_ = LoadIconAsync();
}

private async Task LoadIconAsync()
{
long hash = Package.GetHash();
if (_iconCache.TryGetValue(hash, out Bitmap? cached))
{
if (cached is not null)
IconBitmap = cached;
return;
}

try
{
var uri = Package.GetIconUrlIfAny();
if (uri is null) { _iconCache[hash] = null; return; }

Bitmap bitmap;
if (uri.IsFile)
{
bitmap = new Bitmap(uri.LocalPath);
}
else if (uri.Scheme is "http" or "https")
{
var bytes = await _iconHttpClient.GetByteArrayAsync(uri);
using var ms = new MemoryStream(bytes);
bitmap = new Bitmap(ms);
}
else { _iconCache[hash] = null; return; }

_iconCache[hash] = bitmap;
await Dispatcher.UIThread.InvokeAsync(() => IconBitmap = bitmap);
}
catch { _iconCache[hash] = null; }
}

private void Package_PropertyChanged(object? sender, PropertyChangedEventArgs e)
Expand All @@ -72,6 +136,8 @@ private void Package_PropertyChanged(object? sender, PropertyChangedEventArgs e)
UpdateDisplayState();
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ListedOpacity)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ListedNameTooltip)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TagIconPath)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TagIconVisible)));
}
else if (e.PropertyName == nameof(Package.IsChecked))
{
Expand All @@ -91,6 +157,21 @@ private void UpdateDisplayState()
_ => 1.0f,
};
ListedNameTooltip = Package.Name;

string tagName = Package.Tag switch
{
PackageTag.AlreadyInstalled => "installed_filled",
PackageTag.IsUpgradable => "upgradable_filled",
PackageTag.Pinned => "pin_filled",
PackageTag.OnQueue => "sandclock",
PackageTag.BeingProcessed => "loading_filled",
PackageTag.Failed => "warning_filled",
_ => "",
};
TagIconVisible = tagName.Length > 0;
TagIconPath = TagIconVisible
? $"avares://UniGetUI.Avalonia/Assets/Symbols/{tagName}.svg"
: "";
}

public void Dispose()
Expand Down
1 change: 1 addition & 0 deletions src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<AssemblyName>UniGetUI.Avalonia</AssemblyName>
<ApplicationIcon>..\UniGetUI\icon.ico</ApplicationIcon>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<ApplicationManifest Condition="$([MSBuild]::IsOSPlatform('Windows'))">app.manifest</ApplicationManifest>
</PropertyGroup>

<PropertyGroup Condition="'$(EnableAvaloniaDiagnostics)' == 'true'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,10 @@
[ObservableProperty] private bool _skipMinorChecked;
[ObservableProperty] private bool _autoUpdateChecked;

partial void OnAdminCheckedChanged(bool _) => Refresh();

Check warning on line 126 in src/UniGetUI.Avalonia/ViewModels/DialogPages/InstallOptionsViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Partial method declarations 'void InstallOptionsViewModel.OnAdminCheckedChanged(bool value)' and 'void InstallOptionsViewModel.OnAdminCheckedChanged(bool _)' have signature differences.
partial void OnInteractiveCheckedChanged(bool _) => Refresh();

Check warning on line 127 in src/UniGetUI.Avalonia/ViewModels/DialogPages/InstallOptionsViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Partial method declarations 'void InstallOptionsViewModel.OnInteractiveCheckedChanged(bool value)' and 'void InstallOptionsViewModel.OnInteractiveCheckedChanged(bool _)' have signature differences.
partial void OnSkipHashCheckedChanged(bool _) => Refresh();

Check warning on line 128 in src/UniGetUI.Avalonia/ViewModels/DialogPages/InstallOptionsViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Partial method declarations 'void InstallOptionsViewModel.OnSkipHashCheckedChanged(bool value)' and 'void InstallOptionsViewModel.OnSkipHashCheckedChanged(bool _)' have signature differences.
partial void OnSelectedVersionChanged(string? _) => Refresh();

Check warning on line 129 in src/UniGetUI.Avalonia/ViewModels/DialogPages/InstallOptionsViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Partial method declarations 'void InstallOptionsViewModel.OnSelectedVersionChanged(string? value)' and 'void InstallOptionsViewModel.OnSelectedVersionChanged(string? _)' have signature differences.

// ── Architecture / Scope / Location tab ───────────────────────────────────
[ObservableProperty] private bool _archEnabled;
Expand All @@ -140,17 +140,17 @@
[ObservableProperty] private string _locationText = "";
[ObservableProperty] private bool _locationEnabled;

partial void OnSelectedArchChanged(string? _) => Refresh();

Check warning on line 143 in src/UniGetUI.Avalonia/ViewModels/DialogPages/InstallOptionsViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Partial method declarations 'void InstallOptionsViewModel.OnSelectedArchChanged(string? value)' and 'void InstallOptionsViewModel.OnSelectedArchChanged(string? _)' have signature differences.
partial void OnSelectedScopeChanged(string? _) => Refresh();

Check warning on line 144 in src/UniGetUI.Avalonia/ViewModels/DialogPages/InstallOptionsViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Partial method declarations 'void InstallOptionsViewModel.OnSelectedScopeChanged(string? value)' and 'void InstallOptionsViewModel.OnSelectedScopeChanged(string? _)' have signature differences.

// ── CLI params tab ────────────────────────────────────────────────────────
[ObservableProperty] private string _paramsInstall = "";
[ObservableProperty] private string _paramsUpdate = "";
[ObservableProperty] private string _paramsUninstall = "";

partial void OnParamsInstallChanged(string _) => Refresh();

Check warning on line 151 in src/UniGetUI.Avalonia/ViewModels/DialogPages/InstallOptionsViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Partial method declarations 'void InstallOptionsViewModel.OnParamsInstallChanged(string value)' and 'void InstallOptionsViewModel.OnParamsInstallChanged(string _)' have signature differences.
partial void OnParamsUpdateChanged(string _) => Refresh();

Check warning on line 152 in src/UniGetUI.Avalonia/ViewModels/DialogPages/InstallOptionsViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Partial method declarations 'void InstallOptionsViewModel.OnParamsUpdateChanged(string value)' and 'void InstallOptionsViewModel.OnParamsUpdateChanged(string _)' have signature differences.
partial void OnParamsUninstallChanged(string _) => Refresh();

Check warning on line 153 in src/UniGetUI.Avalonia/ViewModels/DialogPages/InstallOptionsViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Partial method declarations 'void InstallOptionsViewModel.OnParamsUninstallChanged(string value)' and 'void InstallOptionsViewModel.OnParamsUninstallChanged(string _)' have signature differences.

// ── Pre/Post commands tab ─────────────────────────────────────────────────
[ObservableProperty] private string _preInstallText = "";
Expand Down Expand Up @@ -305,6 +305,10 @@
}

// ── Commands ──────────────────────────────────────────────────────────────

/// <summary>Captures the current UI state into the options object without closing.</summary>
public void ApplyChanges() => ApplyToOptions();

[RelayCommand]
private void Save()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,40 @@
using System.Collections.ObjectModel;
using Avalonia.Media;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using UniGetUI.Avalonia.ViewModels.Pages.LogPages;
using UniGetUI.PackageOperations;

namespace UniGetUI.Avalonia.ViewModels.DialogPages;

public partial class OperationOutputViewModel : ObservableObject
{
[ObservableProperty] private string _title = "";
[ObservableProperty] private string _outputText = "";
public ObservableCollection<LogLineItem> OutputLines { get; } = new();

private static readonly IBrush _errorBrush = new SolidColorBrush(Color.Parse("#FF6B6B"));
private static readonly IBrush _debugBrush = new SolidColorBrush(Color.Parse("#888888"));
private static readonly IBrush _normalBrush = Brushes.White;

public OperationOutputViewModel(AbstractOperation operation)
{
Title = operation.Metadata.Title;
OutputText = string.Join("\n", operation.GetOutput().Select(x => x.Item1));

foreach (var (text, type) in operation.GetOutput())
OutputLines.Add(MakeLine(text, type));

operation.LogLineAdded += (_, ev) =>
Dispatcher.UIThread.Post(() => OutputLines.Add(MakeLine(ev.Item1, ev.Item2)));
}

private LogLineItem MakeLine(string text, AbstractOperation.LineType type)
{
IBrush brush = type switch
{
AbstractOperation.LineType.Error => _errorBrush,
AbstractOperation.LineType.VerboseDetails => _debugBrush,
_ => _normalBrush,
};
return new LogLineItem(text, brush);
}
}
18 changes: 18 additions & 0 deletions src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,24 @@ public partial class MainWindowViewModel : ViewModelBase
[ObservableProperty]
private bool _operationsPanelVisible;

[ObservableProperty]
private bool _operationsPanelExpanded = true;

[RelayCommand]
private void ToggleOperationsPanel() => OperationsPanelExpanded = !OperationsPanelExpanded;

[RelayCommand]
private void RetryFailedOperations() => AvaloniaOperationRegistry.RetryFailed();

[RelayCommand]
private void ClearSuccessfulOperations() => AvaloniaOperationRegistry.ClearSuccessful();

[RelayCommand]
private void ClearFinishedOperations() => AvaloniaOperationRegistry.ClearFinished();

[RelayCommand]
private void CancelAllOperations() => AvaloniaOperationRegistry.CancelAll();

// ─── Sidebar ─────────────────────────────────────────────────────────────
public SidebarViewModel Sidebar { get; } = new();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,16 @@ private async Task DoCloudBackup()
{
_isLoading = true;
UpdateCloudControlsEnabled();
try { await DoCloudBackupStatic(); }
finally
{
_isLoading = false;
UpdateCloudControlsEnabled();
}
}

public static async Task DoCloudBackupStatic()
{
try
{
var packages = InstalledPackagesLoader.Instance?.Packages.ToList() ?? [];
Expand All @@ -236,11 +246,6 @@ private async Task DoCloudBackup()
Logger.Error("An error occurred while performing a CLOUD backup:");
Logger.Error(ex);
}
finally
{
_isLoading = false;
UpdateCloudControlsEnabled();
}
}

[RelayCommand]
Expand Down
Loading
Loading