From 9b21d8fb30b2023c2fb246b9a76cb6429d681920 Mon Sep 17 00:00:00 2001 From: Gabriel Dufresne Date: Thu, 26 Mar 2026 08:29:43 -0400 Subject: [PATCH 01/15] Added about view --- .../Assets/Styles/Styles.Common.axaml | 7 ++ .../UniGetUI.Avalonia.csproj | 3 + .../ViewModels/MainWindowViewModel.cs | 5 +- .../Pages/AboutPages/AboutPageViewModel.cs | 28 ++++++ .../Pages/AboutPages/ContributorsViewModel.cs | 60 +++++++++++++ .../AboutPages/ThirdPartyLicensesViewModel.cs | 37 ++++++++ .../Pages/AboutPages/TranslatorsViewModel.cs | 63 +++++++++++++ .../Views/DialogPages/AboutWindow.axaml | 34 +++++++ .../Views/DialogPages/AboutWindow.axaml.cs | 11 +++ .../Views/Pages/AboutPages/AboutPage.axaml | 90 +++++++++++++++++++ .../Views/Pages/AboutPages/AboutPage.axaml.cs | 13 +++ .../Views/Pages/AboutPages/Contributors.axaml | 63 +++++++++++++ .../Pages/AboutPages/Contributors.axaml.cs | 21 +++++ .../Pages/AboutPages/ThirdPartyLicenses.axaml | 66 ++++++++++++++ .../AboutPages/ThirdPartyLicenses.axaml.cs | 27 ++++++ .../Views/Pages/AboutPages/Translators.axaml | 76 ++++++++++++++++ .../Pages/AboutPages/Translators.axaml.cs | 24 +++++ 17 files changed, 627 insertions(+), 1 deletion(-) create mode 100644 src/UniGetUI.Avalonia/ViewModels/Pages/AboutPages/AboutPageViewModel.cs create mode 100644 src/UniGetUI.Avalonia/ViewModels/Pages/AboutPages/ContributorsViewModel.cs create mode 100644 src/UniGetUI.Avalonia/ViewModels/Pages/AboutPages/ThirdPartyLicensesViewModel.cs create mode 100644 src/UniGetUI.Avalonia/ViewModels/Pages/AboutPages/TranslatorsViewModel.cs create mode 100644 src/UniGetUI.Avalonia/Views/DialogPages/AboutWindow.axaml create mode 100644 src/UniGetUI.Avalonia/Views/DialogPages/AboutWindow.axaml.cs create mode 100644 src/UniGetUI.Avalonia/Views/Pages/AboutPages/AboutPage.axaml create mode 100644 src/UniGetUI.Avalonia/Views/Pages/AboutPages/AboutPage.axaml.cs create mode 100644 src/UniGetUI.Avalonia/Views/Pages/AboutPages/Contributors.axaml create mode 100644 src/UniGetUI.Avalonia/Views/Pages/AboutPages/Contributors.axaml.cs create mode 100644 src/UniGetUI.Avalonia/Views/Pages/AboutPages/ThirdPartyLicenses.axaml create mode 100644 src/UniGetUI.Avalonia/Views/Pages/AboutPages/ThirdPartyLicenses.axaml.cs create mode 100644 src/UniGetUI.Avalonia/Views/Pages/AboutPages/Translators.axaml create mode 100644 src/UniGetUI.Avalonia/Views/Pages/AboutPages/Translators.axaml.cs diff --git a/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml b/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml index 0ffcf9ff6..ad73738f7 100644 --- a/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml +++ b/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml @@ -27,6 +27,13 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RestartRequired?.Invoke(s, e); - - BuildBackupInfoCard(); - } - - private void BuildBackupInfoCard() - { - var stack = new StackPanel { Orientation = Orientation.Vertical }; - foreach (var line in new[] - { - "The backup will include the complete list of the installed packages and their installation options. Ignored updates and skipped versions will also be saved.", - "The backup will NOT include any binary file nor any program's saved data.", - "The size of the backup is estimated to be less than 1MB.", - "The backup will be performed after login.", - }) - { - stack.Children.Add(new TextBlock - { - Text = " \u25cf " + CoreTools.Translate(line), - TextWrapping = TextWrapping.Wrap, - }); - } - BackupInfoCardHolder.Content = new SettingsCard - { - CornerRadius = new CornerRadius(8), - Description = stack, - }; + ((BackupViewModel)DataContext).RestartRequired += (s, e) => RestartRequired?.Invoke(s, e); } } diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Experimental.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Experimental.axaml index 6889c6241..ff7c89ffc 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Experimental.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Experimental.axaml @@ -56,6 +56,7 @@ SettingName="IconDataBaseURL" Text="{t:Translate Use a custom icon and screenshot database URL}" Placeholder="{t:Translate Leave empty for default}" + HelpUrl="https://www.marticliment.com/unigetui/help/icons-and-screenshots#custom-source" ValueChangedCommand="{Binding ShowRestartRequiredCommand}" CornerRadius="0,0,8,8"/> diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/InstallOptionsPanel.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/InstallOptionsPanel.axaml new file mode 100644 index 000000000..b93431268 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/InstallOptionsPanel.axaml @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/InstallOptionsPanel.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/InstallOptionsPanel.axaml.cs new file mode 100644 index 000000000..02fa838f3 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/InstallOptionsPanel.axaml.cs @@ -0,0 +1,41 @@ +using Avalonia.Controls; +using UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; +using UniGetUI.PackageEngine.Interfaces; + +namespace UniGetUI.Avalonia.Views.Pages.SettingsPages; + +public sealed partial class InstallOptionsPanel : UserControl +{ + private InstallOptionsPanelViewModel ViewModel => (InstallOptionsPanelViewModel)DataContext!; + + public event EventHandler? NavigateToAdministratorRequested; + + public InstallOptionsPanel(IPackageManager manager) + { + DataContext = new InstallOptionsPanelViewModel(manager); + InitializeComponent(); + + ViewModel.NavigateToAdministratorRequested += (s, e) => + NavigateToAdministratorRequested?.Invoke(s, e); + + ViewModel.PropertyChanged += (_, e) => + { + if (e.PropertyName == nameof(InstallOptionsPanelViewModel.HasChanges)) + { + if (ViewModel.HasChanges) + ApplyButton.Classes.Add("accent"); + else + ApplyButton.Classes.Remove("accent"); + } + }; + + // Wire location picker (needs Visual reference for StorageProvider) + SelectDirButton.Click += (_, _) => + _ = ViewModel.SelectLocationCommand.ExecuteAsync(this); + + // Mark changed whenever the user edits a CLI textbox + CustomInstallBox.TextChanged += (_, _) => ViewModel.MarkChanged(); + CustomUpdateBox.TextChanged += (_, _) => ViewModel.MarkChanged(); + CustomUninstallBox.TextChanged += (_, _) => ViewModel.MarkChanged(); + } +} diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/ManagersHomepage.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/ManagersHomepage.axaml index e6533238e..328fca3dc 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/ManagersHomepage.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/ManagersHomepage.axaml @@ -11,8 +11,7 @@ + Margin="0,8"/> diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/ManagersHomepage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/ManagersHomepage.axaml.cs index 495ebb71f..8cae92f5d 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/ManagersHomepage.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/ManagersHomepage.axaml.cs @@ -1,34 +1,153 @@ +using Avalonia; using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; using UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; using UniGetUI.Avalonia.Views.Controls.Settings; using UniGetUI.Core.Tools; using UniGetUI.PackageEngine; +using UniGetUI.PackageEngine.Interfaces; +using CoreSettings = UniGetUI.Core.SettingsEngine.Settings; namespace UniGetUI.Avalonia.Views.Pages.SettingsPages; public sealed partial class ManagersHomepage : UserControl, ISettingsPage { public bool CanGoBack => false; - public string ShortTitle => CoreTools.Translate("Package Managers"); + public string ShortTitle => CoreTools.Translate("Package manager preferences"); public event EventHandler? RestartRequired { add { } remove { } } public event EventHandler? NavigationRequested { add { } remove { } } + public event EventHandler? ManagerNavigationRequested; + + private readonly List<(ToggleSwitch Toggle, IPackageManager Manager, Border Badge, TextBlock BadgeText)> _rows = []; + private bool _isLoadingToggles; public ManagersHomepage() { DataContext = new ManagersHomepageViewModel(); InitializeComponent(); - // Build the manager buttons dynamically (manager list is only known at runtime) - foreach (var manager in PEInterface.Managers) + int count = PEInterface.Managers.Length; + for (int i = 0; i < count; i++) { + var manager = PEInterface.Managers[i]; + bool isFirst = i == 0; + bool isLast = i == count - 1; + + CornerRadius radius = isFirst && isLast ? new CornerRadius(8) + : isFirst ? new CornerRadius(8, 8, 0, 0) + : isLast ? new CornerRadius(0, 0, 8, 8) + : new CornerRadius(0); + var thickness = isFirst ? new Thickness(1) : new Thickness(1, 0, 1, 1); + + // ── Status badge ───────────────────────────────────────────────── + var badgeText = new TextBlock + { + FontSize = 12, + FontWeight = FontWeight.SemiBold, + VerticalAlignment = VerticalAlignment.Center, + }; + var badge = new Border + { + CornerRadius = new CornerRadius(4), + Padding = new Thickness(6, 3, 6, 3), + Child = badgeText, + }; + + // ── Enable/disable toggle ──────────────────────────────────────── + var toggle = new ToggleSwitch + { + OnContent = "", + OffContent = "", + VerticalAlignment = VerticalAlignment.Center, + }; + toggle.Loaded += (_, _) => + { + _isLoadingToggles = true; + toggle.IsChecked = manager.IsEnabled(); + _isLoadingToggles = false; + ApplyStatusBadge(manager, badge, badgeText); + }; + toggle.IsCheckedChanged += async (_, _) => + { + if (_isLoadingToggles) return; + CoreSettings.SetDictionaryItem(CoreSettings.K.DisabledManagers, manager.Name, toggle.IsChecked != true); + await Task.Run(manager.Initialize); + ApplyStatusBadge(manager, badge, badgeText); + }; + + var toggleAndBadge = new StackPanel + { + Orientation = Orientation.Vertical, + Spacing = 4, + VerticalAlignment = VerticalAlignment.Center, + }; + toggleAndBadge.Children.Add(toggle); + toggleAndBadge.Children.Add(badge); + + var rightContent = toggleAndBadge; + var btn = new SettingsPageButton { Text = manager.DisplayName, - UnderText = CoreTools.Translate(manager.IsEnabled() ? "Enabled" : "Disabled"), + UnderText = manager.Properties.Description.Split("
")[0], + Icon = manager.Properties.IconId, + CornerRadius = radius, + BorderThickness = thickness, + Content = rightContent, }; - // TODO: navigate to PackageManagerPage for this manager (Phase 5) + + var capturedManager = manager; + btn.Click += (_, _) => ManagerNavigationRequested?.Invoke(this, capturedManager); + ManagersPanel.Children.Add(btn); + _rows.Add((toggle, manager, badge, badgeText)); + } + } + + /// Re-sync toggle states after returning from a sub-page. + public void RefreshToggles() + { + _isLoadingToggles = true; + foreach (var (toggle, manager, badge, badgeText) in _rows) + { + toggle.IsChecked = manager.IsEnabled(); + ApplyStatusBadge(manager, badge, badgeText); } + _isLoadingToggles = false; + } + + private void ApplyStatusBadge(IPackageManager manager, Border badge, TextBlock text) + { + string bgKey, fgKey, label; + if (!manager.IsEnabled()) + { + bgKey = "WarningBannerBackground"; + fgKey = "StatusWarningForeground"; + label = CoreTools.Translate("Disabled"); + } + else if (manager.Status.Found) + { + bgKey = "StatusSuccessBackground"; + fgKey = "StatusSuccessForeground"; + label = CoreTools.Translate("Ready"); + } + else + { + bgKey = "StatusErrorBackground"; + fgKey = "StatusErrorForeground"; + label = CoreTools.Translate("Not found"); + } + badge.Background = LookupBrush(bgKey); + text.Foreground = LookupBrush(fgKey); + text.Text = label; + } + + private IBrush LookupBrush(string key) + { + if (this.TryFindResource(key, ActualThemeVariant, out var res) && res is IBrush brush) + return brush; + return Brushes.Transparent; } } diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml new file mode 100644 index 000000000..41ce2f5dc --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs new file mode 100644 index 000000000..0e6be2d57 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs @@ -0,0 +1,439 @@ +using Avalonia.Controls; +using Avalonia.Layout; +using UniGetUI.Avalonia.ViewModels; +using Avalonia.Media; +using UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; +using UniGetUI.Avalonia.Views.Controls; +using UniGetUI.Avalonia.Views.Controls.Settings; +using UniGetUI.Core.SettingsEngine.SecureSettings; +using UniGetUI.Core.Tools; +using UniGetUI.Interface.Enums; +using UniGetUI.PackageEngine.Interfaces; +using UniGetUI.PackageEngine.Managers.VcpkgManager; +using CoreSettings = UniGetUI.Core.SettingsEngine.Settings; +using Thickness = global::Avalonia.Thickness; +using CornerRadius = global::Avalonia.CornerRadius; + +namespace UniGetUI.Avalonia.Views.Pages.SettingsPages; + +public sealed partial class PackageManagerPage : UserControl, ISettingsPage +{ + private PackageManagerViewModel ViewModel => (PackageManagerViewModel)DataContext!; + + public bool CanGoBack => true; + public string ShortTitle => ViewModel.PageTitle; + + public event EventHandler? RestartRequired; + public event EventHandler? NavigationRequested; + + public PackageManagerPage(IPackageManager manager) + { + DataContext = new PackageManagerViewModel(manager); + InitializeComponent(); + + ViewModel.PropertyChanged += (_, e) => + { + if (e.PropertyName is nameof(PackageManagerViewModel.Severity) + or nameof(PackageManagerViewModel.StatusTitle) + or nameof(PackageManagerViewModel.StatusMessage)) + ApplyStatusBrushes(); + }; + + ViewModel.RestartRequired += (s, e) => RestartRequired?.Invoke(s, e); + ViewModel.NavigateToAdministratorRequested += (_, _) => NavigationRequested?.Invoke(this, typeof(Administrator)); + + BuildPage(); + ApplyStatusBrushes(); + } + + // ── Dynamic UI construction ─────────────────────────────────────────────── + + private void BuildPage() + { + var manager = ViewModel.Manager; + + // ── Enable/Disable toggle + EnableManager.DictionaryName = CoreSettings.K.DisabledManagers; + EnableManager.KeyName = manager.Name; + EnableManager.Text = CoreTools.Translate("Enable {pm}").Replace("{pm}", manager.DisplayName); + ExtraControls.IsEnabled = manager.IsEnabled(); + EnableManager.StateChanged += (_, _) => + { + ExtraControls.IsEnabled = manager.IsEnabled(); + _ = ViewModel.ReloadManagerCommand.ExecuteAsync(null); + }; + + // ── Executable picker card + bool customPathsAllowed = SecureSettings.Get(SecureSettings.K.AllowCustomManagerPaths); + var execGrid = new Grid + { + ColumnDefinitions = new ColumnDefinitions("*,Auto"), + RowDefinitions = new RowDefinitions("Auto,Auto,Auto"), + HorizontalAlignment = HorizontalAlignment.Stretch, + }; + + var execHint = new TextBlock + { + Text = CoreTools.Translate("Not finding the file you are looking for? Make sure it has been added to path."), + FontSize = 12, + FontWeight = FontWeight.SemiBold, + Opacity = 0.7, + TextWrapping = TextWrapping.Wrap, + }; + Grid.SetColumnSpan(execHint, 2); + execGrid.Children.Add(execHint); + + var execCombo = new ComboBox { HorizontalAlignment = HorizontalAlignment.Stretch }; + foreach (var path in manager.FindCandidateExecutableFiles()) + execCombo.Items.Add(path); + + string savedPath = CoreSettings.GetDictionaryItem(CoreSettings.K.ManagerPaths, manager.Name) ?? ""; + if (string.IsNullOrEmpty(savedPath)) + { + var (found, path) = manager.GetExecutableFile(); + savedPath = found ? path : ""; + } + execCombo.SelectedItem = savedPath; + execCombo.IsEnabled = customPathsAllowed; + execCombo.SelectionChanged += (s, _) => + { + if (s is ComboBox combo && combo.SelectedItem?.ToString() is { Length: > 0 } selected) + ViewModel.OnExecutableSelected(selected); + }; + Grid.SetRow(execCombo, 1); + Grid.SetColumnSpan(execCombo, 2); + execGrid.Children.Add(execCombo); + + if (!customPathsAllowed) + { + var securityWarning = new TextBlock + { + Text = CoreTools.Translate("For security reasons, changing the executable file is disabled by default"), + FontSize = 12, + FontWeight = FontWeight.SemiBold, + Opacity = 0.7, + TextWrapping = TextWrapping.Wrap, + Classes = { "setting-warning-text" }, + }; + Grid.SetRow(securityWarning, 2); + execGrid.Children.Add(securityWarning); + + var goToSecureBtn = new Button + { + Content = new TextBlock { Text = CoreTools.Translate("Change this"), FontSize = 12, Classes = { "hyperlink" } }, + Background = Brushes.Transparent, + BorderThickness = new Thickness(0), + Padding = new Thickness(0), + }; + goToSecureBtn.Click += (_, _) => ViewModel.NavigateToAdministratorCommand.Execute(null); + Grid.SetRow(goToSecureBtn, 2); + Grid.SetColumn(goToSecureBtn, 1); + execGrid.Children.Add(goToSecureBtn); + } + + ExecutableHolder.Content = new SettingsCard + { + BorderThickness = new Thickness(1, 0, 1, 0), + CornerRadius = new CornerRadius(0), + Header = CoreTools.Translate("Select the executable to be used. The following list shows the executables found by UniGetUI"), + Description = execGrid, + Margin = new Thickness(0, 2, 0, 2), + }; + + // ── Current path card + var copyIcon = new SvgIcon + { + Path = "avares://UniGetUI.Avalonia/Assets/Symbols/copy.svg", + Width = 24, + Height = 24, + }; + var copyBtn = new Button + { + Content = copyIcon, + Padding = new Thickness(8), + VerticalAlignment = VerticalAlignment.Center, + Background = Brushes.Transparent, + BorderThickness = new Thickness(0), + }; + var pathCard = new SettingsCard + { + BorderThickness = new Thickness(1, 0, 1, 1), + CornerRadius = new CornerRadius(0, 0, 8, 8), + Header = CoreTools.Translate("Current executable file:"), + Content = copyBtn, + }; + var pathLabel = new TextBlock + { + FontFamily = new FontFamily("Courier New"), + FontSize = 14, + TextWrapping = TextWrapping.Wrap, + }; + pathLabel.Text = ViewModel.PathLabelText; + ViewModel.PropertyChanged += (_, e) => + { + if (e.PropertyName == nameof(PackageManagerViewModel.PathLabelText)) + pathLabel.Text = ViewModel.PathLabelText; + }; + pathCard.Description = pathLabel; + copyBtn.Click += (_, _) => _ = CopyPathAndFlashIcon(pathLabel.Text, copyBtn, copyIcon); + PathHolder.Content = pathCard; + + // ── Install options panel + var installOptions = new InstallOptionsPanel(manager); + installOptions.NavigateToAdministratorRequested += (_, _) => + NavigationRequested?.Invoke(this, typeof(Administrator)); + InstallOptionsHolder.Content = installOptions; + + // ── Disable notifications card + var disableNotifsCard = new CheckboxCard_Dict + { + Text = CoreTools.Translate("Ignore packages from {pm} when showing a notification about updates") + .Replace("{pm}", manager.DisplayName), + DictionaryName = CoreSettings.K.DisabledPackageManagerNotifications, + ForceInversion = true, + KeyName = manager.Name, + }; + + BuildExtraControls(disableNotifsCard); + + // ── Logs card + ManagerLogs.Text = CoreTools.Translate("View {0} logs", manager.DisplayName); + ManagerLogs.Icon = UniGetUI.Interface.Enums.IconType.Console; + ManagerLogs.Click += (_, _) => + { + if (TopLevel.GetTopLevel(this) is Window { DataContext: MainWindowViewModel vm }) + vm.OpenManagerLogs(manager); + }; + + // ── Pip AppExecution Alias warning + if (manager.Name == "Pip") + { + ManagerLogs.CornerRadius = new CornerRadius(8, 8, 0, 0); + AppExecutionAliasWarning.IsVisible = true; + AppExecutionAliasLabel.Text = + "If Python cannot be found or is not listing packages but is installed on the system, " + + "you may need to disable the \"python.exe\" App Execution Alias in the settings."; + } + } + + private void BuildExtraControls(CheckboxCard_Dict disableNotifsCard) + { + ExtraControls.Children.Clear(); + var manager = ViewModel.Manager; + bool managerHasSources = manager.Capabilities.SupportsCustomSources && manager.Name != "Vcpkg"; + + if (managerHasSources) + { + ExtraControls.Children.Add(new SourceManagerCard(manager) + { + Margin = new Thickness(0, 0, 0, 16), + }); + ExtraControls.Children.Add(new TextBlock + { + Margin = new Thickness(44, 24, 4, 8), + FontWeight = FontWeight.SemiBold, + Text = CoreTools.Translate("Advanced options"), + }); + } + + switch (manager.Name) + { + case "WinGet": + disableNotifsCard.CornerRadius = new CornerRadius(8, 8, 0, 0); + disableNotifsCard.BorderThickness = new Thickness(1, 1, 1, 0); + ExtraControls.Children.Add(disableNotifsCard); + + var wingetResetBtn = new ButtonCard + { + Text = CoreTools.Translate("Reset WinGet") + + $" ({CoreTools.Translate("This may help if no packages are listed")})", + ButtonText = CoreTools.AutoTranslated("Reset"), + CornerRadius = new CornerRadius(0), + }; + wingetResetBtn.Click += (_, _) => { /* TODO: HandleBrokenWinGet */ }; + ExtraControls.Children.Add(wingetResetBtn); + + ExtraControls.Children.Add(new CheckboxCard + { + Text = CoreTools.Translate("Force install location parameter when updating packages with custom locations"), + SettingName = CoreSettings.K.WinGetForceLocationOnUpdate, + CornerRadius = new CornerRadius(0), + BorderThickness = new Thickness(1, 0, 1, 1), + }); + + var wingetUseBundled = new CheckboxCard + { + Text = $"{CoreTools.Translate("Use bundled WinGet instead of system WinGet")} ({CoreTools.Translate("This may help if WinGet packages are not shown")})", + SettingName = CoreSettings.K.ForceLegacyBundledWinGet, + CornerRadius = new CornerRadius(0, 0, 8, 8), + BorderThickness = new Thickness(1, 0, 1, 1), + }; + wingetUseBundled.StateChanged += (_, _) => _ = ViewModel.ReloadManagerCommand.ExecuteAsync(null); + ExtraControls.Children.Add(wingetUseBundled); + break; + + case "Scoop": + disableNotifsCard.CornerRadius = new CornerRadius(8, 8, 0, 0); + disableNotifsCard.BorderThickness = new Thickness(1, 1, 1, 0); + ExtraControls.Children.Add(disableNotifsCard); + + var scoopInstall = new ButtonCard + { + Text = CoreTools.AutoTranslated("Install Scoop"), + ButtonText = CoreTools.AutoTranslated("Install"), + CornerRadius = new CornerRadius(0), + }; + scoopInstall.Click += (_, _) => ViewModel.ScoopInstallCommand.Execute(null); + ExtraControls.Children.Add(scoopInstall); + + var scoopUninstall = new ButtonCard + { + Text = CoreTools.AutoTranslated("Uninstall Scoop (and its packages)"), + ButtonText = CoreTools.AutoTranslated("Uninstall"), + CornerRadius = new CornerRadius(0), + BorderThickness = new Thickness(1, 0, 1, 0), + }; + scoopUninstall.Click += (_, _) => ViewModel.ScoopUninstallCommand.Execute(null); + ExtraControls.Children.Add(scoopUninstall); + + var scoopCleanup = new ButtonCard + { + Text = CoreTools.AutoTranslated("Run cleanup and clear cache"), + ButtonText = CoreTools.AutoTranslated("Run"), + CornerRadius = new CornerRadius(0), + }; + scoopCleanup.Click += (_, _) => ViewModel.ScoopCleanupCommand.Execute(null); + ExtraControls.Children.Add(scoopCleanup); + + ExtraControls.Children.Add(new CheckboxCard + { + CornerRadius = new CornerRadius(0, 0, 8, 8), + BorderThickness = new Thickness(1, 0, 1, 1), + SettingName = CoreSettings.K.EnableScoopCleanup, + Text = "Enable Scoop cleanup on launch", + }); + break; + + case "Chocolatey": + disableNotifsCard.CornerRadius = new CornerRadius(8, 8, 0, 0); + disableNotifsCard.BorderThickness = new Thickness(1, 1, 1, 0); + ExtraControls.Children.Add(disableNotifsCard); + + var chocoSysChoco = new CheckboxCard + { + Text = CoreTools.AutoTranslated("Use system Chocolatey"), + SettingName = CoreSettings.K.UseSystemChocolatey, + CornerRadius = new CornerRadius(0, 0, 8, 8), + }; + chocoSysChoco.StateChanged += (_, _) => _ = ViewModel.ReloadManagerCommand.ExecuteAsync(null); + ExtraControls.Children.Add(chocoSysChoco); + break; + + case "Vcpkg": + disableNotifsCard.CornerRadius = new CornerRadius(8, 8, 0, 0); + disableNotifsCard.BorderThickness = new Thickness(1, 1, 1, 0); + ExtraControls.Children.Add(disableNotifsCard); + + CoreSettings.SetValue(CoreSettings.K.DefaultVcpkgTriplet, Vcpkg.GetDefaultTriplet()); + var vcpkgTriplet = new ComboboxCard + { + Text = CoreTools.Translate("Default vcpkg triplet"), + SettingName = CoreSettings.K.DefaultVcpkgTriplet, + CornerRadius = new CornerRadius(0), + }; + foreach (string triplet in Vcpkg.GetSystemTriplets()) + vcpkgTriplet.AddItem(triplet, triplet, false); + vcpkgTriplet.ShowAddedItems(); + ExtraControls.Children.Add(vcpkgTriplet); + ExtraControls.Children.Add(BuildVcpkgRootCard()); + break; + + default: + disableNotifsCard.CornerRadius = new CornerRadius(8); + disableNotifsCard.BorderThickness = new Thickness(1); + ExtraControls.Children.Add(disableNotifsCard); + break; + } + } + + private ButtonCard BuildVcpkgRootCard() + { + var vcpkgRootCard = new ButtonCard + { + Text = "Change vcpkg root location", + ButtonText = "Select", + CornerRadius = new CornerRadius(0, 0, 8, 8), + BorderThickness = new Thickness(1, 0, 1, 1), + }; + + var rootLabel = new TextBlock + { + VerticalAlignment = VerticalAlignment.Center, + Text = ViewModel.VcpkgRootPath, + }; + var resetBtn = new Button + { + Content = CoreTools.Translate("Reset"), + IsEnabled = ViewModel.IsCustomVcpkgRootSet, + Margin = new Thickness(4, 0), + }; + var openBtn = new Button + { + Content = CoreTools.Translate("Open"), + IsEnabled = ViewModel.IsCustomVcpkgRootSet, + Margin = new Thickness(4, 0), + }; + + ViewModel.PropertyChanged += (_, e) => + { + if (e.PropertyName == nameof(PackageManagerViewModel.VcpkgRootPath)) + rootLabel.Text = ViewModel.VcpkgRootPath; + if (e.PropertyName == nameof(PackageManagerViewModel.IsCustomVcpkgRootSet)) + { + resetBtn.IsEnabled = ViewModel.IsCustomVcpkgRootSet; + openBtn.IsEnabled = ViewModel.IsCustomVcpkgRootSet; + } + }; + + resetBtn.Click += (_, _) => ViewModel.ResetVcpkgRootCommand.Execute(null); + openBtn.Click += (_, _) => ViewModel.OpenVcpkgRootCommand.Execute(null); + + var descPanel = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 4 }; + descPanel.Children.Add(rootLabel); + descPanel.Children.Add(resetBtn); + descPanel.Children.Add(openBtn); + vcpkgRootCard.Description = descPanel; + + vcpkgRootCard.Click += (_, _) => ViewModel.PickVcpkgRootCommand.Execute(vcpkgRootCard); + return vcpkgRootCard; + } + + // ── View-only: brush lookup (needs ActualThemeVariant) ─────────────────── + + private void ApplyStatusBrushes() + { + StatusBar.Classes.Remove("status-success"); + StatusBar.Classes.Remove("status-warning"); + StatusBar.Classes.Remove("status-error"); + StatusBar.Classes.Remove("status-info"); + + string cls = ViewModel.Severity switch + { + ManagerStatusSeverity.Success => "status-success", + ManagerStatusSeverity.Warning => "status-warning", + ManagerStatusSeverity.Error => "status-error", + _ => "status-info", + }; + StatusBar.Classes.Add(cls); + } + + private async Task CopyPathAndFlashIcon(string? text, Button btn, SvgIcon icon) + { + if (string.IsNullOrEmpty(text)) return; + if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard) + await clipboard.SetTextAsync(text); + btn.Content = new TextBlock { Text = "✓", FontSize = 20, VerticalAlignment = VerticalAlignment.Center }; + await Task.Delay(1000); + btn.Content = icon; + } +} diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/SettingsBasePage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/SettingsBasePage.axaml.cs index 5703ebaf0..82816cfec 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/SettingsBasePage.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/SettingsBasePage.axaml.cs @@ -69,6 +69,10 @@ private void NavigateToPage(UserControl page) sp.RestartRequired += Page_RestartRequired; VM.Title = sp.ShortTitle; } + + // Refresh toggle states when returning to the managers list + if (page is ManagersHomepage mh) + mh.RefreshToggles(); } private void NavigateBack() @@ -117,8 +121,15 @@ private void Page_RestartRequired(object? sender, EventArgs e) => private SettingsHomepage GetSettingsHomepage() => _settingsHomepage ??= new SettingsHomepage(); - private ManagersHomepage GetManagersHomepage() => - _managersHomepage ??= new ManagersHomepage(); + private ManagersHomepage GetManagersHomepage() + { + if (_managersHomepage is null) + { + _managersHomepage = new ManagersHomepage(); + _managersHomepage.ManagerNavigationRequested += (_, manager) => NavigateTo(manager); + } + return _managersHomepage; + } // ── IInnerNavigationPage ────────────────────────────────────────────── @@ -150,7 +161,10 @@ public void OnLeave() { } public void NavigateTo(IPackageManager manager) { - // TODO: navigate to PackageManagerPage for this manager (Phase 5) + if (_currentContent is not null) + _history.Push(_currentContent); + + NavigateToPage(new PackageManagerPage(manager)); } public void NavigateTo(Type page) diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/SourceManagerCard.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/SourceManagerCard.axaml new file mode 100644 index 000000000..e601156de --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/SourceManagerCard.axaml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Views/Pages/LogPages/BaseLogPage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/LogPages/BaseLogPage.axaml.cs new file mode 100644 index 000000000..83cb81505 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/LogPages/BaseLogPage.axaml.cs @@ -0,0 +1,69 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Platform.Storage; +using Avalonia.Threading; +using UniGetUI.Avalonia.ViewModels.Pages.LogPages; +using UniGetUI.Avalonia.Views.Pages; +using UniGetUI.Core.Tools; + +namespace UniGetUI.Avalonia.Views.Pages.LogPages; + +public partial class BaseLogPage : UserControl, IEnterLeaveListener, IKeyboardShortcutListener +{ + protected readonly BaseLogPageViewModel ViewModel; + + protected BaseLogPage(BaseLogPageViewModel viewModel) + { + ViewModel = viewModel; + DataContext = ViewModel; + InitializeComponent(); + + ViewModel.CopyTextRequested += OnCopyTextRequested; + ViewModel.ExportTextRequested += OnExportTextRequested; + ViewModel.ScrollToBottomRequested += OnScrollToBottomRequested; + } + + private async void OnCopyTextRequested(object? sender, string text) + { + var clipboard = TopLevel.GetTopLevel(this)?.Clipboard; + if (clipboard is not null) + await clipboard.SetTextAsync(text); + } + + private async void OnExportTextRequested(object? sender, string text) + { + var topLevel = TopLevel.GetTopLevel(this); + if (topLevel is null) return; + + var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions + { + Title = CoreTools.Translate("Export log"), + SuggestedFileName = CoreTools.Translate("UniGetUI Log"), + FileTypeChoices = + [ + new FilePickerFileType(CoreTools.Translate("Text")) { Patterns = ["*.txt"] }, + ], + }); + + if (file is not null) + await File.WriteAllTextAsync(file.Path.LocalPath, text); + } + + private void OnScrollToBottomRequested(object? sender, EventArgs e) + { + Dispatcher.UIThread.Post(() => + { + MainScroller.Offset = new Vector(MainScroller.Offset.X, double.MaxValue); + }, DispatcherPriority.Background); + } + + public void OnEnter() => ViewModel.LoadLog(); + + public void OnLeave() => ViewModel.ClearLog(); + + public void ReloadTriggered() => ViewModel.LoadLog(isReload: true); + + public void SelectAllTriggered() { } + + public void SearchTriggered() { } +} diff --git a/src/UniGetUI.Avalonia/Views/Pages/LogPages/ManagerLogsPage.cs b/src/UniGetUI.Avalonia/Views/Pages/LogPages/ManagerLogsPage.cs new file mode 100644 index 000000000..60a465fa4 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/LogPages/ManagerLogsPage.cs @@ -0,0 +1,18 @@ +using UniGetUI.Avalonia.ViewModels.Pages.LogPages; +using UniGetUI.PackageEngine.Interfaces; + +namespace UniGetUI.Avalonia.Views.Pages; + +public class ManagerLogsPage : LogPages.BaseLogPage +{ + private readonly ManagerLogsPageViewModel _viewModel; + + public ManagerLogsPage() : this(new ManagerLogsPageViewModel()) { } + + private ManagerLogsPage(ManagerLogsPageViewModel vm) : base(vm) + { + _viewModel = vm; + } + + public void LoadForManager(IPackageManager manager) => _viewModel.LoadForManager(manager); +} diff --git a/src/UniGetUI.Avalonia/Views/Pages/LogPages/OperationHistoryPage.cs b/src/UniGetUI.Avalonia/Views/Pages/LogPages/OperationHistoryPage.cs new file mode 100644 index 000000000..2abfbd630 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/LogPages/OperationHistoryPage.cs @@ -0,0 +1,8 @@ +using UniGetUI.Avalonia.ViewModels.Pages.LogPages; + +namespace UniGetUI.Avalonia.Views.Pages; + +public class OperationHistoryPage : LogPages.BaseLogPage +{ + public OperationHistoryPage() : base(new OperationHistoryPageViewModel()) { } +} diff --git a/src/UniGetUI.Avalonia/Views/Pages/LogPages/UniGetUILogPage.cs b/src/UniGetUI.Avalonia/Views/Pages/LogPages/UniGetUILogPage.cs new file mode 100644 index 000000000..6492e787a --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/LogPages/UniGetUILogPage.cs @@ -0,0 +1,8 @@ +using UniGetUI.Avalonia.ViewModels.Pages.LogPages; + +namespace UniGetUI.Avalonia.Views.Pages; + +public class UniGetUILogPage : LogPages.BaseLogPage +{ + public UniGetUILogPage() : base(new UniGetUILogPageViewModel()) { } +} diff --git a/src/UniGetUI.Avalonia/Views/SoftwarePages/PageStubs.cs b/src/UniGetUI.Avalonia/Views/SoftwarePages/PageStubs.cs index 9055331ef..9bffc3e57 100644 --- a/src/UniGetUI.Avalonia/Views/SoftwarePages/PageStubs.cs +++ b/src/UniGetUI.Avalonia/Views/SoftwarePages/PageStubs.cs @@ -1,91 +1,4 @@ -using System.Diagnostics; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Layout; -using Avalonia.Media; -using UniGetUI.Avalonia.Views.Controls; -using UniGetUI.Core.Tools; -using UniGetUI.PackageEngine.Interfaces; - namespace UniGetUI.Avalonia.Views.Pages; -// --------------------------------------------------------------------------- -// Stub classes for pages not yet ported — concrete implementations TBD -// --------------------------------------------------------------------------- - -public class UniGetUILogPage : UserControl { } - -public class ManagerLogsPage : UserControl -{ - public void LoadForManager(IPackageManager manager) { } -} - -public class OperationHistoryPage : UserControl { } - -public class HelpPage : UserControl -{ - private const string HelpBaseUrl = "https://marticliment.com/unigetui/help/"; - private string _currentUrl = HelpBaseUrl; - - public HelpPage() - { - var icon = new SvgIcon - { - Path = "avares://UniGetUI.Avalonia/Assets/Symbols/help.svg", - Width = 64, - Height = 64, - Foreground = Application.Current?.FindResource("SystemControlForegroundBaseHighBrush") as IBrush ?? Brushes.White, - HorizontalAlignment = HorizontalAlignment.Center, - }; - - var title = new TextBlock - { - Text = CoreTools.Translate("Help & Documentation"), - FontSize = 22, - FontWeight = FontWeight.SemiBold, - HorizontalAlignment = HorizontalAlignment.Center, - Margin = new Thickness(0, 16, 0, 8), - }; - - var subtitle = new TextBlock - { - Text = CoreTools.Translate("Embedded web view is not yet available on this platform."), - FontSize = 14, - Opacity = 0.7, - HorizontalAlignment = HorizontalAlignment.Center, - TextAlignment = TextAlignment.Center, - }; - - var openBtn = new Button - { - Content = CoreTools.Translate("Open documentation in browser"), - HorizontalAlignment = HorizontalAlignment.Center, - Margin = new Thickness(0, 24, 0, 0), - Padding = new Thickness(20, 10), - CornerRadius = new CornerRadius(6), - }; - openBtn.Click += (_, _) => - Process.Start(new ProcessStartInfo(_currentUrl) { UseShellExecute = true }); - - var panel = new StackPanel - { - Orientation = Orientation.Vertical, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center, - MaxWidth = 480, - }; - panel.Children.Add(icon); - panel.Children.Add(title); - panel.Children.Add(subtitle); - panel.Children.Add(openBtn); - - Content = panel; - } - - public void NavigateTo(string uriAttachment) - { - _currentUrl = string.IsNullOrEmpty(uriAttachment) - ? HelpBaseUrl - : HelpBaseUrl + uriAttachment; - } -} +// All log pages and HelpPage have been ported to proper AXAML views. +// This file is intentionally left as a placeholder. From d79d80b3d149c3cfeeffd5779887b06bc1d10fb3 Mon Sep 17 00:00:00 2001 From: Gabriel Dufresne Date: Mon, 30 Mar 2026 08:39:16 -0400 Subject: [PATCH 05/15] Added the help webview --- src/UniGetUI.Avalonia/App.axaml.cs | 18 +---- .../MarkupExtensions/TranslateExtension.cs | 1 + .../UniGetUI.Avalonia.csproj | 13 +-- .../ViewModels/Pages/HelpPageViewModel.cs | 16 ++-- src/UniGetUI.Avalonia/Views/MainWindow.axaml | 1 - .../Views/Pages/HelpPage.axaml | 74 +++++++++++++++++ .../Views/Pages/HelpPage.axaml.cs | 81 +++++++++++++++++++ .../Views/Pages/LogPages/BaseLogPage.axaml.cs | 2 +- .../SettingsPages/PackageManagerPage.axaml.cs | 1 + .../AbstractPackagesPage.axaml.cs | 4 +- 10 files changed, 174 insertions(+), 37 deletions(-) create mode 100644 src/UniGetUI.Avalonia/Views/Pages/HelpPage.axaml create mode 100644 src/UniGetUI.Avalonia/Views/Pages/HelpPage.axaml.cs diff --git a/src/UniGetUI.Avalonia/App.axaml.cs b/src/UniGetUI.Avalonia/App.axaml.cs index d3ef1afde..de22baae0 100644 --- a/src/UniGetUI.Avalonia/App.axaml.cs +++ b/src/UniGetUI.Avalonia/App.axaml.cs @@ -1,7 +1,6 @@ using System.Diagnostics; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Data.Core.Plugins; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Styling; using Avalonia.Styling; @@ -31,15 +30,12 @@ public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - // Avoid duplicate validations from both Avalonia and the CommunityToolkit. - // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins - DisableAvaloniaDataAnnotationValidation(); if (OperatingSystem.IsMacOS()) ExpandMacOSPath(); PEInterface.LoadLoaders(); ApplyTheme(CoreSettings.GetValue(CoreSettings.K.PreferredTheme)); var mainWindow = new MainWindow(); -#if DEBUG +#if AVALONIA_DIAGNOSTICS_ENABLED mainWindow.AttachDevTools(); #endif desktop.MainWindow = mainWindow; @@ -86,16 +82,4 @@ public static void ApplyTheme(string value) }; } - private static void DisableAvaloniaDataAnnotationValidation() - { - // Get an array of plugins to remove - var dataValidationPluginsToRemove = - BindingPlugins.DataValidators.OfType().ToArray(); - - // remove each entry found - foreach (var plugin in dataValidationPluginsToRemove) - { - BindingPlugins.DataValidators.Remove(plugin); - } - } } diff --git a/src/UniGetUI.Avalonia/MarkupExtensions/TranslateExtension.cs b/src/UniGetUI.Avalonia/MarkupExtensions/TranslateExtension.cs index b068c219f..d4efcc9b3 100644 --- a/src/UniGetUI.Avalonia/MarkupExtensions/TranslateExtension.cs +++ b/src/UniGetUI.Avalonia/MarkupExtensions/TranslateExtension.cs @@ -1,4 +1,5 @@ using Avalonia.Markup.Xaml; +using Avalonia.Metadata; using UniGetUI.Core.Tools; namespace UniGetUI.Avalonia.MarkupExtensions; diff --git a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj index 594cbc666..d286f7f54 100644 --- a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj +++ b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj @@ -26,17 +26,18 @@ - - - - + + + + - - + + + diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/HelpPageViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/HelpPageViewModel.cs index d7d694085..eae5f7f57 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/HelpPageViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/HelpPageViewModel.cs @@ -5,16 +5,14 @@ namespace UniGetUI.Avalonia.ViewModels.Pages; public partial class HelpPageViewModel : ViewModels.ViewModelBase { - private const string HelpBaseUrl = "https://marticliment.com/unigetui/help/"; - private string _currentUrl = HelpBaseUrl; + public const string HelpBaseUrl = "https://marticliment.com/unigetui/help/"; + + // Kept in sync from the WebView's NavigationCompleted event via code-behind + public string CurrentUrl { get; set; } = HelpBaseUrl; [RelayCommand] - private void OpenInBrowser() => CoreTools.Launch(_currentUrl); + private void OpenInBrowser() => CoreTools.Launch(CurrentUrl); - public void NavigateTo(string uriAttachment) - { - _currentUrl = string.IsNullOrEmpty(uriAttachment) - ? HelpBaseUrl - : HelpBaseUrl + uriAttachment; - } + public string GetInitialUrl(string uriAttachment) => + string.IsNullOrEmpty(uriAttachment) ? HelpBaseUrl : HelpBaseUrl + uriAttachment; } diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml b/src/UniGetUI.Avalonia/Views/MainWindow.axaml index c5c212504..9266536a0 100644 --- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml +++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml @@ -13,7 +13,6 @@ MinWidth="700" MinHeight="500" ExtendClientAreaToDecorationsHint="True" - ExtendClientAreaChromeHints="PreferSystemChrome" ExtendClientAreaTitleBarHeightHint="-1"> diff --git a/src/UniGetUI.Avalonia/Views/Pages/HelpPage.axaml b/src/UniGetUI.Avalonia/Views/Pages/HelpPage.axaml new file mode 100644 index 000000000..a2cf70ce5 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/HelpPage.axaml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Views/Pages/HelpPage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/HelpPage.axaml.cs new file mode 100644 index 000000000..268ef295d --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/HelpPage.axaml.cs @@ -0,0 +1,81 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using UniGetUI.Avalonia.ViewModels.Pages; +using UniGetUI.Avalonia.Views.Pages; + +namespace UniGetUI.Avalonia.Views.Pages; + +public partial class HelpPage : UserControl, IEnterLeaveListener +{ + private readonly HelpPageViewModel _viewModel; + private string _pendingNavigation = HelpPageViewModel.HelpBaseUrl; + + public HelpPage() + { + _viewModel = new HelpPageViewModel(); + DataContext = _viewModel; + InitializeComponent(); + + WebViewControl.NavigationStarted += OnNavigationStarted; + WebViewControl.NavigationCompleted += OnNavigationCompleted; + } + + private void OnNavigationStarted(object? sender, WebViewNavigationStartingEventArgs e) + { + NavProgressBar.IsVisible = true; + + // Add iframe query param so the help site shows the embedded view + string url = e.Request?.ToString() ?? ""; + if (url.Contains("marticliment.com") && !url.Contains("isWingetUIIframe")) + { + e.Cancel = true; + string modified = url.Contains('?') + ? url + "&isWingetUIIframe" + : url + "?isWingetUIIframe"; + WebViewControl.Navigate(new Uri(modified)); + } + } + + private void OnNavigationCompleted(object? sender, WebViewNavigationCompletedEventArgs e) + { + NavProgressBar.IsVisible = false; + _viewModel.CurrentUrl = WebViewControl.Source?.ToString() ?? HelpPageViewModel.HelpBaseUrl; + + BackButton.IsEnabled = WebViewControl.CanGoBack; + ForwardButton.IsEnabled = WebViewControl.CanGoForward; + } + + public void NavigateTo(string uriAttachment) + { + string url = _viewModel.GetInitialUrl(uriAttachment); + if (WebViewControl.IsLoaded) + WebViewControl.Navigate(new Uri(url)); + else + _pendingNavigation = url; + } + + public void OnEnter() + { + WebViewControl.Navigate(new Uri(_pendingNavigation)); + } + + public void OnLeave() { } + + private void BackButton_Click(object? sender, RoutedEventArgs e) + { + if (WebViewControl.CanGoBack) + WebViewControl.GoBack(); + } + + private void ForwardButton_Click(object? sender, RoutedEventArgs e) + { + if (WebViewControl.CanGoForward) + WebViewControl.GoForward(); + } + + private void HomeButton_Click(object? sender, RoutedEventArgs e) => + WebViewControl.Navigate(new Uri(HelpPageViewModel.HelpBaseUrl)); + + private void ReloadButton_Click(object? sender, RoutedEventArgs e) => + WebViewControl.Refresh(); +} diff --git a/src/UniGetUI.Avalonia/Views/Pages/LogPages/BaseLogPage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/LogPages/BaseLogPage.axaml.cs index 83cb81505..b9d3fa70f 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/LogPages/BaseLogPage.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/LogPages/BaseLogPage.axaml.cs @@ -1,9 +1,9 @@ using Avalonia; using Avalonia.Controls; +using Avalonia.Input.Platform; using Avalonia.Platform.Storage; using Avalonia.Threading; using UniGetUI.Avalonia.ViewModels.Pages.LogPages; -using UniGetUI.Avalonia.Views.Pages; using UniGetUI.Core.Tools; namespace UniGetUI.Avalonia.Views.Pages.LogPages; diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs index 0e6be2d57..8e6972cc0 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs @@ -1,4 +1,5 @@ using Avalonia.Controls; +using Avalonia.Input.Platform; using Avalonia.Layout; using UniGetUI.Avalonia.ViewModels; using Avalonia.Media; diff --git a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs index b3bf665f0..945579396 100644 --- a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs @@ -2,10 +2,8 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Input; -using Avalonia.Layout; -using Avalonia.Media; +using Avalonia.Input.Platform; using UniGetUI.Avalonia.ViewModels.Pages; -using UniGetUI.Avalonia.Views; using UniGetUI.Avalonia.Views.Controls; using UniGetUI.Core.Tools; using UniGetUI.PackageEngine.Interfaces; From 7896a9812c1a49d074f624626e764d40e1d33744 Mon Sep 17 00:00:00 2001 From: Gabriel Dufresne Date: Mon, 30 Mar 2026 08:56:33 -0400 Subject: [PATCH 06/15] Added releaseNotes page --- .../ViewModels/MainWindowViewModel.cs | 3 +- .../Pages/ReleaseNotesPageViewModel.cs | 16 ++++++++ .../Views/Pages/ReleaseNotesPage.axaml | 39 +++++++++++++++++++ .../Views/Pages/ReleaseNotesPage.axaml.cs | 35 +++++++++++++++++ 4 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 src/UniGetUI.Avalonia/ViewModels/Pages/ReleaseNotesPageViewModel.cs create mode 100644 src/UniGetUI.Avalonia/Views/Pages/ReleaseNotesPage.axaml create mode 100644 src/UniGetUI.Avalonia/Views/Pages/ReleaseNotesPage.axaml.cs diff --git a/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs index 404c9645f..8a8f8c699 100644 --- a/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs @@ -36,6 +36,7 @@ public partial class MainWindowViewModel : ViewModelBase private ManagerLogsPage? ManagerLogPage; private OperationHistoryPage? OperationHistoryPage; private HelpPage? HelpPage; + private ReleaseNotesPage? ReleaseNotesPage; // ─── Navigation state ──────────────────────────────────────────────────── private PageType _oldPage = PageType.Null; @@ -219,6 +220,7 @@ private Control GetPageForType(PageType type) => PageType.ManagerLog => ManagerLogPage ??= new ManagerLogsPage(), PageType.OperationHistory => OperationHistoryPage ??= new OperationHistoryPage(), PageType.Help => HelpPage ??= new HelpPage(), + PageType.ReleaseNotes => ReleaseNotesPage ??= new ReleaseNotesPage(), PageType.Null => throw new InvalidOperationException("Page type is Null"), _ => throw new InvalidDataException($"Unknown page type {type}"), }; @@ -251,7 +253,6 @@ public void NavigateTo(PageType newPage_t, bool toHistory = true) { if (newPage_t is PageType.About) { _ = ShowAboutDialog(); return; } if (newPage_t is PageType.Quit) { (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.Shutdown(); return; } - if (newPage_t is PageType.ReleaseNotes) { /* TODO: DialogHelper.ShowReleaseNotes(); */ return; } Sidebar.SelectNavButtonForPage(newPage_t); diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/ReleaseNotesPageViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/ReleaseNotesPageViewModel.cs new file mode 100644 index 000000000..000e2a3f4 --- /dev/null +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/ReleaseNotesPageViewModel.cs @@ -0,0 +1,16 @@ +using CommunityToolkit.Mvvm.Input; +using UniGetUI.Core.Data; +using UniGetUI.Core.Tools; + +namespace UniGetUI.Avalonia.ViewModels.Pages; + +public partial class ReleaseNotesPageViewModel : ViewModels.ViewModelBase +{ + public string ReleaseNotesUrl { get; } = CoreData.GetGitHubReleasePageUrl(); + + // Kept in sync from the WebView's NavigationCompleted event via code-behind + public string CurrentUrl { get; set; } = CoreData.GetGitHubReleasePageUrl(); + + [RelayCommand] + private void OpenInBrowser() => CoreTools.Launch(CurrentUrl); +} diff --git a/src/UniGetUI.Avalonia/Views/Pages/ReleaseNotesPage.axaml b/src/UniGetUI.Avalonia/Views/Pages/ReleaseNotesPage.axaml new file mode 100644 index 000000000..e20ef4c90 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/ReleaseNotesPage.axaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Views/Pages/ReleaseNotesPage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/ReleaseNotesPage.axaml.cs new file mode 100644 index 000000000..842cc529b --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/ReleaseNotesPage.axaml.cs @@ -0,0 +1,35 @@ +using Avalonia.Controls; +using UniGetUI.Avalonia.ViewModels.Pages; + +namespace UniGetUI.Avalonia.Views.Pages; + +public partial class ReleaseNotesPage : UserControl, IEnterLeaveListener +{ + private readonly ReleaseNotesPageViewModel _viewModel; + private bool _loaded; + + public ReleaseNotesPage() + { + _viewModel = new ReleaseNotesPageViewModel(); + DataContext = _viewModel; + InitializeComponent(); + + WebViewControl.NavigationStarted += (_, _) => NavProgressBar.IsVisible = true; + WebViewControl.NavigationCompleted += (_, e) => + { + NavProgressBar.IsVisible = false; + _viewModel.CurrentUrl = WebViewControl.Source?.ToString() ?? _viewModel.ReleaseNotesUrl; + }; + } + + public void OnEnter() + { + if (!_loaded) + { + WebViewControl.Navigate(new Uri(_viewModel.ReleaseNotesUrl)); + _loaded = true; + } + } + + public void OnLeave() { } +} From b2f273f3e0233eaba69ec6beab29cb3db75e9f69 Mon Sep 17 00:00:00 2001 From: Gabriel Dufresne Date: Mon, 30 Mar 2026 09:11:39 -0400 Subject: [PATCH 07/15] added the uniget logo for the macOS dock --- src/UniGetUI.Avalonia/App.axaml.cs | 8 ++++ .../Infrastructure/MacOsNotificationBridge.cs | 37 +++++++++++++++++++ .../UniGetUI.Avalonia.csproj | 1 + 3 files changed, 46 insertions(+) diff --git a/src/UniGetUI.Avalonia/App.axaml.cs b/src/UniGetUI.Avalonia/App.axaml.cs index de22baae0..2f615a27c 100644 --- a/src/UniGetUI.Avalonia/App.axaml.cs +++ b/src/UniGetUI.Avalonia/App.axaml.cs @@ -3,7 +3,9 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Styling; +using Avalonia.Platform; using Avalonia.Styling; +using UniGetUI.Avalonia.Infrastructure; using UniGetUI.Avalonia.Views; using UniGetUI.PackageEngine; using CoreSettings = global::UniGetUI.Core.SettingsEngine.Settings; @@ -31,7 +33,13 @@ public override void OnFrameworkInitializationCompleted() if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { if (OperatingSystem.IsMacOS()) + { ExpandMacOSPath(); + using var stream = AssetLoader.Open(new Uri("avares://UniGetUI.Avalonia/Assets/icon.png")); + using var ms = new MemoryStream(); + stream.CopyTo(ms); + MacOsNotificationBridge.SetDockIcon(ms.ToArray()); + } PEInterface.LoadLoaders(); ApplyTheme(CoreSettings.GetValue(CoreSettings.K.PreferredTheme)); var mainWindow = new MainWindow(); diff --git a/src/UniGetUI.Avalonia/Infrastructure/MacOsNotificationBridge.cs b/src/UniGetUI.Avalonia/Infrastructure/MacOsNotificationBridge.cs index 61985236b..6ffd90b7b 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/MacOsNotificationBridge.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/MacOsNotificationBridge.cs @@ -207,6 +207,40 @@ private static IntPtr ToNSString(string s) private static IntPtr Sel(string name) => sel_registerName(name); + // ── Dock icon ────────────────────────────────────────────────────────── + + public static void SetDockIcon(byte[] pngBytes) + { + if (!OperatingSystem.IsMacOS()) return; + try + { + var handle = GCHandle.Alloc(pngBytes, GCHandleType.Pinned); + try + { + IntPtr nsData = MsgSendBytes( + objc_getClass("NSData"), Sel("dataWithBytes:length:"), + handle.AddrOfPinnedObject(), pngBytes.Length); + + IntPtr nsImage = MsgSend( + MsgSend(objc_getClass("NSImage"), Sel("alloc")), + Sel("initWithData:"), nsData); + + IntPtr nsApp = MsgSend(objc_getClass("NSApplication"), Sel("sharedApplication")); + MsgSend(nsApp, Sel("setApplicationIconImage:"), nsImage); + MsgSend(nsImage, Sel("autorelease")); + } + finally + { + handle.Free(); + } + } + catch (Exception ex) + { + Logger.Warn("Failed to set macOS dock icon"); + Logger.Warn(ex); + } + } + // ── ObjC runtime P/Invoke ────────────────────────────────────────────── [DllImport("/usr/lib/libobjc.A.dylib")] @@ -220,4 +254,7 @@ private static IntPtr ToNSString(string s) [DllImport("/usr/lib/libobjc.A.dylib", EntryPoint = "objc_msgSend")] private static extern IntPtr MsgSend(IntPtr receiver, IntPtr sel, IntPtr arg); + + [DllImport("/usr/lib/libobjc.A.dylib", EntryPoint = "objc_msgSend")] + private static extern IntPtr MsgSendBytes(IntPtr receiver, IntPtr sel, IntPtr bytes, nint length); } diff --git a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj index d286f7f54..d2ecf34ad 100644 --- a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj +++ b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj @@ -58,6 +58,7 @@ + From 4ae64006fd9dae2186e269b251db3e76212a485f Mon Sep 17 00:00:00 2001 From: Gabriel Dufresne Date: Mon, 30 Mar 2026 11:18:01 -0400 Subject: [PATCH 08/15] added the title bar login button --- src/UniGetUI.Avalonia/App.axaml.cs | 6 +- .../UniGetUI.Avalonia.csproj | 5 +- .../Controls/UserAvatarViewModel.cs | 115 ++++++++++++++++++ .../Views/Controls/UserAvatarControl.axaml | 108 ++++++++++++++++ .../Views/Controls/UserAvatarControl.axaml.cs | 13 ++ src/UniGetUI.Avalonia/Views/MainWindow.axaml | 31 +++-- 6 files changed, 264 insertions(+), 14 deletions(-) create mode 100644 src/UniGetUI.Avalonia/ViewModels/Controls/UserAvatarViewModel.cs create mode 100644 src/UniGetUI.Avalonia/Views/Controls/UserAvatarControl.axaml create mode 100644 src/UniGetUI.Avalonia/Views/Controls/UserAvatarControl.axaml.cs diff --git a/src/UniGetUI.Avalonia/App.axaml.cs b/src/UniGetUI.Avalonia/App.axaml.cs index 2f615a27c..8ea526ca4 100644 --- a/src/UniGetUI.Avalonia/App.axaml.cs +++ b/src/UniGetUI.Avalonia/App.axaml.cs @@ -17,6 +17,9 @@ public partial class App : Application public override void Initialize() { AvaloniaXamlLoader.Load(this); +#if AVALONIA_DIAGNOSTICS_ENABLED + this.AttachDeveloperTools(); +#endif string platform = OperatingSystem.IsWindows() ? "Windows" : OperatingSystem.IsMacOS() ? "macOS" @@ -43,9 +46,6 @@ public override void OnFrameworkInitializationCompleted() PEInterface.LoadLoaders(); ApplyTheme(CoreSettings.GetValue(CoreSettings.K.PreferredTheme)); var mainWindow = new MainWindow(); -#if AVALONIA_DIAGNOSTICS_ENABLED - mainWindow.AttachDevTools(); -#endif desktop.MainWindow = mainWindow; _ = Task.Run(PEInterface.LoadManagers); } diff --git a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj index d2ecf34ad..a9bf5a5e3 100644 --- a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj +++ b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj @@ -34,7 +34,6 @@ - @@ -104,6 +103,10 @@ App.axaml + + UserAvatarControl.axaml + Code + SidebarView.axaml diff --git a/src/UniGetUI.Avalonia/ViewModels/Controls/UserAvatarViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Controls/UserAvatarViewModel.cs new file mode 100644 index 000000000..a363aa7c7 --- /dev/null +++ b/src/UniGetUI.Avalonia/ViewModels/Controls/UserAvatarViewModel.cs @@ -0,0 +1,115 @@ +using Avalonia.Media.Imaging; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.Input; +using MvvmRelayCommand = CommunityToolkit.Mvvm.Input.RelayCommand; +using Octokit; +using UniGetUI.Avalonia.Infrastructure; +using UniGetUI.Core.Logging; +using UniGetUI.Core.Tools; + +namespace UniGetUI.Avalonia.ViewModels.Controls; + +public class UserAvatarViewModel : ViewModelBase +{ + private bool _isAuthenticated; + public bool IsAuthenticated + { + get => _isAuthenticated; + private set => SetProperty(ref _isAuthenticated, value); + } + + private string _userDisplayName = ""; + public string UserDisplayName + { + get => _userDisplayName; + private set => SetProperty(ref _userDisplayName, value); + } + + private Bitmap? _avatarBitmap; + public Bitmap? AvatarBitmap + { + get => _avatarBitmap; + private set => SetProperty(ref _avatarBitmap, value); + } + + public IAsyncRelayCommand LoginCommand { get; } + public IRelayCommand LogoutCommand { get; } + public IRelayCommand MoreDetailsCommand { get; } + + public UserAvatarViewModel() + { + LoginCommand = new AsyncRelayCommand(LoginAsync); + LogoutCommand = new MvvmRelayCommand(Logout); + MoreDetailsCommand = new MvvmRelayCommand(() => CoreTools.Launch("https://devolutions.net/unigetui")); + GitHubAuthService.AuthStatusChanged += (_, _) => _ = RefreshAsync(); + _ = RefreshAsync(); + } + + private async Task RefreshAsync() + { + var service = new GitHubAuthService(); + bool authenticated = service.IsAuthenticated(); + + string displayName = ""; + Bitmap? bitmap = null; + + if (authenticated) + { + try + { + var client = service.CreateGitHubClient(); + if (client is not null) + { + User user = await client.User.Current(); + displayName = string.IsNullOrEmpty(user.Name) + ? $"@{user.Login}" + : $"{user.Name} (@{user.Login})"; + + if (!string.IsNullOrEmpty(user.AvatarUrl)) + { + using var http = new HttpClient(); + byte[] bytes = await http.GetByteArrayAsync(user.AvatarUrl); + using var ms = new MemoryStream(bytes); + bitmap = new Bitmap(ms); + } + } + } + catch (Exception ex) + { + Logger.Warn("UserAvatarViewModel: failed to fetch GitHub user info"); + Logger.Warn(ex); + authenticated = false; + } + } + + await Dispatcher.UIThread.InvokeAsync(() => + { + IsAuthenticated = authenticated; + UserDisplayName = displayName; + AvatarBitmap = bitmap; + }); + } + + private async Task LoginAsync() + { + try + { + await new GitHubAuthService().SignInAsync(); + } + catch (Exception ex) + { + Logger.Error("UserAvatarViewModel: login failed"); + Logger.Error(ex); + } + } + + private void Logout() + { + try { new GitHubAuthService().SignOut(); } + catch (Exception ex) + { + Logger.Error("UserAvatarViewModel: logout failed"); + Logger.Error(ex); + } + } +} diff --git a/src/UniGetUI.Avalonia/Views/Controls/UserAvatarControl.axaml b/src/UniGetUI.Avalonia/Views/Controls/UserAvatarControl.axaml new file mode 100644 index 000000000..93e3f2cdb --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Controls/UserAvatarControl.axaml @@ -0,0 +1,108 @@ + + + + + diff --git a/src/UniGetUI.Avalonia/Views/Controls/UserAvatarControl.axaml.cs b/src/UniGetUI.Avalonia/Views/Controls/UserAvatarControl.axaml.cs new file mode 100644 index 000000000..3ca931c33 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Controls/UserAvatarControl.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia.Controls; +using UniGetUI.Avalonia.ViewModels.Controls; + +namespace UniGetUI.Avalonia.Views.Controls; + +public partial class UserAvatarControl : UserControl +{ + public UserAvatarControl() + { + DataContext = new UserAvatarViewModel(); + InitializeComponent(); + } +} diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml b/src/UniGetUI.Avalonia/Views/MainWindow.axaml index 9266536a0..5e36482f0 100644 --- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml +++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml @@ -152,7 +152,8 @@ - + + - + - - + + + + From a7850b5c91854618a444f6471cce8fd550533115 Mon Sep 17 00:00:00 2001 From: Gabriel Dufresne Date: Mon, 30 Mar 2026 14:26:49 -0400 Subject: [PATCH 09/15] Some small UI Fixes --- .../DialogPages/OperationViewModel.cs | 219 +++++++++++++++++- .../Pages/SettingsPages/InternetViewModel.cs | 82 +++++-- .../DialogPages/PackageDetailsWindow.axaml | 25 +- src/UniGetUI.Avalonia/Views/MainWindow.axaml | 96 +++++--- .../Views/Pages/SettingsPages/Internet.axaml | 2 - .../SoftwarePages/AbstractPackagesPage.axaml | 3 +- 6 files changed, 353 insertions(+), 74 deletions(-) diff --git a/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationViewModel.cs index 1011cd473..443ea1f7b 100644 --- a/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationViewModel.cs @@ -1,29 +1,48 @@ using System.Collections.ObjectModel; +using System.IO; using System.Windows.Input; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using UniGetUI.Avalonia.Infrastructure; +using UniGetUI.Avalonia.Views; +using UniGetUI.Avalonia.Views.Controls; using UniGetUI.Avalonia.Views.DialogPages; using UniGetUI.Core.Tools; +using UniGetUI.PackageEngine.Classes.Packages.Classes; using UniGetUI.PackageEngine.Enums; +using UniGetUI.PackageEngine.Operations; +using UniGetUI.PackageEngine.PackageClasses; using UniGetUI.PackageOperations; namespace UniGetUI.Avalonia.ViewModels; +/// Badge displayed next to the progress bar (Admin, Interactive, …). +public sealed record OperationBadgeVm( + string Label, + string IconPath, + string Primary, + string Secondary +); + public sealed partial class OperationViewModel : ViewModelBase { public AbstractOperation Operation { get; } - /// Short badge labels shown next to the progress bar (Admin, Interactive, …). - public ObservableCollection Badges { get; } = []; + public ObservableCollection Badges { get; } = []; public ICommand ButtonCommand { get; } public ICommand ShowDetailsCommand { get; } + /// Flyout for the "…" button; rebuilt each time the operation status changes. + public MenuFlyout OpMenu { get; } = new(); + private OperationStatus? _menuState; + // ── Bindable properties ─────────────────────────────────────────────────── [ObservableProperty] private string _title; [ObservableProperty] private string _liveLine; @@ -32,6 +51,10 @@ public sealed partial class OperationViewModel : ViewModelBase [ObservableProperty] private double _progressValue; [ObservableProperty] private IBrush _progressBrush; [ObservableProperty] private IBrush _backgroundBrush; + [ObservableProperty] private IImage? _packageIcon; + + private static readonly Uri _fallbackIconUri = + new("avares://UniGetUI.Avalonia/Assets/package_color.png"); public OperationViewModel(AbstractOperation operation) { @@ -47,6 +70,8 @@ public OperationViewModel(AbstractOperation operation) _progressBrush = new SolidColorBrush(Color.Parse("#888888")); _backgroundBrush = Brushes.Transparent; + _ = LoadIconAsync(); + // Route all background-thread events to the UI thread operation.LogLineAdded += (_, ev) => Dispatcher.UIThread.Post(() => LiveLine = ev.Item1); @@ -58,15 +83,69 @@ public OperationViewModel(AbstractOperation operation) Dispatcher.UIThread.Post(() => { Badges.Clear(); - if (badges.AsAdministrator) Badges.Add(CoreTools.Translate("Administrator")); - if (badges.Interactive) Badges.Add(CoreTools.Translate("Interactive")); - if (badges.SkipHashCheck) Badges.Add(CoreTools.Translate("Skip hash check")); + if (badges.AsAdministrator) + Badges.Add(new( + CoreTools.Translate("Administrator privileges"), + "avares://UniGetUI.Avalonia/Assets/Symbols/uac.svg", + CoreTools.Translate("This operation is running with administrator privileges."), + "" + )); + if (badges.Interactive) + Badges.Add(new( + CoreTools.Translate("Interactive operation"), + "avares://UniGetUI.Avalonia/Assets/Symbols/interactive.svg", + CoreTools.Translate("This operation is running interactively."), + CoreTools.Translate("You will likely need to interact with the installer.") + )); + if (badges.SkipHashCheck) + Badges.Add(new( + CoreTools.Translate("Integrity checks skipped"), + "avares://UniGetUI.Avalonia/Assets/Symbols/checksum.svg", + CoreTools.Translate("Integrity checks will not be performed during this operation."), + CoreTools.Translate("Proceed at your own risk.") + )); }); // Sync with current status in case the operation already started ApplyStatus(operation.Status); } + // ── Icon loading ────────────────────────────────────────────────────────── + private async Task LoadIconAsync() + { + try + { + var uri = await Operation.GetOperationIcon(); + Bitmap? bmp = null; + if (uri.Scheme is "http" or "https") + { + using var http = new HttpClient(CoreTools.GenericHttpClientParameters); + var bytes = await http.GetByteArrayAsync(uri); + using var ms = new MemoryStream(bytes); + bmp = new Bitmap(ms); + } + else if (uri.Scheme is "avares") + { + using var stream = AssetLoader.Open(uri); + bmp = new Bitmap(stream); + } + + if (bmp is not null) + { + PackageIcon = bmp; + return; + } + } + catch { /* icon is optional; fall through to fallback */ } + + try + { + using var stream = AssetLoader.Open(_fallbackIconUri); + PackageIcon = new Bitmap(stream); + } + catch { } + } + // ── Status → visual properties ──────────────────────────────────────────── private void ApplyStatus(OperationStatus status) { @@ -111,8 +190,129 @@ private void ApplyStatus(OperationStatus status) ButtonText = CoreTools.Translate("Close"); break; } + + RebuildMenu(status); + } + + // ── "…" menu ───────────────────────────────────────────────────────────── + private void RebuildMenu(OperationStatus status) + { + if (_menuState == status) return; + _menuState = status; + + OpMenu.Items.Clear(); + + // ── Operation-specific items (package details, install options, etc.) ── + if (Operation is PackageOperation packageOp) + { + bool notVirtual = !packageOp.Package.Source.IsVirtualManager; + + OpMenu.Items.Add(Item("Package details", "info_round.svg", + notVirtual, () => ShowPackageDetails(packageOp))); + + OpMenu.Items.Add(Item("Installation options", "options.svg", + notVirtual, () => _ = ShowInstallOptionsAsync(packageOp))); + + string? location = packageOp.Package.Manager.DetailsHelper.GetInstallLocation(packageOp.Package); + OpMenu.Items.Add(Item("Open install location", "open_folder.svg", + location is not null && Directory.Exists(location), + () => CoreTools.Launch(location))); + + OpMenu.Items.Add(new Separator()); + } + else if (Operation is DownloadOperation downloadOp) + { + bool succeeded = status is OperationStatus.Succeeded; + + OpMenu.Items.Add(Item("Open", "launch.svg", + succeeded, () => CoreTools.Launch(downloadOp.DownloadLocation))); + + OpMenu.Items.Add(Item("Show in explorer", "open_folder.svg", + succeeded, () => _ = CoreTools.ShowFileOnExplorer(downloadOp.DownloadLocation))); + + OpMenu.Items.Add(new Separator()); + } + + // ── Queue management ────────────────────────────────────────────────── + if (status is OperationStatus.InQueue) + { + OpMenu.Items.Add(Item("Run now", "forward.svg", true, Operation.SkipQueue)); + OpMenu.Items.Add(Item("Run next", "forward.svg", true, Operation.RunNext)); + OpMenu.Items.Add(Item("Run last", "backward.svg", true, Operation.BackOfTheQueue)); + OpMenu.Items.Add(new Separator()); + } + + // ── Cancel / Retry ──────────────────────────────────────────────────── + if (status is OperationStatus.InQueue or OperationStatus.Running) + { + OpMenu.Items.Add(Item("Cancel", "cross.svg", true, Operation.Cancel)); + } + else + { + OpMenu.Items.Add(Item("Retry", "reload.svg", true, + () => Operation.Retry(AbstractOperation.RetryMode.Retry))); + + if (Operation is PackageOperation pkgOp) + { + var caps = pkgOp.Package.Manager.Capabilities; + + if (!pkgOp.Options.RunAsAdministrator && caps.CanRunAsAdmin) + OpMenu.Items.Add(Item("Retry as administrator", "uac.svg", true, + () => Operation.Retry(AbstractOperation.RetryMode.Retry_AsAdmin))); + + if (!pkgOp.Options.InteractiveInstallation && caps.CanRunInteractively) + OpMenu.Items.Add(Item("Retry interactively", "interactive.svg", true, + () => Operation.Retry(AbstractOperation.RetryMode.Retry_Interactive))); + + if (!pkgOp.Options.SkipHashCheck && caps.CanSkipIntegrityChecks) + OpMenu.Items.Add(Item("Retry skipping integrity checks", "checksum.svg", true, + () => Operation.Retry(AbstractOperation.RetryMode.Retry_SkipIntegrity))); + } + else if (Operation is SourceOperation srcOp && !srcOp.ForceAsAdministrator) + { + OpMenu.Items.Add(Item("Retry as administrator", "uac.svg", true, + () => Operation.Retry(AbstractOperation.RetryMode.Retry_AsAdmin))); + } + } } + /// Creates a MenuItem with an SVG icon, translated header, and a SyncCommand. + private static MenuItem Item(string translationKey, string svgName, bool enabled, Action action) => + new() + { + Header = CoreTools.Translate(translationKey), + IsEnabled = enabled, + Command = new SyncCommand(action), + Icon = new SvgIcon + { + Path = $"avares://UniGetUI.Avalonia/Assets/Symbols/{svgName}", + Width = 16, + Height = 16, + Foreground = Brushes.White, + }, + }; + + // ── Package details / install options ──────────────────────────────────── + private static void ShowPackageDetails(PackageOperation packageOp) + { + if (GetMainWindow() is not { } mainWindow) return; + var win = new PackageDetailsWindow(packageOp.Package, OperationType.None); + _ = win.ShowDialog(mainWindow); + } + + private static async Task ShowInstallOptionsAsync(PackageOperation packageOp) + { + if (GetMainWindow() is not { } mainWindow) return; + var opts = await InstallOptionsFactory.LoadApplicableAsync(packageOp.Package); + var win = new InstallOptionsWindow(packageOp.Package, OperationType.None, opts); + await win.ShowDialog(mainWindow); + await InstallOptionsFactory.SaveForPackageAsync(opts, packageOp.Package); + } + + private static Window? GetMainWindow() => + Application.Current?.ApplicationLifetime + is IClassicDesktopStyleApplicationLifetime { MainWindow: Window mw } ? mw : null; + // ── Button / details actions ────────────────────────────────────────────── private void ButtonClick() { @@ -124,12 +324,9 @@ private void ButtonClick() private void ShowDetails() { - if (Application.Current?.ApplicationLifetime - is IClassicDesktopStyleApplicationLifetime { MainWindow: Window mainWindow }) - { - var win = new OperationOutputWindow(Operation); - _ = win.ShowDialog(mainWindow); - } + if (GetMainWindow() is not { } mainWindow) return; + var win = new OperationOutputWindow(Operation); + _ = win.ShowDialog(mainWindow); } // ── Minimal ICommand implementation ─────────────────────────────────────── diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/InternetViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/InternetViewModel.cs index 606c00b81..72f0df4e6 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/InternetViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/InternetViewModel.cs @@ -84,44 +84,75 @@ public Control BuildProxyCompatTable() var yesStr = CoreTools.Translate("Yes"); var partStr = CoreTools.Translate("Partially"); - var headerRow = new Grid { ColumnDefinitions = new ColumnDefinitions("*,Auto,Auto,Auto,*"), Margin = new Thickness(0, 0, 0, 8) }; - headerRow.Children.Add(WithCol(new TextBlock { Text = CoreTools.Translate("Package manager"), FontWeight = FontWeight.Bold, TextWrapping = TextWrapping.Wrap }, 1)); - headerRow.Children.Add(WithCol(new TextBlock { Text = CoreTools.Translate("Compatible with proxy"), FontWeight = FontWeight.Bold, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(16, 0, 0, 0) }, 2)); - headerRow.Children.Add(WithCol(new TextBlock { Text = CoreTools.Translate("Compatible with authentication"), FontWeight = FontWeight.Bold, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(16, 0, 0, 0) }, 3)); + var managers = PEInterface.Managers.ToList(); - var managerCol = new StackPanel { Orientation = Orientation.Vertical, Spacing = 6 }; - var proxyCol = new StackPanel { Orientation = Orientation.Vertical, Spacing = 6 }; - var authCol = new StackPanel { Orientation = Orientation.Vertical, Spacing = 6 }; - - foreach (var manager in PEInterface.Managers) + // Single unified grid: row 0 = column headers, rows 1..N = data rows. + // All columns are shared, so widths are consistent throughout. + var table = new Grid + { + ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"), + ColumnSpacing = 24, + RowSpacing = 8, + }; + for (int i = 0; i <= managers.Count; i++) + table.RowDefinitions.Add(new RowDefinition(GridLength.Auto)); + + // Header row (row 0) + var h1 = new TextBlock { Text = CoreTools.Translate("Package manager"), FontWeight = FontWeight.Bold, TextWrapping = TextWrapping.Wrap }; + var h2 = new TextBlock { Text = CoreTools.Translate("Compatible with proxy"), FontWeight = FontWeight.Bold, TextWrapping = TextWrapping.Wrap, HorizontalAlignment = HorizontalAlignment.Center }; + var h3 = new TextBlock { Text = CoreTools.Translate("Compatible with authentication"), FontWeight = FontWeight.Bold, TextWrapping = TextWrapping.Wrap, HorizontalAlignment = HorizontalAlignment.Center }; + SetCell(h1, 0, 0); SetCell(h2, 0, 1); SetCell(h3, 0, 2); + table.Children.Add(h1); table.Children.Add(h2); table.Children.Add(h3); + + // Data rows + for (int i = 0; i < managers.Count; i++) { - managerCol.Children.Add(new TextBlock { Text = manager.DisplayName, TextAlignment = TextAlignment.Center }); + var manager = managers[i]; + int row = i + 1; + + var name = new TextBlock { Text = manager.DisplayName, VerticalAlignment = VerticalAlignment.Center, TextAlignment = TextAlignment.Center }; + SetCell(name, row, 0); var proxyLevel = manager.Capabilities.SupportsProxy; - proxyCol.Children.Add(StatusBadge( + var proxyBadge = StatusBadge( proxyLevel is ProxySupport.No ? noStr : (proxyLevel is ProxySupport.Partially ? partStr : yesStr), - proxyLevel is ProxySupport.Yes ? Colors.Green : (proxyLevel is ProxySupport.Partially ? Colors.Orange : Colors.Red))); + proxyLevel is ProxySupport.Yes ? Colors.Green : (proxyLevel is ProxySupport.Partially ? Colors.Orange : Colors.Red)); + SetCell(proxyBadge, row, 1); - authCol.Children.Add(StatusBadge( + var authBadge = StatusBadge( manager.Capabilities.SupportsProxyAuth ? yesStr : noStr, - manager.Capabilities.SupportsProxyAuth ? Colors.Green : Colors.Red)); + manager.Capabilities.SupportsProxyAuth ? Colors.Green : Colors.Red); + SetCell(authBadge, row, 2); + + table.Children.Add(name); + table.Children.Add(proxyBadge); + table.Children.Add(authBadge); } - var dataRow = new Grid { ColumnDefinitions = new ColumnDefinitions("*,Auto,Auto,Auto,*"), ColumnSpacing = 16 }; - dataRow.Children.Add(WithCol(managerCol, 1)); - dataRow.Children.Add(WithCol(proxyCol, 2)); - dataRow.Children.Add(WithCol(authCol, 3)); + var title = new TextBlock + { + Text = CoreTools.Translate("Proxy compatibility table"), + FontWeight = FontWeight.SemiBold, + Margin = new Thickness(0, 0, 0, 12), + }; - var tableStack = new StackPanel { Orientation = Orientation.Vertical }; - tableStack.Children.Add(headerRow); - tableStack.Children.Add(dataRow); + var centerWrapper = new Grid { ColumnDefinitions = new ColumnDefinitions("*,Auto,*") }; + Grid.SetColumn(table, 1); + centerWrapper.Children.Add(table); - return new SettingsCard + var stack = new StackPanel { Orientation = Orientation.Vertical }; + stack.Children.Add(title); + stack.Children.Add(centerWrapper); + + var border = new Border { CornerRadius = new CornerRadius(8), - Header = CoreTools.Translate("Proxy compatibility table"), - Description = tableStack, + BorderThickness = new Thickness(1), + Padding = new Thickness(16, 12), + Child = stack, }; + border.Classes.Add("settings-card"); + return border; } private static Border StatusBadge(string text, Color color) => new Border @@ -129,12 +160,13 @@ public Control BuildProxyCompatTable() CornerRadius = new CornerRadius(4), Padding = new Thickness(4, 2), BorderThickness = new Thickness(1), + HorizontalAlignment = HorizontalAlignment.Stretch, Background = new SolidColorBrush(Color.FromArgb(60, color.R, color.G, color.B)), BorderBrush = new SolidColorBrush(Color.FromArgb(120, color.R, color.G, color.B)), Child = new TextBlock { Text = text, TextAlignment = TextAlignment.Center }, }; - private static Control WithCol(Control c, int col) { Grid.SetColumn(c, col); return c; } + private static void SetCell(Control c, int row, int col) { Grid.SetRow(c, row); Grid.SetColumn(c, col); } private async Task SaveCredentialsAsync() { diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml index be497430f..a801e7398 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml +++ b/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml @@ -12,6 +12,12 @@ WindowStartupLocation="CenterOwner" Title="{Binding PackageName}"> + + + + @@ -71,14 +77,14 @@ - + - + @@ -149,7 +155,7 @@ - + - - + + + @@ -208,8 +252,6 @@ VerticalAlignment="Center" Height="50" Margin="0,10,350,0"/> - - diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Internet.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Internet.axaml index e4896a2c2..13baa7eaa 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Internet.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Internet.axaml @@ -8,7 +8,6 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:DataType="vm:InternetViewModel"> - @@ -54,7 +53,6 @@ Text="{t:Translate Wait for the device to be connected to the internet before attempting to do tasks that require internet connectivity.}" StateChangedCommand="{Binding ShowRestartRequiredCommand}" CornerRadius="8"/> - diff --git a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml index 173145534..22ae3eefd 100644 --- a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml +++ b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml @@ -5,7 +5,6 @@ xmlns:controls="using:UniGetUI.Avalonia.Views.Controls" xmlns:pkg="using:UniGetUI.PackageEngine.PackageClasses" xmlns:t="using:UniGetUI.Avalonia.MarkupExtensions" - xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" @@ -633,7 +632,7 @@ Date: Mon, 30 Mar 2026 14:45:58 -0400 Subject: [PATCH 10/15] hide some settings for macOS --- .../DialogPages/InstallOptionsViewModel.cs | 2 +- .../DialogPages/OperationViewModel.cs | 4 +- .../SettingsPages/AdministratorViewModel.cs | 2 + .../SettingsPages/ExperimentalViewModel.cs | 2 + .../InstallOptionsPanelViewModel.cs | 2 +- .../SettingsPages/Interface_PViewModel.cs | 2 + .../SettingsPages/NotificationsViewModel.cs | 3 ++ .../DialogPages/InstallOptionsWindow.axaml | 1 + .../Pages/SettingsPages/Administrator.axaml | 51 ++++++++++--------- .../Pages/SettingsPages/Experimental.axaml | 5 +- .../SettingsPages/InstallOptionsPanel.axaml | 1 + .../Pages/SettingsPages/Interface_P.axaml | 2 +- .../Pages/SettingsPages/Notifications.axaml | 6 +-- .../SoftwarePages/DiscoverSoftwarePage.cs | 3 +- .../SoftwarePages/InstalledPackagesPage.cs | 3 +- .../Views/SoftwarePages/PackageBundlesPage.cs | 4 +- .../SoftwarePages/SoftwareUpdatesPage.cs | 3 +- 17 files changed, 57 insertions(+), 39 deletions(-) diff --git a/src/UniGetUI.Avalonia/ViewModels/DialogPages/InstallOptionsViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/DialogPages/InstallOptionsViewModel.cs index 1315e81fc..362d45cbe 100644 --- a/src/UniGetUI.Avalonia/ViewModels/DialogPages/InstallOptionsViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/DialogPages/InstallOptionsViewModel.cs @@ -167,7 +167,7 @@ public InstallOptionsViewModel(IPackage package, OperationType operation, Instal DialogTitle = CoreTools.Translate("{0} installation options", package.Name); // Capability flags - CanRunAsAdmin = caps.CanRunAsAdmin; + CanRunAsAdmin = OperatingSystem.IsWindows() && caps.CanRunAsAdmin; CanRunInteractively = caps.CanRunInteractively; CanSkipHash = caps.CanSkipIntegrityChecks; CanUninstallPrev = caps.CanUninstallPreviousVersionsAfterUpdate; diff --git a/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationViewModel.cs index 443ea1f7b..f94aa6bc4 100644 --- a/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationViewModel.cs @@ -256,7 +256,7 @@ private void RebuildMenu(OperationStatus status) { var caps = pkgOp.Package.Manager.Capabilities; - if (!pkgOp.Options.RunAsAdministrator && caps.CanRunAsAdmin) + if (OperatingSystem.IsWindows() && !pkgOp.Options.RunAsAdministrator && caps.CanRunAsAdmin) OpMenu.Items.Add(Item("Retry as administrator", "uac.svg", true, () => Operation.Retry(AbstractOperation.RetryMode.Retry_AsAdmin))); @@ -268,7 +268,7 @@ private void RebuildMenu(OperationStatus status) OpMenu.Items.Add(Item("Retry skipping integrity checks", "checksum.svg", true, () => Operation.Retry(AbstractOperation.RetryMode.Retry_SkipIntegrity))); } - else if (Operation is SourceOperation srcOp && !srcOp.ForceAsAdministrator) + else if (OperatingSystem.IsWindows() && Operation is SourceOperation srcOp && !srcOp.ForceAsAdministrator) { OpMenu.Items.Add(Item("Retry as administrator", "uac.svg", true, () => Operation.Retry(AbstractOperation.RetryMode.Retry_AsAdmin))); diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/AdministratorViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/AdministratorViewModel.cs index fc1c5ea48..269894d27 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/AdministratorViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/AdministratorViewModel.cs @@ -22,6 +22,8 @@ public partial class AdministratorViewModel : ViewModelBase public string WarningBody2 { get; } = CoreTools.Translate("The settings will list, in their descriptions, the potential security issues they may have."); + public bool IsWindows { get; } = OperatingSystem.IsWindows(); + // ── Observable state ───────────────────────────────────────────────── /// True when elevation is NOT prohibited — controls enabled-state of the cache-admin-rights cards. [ObservableProperty] private bool _isElevationEnabled; diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/ExperimentalViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/ExperimentalViewModel.cs index 869b36b79..47f103135 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/ExperimentalViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/ExperimentalViewModel.cs @@ -5,6 +5,8 @@ namespace UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; public partial class ExperimentalViewModel : ViewModelBase { + public bool IsWindows { get; } = OperatingSystem.IsWindows(); + public event EventHandler? RestartRequired; [RelayCommand] diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/InstallOptionsPanelViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/InstallOptionsPanelViewModel.cs index 39580ebd3..8ae454da9 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/InstallOptionsPanelViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/InstallOptionsPanelViewModel.cs @@ -198,7 +198,7 @@ private async Task DoLoadOptions() // Checkboxes — load value, then set enabled per capability AdminChecked = options.RunAsAdministrator; - AdminEnabled = true; + AdminEnabled = OperatingSystem.IsWindows(); InteractiveChecked = options.InteractiveInstallation; InteractiveEnabled = _manager.Capabilities.CanRunInteractively; diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/Interface_PViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/Interface_PViewModel.cs index b4f2a3f24..c0a4e02df 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/Interface_PViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/Interface_PViewModel.cs @@ -11,6 +11,8 @@ namespace UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; public partial class Interface_PViewModel : ViewModelBase { + public bool IsWindows { get; } = OperatingSystem.IsWindows(); + [ObservableProperty] private string _iconCacheSizeText = ""; public event EventHandler? RestartRequired; diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/NotificationsViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/NotificationsViewModel.cs index 5db3b75be..efaa49d04 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/NotificationsViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/NotificationsViewModel.cs @@ -10,6 +10,9 @@ public partial class NotificationsViewModel : ViewModelBase [ObservableProperty] private bool _isSystemTrayEnabled; [ObservableProperty] private bool _isNotificationsEnabled; + /// True when the system-tray-disabled warning should be shown (Windows only). + public bool IsSystemTrayWarningVisible => OperatingSystem.IsWindows() && !IsSystemTrayEnabled; + public NotificationsViewModel() { _isSystemTrayEnabled = !CoreSettings.Get(CoreSettings.K.DisableSystemTray); diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/InstallOptionsWindow.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/InstallOptionsWindow.axaml index affbb208e..3b28b01a8 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/InstallOptionsWindow.axaml +++ b/src/UniGetUI.Avalonia/Views/DialogPages/InstallOptionsWindow.axaml @@ -67,6 +67,7 @@ - - - - - - - - - + + + + + + + + + + + + - + + CornerRadius="8" + IsVisible="{Binding IsWindows}"/> diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/InstallOptionsPanel.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/InstallOptionsPanel.axaml index b93431268..52b590632 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/InstallOptionsPanel.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/InstallOptionsPanel.axaml @@ -52,6 +52,7 @@ - + diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Notifications.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Notifications.axaml index 614a6a607..366ad9e23 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Notifications.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Notifications.axaml @@ -16,18 +16,18 @@ FontWeight="SemiBold" Margin="44,32,4,8"/> - + + IsVisible="{Binding IsSystemTrayWarningVisible}"/> + IsEnabled="{Binding IsSystemTrayWarningVisible, Converter={x:Static BoolConverters.Not}}"/> _ = LaunchInstall([SelectedItem!], elevated: true); diff --git a/src/UniGetUI.Avalonia/Views/SoftwarePages/InstalledPackagesPage.cs b/src/UniGetUI.Avalonia/Views/SoftwarePages/InstalledPackagesPage.cs index e72611adf..367f7ac86 100644 --- a/src/UniGetUI.Avalonia/Views/SoftwarePages/InstalledPackagesPage.cs +++ b/src/UniGetUI.Avalonia/Views/SoftwarePages/InstalledPackagesPage.cs @@ -66,7 +66,7 @@ public InstalledPackagesPage() : base(new PackagesPageData protected override void GenerateToolBar(PackagesPageViewModel vm) { // ── Dropdown: uninstall variants ──────────────────────────────────── - var uninstallAsAdmin = new MenuItem { Header = CoreTools.Translate("Uninstall as administrator") }; + var uninstallAsAdmin = new MenuItem { Header = CoreTools.Translate("Uninstall as administrator"), IsVisible = OperatingSystem.IsWindows() }; var uninstallInteractive = new MenuItem { Header = CoreTools.Translate("Interactive uninstall") }; var downloadInstallers = new MenuItem { Header = CoreTools.Translate("Download selected installers") }; @@ -141,6 +141,7 @@ protected override void GenerateToolBar(PackagesPageViewModel vm) { Header = CoreTools.AutoTranslated("Uninstall as administrator"), Icon = LoadMenuIcon("uac"), + IsVisible = OperatingSystem.IsWindows(), }; _menuAsAdmin.Click += (_, _) => _ = LaunchUninstall([SelectedItem!], elevated: true); diff --git a/src/UniGetUI.Avalonia/Views/SoftwarePages/PackageBundlesPage.cs b/src/UniGetUI.Avalonia/Views/SoftwarePages/PackageBundlesPage.cs index 382b5bc98..6dbf24c7f 100644 --- a/src/UniGetUI.Avalonia/Views/SoftwarePages/PackageBundlesPage.cs +++ b/src/UniGetUI.Avalonia/Views/SoftwarePages/PackageBundlesPage.cs @@ -79,7 +79,7 @@ public PackageBundlesPage() : base(new PackagesPageData // ─── Toolbar ────────────────────────────────────────────────────────────── protected override void GenerateToolBar(PackagesPageViewModel vm) { - var installAsAdmin = new MenuItem { Header = CoreTools.Translate("Install as administrator") }; + var installAsAdmin = new MenuItem { Header = CoreTools.Translate("Install as administrator"), IsVisible = OperatingSystem.IsWindows() }; var installInteractive = new MenuItem { Header = CoreTools.Translate("Interactive installation") }; var installSkipHash = new MenuItem { Header = CoreTools.Translate("Skip integrity checks") }; var downloadInstallers = new MenuItem { Header = CoreTools.Translate("Download selected installers") }; @@ -146,7 +146,7 @@ private static IReadOnlyList GetCheckedNonInstalledPackages(PackagesPa } }; - _menuAsAdmin = new MenuItem { Header = CoreTools.AutoTranslated("Install as administrator"), Icon = LoadMenuIcon("uac") }; + _menuAsAdmin = new MenuItem { Header = CoreTools.AutoTranslated("Install as administrator"), Icon = LoadMenuIcon("uac"), IsVisible = OperatingSystem.IsWindows() }; _menuAsAdmin.Click += (_, _) => _ = ImportAndInstallPackage(SelectedItem is { } p ? [p] : [], elevated: true); _menuInteractive = new MenuItem { Header = CoreTools.AutoTranslated("Interactive installation"), Icon = LoadMenuIcon("interactive") }; diff --git a/src/UniGetUI.Avalonia/Views/SoftwarePages/SoftwareUpdatesPage.cs b/src/UniGetUI.Avalonia/Views/SoftwarePages/SoftwareUpdatesPage.cs index 0715b7b58..abed54b85 100644 --- a/src/UniGetUI.Avalonia/Views/SoftwarePages/SoftwareUpdatesPage.cs +++ b/src/UniGetUI.Avalonia/Views/SoftwarePages/SoftwareUpdatesPage.cs @@ -49,7 +49,7 @@ public SoftwareUpdatesPage() : base(new PackagesPageData protected override void GenerateToolBar(PackagesPageViewModel vm) { // ── Dropdown: update variants ─────────────────────────────────────── - var updateAsAdmin = new MenuItem { Header = CoreTools.Translate("Update as administrator") }; + var updateAsAdmin = new MenuItem { Header = CoreTools.Translate("Update as administrator"), IsVisible = OperatingSystem.IsWindows() }; var updateSkipHash = new MenuItem { Header = CoreTools.Translate("Skip integrity checks") }; var updateInteractive = new MenuItem { Header = CoreTools.Translate("Interactive update") }; var downloadInstallers = new MenuItem { Header = CoreTools.Translate("Download selected installers") }; @@ -130,6 +130,7 @@ protected override void GenerateToolBar(PackagesPageViewModel vm) { Header = CoreTools.AutoTranslated("Update as administrator"), Icon = LoadMenuIcon("uac"), + IsVisible = OperatingSystem.IsWindows(), }; _menuAsAdmin.Click += (_, _) => _ = LaunchUpdate([SelectedItem!], elevated: true); From 3d1cbe0f27594d2283eb0e9675400168044f2975 Mon Sep 17 00:00:00 2001 From: Gabriel Dufresne Date: Mon, 30 Mar 2026 16:12:59 -0400 Subject: [PATCH 11/15] Added the different InfoBar --- .../ViewModels/InfoBarViewModel.cs | 28 ++++++ .../ViewModels/MainWindowViewModel.cs | 65 +++++++------ .../Views/Controls/InfoBar.axaml | 69 +++++++++++++ .../Views/Controls/InfoBar.axaml.cs | 97 +++++++++++++++++++ src/UniGetUI.Avalonia/Views/MainWindow.axaml | 23 ++++- .../Views/MainWindow.axaml.cs | 15 ++- 6 files changed, 261 insertions(+), 36 deletions(-) create mode 100644 src/UniGetUI.Avalonia/ViewModels/InfoBarViewModel.cs create mode 100644 src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml create mode 100644 src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml.cs diff --git a/src/UniGetUI.Avalonia/ViewModels/InfoBarViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/InfoBarViewModel.cs new file mode 100644 index 000000000..bdc6e9f7f --- /dev/null +++ b/src/UniGetUI.Avalonia/ViewModels/InfoBarViewModel.cs @@ -0,0 +1,28 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using System.Windows.Input; + +namespace UniGetUI.Avalonia.ViewModels; + +public enum InfoBarSeverity { Informational, Warning, Error, Success } + +public partial class InfoBarViewModel : ObservableObject +{ + [ObservableProperty] private bool _isOpen; + [ObservableProperty] private string _title = ""; + [ObservableProperty] private string _message = ""; + [ObservableProperty] private InfoBarSeverity _severity = InfoBarSeverity.Informational; + [ObservableProperty] private bool _isClosable = true; + [ObservableProperty] private string _actionButtonText = ""; + [ObservableProperty] private ICommand? _actionButtonCommand; + + public Action? OnClosed { get; set; } + + partial void OnIsOpenChanged(bool value) + { + if (!value) OnClosed?.Invoke(); + } + + [RelayCommand] + private void Close() => IsOpen = false; +} diff --git a/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs index 8a8f8c699..33a129771 100644 --- a/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs @@ -102,26 +102,10 @@ private void OnPageViewModelPropertyChanged(object? sender, System.ComponentMode } // ─── Banners ───────────────────────────────────────────────────────────── - [ObservableProperty] - private bool _updatesBannerVisible; - - [ObservableProperty] - private string _updatesBannerText = ""; - - [ObservableProperty] - private bool _errorBannerVisible; - - [ObservableProperty] - private string _errorBannerText = ""; - - [ObservableProperty] - private bool _winGetWarningBannerVisible; - - [ObservableProperty] - private string _winGetWarningBannerText = ""; - - [ObservableProperty] - private bool _telemetryWarnerVisible; + public InfoBarViewModel UpdatesBanner { get; } = new() { Severity = InfoBarSeverity.Success }; + public InfoBarViewModel ErrorBanner { get; } = new() { Severity = InfoBarSeverity.Error }; + public InfoBarViewModel WinGetWarningBanner { get; } = new() { Severity = InfoBarSeverity.Warning }; + public InfoBarViewModel TelemetryWarner { get; } = new() { Severity = InfoBarSeverity.Informational }; // ─── Constructor ───────────────────────────────────────────────────────── [RelayCommand] @@ -174,19 +158,46 @@ public MainWindowViewModel() Sidebar.NavigationRequested += (_, pageType) => NavigateTo(pageType); + AvaloniaAutoUpdater.UpdateAvailable += version => Dispatcher.UIThread.Post(() => + { + UpdatesBanner.Title = CoreTools.Translate("UniGetUI {0} is ready to be installed.", version); + UpdatesBanner.Message = CoreTools.Translate("The update process will start after closing UniGetUI"); + UpdatesBanner.ActionButtonText = CoreTools.Translate("Update now"); + UpdatesBanner.ActionButtonCommand = new CommunityToolkit.Mvvm.Input.RelayCommand(AvaloniaAutoUpdater.TriggerInstall); + UpdatesBanner.IsClosable = true; + UpdatesBanner.IsOpen = true; + }); + // Keep OperationsPanelVisible in sync with the live operations list Operations.CollectionChanged += (_, _) => OperationsPanelVisible = Operations.Count > 0; - if (CoreTools.IsAdministrator() && !Settings.Get(Settings.K.AlreadyWarnedAboutAdmin)) + if (OperatingSystem.IsWindows() && CoreTools.IsAdministrator() && !Settings.Get(Settings.K.AlreadyWarnedAboutAdmin)) { Settings.Set(Settings.K.AlreadyWarnedAboutAdmin, true); - // TODO: _ = DialogHelper.WarnAboutAdminRights(); + WinGetWarningBanner.Title = CoreTools.Translate("Administrator privileges"); + WinGetWarningBanner.Message = CoreTools.Translate( + "UniGetUI has been ran as administrator, which is not recommended. When running UniGetUI as administrator, EVERY operation launched from UniGetUI will have administrator privileges. You can still use the program, but we highly recommend not running UniGetUI with administrator privileges." + ); + WinGetWarningBanner.IsClosable = true; + WinGetWarningBanner.IsOpen = true; } if (!Settings.Get(Settings.K.ShownTelemetryBanner)) { - // TODO: DialogHelper.ShowTelemetryBanner(); + TelemetryWarner.Title = CoreTools.Translate("Share anonymous usage data"); + TelemetryWarner.Message = CoreTools.Translate( + "UniGetUI collects anonymous usage data in order to improve the user experience." + ); + TelemetryWarner.IsClosable = true; + TelemetryWarner.ActionButtonText = CoreTools.Translate("Accept"); + TelemetryWarner.ActionButtonCommand = new CommunityToolkit.Mvvm.Input.RelayCommand(() => + { + TelemetryWarner.IsOpen = false; + Settings.Set(Settings.K.ShownTelemetryBanner, true); + }); + TelemetryWarner.OnClosed = () => Settings.Set(Settings.K.ShownTelemetryBanner, true); + TelemetryWarner.IsOpen = true; } LoadDefaultPage(); @@ -202,7 +213,7 @@ public void LoadDefaultPage() "installed" => PageType.Installed, "bundles" => PageType.Bundles, "settings" => PageType.Settings, - _ => UpgradablePackagesLoader.Instance?.Count() > 0 ? PageType.Updates : PageType.Discover, + _ => UpgradablePackagesLoader.Instance is { } l && l.Count() > 0 ? PageType.Updates : PageType.Discover, }; NavigateTo(type); } @@ -352,12 +363,6 @@ private async Task ShowAboutDialog() Sidebar.SelectNavButtonForPage(_currentPage); } - // ─── Banner close commands ──────────────────────────────────────────────── - [RelayCommand] private void CloseUpdatesBanner() => UpdatesBannerVisible = false; - [RelayCommand] private void CloseErrorBanner() => ErrorBannerVisible = false; - [RelayCommand] private void CloseWinGetWarningBanner() => WinGetWarningBannerVisible = false; - [RelayCommand] private void CloseTelemetryWarner() => TelemetryWarnerVisible = false; - // ─── Search box ────────────────────────────────────────────────────────── [RelayCommand] public void SubmitGlobalSearch() diff --git a/src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml b/src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml new file mode 100644 index 000000000..31380bb5e --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml.cs b/src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml.cs new file mode 100644 index 000000000..4c317f968 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml.cs @@ -0,0 +1,97 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using UniGetUI.Avalonia.ViewModels; + +namespace UniGetUI.Avalonia.Views.Controls; + +public partial class InfoBar : UserControl +{ + // Icon path data for each severity + private const string InfoPath = "M12,2A10,10,0,1,0,22,12,10,10,0,0,0,12,2Zm1,15H11V11h2Zm0-8H11V7h2Z"; + private const string WarningPath = "M12,2,1,21H23Zm1,14H11V14h2Zm0-4H11V9h2Z"; + private const string ErrorPath = "M12,2A10,10,0,1,0,22,12,10,10,0,0,0,12,2Zm1,13H11V13h2Zm0-6H11V7h2Z"; + private const string SuccessPath = "M12,2A10,10,0,1,0,22,12,10,10,0,0,0,12,2ZM10,17,5,12l1.41-1.41L10,14.17l7.59-7.59L19,8Z"; + + public InfoBar() + { + InitializeComponent(); + DataContextChanged += OnDataContextChanged; + } + + private InfoBarViewModel? _vm; + + private void OnDataContextChanged(object? sender, EventArgs e) + { + if (_vm is not null) + _vm.PropertyChanged -= OnViewModelPropertyChanged; + + _vm = DataContext as InfoBarViewModel; + + if (_vm is not null) + { + _vm.PropertyChanged += OnViewModelPropertyChanged; + ApplySeverity(_vm.Severity); + } + } + + private void OnViewModelPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(InfoBarViewModel.Severity) && _vm is not null) + ApplySeverity(_vm.Severity); + } + + private void ApplySeverity(InfoBarSeverity severity) + { + // Update strip colour + var stripColor = severity switch + { + InfoBarSeverity.Warning => Color.Parse("#F7A800"), + InfoBarSeverity.Error => Color.Parse("#C42B1C"), + InfoBarSeverity.Success => Color.Parse("#107C10"), + _ => Color.Parse("#0078D4"), + }; + SeverityStrip.Background = new SolidColorBrush(stripColor); + + // Update body background/border from theme resources + string bgKey = severity switch + { + InfoBarSeverity.Warning => "WarningBannerBackground", + InfoBarSeverity.Error => "StatusErrorBackground", + InfoBarSeverity.Success => "StatusSuccessBackground", + _ => "StatusInfoBackground", + }; + string borderKey = severity switch + { + InfoBarSeverity.Warning => "WarningBannerBorderBrush", + InfoBarSeverity.Error => "StatusErrorBorderBrush", + InfoBarSeverity.Success => "StatusSuccessBorderBrush", + _ => "StatusInfoBorderBrush", + }; + + var theme = Application.Current?.ActualThemeVariant; + if (Application.Current?.TryGetResource(bgKey, theme, out var bg) == true && bg is IBrush bgBrush) + BodyBorder.Background = bgBrush; + if (Application.Current?.TryGetResource(borderKey, theme, out var border) == true && border is IBrush borderBrush) + BodyBorder.BorderBrush = borderBrush; + + // Update icon + SeverityIcon.Data = Geometry.Parse(severity switch + { + InfoBarSeverity.Warning => WarningPath, + InfoBarSeverity.Error => ErrorPath, + InfoBarSeverity.Success => SuccessPath, + _ => InfoPath, + }); + + // Icon foreground + var iconColor = severity switch + { + InfoBarSeverity.Warning => Color.Parse("#F7A800"), + InfoBarSeverity.Error => Color.Parse("#C42B1C"), + InfoBarSeverity.Success => Color.Parse("#107C10"), + _ => Color.Parse("#0078D4"), + }; + SeverityIcon.Foreground = new SolidColorBrush(iconColor); + } +} diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml b/src/UniGetUI.Avalonia/Views/MainWindow.axaml index 02cee80b2..be6f207cc 100644 --- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml +++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml @@ -41,11 +41,24 @@ Opacity="0.2" Background="{DynamicResource SystemBaseMediumColor}"/> - - + + + + + + + + + + + + + + + InfoBarSeverity.Error, + RuntimeNotificationLevel.Success => InfoBarSeverity.Success, + _ => InfoBarSeverity.Informational, + }; + ViewModel.ErrorBanner.ActionButtonText = ""; + ViewModel.ErrorBanner.ActionButtonCommand = null; + ViewModel.ErrorBanner.Title = title; + ViewModel.ErrorBanner.Message = message; + ViewModel.ErrorBanner.Severity = severity; + ViewModel.ErrorBanner.IsOpen = true; } public void UpdateSystemTrayStatus() From 57473cf46f9f7a21984e72b0a0311dbb37055072 Mon Sep 17 00:00:00 2001 From: Gabriel Dufresne Date: Tue, 31 Mar 2026 08:13:46 -0400 Subject: [PATCH 12/15] Fixed some issue copilot pointed out --- .../Assets/Styles/Styles.Linux.axaml | 6 ++ .../Infrastructure/GitHubAuthApiRunner.cs | 92 ------------------ .../Infrastructure/GitHubAuthService.cs | 96 ++++--------------- .../Infrastructure/Secrets.cs | 1 - .../Infrastructure/generate-secrets.sh | 3 - .../UniGetUI.Avalonia.csproj | 1 - .../DialogPages/OperationViewModel.cs | 5 +- .../SourceManagerCardViewModel.cs | 8 +- src/UniGetUI.Avalonia/Views/MainWindow.axaml | 27 +++--- .../Views/MainWindow.axaml.cs | 2 +- .../SettingsPages/PackageManagerPage.axaml.cs | 10 +- 11 files changed, 57 insertions(+), 194 deletions(-) delete mode 100644 src/UniGetUI.Avalonia/Infrastructure/GitHubAuthApiRunner.cs diff --git a/src/UniGetUI.Avalonia/Assets/Styles/Styles.Linux.axaml b/src/UniGetUI.Avalonia/Assets/Styles/Styles.Linux.axaml index 739701738..5364da18e 100644 --- a/src/UniGetUI.Avalonia/Assets/Styles/Styles.Linux.axaml +++ b/src/UniGetUI.Avalonia/Assets/Styles/Styles.Linux.axaml @@ -29,8 +29,11 @@ + + + @@ -62,8 +65,11 @@ + + + diff --git a/src/UniGetUI.Avalonia/Infrastructure/GitHubAuthApiRunner.cs b/src/UniGetUI.Avalonia/Infrastructure/GitHubAuthApiRunner.cs deleted file mode 100644 index 0a785538f..000000000 --- a/src/UniGetUI.Avalonia/Infrastructure/GitHubAuthApiRunner.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Net; -using System.Net.Sockets; -using System.Text; -using UniGetUI.Core.Logging; - -namespace UniGetUI.Avalonia.Infrastructure; - -internal sealed class GitHubAuthApiRunner : IDisposable -{ - public event EventHandler? OnLogin; - - private TcpListener? _listener; - private CancellationTokenSource? _cts; - - public Task Start() - { - _cts = new CancellationTokenSource(); - _listener = new TcpListener(IPAddress.Loopback, 58642); - _listener.Start(); - Logger.Info("GitHub auth callback server running on http://127.0.0.1:58642"); - _ = ListenAsync(_cts.Token); - return Task.CompletedTask; - } - - private async Task ListenAsync(CancellationToken ct) - { - while (!ct.IsCancellationRequested) - { - try - { - var client = await _listener!.AcceptTcpClientAsync(ct); - _ = HandleClientAsync(client); - } - catch when (ct.IsCancellationRequested) { break; } - catch (Exception ex) { Logger.Error(ex); break; } - } - } - - private async Task HandleClientAsync(TcpClient client) - { - using var _ = client; - using var stream = client.GetStream(); - - // Read the HTTP request headers - var buffer = new byte[4096]; - int read = await stream.ReadAsync(buffer); - var requestText = Encoding.UTF8.GetString(buffer, 0, read); - - // Extract ?code= from the request line: "GET /?code=xxx&... HTTP/1.1" - string? code = null; - var firstLine = requestText.Split('\n', 2)[0]; - var codeMatch = System.Text.RegularExpressions.Regex.Match(firstLine, @"[?&]code=([^& ]+)"); - if (codeMatch.Success) - code = Uri.UnescapeDataString(codeMatch.Groups[1].Value); - - const string body = """ -
- UniGetUI authentication -

Authentication successful

-

You can now close this window and return to UniGetUI

-
- """; - - var bodyBytes = Encoding.UTF8.GetBytes(body); - var header = $"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {bodyBytes.Length}\r\nConnection: close\r\n\r\n"; - await stream.WriteAsync(Encoding.UTF8.GetBytes(header)); - await stream.WriteAsync(bodyBytes); - - if (!string.IsNullOrEmpty(code)) - { - Logger.ImportantInfo("[AUTH API] Received authentication token from GitHub"); - OnLogin?.Invoke(this, code); - } - } - - public Task Stop() - { - _cts?.Cancel(); - try { _listener?.Stop(); } catch { /* ignore */ } - return Task.CompletedTask; - } - - public void Dispose() - { - _cts?.Cancel(); - _cts?.Dispose(); - try { _listener?.Stop(); } catch { /* ignore */ } - } -} diff --git a/src/UniGetUI.Avalonia/Infrastructure/GitHubAuthService.cs b/src/UniGetUI.Avalonia/Infrastructure/GitHubAuthService.cs index c7ce4c6b7..a0ad1561d 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/GitHubAuthService.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/GitHubAuthService.cs @@ -9,14 +9,17 @@ namespace UniGetUI.Avalonia.Infrastructure; internal class GitHubAuthService { - private static readonly TimeSpan LoginTimeout = TimeSpan.FromMinutes(2); 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? AuthStatusChanged; + /// + /// Fired when the device flow has started. Provides the user code and verification URI + /// that must be shown to the user so they can authorize the app at GitHub. + /// + public static event EventHandler<(string UserCode, string VerificationUri)>? DeviceFlowStarted; + public GitHubAuthService() { _client = new GitHubClient(new ProductHeaderValue("UniGetUI", CoreData.VersionName)); @@ -34,94 +37,33 @@ public GitHubAuthService() }; } - private GitHubAuthApiRunner? _loginBackend; - private string? _codeFromApi; - public async Task SignInAsync() { try { - 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); - - _codeFromApi = null; - if (_loginBackend is not null) - { - try { await _loginBackend.Stop(); _loginBackend.Dispose(); _loginBackend = null; } - catch (Exception ex) { Logger.Warn(ex); } - } - - _loginBackend = new GitHubAuthApiRunner(); - _loginBackend.OnLogin += BackendOnLogin; - await _loginBackend.Start(); - - CoreTools.Launch(oauthLoginUrl.ToString()); + Logger.Info("Initiating GitHub sign-in using device flow..."); - var timeoutAt = DateTime.UtcNow.Add(LoginTimeout); - while (_codeFromApi is null && DateTime.UtcNow < timeoutAt) - await Task.Delay(100); - - if (string.IsNullOrEmpty(_codeFromApi)) - { - Logger.Error("GitHub sign-in timed out before the loopback callback was received."); - AuthStatusChanged?.Invoke(this, EventArgs.Empty); - return false; - } - - return await CompleteSignInAsync(_codeFromApi); - } - catch (Exception ex) - { - Logger.Error("Exception during GitHub sign-in process:"); - Logger.Error(ex); - ClearAuthenticatedUserData(); - AuthStatusChanged?.Invoke(this, EventArgs.Empty); - return false; - } - finally - { - if (_loginBackend is not null) - { - try + var deviceFlow = await _client.Oauth.InitiateDeviceFlow( + new OauthDeviceFlowRequest(_gitHubClientId) { - _loginBackend.OnLogin -= BackendOnLogin; - await _loginBackend.Stop(); - _loginBackend.Dispose(); - } - catch (Exception ex) { Logger.Warn(ex); } - finally { _loginBackend = null; } - } - } - } + Scopes = { "read:user", "gist" }, + }, CancellationToken.None); - private void BackendOnLogin(object? sender, string code) - { - _codeFromApi = code; - } + // Open the verification page and notify the UI layer so it can show the user code. + CoreTools.Launch(deviceFlow.VerificationUri); + DeviceFlowStarted?.Invoke(this, (deviceFlow.UserCode, deviceFlow.VerificationUri)); - private async Task CompleteSignInAsync(string code) - { - try - { - var tokenRequest = new OauthTokenRequest(_gitHubClientId, _gitHubClientSecret, code) - { - RedirectUri = new Uri(RedirectUri), - }; - var token = await _client.Oauth.CreateAccessToken(tokenRequest); + // Octokit handles polling with the correct interval until the user authorises or the code expires. + var token = await _client.Oauth.CreateAccessTokenForDeviceFlow(_gitHubClientId, deviceFlow, CancellationToken.None); if (string.IsNullOrEmpty(token.AccessToken)) { - Logger.Error("Failed to obtain GitHub access token."); + Logger.Error("Failed to obtain GitHub access token via device flow."); AuthStatusChanged?.Invoke(this, EventArgs.Empty); return false; } - Logger.Info("GitHub login successful. Storing access token."); + Logger.Info("GitHub device flow login successful. Storing access token."); SecureGHTokenManager.StoreToken(token.AccessToken); var userClient = new GitHubClient(new ProductHeaderValue("UniGetUI")) @@ -140,7 +82,7 @@ private async Task CompleteSignInAsync(string code) } catch (Exception ex) { - Logger.Error("Exception during GitHub token exchange:"); + Logger.Error("Exception during GitHub device flow sign-in:"); Logger.Error(ex); ClearAuthenticatedUserData(); AuthStatusChanged?.Invoke(this, EventArgs.Empty); diff --git a/src/UniGetUI.Avalonia/Infrastructure/Secrets.cs b/src/UniGetUI.Avalonia/Infrastructure/Secrets.cs index 31dcd9dc1..2e38224e3 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/Secrets.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/Secrets.cs @@ -8,6 +8,5 @@ internal static partial class Secrets * Seeing errors? Build the project (maybe twice) */ public static partial string GetGitHubClientId(); - public static partial string GetGitHubClientSecret(); /* ------------------------------------------------------------------------ */ } diff --git a/src/UniGetUI.Avalonia/Infrastructure/generate-secrets.sh b/src/UniGetUI.Avalonia/Infrastructure/generate-secrets.sh index 3c9c86407..d70a305f9 100755 --- a/src/UniGetUI.Avalonia/Infrastructure/generate-secrets.sh +++ b/src/UniGetUI.Avalonia/Infrastructure/generate-secrets.sh @@ -5,10 +5,8 @@ if [ ! -d "Generated Files" ]; then mkdir -p "Generated Files"; fi if [ ! -d "${OUTPUT_PATH}Generated Files" ]; then mkdir -p "${OUTPUT_PATH}Generated Files"; fi CLIENT_ID="${UNIGETUI_GITHUB_CLIENT_ID}" -CLIENT_SECRET="${UNIGETUI_GITHUB_CLIENT_SECRET}" if [ -z "$CLIENT_ID" ]; then CLIENT_ID="CLIENT_ID_UNSET"; fi -if [ -z "$CLIENT_SECRET" ]; then CLIENT_SECRET="CLIENT_SECRET_UNSET"; fi cat > "Generated Files/Secrets.Generated.cs" << CSEOF // Auto-generated file - do not modify @@ -17,7 +15,6 @@ namespace UniGetUI.Avalonia.Infrastructure internal static partial class Secrets { public static partial string GetGitHubClientId() => "$CLIENT_ID"; - public static partial string GetGitHubClientSecret() => "$CLIENT_SECRET"; } } CSEOF diff --git a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj index a9bf5a5e3..bbc10d5dd 100644 --- a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj +++ b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj @@ -91,7 +91,6 @@ - diff --git a/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationViewModel.cs index f94aa6bc4..f6183591a 100644 --- a/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationViewModel.cs @@ -132,7 +132,7 @@ private async Task LoadIconAsync() if (bmp is not null) { - PackageIcon = bmp; + Dispatcher.UIThread.Post(() => PackageIcon = bmp); return; } } @@ -141,7 +141,8 @@ private async Task LoadIconAsync() try { using var stream = AssetLoader.Open(_fallbackIconUri); - PackageIcon = new Bitmap(stream); + var fallback = new Bitmap(stream); + Dispatcher.UIThread.Post(() => PackageIcon = fallback); } catch { } } diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/SourceManagerCardViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/SourceManagerCardViewModel.cs index 9de11242e..124081d05 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/SourceManagerCardViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/SourceManagerCardViewModel.cs @@ -10,6 +10,7 @@ using UniGetUI.PackageEngine.Classes.Manager; using UniGetUI.PackageEngine.Interfaces; using UniGetUI.PackageEngine.Operations; +using UniGetUI.PackageOperations; namespace UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; @@ -115,10 +116,15 @@ private async Task ConfirmAddSource() SelectedKnownSource = _otherLabel; var op = new AddSourceOperation(source); + op.OperationFinished += OnAddOperationFinished; AvaloniaOperationRegistry.Add(op); _ = op.MainThread(); + } - await Task.Delay(3000); + private void OnAddOperationFinished(object? sender, EventArgs e) + { + if (sender is AbstractOperation op) + op.OperationFinished -= OnAddOperationFinished; _ = DoLoadSources(); } diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml b/src/UniGetUI.Avalonia/Views/MainWindow.axaml index be6f207cc..7cefffcd1 100644 --- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml +++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml @@ -204,17 +204,21 @@ - + + - - - - + - + Margin="0,10,0,0"/> + diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs index 1021c3653..8d8d077bd 100644 --- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs @@ -165,7 +165,7 @@ public static void ApplyProxyVariableToProcess() var creds = Settings.GetProxyCredentials(); if (creds is null) { - content = $"--proxy {proxyUri}"; + content = proxyUri.ToString(); } else { diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs index 8e6972cc0..15adf1aea 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs @@ -211,9 +211,9 @@ private void BuildPage() { ManagerLogs.CornerRadius = new CornerRadius(8, 8, 0, 0); AppExecutionAliasWarning.IsVisible = true; - AppExecutionAliasLabel.Text = + AppExecutionAliasLabel.Text = CoreTools.Translate( "If Python cannot be found or is not listing packages but is installed on the system, " + - "you may need to disable the \"python.exe\" App Execution Alias in the settings."; + "you may need to disable the \"python.exe\" App Execution Alias in the settings."); } } @@ -311,7 +311,7 @@ private void BuildExtraControls(CheckboxCard_Dict disableNotifsCard) CornerRadius = new CornerRadius(0, 0, 8, 8), BorderThickness = new Thickness(1, 0, 1, 1), SettingName = CoreSettings.K.EnableScoopCleanup, - Text = "Enable Scoop cleanup on launch", + Text = CoreTools.AutoTranslated("Enable Scoop cleanup on launch"), }); break; @@ -361,8 +361,8 @@ private ButtonCard BuildVcpkgRootCard() { var vcpkgRootCard = new ButtonCard { - Text = "Change vcpkg root location", - ButtonText = "Select", + Text = CoreTools.AutoTranslated("Change vcpkg root location"), + ButtonText = CoreTools.AutoTranslated("Select"), CornerRadius = new CornerRadius(0, 0, 8, 8), BorderThickness = new Thickness(1, 0, 1, 1), }; From 6e51028fd1529d4c64a01fe87152a26b733540d3 Mon Sep 17 00:00:00 2001 From: Gabriel Dufresne Date: Tue, 31 Mar 2026 08:16:57 -0400 Subject: [PATCH 13/15] Fixed whiteSpace --- .../Controls/UserAvatarViewModel.cs | 2 +- .../DialogPages/OperationViewModel.cs | 4 +- .../ViewModels/InfoBarViewModel.cs | 2 +- .../ViewModels/MainWindowViewModel.cs | 2 +- .../Pages/LogPages/BaseLogPageViewModel.cs | 26 ++-- .../InstallOptionsPanelViewModel.cs | 142 +++++++++--------- .../SettingsPages/PackageManagerViewModel.cs | 6 +- .../SourceManagerCardViewModel.cs | 14 +- .../Views/Controls/InfoBar.axaml.cs | 28 ++-- .../Views/MainWindow.axaml.cs | 12 +- .../InstallOptionsPanel.axaml.cs | 4 +- .../SettingsPages/PackageManagerPage.axaml.cs | 10 +- 12 files changed, 126 insertions(+), 126 deletions(-) diff --git a/src/UniGetUI.Avalonia/ViewModels/Controls/UserAvatarViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Controls/UserAvatarViewModel.cs index a363aa7c7..15ab82e49 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Controls/UserAvatarViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Controls/UserAvatarViewModel.cs @@ -1,11 +1,11 @@ using Avalonia.Media.Imaging; using Avalonia.Threading; using CommunityToolkit.Mvvm.Input; -using MvvmRelayCommand = CommunityToolkit.Mvvm.Input.RelayCommand; using Octokit; using UniGetUI.Avalonia.Infrastructure; using UniGetUI.Core.Logging; using UniGetUI.Core.Tools; +using MvvmRelayCommand = CommunityToolkit.Mvvm.Input.RelayCommand; namespace UniGetUI.Avalonia.ViewModels.Controls; diff --git a/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationViewModel.cs index f6183591a..1856922f4 100644 --- a/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationViewModel.cs @@ -237,8 +237,8 @@ private void RebuildMenu(OperationStatus status) // ── Queue management ────────────────────────────────────────────────── if (status is OperationStatus.InQueue) { - OpMenu.Items.Add(Item("Run now", "forward.svg", true, Operation.SkipQueue)); - OpMenu.Items.Add(Item("Run next", "forward.svg", true, Operation.RunNext)); + OpMenu.Items.Add(Item("Run now", "forward.svg", true, Operation.SkipQueue)); + OpMenu.Items.Add(Item("Run next", "forward.svg", true, Operation.RunNext)); OpMenu.Items.Add(Item("Run last", "backward.svg", true, Operation.BackOfTheQueue)); OpMenu.Items.Add(new Separator()); } diff --git a/src/UniGetUI.Avalonia/ViewModels/InfoBarViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/InfoBarViewModel.cs index bdc6e9f7f..63c814d1e 100644 --- a/src/UniGetUI.Avalonia/ViewModels/InfoBarViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/InfoBarViewModel.cs @@ -1,6 +1,6 @@ +using System.Windows.Input; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using System.Windows.Input; namespace UniGetUI.Avalonia.ViewModels; diff --git a/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs index 33a129771..18c57e0cf 100644 --- a/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs @@ -9,9 +9,9 @@ using UniGetUI.Avalonia.Infrastructure; using UniGetUI.Avalonia.ViewModels.Pages; using UniGetUI.Avalonia.Views; +using UniGetUI.Avalonia.Views.DialogPages; using UniGetUI.Avalonia.Views.Pages; using UniGetUI.Avalonia.Views.Pages.LogPages; -using UniGetUI.Avalonia.Views.DialogPages; using UniGetUI.Avalonia.Views.Pages.SettingsPages; using UniGetUI.Core.Data; using UniGetUI.Core.SettingsEngine; diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/LogPages/BaseLogPageViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/LogPages/BaseLogPageViewModel.cs index a5f96ca67..04eaa2fc1 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/LogPages/BaseLogPageViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/LogPages/BaseLogPageViewModel.cs @@ -47,12 +47,12 @@ protected static IBrush GetSeverityBrush(LogEntry.SeverityLevel severity, bool i { var color = severity switch { - LogEntry.SeverityLevel.Debug => isDark ? Color.FromRgb(130, 130, 130) : Color.FromRgb(125, 125, 225), - LogEntry.SeverityLevel.Info => isDark ? Color.FromRgb(190, 190, 190) : Color.FromRgb(50, 50, 150), - LogEntry.SeverityLevel.Success => isDark ? Color.FromRgb(250, 250, 250) : Color.FromRgb(0, 0, 0), - LogEntry.SeverityLevel.Warning => isDark ? Color.FromRgb(255, 255, 90) : Color.FromRgb(150, 150, 0), - LogEntry.SeverityLevel.Error => isDark ? Color.FromRgb(255, 80, 80) : Color.FromRgb(205, 0, 0), - _ => isDark ? Color.FromRgb(130, 130, 130) : Color.FromRgb(125, 125, 225), + LogEntry.SeverityLevel.Debug => isDark ? Color.FromRgb(130, 130, 130) : Color.FromRgb(125, 125, 225), + LogEntry.SeverityLevel.Info => isDark ? Color.FromRgb(190, 190, 190) : Color.FromRgb(50, 50, 150), + LogEntry.SeverityLevel.Success => isDark ? Color.FromRgb(250, 250, 250) : Color.FromRgb(0, 0, 0), + LogEntry.SeverityLevel.Warning => isDark ? Color.FromRgb(255, 255, 90) : Color.FromRgb(150, 150, 0), + LogEntry.SeverityLevel.Error => isDark ? Color.FromRgb(255, 80, 80) : Color.FromRgb(205, 0, 0), + _ => isDark ? Color.FromRgb(130, 130, 130) : Color.FromRgb(125, 125, 225), }; return new SolidColorBrush(color); } @@ -61,13 +61,13 @@ protected static IBrush GetManagerColorBrush(char colorCode, bool isDark) { var color = colorCode switch { - '0' => isDark ? Color.FromRgb(250, 250, 250) : Color.FromRgb(0, 0, 0), - '1' => isDark ? Color.FromRgb(190, 190, 190) : Color.FromRgb(50, 50, 150), - '2' => isDark ? Color.FromRgb(255, 80, 80) : Color.FromRgb(205, 0, 0), - '3' => isDark ? Color.FromRgb(120, 120, 255) : Color.FromRgb(0, 0, 205), - '4' => isDark ? Color.FromRgb(80, 255, 80) : Color.FromRgb(0, 205, 0), - '5' => isDark ? Color.FromRgb(255, 255, 90) : Color.FromRgb(150, 150, 0), - _ => isDark ? Color.FromRgb(255, 255, 90) : Color.FromRgb(150, 150, 0), + '0' => isDark ? Color.FromRgb(250, 250, 250) : Color.FromRgb(0, 0, 0), + '1' => isDark ? Color.FromRgb(190, 190, 190) : Color.FromRgb(50, 50, 150), + '2' => isDark ? Color.FromRgb(255, 80, 80) : Color.FromRgb(205, 0, 0), + '3' => isDark ? Color.FromRgb(120, 120, 255) : Color.FromRgb(0, 0, 205), + '4' => isDark ? Color.FromRgb(80, 255, 80) : Color.FromRgb(0, 205, 0), + '5' => isDark ? Color.FromRgb(255, 255, 90) : Color.FromRgb(150, 150, 0), + _ => isDark ? Color.FromRgb(255, 255, 90) : Color.FromRgb(150, 150, 0), }; return new SolidColorBrush(color); } diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/InstallOptionsPanelViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/InstallOptionsPanelViewModel.cs index 8ae454da9..ed73b36b5 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/InstallOptionsPanelViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/InstallOptionsPanelViewModel.cs @@ -64,46 +64,46 @@ public partial class InstallOptionsPanelViewModel : ViewModelBase [ObservableProperty] private string _customUninstall = ""; // ── Translated labels (static) ──────────────────────────────────────────── - public string AdminLabel { get; } = CoreTools.Translate("Run as admin"); - public string InteractiveLabel { get; } = CoreTools.Translate("Interactive installation"); - public string SkipHashLabel { get; } = CoreTools.Translate("Skip hash check"); - public string PreReleaseLabel { get; } = CoreTools.Translate("Allow pre-release versions"); + public string AdminLabel { get; } = CoreTools.Translate("Run as admin"); + public string InteractiveLabel { get; } = CoreTools.Translate("Interactive installation"); + public string SkipHashLabel { get; } = CoreTools.Translate("Skip hash check"); + public string PreReleaseLabel { get; } = CoreTools.Translate("Allow pre-release versions"); public string UninstallPrevLabel { get; } = CoreTools.Translate("Uninstall previous versions when updated"); - public string ArchLabel { get; } = CoreTools.Translate("Architecture to install:"); - public string ScopeLabel { get; } = CoreTools.Translate("Installation scope:"); - public string LocationLabel { get; } = CoreTools.Translate("Install location:"); - public string SelectDirLabel { get; } = CoreTools.Translate("Select"); - public string ResetDirLabel { get; } = CoreTools.Translate("Reset"); - public string InstallArgsLabel { get; } = CoreTools.Translate("Custom install arguments:"); - public string UpdateArgsLabel { get; } = CoreTools.Translate("Custom update arguments:"); + public string ArchLabel { get; } = CoreTools.Translate("Architecture to install:"); + public string ScopeLabel { get; } = CoreTools.Translate("Installation scope:"); + public string LocationLabel { get; } = CoreTools.Translate("Install location:"); + public string SelectDirLabel { get; } = CoreTools.Translate("Select"); + public string ResetDirLabel { get; } = CoreTools.Translate("Reset"); + public string InstallArgsLabel { get; } = CoreTools.Translate("Custom install arguments:"); + public string UpdateArgsLabel { get; } = CoreTools.Translate("Custom update arguments:"); public string UninstallArgsLabel { get; } = CoreTools.Translate("Custom uninstall arguments:"); - public string ResetLabel { get; } = CoreTools.Translate("Reset"); - public string ApplyLabel { get; } = CoreTools.Translate("Apply"); - public string CliDisabledLabel { get; } = CoreTools.Translate("For security reasons, custom command-line arguments are disabled by default. Go to UniGetUI security settings to change this."); - public string GoToSecurityLabel { get; } = CoreTools.Translate("Go to UniGetUI security settings"); + public string ResetLabel { get; } = CoreTools.Translate("Reset"); + public string ApplyLabel { get; } = CoreTools.Translate("Apply"); + public string CliDisabledLabel { get; } = CoreTools.Translate("For security reasons, custom command-line arguments are disabled by default. Go to UniGetUI security settings to change this."); + public string GoToSecurityLabel { get; } = CoreTools.Translate("Go to UniGetUI security settings"); public string HeaderText => CoreTools.Translate( "The following options will be applied by default each time a {0} package is installed, upgraded or uninstalled.", _manager.DisplayName); - public double CliOpacity => CliSectionEnabled ? 1.0 : 0.5; - public double ArchOpacity => ArchitectureEnabled ? 1.0 : 0.5; - public double ScopeOpacity => ScopeEnabled ? 1.0 : 0.5; - public double LocationOpacity => LocationSelectEnabled ? 1.0 : 0.5; + public double CliOpacity => CliSectionEnabled ? 1.0 : 0.5; + public double ArchOpacity => ArchitectureEnabled ? 1.0 : 0.5; + public double ScopeOpacity => ScopeEnabled ? 1.0 : 0.5; + public double LocationOpacity => LocationSelectEnabled ? 1.0 : 0.5; - partial void OnCliSectionEnabledChanged(bool value) => OnPropertyChanged(nameof(CliOpacity)); - partial void OnArchitectureEnabledChanged(bool value) => OnPropertyChanged(nameof(ArchOpacity)); - partial void OnScopeEnabledChanged(bool value) => OnPropertyChanged(nameof(ScopeOpacity)); + partial void OnCliSectionEnabledChanged(bool value) => OnPropertyChanged(nameof(CliOpacity)); + partial void OnArchitectureEnabledChanged(bool value) => OnPropertyChanged(nameof(ArchOpacity)); + partial void OnScopeEnabledChanged(bool value) => OnPropertyChanged(nameof(ScopeOpacity)); partial void OnLocationSelectEnabledChanged(bool value) => OnPropertyChanged(nameof(LocationOpacity)); // Mark HasChanges when user edits options (guards against firing during load) - partial void OnAdminCheckedChanged(bool _) => HasChanges = !IsLoading; - partial void OnInteractiveCheckedChanged(bool _) => HasChanges = !IsLoading; - partial void OnSkipHashCheckedChanged(bool _) => HasChanges = !IsLoading; - partial void OnPreReleaseCheckedChanged(bool _) => HasChanges = !IsLoading; + partial void OnAdminCheckedChanged(bool _) => HasChanges = !IsLoading; + partial void OnInteractiveCheckedChanged(bool _) => HasChanges = !IsLoading; + partial void OnSkipHashCheckedChanged(bool _) => HasChanges = !IsLoading; + partial void OnPreReleaseCheckedChanged(bool _) => HasChanges = !IsLoading; partial void OnUninstallPreviousCheckedChanged(bool _) => HasChanges = !IsLoading; - partial void OnSelectedArchitectureChanged(string? _) => HasChanges = !IsLoading; - partial void OnSelectedScopeChanged(string? _) => HasChanges = !IsLoading; + partial void OnSelectedArchitectureChanged(string? _) => HasChanges = !IsLoading; + partial void OnSelectedScopeChanged(string? _) => HasChanges = !IsLoading; public InstallOptionsPanelViewModel(IPackageManager manager) { @@ -136,10 +136,10 @@ private async Task SaveOptions() var options = new InstallOptions { - RunAsAdministrator = AdminChecked, - SkipHashCheck = SkipHashChecked, - InteractiveInstallation = InteractiveChecked, - PreRelease = PreReleaseChecked, + RunAsAdministrator = AdminChecked, + SkipHashCheck = SkipHashChecked, + InteractiveInstallation = InteractiveChecked, + PreRelease = PreReleaseChecked, UninstallPreviousVersionsOnUpdate = UninstallPreviousChecked, }; @@ -156,8 +156,8 @@ SelectedArchitecture is { } arch && LocationText != _defaultLocationLabel) options.CustomInstallLocation = LocationText; - options.CustomParameters_Install = CustomInstall.Split(' ').Where(x => x.Any()).ToList(); - options.CustomParameters_Update = CustomUpdate.Split(' ').Where(x => x.Any()).ToList(); + options.CustomParameters_Install = CustomInstall.Split(' ').Where(x => x.Any()).ToList(); + options.CustomParameters_Update = CustomUpdate.Split(' ').Where(x => x.Any()).ToList(); options.CustomParameters_Uninstall = CustomUninstall.Split(' ').Where(x => x.Any()).ToList(); await InstallOptionsFactory.SaveForManagerAsync(options, _manager); @@ -175,16 +175,16 @@ private async Task ResetOptions() private void DisableAllInput() { - AdminEnabled = false; - InteractiveEnabled = false; - SkipHashEnabled = false; - PreReleaseEnabled = false; + AdminEnabled = false; + InteractiveEnabled = false; + SkipHashEnabled = false; + PreReleaseEnabled = false; UninstallPreviousEnabled = false; - ArchitectureEnabled = false; - ScopeEnabled = false; - LocationSelectEnabled = false; - LocationResetEnabled = false; - CliSectionEnabled = false; + ArchitectureEnabled = false; + ScopeEnabled = false; + LocationSelectEnabled = false; + LocationResetEnabled = false; + CliSectionEnabled = false; } private async Task DoLoadOptions() @@ -197,61 +197,61 @@ private async Task DoLoadOptions() await Task.Delay(300); // Checkboxes — load value, then set enabled per capability - AdminChecked = options.RunAsAdministrator; - AdminEnabled = OperatingSystem.IsWindows(); + AdminChecked = options.RunAsAdministrator; + AdminEnabled = OperatingSystem.IsWindows(); - InteractiveChecked = options.InteractiveInstallation; - InteractiveEnabled = _manager.Capabilities.CanRunInteractively; + InteractiveChecked = options.InteractiveInstallation; + InteractiveEnabled = _manager.Capabilities.CanRunInteractively; - SkipHashChecked = options.SkipHashCheck; - SkipHashEnabled = _manager.Capabilities.CanSkipIntegrityChecks; + SkipHashChecked = options.SkipHashCheck; + SkipHashEnabled = _manager.Capabilities.CanSkipIntegrityChecks; - PreReleaseChecked = options.PreRelease; - PreReleaseEnabled = _manager.Capabilities.SupportsPreRelease; + PreReleaseChecked = options.PreRelease; + PreReleaseEnabled = _manager.Capabilities.SupportsPreRelease; - UninstallPreviousChecked = options.UninstallPreviousVersionsOnUpdate; - UninstallPreviousEnabled = _manager.Capabilities.CanUninstallPreviousVersionsAfterUpdate; + UninstallPreviousChecked = options.UninstallPreviousVersionsOnUpdate; + UninstallPreviousEnabled = _manager.Capabilities.CanUninstallPreviousVersionsAfterUpdate; // Architecture - ArchitectureEnabled = _manager.Capabilities.SupportsCustomArchitectures; + ArchitectureEnabled = _manager.Capabilities.SupportsCustomArchitectures; string? matchedArch = ArchitectureItems.Contains(options.Architecture) ? options.Architecture : null; - SelectedArchitecture = matchedArch ?? ArchitectureItems.FirstOrDefault(); + SelectedArchitecture = matchedArch ?? ArchitectureItems.FirstOrDefault(); // Scope - ScopeEnabled = _manager.Capabilities.SupportsCustomScopes; - string? matchedScope = null; + ScopeEnabled = _manager.Capabilities.SupportsCustomScopes; + string? matchedScope = null; if (!string.IsNullOrEmpty(options.InstallationScope) && CommonTranslations.ScopeNames.TryGetValue(options.InstallationScope, out string? display)) { string translated = CoreTools.Translate(display); if (ScopeItems.Contains(translated)) matchedScope = translated; } - SelectedScope = matchedScope ?? ScopeItems.FirstOrDefault(); + SelectedScope = matchedScope ?? ScopeItems.FirstOrDefault(); // Location - LocationSelectEnabled = _manager.Capabilities.SupportsCustomLocations; + LocationSelectEnabled = _manager.Capabilities.SupportsCustomLocations; if (!string.IsNullOrEmpty(options.CustomInstallLocation)) { - LocationText = options.CustomInstallLocation; + LocationText = options.CustomInstallLocation; LocationResetEnabled = true; } else { - LocationText = _manager.Capabilities.SupportsCustomLocations + LocationText = _manager.Capabilities.SupportsCustomLocations ? _defaultLocationLabel : CoreTools.Translate("Install location can't be changed for {0} packages", _manager.DisplayName); LocationResetEnabled = false; } // CLI - bool isCLI = SecureSettings.Get(SecureSettings.K.AllowCLIArguments); - CliSectionEnabled = isCLI; + bool isCLI = SecureSettings.Get(SecureSettings.K.AllowCLIArguments); + CliSectionEnabled = isCLI; CliDisabledWarningVisible = !isCLI; - CustomInstall = string.Join(' ', options.CustomParameters_Install); - CustomUpdate = string.Join(' ', options.CustomParameters_Update); - CustomUninstall = string.Join(' ', options.CustomParameters_Uninstall); + CustomInstall = string.Join(' ', options.CustomParameters_Install); + CustomUpdate = string.Join(' ', options.CustomParameters_Update); + CustomUninstall = string.Join(' ', options.CustomParameters_Uninstall); - IsLoading = false; + IsLoading = false; } // ── Location picker ─────────────────────────────────────────────────────── @@ -265,17 +265,17 @@ private async Task SelectLocation(Visual? visual) if (folders is not [{ } folder]) return; var path = folder.TryGetLocalPath(); if (string.IsNullOrEmpty(path)) return; - LocationText = path.TrimEnd('/').TrimEnd('\\') + "/%PACKAGE%"; + LocationText = path.TrimEnd('/').TrimEnd('\\') + "/%PACKAGE%"; LocationResetEnabled = true; - HasChanges = true; + HasChanges = true; } [RelayCommand] private void ResetLocation() { - LocationText = _defaultLocationLabel; + LocationText = _defaultLocationLabel; LocationResetEnabled = false; - HasChanges = true; + HasChanges = true; } // ── Navigation ──────────────────────────────────────────────────────────── diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/PackageManagerViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/PackageManagerViewModel.cs index 25a00fa47..c698cd539 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/PackageManagerViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/PackageManagerViewModel.cs @@ -21,9 +21,9 @@ public partial class PackageManagerViewModel : ViewModelBase public event EventHandler? NavigateToAdministratorRequested; // ── Section headings ────────────────────────────────────────────────────── - public string PageTitle => CoreTools.Translate("{0} settings", Manager.DisplayName); - public string StatusSectionTitle => CoreTools.Translate("{0} status", Manager.DisplayName); - public string InstallSectionTitle => CoreTools.Translate("Default installation options for {0} packages", Manager.DisplayName); + public string PageTitle => CoreTools.Translate("{0} settings", Manager.DisplayName); + public string StatusSectionTitle => CoreTools.Translate("{0} status", Manager.DisplayName); + public string InstallSectionTitle => CoreTools.Translate("Default installation options for {0} packages", Manager.DisplayName); public string SettingsSectionTitle => CoreTools.Translate("{0} settings", Manager.DisplayName); // ── Status bar ──────────────────────────────────────────────────────────── diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/SourceManagerCardViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/SourceManagerCardViewModel.cs index 124081d05..eca52650f 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/SourceManagerCardViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/SourceManagerCardViewModel.cs @@ -3,9 +3,9 @@ using System.Collections.ObjectModel; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; -using UniGetUI.Core.Logging; using CommunityToolkit.Mvvm.Input; using UniGetUI.Avalonia.Infrastructure; +using UniGetUI.Core.Logging; using UniGetUI.Core.Tools; using UniGetUI.PackageEngine.Classes.Manager; using UniGetUI.PackageEngine.Interfaces; @@ -29,12 +29,12 @@ public partial class SourceManagerCardViewModel : ViewModelBase [ObservableProperty] private string _newSourceUrl = ""; [ObservableProperty] private bool _nameUrlEditable = true; - public string TitleText => CoreTools.Translate("Manage {0} sources", _manager.DisplayName); - public string AddLabel { get; } = CoreTools.Translate("Add source"); + public string TitleText => CoreTools.Translate("Manage {0} sources", _manager.DisplayName); + public string AddLabel { get; } = CoreTools.Translate("Add source"); public string AddConfirmLabel { get; } = CoreTools.Translate("Add"); - public string CancelLabel { get; } = CoreTools.Translate("Cancel"); - public string NameHint { get; } = CoreTools.Translate("Source name"); - public string UrlHint { get; } = CoreTools.Translate("Source URL"); + public string CancelLabel { get; } = CoreTools.Translate("Cancel"); + public string NameHint { get; } = CoreTools.Translate("Source name"); + public string UrlHint { get; } = CoreTools.Translate("Source URL"); public SourceManagerCardViewModel(IPackageManager manager) { @@ -68,7 +68,7 @@ private async Task DoLoadSources() try { - var loaded = await Task.Run(() => _manager.SourcesHelper.GetSources()); + var loaded = await Task.Run(_manager.SourcesHelper.GetSources); foreach (var s in loaded) Sources.Add(s); } diff --git a/src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml.cs b/src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml.cs index 4c317f968..e025f033d 100644 --- a/src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml.cs @@ -8,9 +8,9 @@ namespace UniGetUI.Avalonia.Views.Controls; public partial class InfoBar : UserControl { // Icon path data for each severity - private const string InfoPath = "M12,2A10,10,0,1,0,22,12,10,10,0,0,0,12,2Zm1,15H11V11h2Zm0-8H11V7h2Z"; + private const string InfoPath = "M12,2A10,10,0,1,0,22,12,10,10,0,0,0,12,2Zm1,15H11V11h2Zm0-8H11V7h2Z"; private const string WarningPath = "M12,2,1,21H23Zm1,14H11V14h2Zm0-4H11V9h2Z"; - private const string ErrorPath = "M12,2A10,10,0,1,0,22,12,10,10,0,0,0,12,2Zm1,13H11V13h2Zm0-6H11V7h2Z"; + private const string ErrorPath = "M12,2A10,10,0,1,0,22,12,10,10,0,0,0,12,2Zm1,13H11V13h2Zm0-6H11V7h2Z"; private const string SuccessPath = "M12,2A10,10,0,1,0,22,12,10,10,0,0,0,12,2ZM10,17,5,12l1.41-1.41L10,14.17l7.59-7.59L19,8Z"; public InfoBar() @@ -46,10 +46,10 @@ private void ApplySeverity(InfoBarSeverity severity) // Update strip colour var stripColor = severity switch { - InfoBarSeverity.Warning => Color.Parse("#F7A800"), - InfoBarSeverity.Error => Color.Parse("#C42B1C"), - InfoBarSeverity.Success => Color.Parse("#107C10"), - _ => Color.Parse("#0078D4"), + InfoBarSeverity.Warning => Color.Parse("#F7A800"), + InfoBarSeverity.Error => Color.Parse("#C42B1C"), + InfoBarSeverity.Success => Color.Parse("#107C10"), + _ => Color.Parse("#0078D4"), }; SeverityStrip.Background = new SolidColorBrush(stripColor); @@ -57,16 +57,16 @@ private void ApplySeverity(InfoBarSeverity severity) string bgKey = severity switch { InfoBarSeverity.Warning => "WarningBannerBackground", - InfoBarSeverity.Error => "StatusErrorBackground", + InfoBarSeverity.Error => "StatusErrorBackground", InfoBarSeverity.Success => "StatusSuccessBackground", - _ => "StatusInfoBackground", + _ => "StatusInfoBackground", }; string borderKey = severity switch { InfoBarSeverity.Warning => "WarningBannerBorderBrush", - InfoBarSeverity.Error => "StatusErrorBorderBrush", + InfoBarSeverity.Error => "StatusErrorBorderBrush", InfoBarSeverity.Success => "StatusSuccessBorderBrush", - _ => "StatusInfoBorderBrush", + _ => "StatusInfoBorderBrush", }; var theme = Application.Current?.ActualThemeVariant; @@ -79,18 +79,18 @@ private void ApplySeverity(InfoBarSeverity severity) SeverityIcon.Data = Geometry.Parse(severity switch { InfoBarSeverity.Warning => WarningPath, - InfoBarSeverity.Error => ErrorPath, + InfoBarSeverity.Error => ErrorPath, InfoBarSeverity.Success => SuccessPath, - _ => InfoPath, + _ => InfoPath, }); // Icon foreground var iconColor = severity switch { InfoBarSeverity.Warning => Color.Parse("#F7A800"), - InfoBarSeverity.Error => Color.Parse("#C42B1C"), + InfoBarSeverity.Error => Color.Parse("#C42B1C"), InfoBarSeverity.Success => Color.Parse("#107C10"), - _ => Color.Parse("#0078D4"), + _ => Color.Parse("#0078D4"), }; SeverityIcon.Foreground = new SolidColorBrush(iconColor); } diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs index 8d8d077bd..9bc2da9b9 100644 --- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs @@ -103,16 +103,16 @@ public void ShowBanner(string title, string message, RuntimeNotificationLevel le var severity = level switch { - RuntimeNotificationLevel.Error => InfoBarSeverity.Error, + RuntimeNotificationLevel.Error => InfoBarSeverity.Error, RuntimeNotificationLevel.Success => InfoBarSeverity.Success, - _ => InfoBarSeverity.Informational, + _ => InfoBarSeverity.Informational, }; - ViewModel.ErrorBanner.ActionButtonText = ""; + ViewModel.ErrorBanner.ActionButtonText = ""; ViewModel.ErrorBanner.ActionButtonCommand = null; - ViewModel.ErrorBanner.Title = title; - ViewModel.ErrorBanner.Message = message; + ViewModel.ErrorBanner.Title = title; + ViewModel.ErrorBanner.Message = message; ViewModel.ErrorBanner.Severity = severity; - ViewModel.ErrorBanner.IsOpen = true; + ViewModel.ErrorBanner.IsOpen = true; } public void UpdateSystemTrayStatus() diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/InstallOptionsPanel.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/InstallOptionsPanel.axaml.cs index 02fa838f3..fa233cb05 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/InstallOptionsPanel.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/InstallOptionsPanel.axaml.cs @@ -34,8 +34,8 @@ public InstallOptionsPanel(IPackageManager manager) _ = ViewModel.SelectLocationCommand.ExecuteAsync(this); // Mark changed whenever the user edits a CLI textbox - CustomInstallBox.TextChanged += (_, _) => ViewModel.MarkChanged(); - CustomUpdateBox.TextChanged += (_, _) => ViewModel.MarkChanged(); + CustomInstallBox.TextChanged += (_, _) => ViewModel.MarkChanged(); + CustomUpdateBox.TextChanged += (_, _) => ViewModel.MarkChanged(); CustomUninstallBox.TextChanged += (_, _) => ViewModel.MarkChanged(); } } diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs index 15adf1aea..bcac9ab48 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs @@ -1,8 +1,8 @@ using Avalonia.Controls; using Avalonia.Input.Platform; using Avalonia.Layout; -using UniGetUI.Avalonia.ViewModels; using Avalonia.Media; +using UniGetUI.Avalonia.ViewModels; using UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; using UniGetUI.Avalonia.Views.Controls; using UniGetUI.Avalonia.Views.Controls.Settings; @@ -12,8 +12,8 @@ using UniGetUI.PackageEngine.Interfaces; using UniGetUI.PackageEngine.Managers.VcpkgManager; using CoreSettings = UniGetUI.Core.SettingsEngine.Settings; -using Thickness = global::Avalonia.Thickness; using CornerRadius = global::Avalonia.CornerRadius; +using Thickness = global::Avalonia.Thickness; namespace UniGetUI.Avalonia.Views.Pages.SettingsPages; @@ -138,7 +138,7 @@ private void BuildPage() CornerRadius = new CornerRadius(0), Header = CoreTools.Translate("Select the executable to be used. The following list shows the executables found by UniGetUI"), Description = execGrid, - Margin = new Thickness(0, 2, 0, 2), + Margin = new Thickness(0, 2, 0, 2), }; // ── Current path card @@ -422,8 +422,8 @@ private void ApplyStatusBrushes() { ManagerStatusSeverity.Success => "status-success", ManagerStatusSeverity.Warning => "status-warning", - ManagerStatusSeverity.Error => "status-error", - _ => "status-info", + ManagerStatusSeverity.Error => "status-error", + _ => "status-info", }; StatusBar.Classes.Add(cls); } From d02f49fc3290af8b0de3248b186ec444201651d7 Mon Sep 17 00:00:00 2001 From: Gabriel Dufresne Date: Tue, 31 Mar 2026 08:29:03 -0400 Subject: [PATCH 14/15] Fixed error CS0759 --- src/UniGetUI.Avalonia/Infrastructure/generate-secrets.ps1 | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/UniGetUI.Avalonia/Infrastructure/generate-secrets.ps1 b/src/UniGetUI.Avalonia/Infrastructure/generate-secrets.ps1 index cd4923eb6..6860fc41f 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/generate-secrets.ps1 +++ b/src/UniGetUI.Avalonia/Infrastructure/generate-secrets.ps1 @@ -12,10 +12,8 @@ if (-not (Test-Path -Path $generatedDir)) { } $clientId = $env:UNIGETUI_GITHUB_CLIENT_ID -$clientSecret = $env:UNIGETUI_GITHUB_CLIENT_SECRET if (-not $clientId) { $clientId = "CLIENT_ID_UNSET" } -if (-not $clientSecret) { $clientSecret = "CLIENT_SECRET_UNSET" } @" // Auto-generated file - do not modify @@ -24,7 +22,6 @@ namespace UniGetUI.Avalonia.Infrastructure internal static partial class Secrets { public static partial string GetGitHubClientId() => `"$clientId`"; - public static partial string GetGitHubClientSecret() => `"$clientSecret`"; } } "@ | Set-Content -Encoding UTF8 "Generated Files\Secrets.Generated.cs" From 44945792975c58f4cd1c295ab19deaf7c4af1c8c Mon Sep 17 00:00:00 2001 From: Gabriel Dufresne Date: Tue, 31 Mar 2026 08:48:17 -0400 Subject: [PATCH 15/15] use the generic HTTP Client Parameters --- .../ViewModels/Controls/UserAvatarViewModel.cs | 2 +- .../ViewModels/Pages/SettingsPages/BackupViewModel.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/UniGetUI.Avalonia/ViewModels/Controls/UserAvatarViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Controls/UserAvatarViewModel.cs index 15ab82e49..fafd483ba 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Controls/UserAvatarViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Controls/UserAvatarViewModel.cs @@ -67,7 +67,7 @@ private async Task RefreshAsync() if (!string.IsNullOrEmpty(user.AvatarUrl)) { - using var http = new HttpClient(); + using var http = new HttpClient(CoreTools.GenericHttpClientParameters); byte[] bytes = await http.GetByteArrayAsync(user.AvatarUrl); using var ms = new MemoryStream(bytes); bitmap = new Bitmap(ms); diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/BackupViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/BackupViewModel.cs index ed2cba699..62ba64b44 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/BackupViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/BackupViewModel.cs @@ -166,7 +166,7 @@ private async Task GenerateLogoutState() try { - using var http = new HttpClient(); + using var http = new HttpClient(CoreTools.GenericHttpClientParameters); var bytes = await http.GetByteArrayAsync(user.AvatarUrl); using var ms = new MemoryStream(bytes); GitHubAvatarBitmap = new Bitmap(ms);