diff --git a/src/UniGetUI.Avalonia/App.axaml b/src/UniGetUI.Avalonia/App.axaml index 78f626305..acfa80dd8 100644 --- a/src/UniGetUI.Avalonia/App.axaml +++ b/src/UniGetUI.Avalonia/App.axaml @@ -2,102 +2,14 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="UniGetUI.Avalonia.App" xmlns:avaloniaApplication1="clr-namespace:UniGetUI.Avalonia" - xmlns:controls="using:UniGetUI.Avalonia.Views.Controls" RequestedThemeVariant="Default"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/src/UniGetUI.Avalonia/App.axaml.cs b/src/UniGetUI.Avalonia/App.axaml.cs index bcaff1376..d3ef1afde 100644 --- a/src/UniGetUI.Avalonia/App.axaml.cs +++ b/src/UniGetUI.Avalonia/App.axaml.cs @@ -3,6 +3,7 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Data.Core.Plugins; using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Styling; using Avalonia.Styling; using UniGetUI.Avalonia.Views; using UniGetUI.PackageEngine; @@ -15,6 +16,15 @@ public partial class App : Application public override void Initialize() { AvaloniaXamlLoader.Load(this); + + string platform = OperatingSystem.IsWindows() ? "Windows" + : OperatingSystem.IsMacOS() ? "macOS" + : "Linux"; + + Styles.Add(new StyleInclude(new Uri("avares://UniGetUI.Avalonia/")) + { + Source = new Uri($"avares://UniGetUI.Avalonia/Assets/Styles/Styles.{platform}.axaml") + }); } public override void OnFrameworkInitializationCompleted() diff --git a/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml b/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml new file mode 100644 index 000000000..0ffcf9ff6 --- /dev/null +++ b/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Assets/Styles/Styles.Linux.axaml b/src/UniGetUI.Avalonia/Assets/Styles/Styles.Linux.axaml new file mode 100644 index 000000000..06928f792 --- /dev/null +++ b/src/UniGetUI.Avalonia/Assets/Styles/Styles.Linux.axaml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Assets/Styles/Styles.Windows.axaml b/src/UniGetUI.Avalonia/Assets/Styles/Styles.Windows.axaml new file mode 100644 index 000000000..06928f792 --- /dev/null +++ b/src/UniGetUI.Avalonia/Assets/Styles/Styles.Windows.axaml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Assets/Styles/Styles.macOS.axaml b/src/UniGetUI.Avalonia/Assets/Styles/Styles.macOS.axaml new file mode 100644 index 000000000..06928f792 --- /dev/null +++ b/src/UniGetUI.Avalonia/Assets/Styles/Styles.macOS.axaml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaOperationRegistry.cs b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaOperationRegistry.cs index 7860121cb..c9f87837b 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaOperationRegistry.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaOperationRegistry.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using Avalonia; +using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Threading; @@ -23,7 +24,7 @@ public static class AvaloniaOperationRegistry public static readonly ObservableCollection Operations = new(); /// Bindable view-models shown in the operations panel. - public static readonly ObservableCollection OperationViewModels = new(); + public static readonly AvaloniaList OperationViewModels = new(); /// /// Register an operation and create its UI view-model. diff --git a/src/UniGetUI.Avalonia/MarkupExtensions/TranslateExtension.cs b/src/UniGetUI.Avalonia/MarkupExtensions/TranslateExtension.cs new file mode 100644 index 000000000..b068c219f --- /dev/null +++ b/src/UniGetUI.Avalonia/MarkupExtensions/TranslateExtension.cs @@ -0,0 +1,21 @@ +using Avalonia.Markup.Xaml; +using UniGetUI.Core.Tools; + +namespace UniGetUI.Avalonia.MarkupExtensions; + +/// +/// Markup extension that translates a string at load time. +/// Usage: Text="{t:Translate Some text}" +/// For strings with commas use named-property form: Text="{t:Translate Text='A, B, C'}" +/// +public class TranslateExtension : MarkupExtension +{ + public TranslateExtension() { } + public TranslateExtension(string text) => Text = text; + + [ConstructorArgument("text")] + public string Text { get; set; } = ""; + + public override object ProvideValue(IServiceProvider serviceProvider) + => CoreTools.Translate(Text); +} diff --git a/src/UniGetUI.Avalonia/Models/PackageCollections.cs b/src/UniGetUI.Avalonia/Models/PackageCollections.cs index d5c0f7c28..c8442fd35 100644 --- a/src/UniGetUI.Avalonia/Models/PackageCollections.cs +++ b/src/UniGetUI.Avalonia/Models/PackageCollections.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using System.ComponentModel; +using Avalonia.Collections; using UniGetUI.Avalonia.ViewModels.Pages; using UniGetUI.Interface.Enums; using UniGetUI.PackageEngine.Interfaces; @@ -58,9 +59,7 @@ public PackageWrapper(IPackage package, PackagesPageViewModel page) { Package = package; _page = page; - VersionComboString = package.IsUpgradable - ? $"{package.VersionString} -> {package.NewVersionString}" - : package.VersionString; + VersionComboString = package.VersionString; Package.PropertyChanged += Package_PropertyChanged; UpdateDisplayState(); @@ -104,7 +103,7 @@ public void Dispose() /// Avalonia-compatible observable collection of PackageWrapper with sorting support /// (replaces WinUI's ObservablePackageCollection that used SortableObservableCollection). /// -public sealed class ObservablePackageCollection : ObservableCollection +public sealed class ObservablePackageCollection : AvaloniaList { public enum Sorter { diff --git a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj index 9e39a8861..29249d077 100644 --- a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj +++ b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj @@ -63,6 +63,9 @@ + + + @@ -132,6 +135,9 @@ ManagersHomepage.axaml Code + + OperationOutputWindow.axaml + diff --git a/src/UniGetUI.Avalonia/ViewModels/DialogPages/ManageIgnoredUpdatesViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/DialogPages/ManageIgnoredUpdatesViewModel.cs index 60f36e274..5a740ddaf 100644 --- a/src/UniGetUI.Avalonia/ViewModels/DialogPages/ManageIgnoredUpdatesViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/DialogPages/ManageIgnoredUpdatesViewModel.cs @@ -14,16 +14,16 @@ public partial class ManageIgnoredUpdatesViewModel : ObservableObject public event EventHandler? CloseRequested; public string Title { get; } = CoreTools.Translate("Manage ignored updates"); - public string Description { get; } = CoreTools.Translate("The packages listed here won't be taken in account when checking for updates. Click the button on their right to stop ignoring their updates."); + public string Description { get; } = CoreTools.Translate("The packages listed here won't be taken in account when checking for updates. Double-click them or click the button on their right to stop ignoring their updates."); public string ResetLabel { get; } = CoreTools.Translate("Reset list"); public string ResetConfirm { get; } = CoreTools.Translate("Do you really want to reset the ignored updates list? This action cannot be reverted"); public string ResetYes { get; } = CoreTools.Translate("Yes"); public string ResetNo { get; } = CoreTools.Translate("No"); public string EmptyLabel { get; } = CoreTools.Translate("No ignored updates"); - public string ColName { get; } = CoreTools.Translate("Package name"); + public string ColName { get; } = CoreTools.Translate("Package Name"); public string ColId { get; } = CoreTools.Translate("Package ID"); public string ColVersion { get; } = CoreTools.Translate("Ignored version"); - public string ColNewVersion { get; } = CoreTools.Translate("Available update"); + public string ColNewVersion { get; } = CoreTools.Translate("New version"); public string ColManager { get; } = CoreTools.Translate("Source"); public ObservableCollection Entries { get; } = []; diff --git a/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationOutputViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationOutputViewModel.cs new file mode 100644 index 000000000..11e07aa2a --- /dev/null +++ b/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationOutputViewModel.cs @@ -0,0 +1,16 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using UniGetUI.PackageOperations; + +namespace UniGetUI.Avalonia.ViewModels.DialogPages; + +public partial class OperationOutputViewModel : ObservableObject +{ + [ObservableProperty] private string _title = ""; + [ObservableProperty] private string _outputText = ""; + + public OperationOutputViewModel(AbstractOperation operation) + { + Title = operation.Metadata.Title; + OutputText = string.Join("\n", operation.GetOutput().Select(x => x.Item1)); + } +} diff --git a/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationViewModel.cs index fdbba1fdf..1011cd473 100644 --- a/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationViewModel.cs @@ -1,21 +1,20 @@ using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Runtime.CompilerServices; using System.Windows.Input; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Media; using Avalonia.Threading; +using CommunityToolkit.Mvvm.ComponentModel; using UniGetUI.Avalonia.Infrastructure; -using UniGetUI.Avalonia.Views; +using UniGetUI.Avalonia.Views.DialogPages; using UniGetUI.Core.Tools; using UniGetUI.PackageEngine.Enums; using UniGetUI.PackageOperations; namespace UniGetUI.Avalonia.ViewModels; -public sealed class OperationViewModel : INotifyPropertyChanged +public sealed partial class OperationViewModel : ViewModelBase { public AbstractOperation Operation { get; } @@ -25,6 +24,15 @@ public sealed class OperationViewModel : INotifyPropertyChanged public ICommand ButtonCommand { get; } public ICommand ShowDetailsCommand { get; } + // ── Bindable properties ─────────────────────────────────────────────────── + [ObservableProperty] private string _title; + [ObservableProperty] private string _liveLine; + [ObservableProperty] private string _buttonText; + [ObservableProperty] private bool _progressIndeterminate; + [ObservableProperty] private double _progressValue; + [ObservableProperty] private IBrush _progressBrush; + [ObservableProperty] private IBrush _backgroundBrush; + public OperationViewModel(AbstractOperation operation) { Operation = operation; @@ -124,61 +132,6 @@ private void ShowDetails() } } - // ── Bindable properties ─────────────────────────────────────────────────── - private string _title; - public string Title - { - get => _title; - set { _title = value; OnPropertyChanged(); } - } - - private string _liveLine; - public string LiveLine - { - get => _liveLine; - set { _liveLine = value; OnPropertyChanged(); } - } - - private string _buttonText; - public string ButtonText - { - get => _buttonText; - set { _buttonText = value; OnPropertyChanged(); } - } - - private bool _progressIndeterminate; - public bool ProgressIndeterminate - { - get => _progressIndeterminate; - set { _progressIndeterminate = value; OnPropertyChanged(); } - } - - private double _progressValue; - public double ProgressValue - { - get => _progressValue; - set { _progressValue = value; OnPropertyChanged(); } - } - - private IBrush _progressBrush; - public IBrush ProgressBrush - { - get => _progressBrush; - set { _progressBrush = value; OnPropertyChanged(); } - } - - private IBrush _backgroundBrush; - public IBrush BackgroundBrush - { - get => _backgroundBrush; - set { _backgroundBrush = value; OnPropertyChanged(); } - } - - public event PropertyChangedEventHandler? PropertyChanged; - - private void OnPropertyChanged([CallerMemberName] string? name = null) - => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); - // ── Minimal ICommand implementation ─────────────────────────────────────── private sealed class SyncCommand(Action action) : ICommand { diff --git a/src/UniGetUI.Avalonia/ViewModels/DialogPages/PackageDetailsViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/DialogPages/PackageDetailsViewModel.cs index 3eeb9679e..20bc1b7fd 100644 --- a/src/UniGetUI.Avalonia/ViewModels/DialogPages/PackageDetailsViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/DialogPages/PackageDetailsViewModel.cs @@ -127,15 +127,15 @@ public partial class PackageDetailsViewModel : ObservableObject public string LabelLicense { get; } = CoreTools.Translate("License") + ":"; public string LabelPackageId { get; } = CoreTools.Translate("Package ID") + ":"; public string LabelManifest { get; } = CoreTools.Translate("Manifest") + ":"; - public string LabelInstallerType { get; } = CoreTools.Translate("Installer type") + ":"; - public string LabelInstallerSize { get; } = CoreTools.Translate("Installer size") + ":"; + public string LabelInstallerType { get; } = CoreTools.Translate("Installer Type") + ":"; + public string LabelInstallerSize { get; } = CoreTools.Translate("Size") + ":"; public string LabelInstallerUrl { get; } = CoreTools.Translate("Installer URL") + ":"; - public string LabelUpdateDate { get; } = CoreTools.Translate("Last updated") + ":"; + public string LabelUpdateDate { get; } = CoreTools.Translate("Last updated:"); public string LabelReleaseNotesUrl { get; } = CoreTools.Translate("Release notes URL") + ":"; public string LabelOpen { get; } = CoreTools.Translate("Open"); public string LabelClose { get; } = CoreTools.Translate("Close"); - public string HeaderDetails { get; } = CoreTools.Translate("Details"); - public string HeaderDeps { get; } = CoreTools.Translate("Dependencies"); + public string HeaderDetails { get; } = CoreTools.Translate("Package details"); + public string HeaderDeps { get; } = CoreTools.Translate("Dependencies:"); public string HeaderReleaseNotes { get; } = CoreTools.Translate("Release notes"); public PackageDetailsViewModel(IPackage package, OperationType role) @@ -297,6 +297,7 @@ private static void OpenUrl(string? url) catch { } } + [RelayCommand] public void RequestClose() => CloseRequested?.Invoke(this, EventArgs.Empty); } diff --git a/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs index 8c66fcde4..4937e3eef 100644 --- a/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs @@ -1,10 +1,11 @@ -using System.Collections.ObjectModel; using System.Collections.Specialized; using Avalonia; +using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using UniGetUI.Avalonia.Infrastructure; using UniGetUI.Avalonia.ViewModels.Pages; using UniGetUI.Avalonia.Views; @@ -46,8 +47,7 @@ public partial class MainWindowViewModel : ViewModelBase public event EventHandler? CurrentPageChanged; // ─── Operations panel ───────────────────────────────────────────────────── - public ObservableCollection Operations - => AvaloniaOperationRegistry.OperationViewModels; + public AvaloniaList Operations => AvaloniaOperationRegistry.OperationViewModels; [ObservableProperty] private bool _operationsPanelVisible; @@ -120,6 +120,9 @@ private void OnPageViewModelPropertyChanged(object? sender, System.ComponentMode private bool _telemetryWarnerVisible; // ─── Constructor ───────────────────────────────────────────────────────── + [RelayCommand] + private void ToggleSidebar() => Sidebar.IsPaneOpen = !Sidebar.IsPaneOpen; + public MainWindowViewModel() { DiscoverPage = new DiscoverSoftwarePage(); @@ -337,7 +340,14 @@ 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() { if (CurrentPageContent is ISearchBoxPage page) diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/AdministratorViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/AdministratorViewModel.cs index 1bdafbb40..fc1c5ea48 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/AdministratorViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/AdministratorViewModel.cs @@ -1,12 +1,68 @@ using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using UniGetUI.Avalonia.ViewModels; +using UniGetUI.Core.SettingsEngine; +using UniGetUI.Core.SettingsEngine.SecureSettings; +using UniGetUI.Core.Tools; namespace UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; public partial class AdministratorViewModel : ViewModelBase { - /// - /// True when elevation is NOT prohibited — controls enabled-state of the cache-admin-rights cards. - /// - [ObservableProperty] private bool _isElevationEnabled = true; + public event EventHandler? RestartRequired; + + // ── Warning banner strings ──────────────────────────────────────────── + public string WarningTitle { get; } = CoreTools.Translate("Warning") + "!"; + + public string WarningBody1 { get; } = + CoreTools.Translate("The following settings may pose a security risk, hence they are disabled by default.") + + " " + + CoreTools.Translate("Enable the settings below if and only if you fully understand what they do, and the implications they may have."); + + public string WarningBody2 { get; } = + CoreTools.Translate("The settings will list, in their descriptions, the potential security issues they may have."); + + // ── Observable state ───────────────────────────────────────────────── + /// True when elevation is NOT prohibited — controls enabled-state of the cache-admin-rights cards. + [ObservableProperty] private bool _isElevationEnabled; + + /// Mirrors AllowCLIArguments toggle — controls IsEnabled of AllowImportingCLIArguments. + [ObservableProperty] private bool _isCLIArgumentsEnabled; + + /// Mirrors AllowPrePostOpCommand toggle — controls IsEnabled of AllowImportingPrePostInstallCommands. + [ObservableProperty] private bool _isPrePostCommandsEnabled; + + public AdministratorViewModel() + { + _isElevationEnabled = !Settings.Get(Settings.K.ProhibitElevation); + _isCLIArgumentsEnabled = SecureSettings.Get(SecureSettings.K.AllowCLIArguments); + _isPrePostCommandsEnabled = SecureSettings.Get(SecureSettings.K.AllowPrePostOpCommand); + } + + // ── Commands ───────────────────────────────────────────────────────── + + [RelayCommand] + private void RestartCache() => _ = CoreTools.ResetUACForCurrentProcess(); + + [RelayCommand] + private void ShowRestartRequired() => RestartRequired?.Invoke(this, EventArgs.Empty); + + [RelayCommand] + private void RefreshElevationState() + { + IsElevationEnabled = !Settings.Get(Settings.K.ProhibitElevation); + RestartRequired?.Invoke(this, EventArgs.Empty); + } + + [RelayCommand] + private void RefreshCLIState() + { + IsCLIArgumentsEnabled = SecureSettings.Get(SecureSettings.K.AllowCLIArguments); + } + + [RelayCommand] + private void RefreshPrePostState() + { + IsPrePostCommandsEnabled = SecureSettings.Get(SecureSettings.K.AllowPrePostOpCommand); + } } diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/BackupViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/BackupViewModel.cs index 30a87fc35..75a5de617 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/BackupViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/BackupViewModel.cs @@ -11,10 +11,23 @@ namespace UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; public partial class BackupViewModel : ViewModelBase { + public event EventHandler? RestartRequired; + [ObservableProperty] private bool _isLocalBackupEnabled; [ObservableProperty] private string _backupDirectoryLabel = ""; - public BackupViewModel() => RefreshDirectoryLabel(); + public BackupViewModel() + { + _isLocalBackupEnabled = CoreSettings.Get(CoreSettings.K.EnablePackageBackup_LOCAL); + RefreshDirectoryLabel(); + } + + [RelayCommand] + private void EnableLocalBackupChanged() + { + IsLocalBackupEnabled = CoreSettings.Get(CoreSettings.K.EnablePackageBackup_LOCAL); + RestartRequired?.Invoke(this, EventArgs.Empty); + } private void RefreshDirectoryLabel() { diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/ExperimentalViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/ExperimentalViewModel.cs index b7bb36ce7..869b36b79 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/ExperimentalViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/ExperimentalViewModel.cs @@ -1,7 +1,12 @@ +using CommunityToolkit.Mvvm.Input; using UniGetUI.Avalonia.ViewModels; namespace UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; public partial class ExperimentalViewModel : ViewModelBase { + public event EventHandler? RestartRequired; + + [RelayCommand] + private void ShowRestartRequired() => RestartRequired?.Invoke(this, EventArgs.Empty); } diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/GeneralViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/GeneralViewModel.cs index c84f116b0..a51bcef0a 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/GeneralViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/GeneralViewModel.cs @@ -5,6 +5,7 @@ using global::Avalonia.Platform.Storage; using UniGetUI.Avalonia.ViewModels; using UniGetUI.Avalonia.Views.DialogPages; +using UniGetUI.Avalonia.Views.Pages.SettingsPages; using UniGetUI.Core.Logging; using UniGetUI.Core.Tools; using CoreSettings = global::UniGetUI.Core.SettingsEngine.Settings; @@ -64,5 +65,13 @@ private void ResetSettings(Visual? _) } public event EventHandler? RestartRequired; + public event EventHandler? NavigationRequested; + private void OnRestartRequired() => RestartRequired?.Invoke(this, EventArgs.Empty); + + [RelayCommand] + private void ShowRestartRequired() => RestartRequired?.Invoke(this, EventArgs.Empty); + + [RelayCommand] + private void NavigateToInterface() => NavigationRequested?.Invoke(this, typeof(Interface_P)); } diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/Interface_PViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/Interface_PViewModel.cs index b3d66352f..b4f2a3f24 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/Interface_PViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/Interface_PViewModel.cs @@ -15,6 +15,9 @@ public partial class Interface_PViewModel : ViewModelBase public event EventHandler? RestartRequired; + [RelayCommand] + private void ShowRestartRequired() => RestartRequired?.Invoke(this, EventArgs.Empty); + [RelayCommand] private static void EditAutostartSettings() => CoreTools.Launch("ms-settings:startupapps"); diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/InternetViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/InternetViewModel.cs index fdab2f0b4..606c00b81 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/InternetViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/InternetViewModel.cs @@ -1,14 +1,175 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using UniGetUI.Avalonia.ViewModels; +using UniGetUI.Avalonia.Views.Controls.Settings; +using UniGetUI.Core.Tools; +using UniGetUI.PackageEngine; +using UniGetUI.PackageEngine.ManagerClasses.Manager; using CoreSettings = global::UniGetUI.Core.SettingsEngine.Settings; namespace UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; public partial class InternetViewModel : ViewModelBase { + public event EventHandler? RestartRequired; + + private TextBox? _usernameBox; + private TextBox? _passwordBox; + private ProgressBar? _savingIndicator; + [ObservableProperty] private bool _isProxyEnabled; [ObservableProperty] private bool _isProxyAuthEnabled; + public InternetViewModel() + { + _isProxyEnabled = CoreSettings.Get(CoreSettings.K.EnableProxy); + _isProxyAuthEnabled = CoreSettings.Get(CoreSettings.K.EnableProxyAuth); + } + + public SettingsCard BuildCredentialsCard() + { + _savingIndicator = new ProgressBar + { + IsIndeterminate = true, + Opacity = 0, + Margin = new Thickness(0, -8, 0, 0), + }; + + _usernameBox = new TextBox + { + Watermark = CoreTools.Translate("Username"), + MinWidth = 200, + Margin = new Thickness(0, 0, 0, 4), + }; + + _passwordBox = new TextBox + { + Watermark = CoreTools.Translate("Password"), + MinWidth = 200, + PasswordChar = '●', + }; + + var creds = CoreSettings.GetProxyCredentials(); + if (creds is not null) + { + _usernameBox.Text = creds.UserName; + _passwordBox.Text = creds.Password; + } + + _usernameBox.TextChanged += (_, _) => _ = SaveCredentialsAsync(); + _passwordBox.TextChanged += (_, _) => _ = SaveCredentialsAsync(); + + var stack = new StackPanel { Orientation = Orientation.Vertical }; + stack.Children.Add(_savingIndicator); + stack.Children.Add(_usernameBox); + stack.Children.Add(_passwordBox); + + return new SettingsCard + { + CornerRadius = new CornerRadius(0, 0, 8, 8), + BorderThickness = new Thickness(1, 0, 1, 1), + Header = CoreTools.Translate("Credentials"), + Description = CoreTools.Translate("It is not guaranteed that the provided credentials will be stored safely"), + Content = stack, + }; + } + + public Control BuildProxyCompatTable() + { + var noStr = CoreTools.Translate("No"); + 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 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) + { + managerCol.Children.Add(new TextBlock { Text = manager.DisplayName, TextAlignment = TextAlignment.Center }); + + var proxyLevel = manager.Capabilities.SupportsProxy; + proxyCol.Children.Add(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))); + + authCol.Children.Add(StatusBadge( + manager.Capabilities.SupportsProxyAuth ? yesStr : noStr, + manager.Capabilities.SupportsProxyAuth ? Colors.Green : Colors.Red)); + } + + 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 tableStack = new StackPanel { Orientation = Orientation.Vertical }; + tableStack.Children.Add(headerRow); + tableStack.Children.Add(dataRow); + + return new SettingsCard + { + CornerRadius = new CornerRadius(8), + Header = CoreTools.Translate("Proxy compatibility table"), + Description = tableStack, + }; + } + + private static Border StatusBadge(string text, Color color) => new Border + { + CornerRadius = new CornerRadius(4), + Padding = new Thickness(4, 2), + BorderThickness = new Thickness(1), + 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 async Task SaveCredentialsAsync() + { + if (_usernameBox is null || _passwordBox is null || _savingIndicator is null) return; + _savingIndicator.Opacity = 1; + string u = _usernameBox.Text ?? ""; + string p = _passwordBox.Text ?? ""; + await Task.Delay(500); + if ((_usernameBox.Text ?? "") != u) return; + if ((_passwordBox.Text ?? "") != p) return; + CoreSettings.SetProxyCredentials(u, p); + InternetViewModel.ApplyProxyToProcess(); + _savingIndicator.Opacity = 0; + } + + [RelayCommand] + private void RefreshProxyEnabled() + { + IsProxyEnabled = CoreSettings.Get(CoreSettings.K.EnableProxy); + ApplyProxyToProcess(); + } + + [RelayCommand] + private void RefreshProxyAuthEnabled() + { + IsProxyAuthEnabled = CoreSettings.Get(CoreSettings.K.EnableProxyAuth); + ApplyProxyToProcess(); + } + + [RelayCommand] + private static void ApplyProxy() => ApplyProxyToProcess(); + + [RelayCommand] + private void ShowRestartRequired() => RestartRequired?.Invoke(this, EventArgs.Empty); + public static void ApplyProxyToProcess() { var proxyUri = CoreSettings.GetProxyUrl(); diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/NotificationsViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/NotificationsViewModel.cs index 2e56f1003..5db3b75be 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/NotificationsViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/NotificationsViewModel.cs @@ -1,10 +1,24 @@ using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using UniGetUI.Avalonia.ViewModels; +using CoreSettings = global::UniGetUI.Core.SettingsEngine.Settings; namespace UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; public partial class NotificationsViewModel : ViewModelBase { - [ObservableProperty] private bool _isSystemTrayEnabled = true; + [ObservableProperty] private bool _isSystemTrayEnabled; [ObservableProperty] private bool _isNotificationsEnabled; + + public NotificationsViewModel() + { + _isSystemTrayEnabled = !CoreSettings.Get(CoreSettings.K.DisableSystemTray); + _isNotificationsEnabled = !CoreSettings.Get(CoreSettings.K.DisableNotifications); + } + + [RelayCommand] + private void UpdateNotificationsEnabled() + { + IsNotificationsEnabled = !CoreSettings.Get(CoreSettings.K.DisableNotifications); + } } diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/OperationsViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/OperationsViewModel.cs index 21162b923..cf0893d2c 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/OperationsViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/OperationsViewModel.cs @@ -1,7 +1,33 @@ +using CommunityToolkit.Mvvm.Input; using UniGetUI.Avalonia.ViewModels; +using UniGetUI.Avalonia.Views.Pages.SettingsPages; +using UniGetUI.PackageOperations; +using CoreSettings = global::UniGetUI.Core.SettingsEngine.Settings; namespace UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; public partial class OperationsViewModel : ViewModelBase { + public event EventHandler? RestartRequired; + public event EventHandler? NavigationRequested; + + /// Items for the parallel operation count ComboboxCard. + public IReadOnlyList ParallelOpCounts { get; } = + [.. Enumerable.Range(1, 10).Select(i => i.ToString()), "15", "20", "30", "50", "75", "100"]; + + [RelayCommand] + private void UpdateMaxOperations() + { + if (int.TryParse(CoreSettings.GetValue(CoreSettings.K.ParallelOperationCount), out int value)) + AbstractOperation.MAX_OPERATIONS = value; + } + + [RelayCommand] + private void ShowRestartRequired() => RestartRequired?.Invoke(this, EventArgs.Empty); + + [RelayCommand] + private void NavigateToUpdates() => NavigationRequested?.Invoke(this, typeof(Updates)); + + [RelayCommand] + private void NavigateToAdministrator() => NavigationRequested?.Invoke(this, typeof(Administrator)); } diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/SettingsBasePageViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/SettingsBasePageViewModel.cs index e47bc0e40..eb8cc66c9 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/SettingsBasePageViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/SettingsBasePageViewModel.cs @@ -1,4 +1,6 @@ +using Avalonia.Controls.ApplicationLifetimes; using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using UniGetUI.Avalonia.ViewModels; using UniGetUI.Core.Tools; @@ -9,6 +11,23 @@ public partial class SettingsBasePageViewModel : ViewModelBase [ObservableProperty] private string _title = ""; [ObservableProperty] private bool _isRestartBannerVisible; + public event EventHandler? BackRequested; + public string RestartBannerText => CoreTools.Translate("Restart UniGetUI to fully apply changes"); public string RestartButtonText => CoreTools.Translate("Restart UniGetUI"); + + [RelayCommand] + private void Back() => BackRequested?.Invoke(this, EventArgs.Empty); + + [RelayCommand] + private static void RestartApp() + { + var exe = Environment.ProcessPath; + if (exe is not null) + System.Diagnostics.Process.Start( + new System.Diagnostics.ProcessStartInfo(exe) { UseShellExecute = true }); + (global::Avalonia.Application.Current?.ApplicationLifetime + as IClassicDesktopStyleApplicationLifetime) + ?.Shutdown(); + } } diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/SettingsHomepageViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/SettingsHomepageViewModel.cs index b7ac97527..06d34dfbc 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/SettingsHomepageViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/SettingsHomepageViewModel.cs @@ -1,7 +1,21 @@ +using CommunityToolkit.Mvvm.Input; using UniGetUI.Avalonia.ViewModels; +using UniGetUI.Avalonia.Views.Pages.SettingsPages; namespace UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; public partial class SettingsHomepageViewModel : ViewModelBase { + public event EventHandler? NavigationRequested; + + [RelayCommand] private void NavigateToGeneral() => NavigationRequested?.Invoke(this, typeof(General)); + [RelayCommand] private void NavigateToInterface() => NavigationRequested?.Invoke(this, typeof(Interface_P)); + [RelayCommand] private void NavigateToNotifications() => NavigationRequested?.Invoke(this, typeof(Notifications)); + [RelayCommand] private void NavigateToUpdates() => NavigationRequested?.Invoke(this, typeof(Updates)); + [RelayCommand] private void NavigateToOperations() => NavigationRequested?.Invoke(this, typeof(Operations)); + [RelayCommand] private void NavigateToInternet() => NavigationRequested?.Invoke(this, typeof(Internet)); + [RelayCommand] private void NavigateToBackup() => NavigationRequested?.Invoke(this, typeof(Backup)); + [RelayCommand] private void NavigateToAdministrator() => NavigationRequested?.Invoke(this, typeof(Administrator)); + [RelayCommand] private void NavigateToExperimental() => NavigationRequested?.Invoke(this, typeof(Experimental)); + [RelayCommand] private void NavigateToManagers() => NavigationRequested?.Invoke(this, typeof(ManagersHomepage)); } diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/UpdatesViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/UpdatesViewModel.cs index f9f90ab56..1111d8c03 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/UpdatesViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/UpdatesViewModel.cs @@ -1,9 +1,52 @@ using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using UniGetUI.Avalonia.ViewModels; +using UniGetUI.Avalonia.Views.Pages.SettingsPages; +using UniGetUI.Core.Tools; +using CoreSettings = global::UniGetUI.Core.SettingsEngine.Settings; namespace UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; public partial class UpdatesViewModel : ViewModelBase { + public event EventHandler? RestartRequired; + public event EventHandler? NavigationRequested; + [ObservableProperty] private bool _isAutoCheckEnabled; + + /// Items for the update interval ComboboxCard, in display/value pairs. + public IReadOnlyList<(string Name, string Value)> IntervalItems { get; } = + [ + (CoreTools.Translate("{0} minutes", 10), "600"), + (CoreTools.Translate("{0} minutes", 30), "1800"), + (CoreTools.Translate("1 hour"), "3600"), + (CoreTools.Translate("{0} hours", 2), "7200"), + (CoreTools.Translate("{0} hours", 4), "14400"), + (CoreTools.Translate("{0} hours", 8), "28800"), + (CoreTools.Translate("{0} hours", 12), "43200"), + (CoreTools.Translate("1 day"), "86400"), + (CoreTools.Translate("{0} days", 2), "172800"), + (CoreTools.Translate("{0} days", 3), "259200"), + (CoreTools.Translate("1 week"), "604800"), + ]; + + public UpdatesViewModel() + { + _isAutoCheckEnabled = !CoreSettings.Get(CoreSettings.K.DisableAutoCheckforUpdates); + } + + [RelayCommand] + private void UpdateAutoCheckEnabled() + { + IsAutoCheckEnabled = !CoreSettings.Get(CoreSettings.K.DisableAutoCheckforUpdates); + } + + [RelayCommand] + private void ShowRestartRequired() => RestartRequired?.Invoke(this, EventArgs.Empty); + + [RelayCommand] + private void NavigateToOperations() => NavigationRequested?.Invoke(this, typeof(Operations)); + + [RelayCommand] + private void NavigateToAdministrator() => NavigationRequested?.Invoke(this, typeof(Administrator)); } diff --git a/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs index 57c0b37d4..b51b9769e 100644 --- a/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs @@ -1,5 +1,9 @@ using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using UniGetUI.Avalonia.Views; +using UniGetUI.Core.Data; +using UniGetUI.Core.SettingsEngine; +using UniGetUI.Core.Tools; namespace UniGetUI.Avalonia.ViewModels; @@ -19,6 +23,18 @@ public partial class SidebarViewModel : ViewModelBase partial void OnUpdatesBadgeCountChanged(int value) => UpdatesBadgeVisible = value > 0; + partial void OnUpdatesBadgeVisibleChanged(bool _) + { + OnPropertyChanged(nameof(UpdatesBadgeExpandedVisible)); + OnPropertyChanged(nameof(UpdatesBadgeCompactVisible)); + } + + partial void OnBundlesBadgeVisibleChanged(bool _) + { + OnPropertyChanged(nameof(BundlesBadgeExpandedVisible)); + OnPropertyChanged(nameof(BundlesBadgeCompactVisible)); + } + // ─── Loading indicators ─────────────────────────────────────────────────── [ObservableProperty] private bool _discoverIsLoading; @@ -29,6 +45,27 @@ partial void OnUpdatesBadgeCountChanged(int value) => [ObservableProperty] private bool _installedIsLoading; + // ─── Pane open/closed ───────────────────────────────────────────────────── + [ObservableProperty] + private bool isPaneOpen = !Settings.Get(Settings.K.CollapseNavMenuOnWideScreen); + + partial void OnIsPaneOpenChanged(bool value) + { + Settings.Set(Settings.K.CollapseNavMenuOnWideScreen, !value); + OnPropertyChanged(nameof(PaneWidth)); + OnPropertyChanged(nameof(UpdatesBadgeExpandedVisible)); + OnPropertyChanged(nameof(UpdatesBadgeCompactVisible)); + OnPropertyChanged(nameof(BundlesBadgeExpandedVisible)); + OnPropertyChanged(nameof(BundlesBadgeCompactVisible)); + } + + public double PaneWidth => IsPaneOpen ? 250 : 72; + + public bool UpdatesBadgeExpandedVisible => UpdatesBadgeVisible && IsPaneOpen; + public bool UpdatesBadgeCompactVisible => UpdatesBadgeVisible && !IsPaneOpen; + public bool BundlesBadgeExpandedVisible => BundlesBadgeVisible && IsPaneOpen; + public bool BundlesBadgeCompactVisible => BundlesBadgeVisible && !IsPaneOpen; + // ─── Selected page ──────────────────────────────────────────────────────── [ObservableProperty] private PageType _selectedPageType = PageType.Null; @@ -36,8 +73,14 @@ partial void OnUpdatesBadgeCountChanged(int value) => // ─── Navigation ────────────────────────────────────────────────────────── public event EventHandler? NavigationRequested; - public void RequestNavigation(PageType page) => - NavigationRequested?.Invoke(this, page); + public string VersionLabel { get; } = CoreTools.Translate("WingetUI Version {0}", CoreData.VersionName); + + [RelayCommand] + public void RequestNavigation(string? pageName) + { + if (Enum.TryParse(pageName, out var page)) + NavigationRequested?.Invoke(this, page); + } public void SelectNavButtonForPage(PageType page) => SelectedPageType = page; diff --git a/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs index 4824c7c8a..69c8ea8dc 100644 --- a/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs @@ -2,11 +2,17 @@ using System.Collections.ObjectModel; using System.ComponentModel; using System.Globalization; +using Avalonia; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using UniGetUI.Avalonia.Infrastructure; using UniGetUI.Avalonia.ViewModels; +using UniGetUI.Avalonia.Views.Controls; using UniGetUI.Core.SettingsEngine; using UniGetUI.Core.Tools; using UniGetUI.PackageEngine.Enums; @@ -19,6 +25,8 @@ namespace UniGetUI.Avalonia.ViewModels.Pages; public enum SearchMode { Both, Name, Id, Exact, Similar } +public enum PackageViewMode { List = 0, Grid = 1, Icons = 2 } + public enum ReloadReason { FirstRun, @@ -116,7 +124,7 @@ public partial class PackagesPageViewModel : ViewModelBase [ObservableProperty] private bool _newVersionHeaderVisible; [ObservableProperty] private bool _reloadButtonVisible; [ObservableProperty] private bool _isFilterPaneOpen; - [ObservableProperty] private int _viewMode; + [ObservableProperty] private PackageViewMode _viewMode; [ObservableProperty] private int _sortFieldIndex; [ObservableProperty] private bool _sortAscending = true; [ObservableProperty] private bool _instantSearch = true; @@ -132,8 +140,8 @@ public partial class PackagesPageViewModel : ViewModelBase // ─── Collections ────────────────────────────────────────────────────────── public ObservablePackageCollection FilteredPackages { get; } = new(); - public ObservableCollection SourceNodes { get; } = new(); - public ObservableCollection ToolBarItems { get; } = new(); + public AvaloniaList SourceNodes { get; } = new(); + public AvaloniaList ToolBarItems { get; } = new(); // ─── Internal state ─────────────────────────────────────────────────────── private string _searchQuery = ""; @@ -152,6 +160,17 @@ public partial class PackagesPageViewModel : ViewModelBase public event Action? ShowingContextMenu; public event Action? FocusListRequested; + // ─── Events: view-side dialog/navigation requests ───────────────────────── + /// Fired when the ViewModel wants to navigate to the Help page. + public event Action? HelpRequested; + /// Fired when the ViewModel wants to show the Manage-Ignored-Updates dialog. + public event Action? ManageIgnoredRequested; + /// + /// Fired when the ViewModel has built a share URL. + /// Arguments: (packageName, url). Both null means "nothing to share". + /// + public event Action? SharePackageRequested; + // ─── Constructor ───────────────────────────────────────────────────────── public PackagesPageViewModel(PackagesPageData data) { @@ -183,8 +202,10 @@ public PackagesPageViewModel(PackagesPageData data) InstantSearch = !Settings.GetDictionaryItem(Settings.K.DisableInstantSearch, PageName); - ViewMode = Settings.GetDictionaryItem(Settings.K.PackageListViewMode, PageName); - if (ViewMode < 0 || ViewMode > 2) ViewMode = 0; + var savedMode = Settings.GetDictionaryItem(Settings.K.PackageListViewMode, PageName); + ViewMode = Enum.IsDefined(typeof(PackageViewMode), savedMode) + ? (PackageViewMode)savedMode + : PackageViewMode.List; _localPackagesNode.PackageName = CoreTools.Translate("Local"); @@ -208,6 +229,108 @@ public PackagesPageViewModel(PackagesPageData data) // Toolbar is generated by the View after construction (see AbstractPackagesPage ctor) } + public Button AddToolbarButton(string svgName, string label, Action onClick, bool showLabel = true) + { + var icon = new SvgIcon + { + Path = $"avares://UniGetUI.Avalonia/Assets/Symbols/{svgName}.svg", + Width = 16, + Height = 16, + VerticalAlignment = VerticalAlignment.Center, + }; + + var content = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 4 }; + content.Children.Add(icon); + if (showLabel) + { + content.Children.Add(new TextBlock + { + Text = label, + FontSize = 12, + VerticalAlignment = VerticalAlignment.Center, + }); + } + + var btn = new Button + { + Height = 36, + Padding = new Thickness(8, 4), + CornerRadius = new CornerRadius(4), + Content = content, + }; + ToolTip.SetTip(btn, label); + btn.Click += (_, _) => onClick(); + ToolBarItems.Add(btn); + return btn; + } + + /// Adds a thin vertical separator to the toolbar. + public void AddToolbarSeparator() + { + ToolBarItems.Add(new Separator + { + Width = 1, + Height = 30, + Margin = new Thickness(4, 4), + Background = Application.Current?.FindResource("AppBorderBrush") as IBrush + ?? new SolidColorBrush(Color.FromArgb(60, 255, 255, 255)), + }); + } + + public async Task ShowInfoDialog(Window owner, string title, string message) + { + object? bgResource = null; + Application.Current?.Resources.TryGetResource("AppWindowBackground", Application.Current.ActualThemeVariant, out bgResource); + var dialog = new Window + { + Width = 460, + Height = 180, + CanResize = false, + ShowInTaskbar = false, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + Title = title, + Background = bgResource as IBrush, + }; + + var okBtn = new Button + { + Content = CoreTools.Translate("OK"), + MinWidth = 80, + CornerRadius = new CornerRadius(4), + HorizontalAlignment = HorizontalAlignment.Right, + }; + okBtn.Classes.Add("accent"); + okBtn.Click += (_, _) => dialog.Close(); + + var root = new Grid + { + Margin = new Thickness(20), + RowDefinitions = new RowDefinitions("Auto,*,Auto"), + RowSpacing = 12, + }; + var titleBlock = new TextBlock + { + Text = title, + FontSize = 16, + FontWeight = FontWeight.SemiBold, + }; + var msgBlock = new TextBlock + { + Text = message, + TextWrapping = TextWrapping.Wrap, + Opacity = 0.85, + }; + Grid.SetRow(titleBlock, 0); + Grid.SetRow(msgBlock, 1); + Grid.SetRow(okBtn, 2); + root.Children.Add(titleBlock); + root.Children.Add(msgBlock); + root.Children.Add(okBtn); + dialog.Content = root; + + await dialog.ShowDialog(owner); + } + // ─── Loader events ──────────────────────────────────────────────────────── private void Loader_PackagesChanged(object? sender, PackagesChangedEvent e) { @@ -366,14 +489,14 @@ public async Task LoadPackages(ReloadReason reason = ReloadReason.External) } // ─── Sorting ────────────────────────────────────────────────────────────── - public string SortFieldName => SortFieldIndex switch + public string SortFieldName => CoreTools.Translate(SortFieldIndex switch { 1 => "Id", 2 => "Version", 3 => "New version", 4 => "Source", _ => "Name", - }; + }); partial void OnSortFieldIndexChanged(int value) { @@ -401,7 +524,24 @@ partial void OnInstantSearchChanged(bool value) partial void OnUpperLowerCaseChanged(bool value) => FilterPackages(); partial void OnIgnoreSpecialCharsChanged(bool value) => FilterPackages(); - partial void OnSearchModeChanged(SearchMode value) => FilterPackages(); + partial void OnSearchModeChanged(SearchMode value) + { + OnPropertyChanged(nameof(SearchMode_Both)); + OnPropertyChanged(nameof(SearchMode_Name)); + OnPropertyChanged(nameof(SearchMode_Id)); + OnPropertyChanged(nameof(SearchMode_Exact)); + OnPropertyChanged(nameof(SearchMode_Similar)); + FilterPackages(); + } + + // One bool property per mode — used for two-way RadioButton bindings. + // Mutual exclusion is enforced by the ViewModel: setting any one to true + // changes SearchMode, which notifies all five properties. + public bool SearchMode_Both { get => SearchMode == SearchMode.Both; set { if (value) SearchMode = SearchMode.Both; } } + public bool SearchMode_Name { get => SearchMode == SearchMode.Name; set { if (value) SearchMode = SearchMode.Name; } } + public bool SearchMode_Id { get => SearchMode == SearchMode.Id; set { if (value) SearchMode = SearchMode.Id; } } + public bool SearchMode_Exact { get => SearchMode == SearchMode.Exact; set { if (value) SearchMode = SearchMode.Exact; } } + public bool SearchMode_Similar { get => SearchMode == SearchMode.Similar; set { if (value) SearchMode = SearchMode.Similar; } } partial void OnAllPackagesCheckedChanged(bool? value) { @@ -498,7 +638,7 @@ private void OnRootSourceNodePropertyChanged(object? sender, PropertyChangedEven // ─── Header texts ───────────────────────────────────────────────────────── public void UpdateHeaderTexts() { - bool isList = ViewMode == 0; + bool isList = ViewMode == PackageViewMode.List; NameHeaderText = isList ? CoreTools.Translate("Package Name") : ""; IdHeaderText = isList ? CoreTools.Translate("Package ID") : ""; VersionHeaderText = isList ? CoreTools.Translate("Version") : ""; @@ -506,17 +646,25 @@ public void UpdateHeaderTexts() SourceHeaderText = isList ? CoreTools.Translate("Source") : ""; } - public bool IsListViewMode => ViewMode == 0; - public bool IsGridViewMode => ViewMode == 1; - public bool IsIconsViewMode => ViewMode == 2; + public bool IsListViewMode => ViewMode == PackageViewMode.List; + public bool IsGridViewMode => ViewMode == PackageViewMode.Grid; + public bool IsIconsViewMode => ViewMode == PackageViewMode.Icons; + + // Shim for SelectedIndex="{Binding ViewModeIndex}" in AXAML (ListBox requires int) + public int ViewModeIndex + { + get => (int)ViewMode; + set => ViewMode = (PackageViewMode)value; + } - partial void OnViewModeChanged(int value) + partial void OnViewModeChanged(PackageViewMode value) { UpdateHeaderTexts(); - Settings.SetDictionaryItem(Settings.K.PackageListViewMode, PageName, value); + Settings.SetDictionaryItem(Settings.K.PackageListViewMode, PageName, (int)value); OnPropertyChanged(nameof(IsListViewMode)); OnPropertyChanged(nameof(IsGridViewMode)); OnPropertyChanged(nameof(IsIconsViewMode)); + OnPropertyChanged(nameof(ViewModeIndex)); } // ─── Package count (called by PackageWrapper.IsChecked setter) ──────────── @@ -561,6 +709,33 @@ public void UpdateSubtitle() [RelayCommand] private async Task Reload() => await LoadPackages(ReloadReason.Manual); [RelayCommand] private void SelectAllSources_Cmd() { SelectAllSources(); FilterPackages(); } [RelayCommand] private void ClearSourceSelection_Cmd() { ClearSourceSelection(); FilterPackages(); } + [RelayCommand] private void RequestHelp() => HelpRequested?.Invoke(); + [RelayCommand] private void RequestManageIgnored() => ManageIgnoredRequested?.Invoke(); + + [RelayCommand] + public void RequestShare(IPackage? package) + { + if (package is null || package.Source.IsVirtualManager) + { + SharePackageRequested?.Invoke(null, null); + return; + } + var url = "https://marticliment.com/unigetui/share?" + + "name=" + System.Web.HttpUtility.UrlEncode(package.Name) + + "&id=" + System.Web.HttpUtility.UrlEncode(package.Id) + + "&sourceName=" + System.Web.HttpUtility.UrlEncode(package.Source.Name) + + "&managerName=" + System.Web.HttpUtility.UrlEncode(package.Manager.Name); + SharePackageRequested?.Invoke(package.Name, url); + } + + // ─── Sort commands ──────────────────────────────────────────────────────── + [RelayCommand] private void SortByName() => SortFieldIndex = 0; + [RelayCommand] private void SortById() => SortFieldIndex = 1; + [RelayCommand] private void SortByVersion() => SortFieldIndex = 2; + [RelayCommand] private void SortByNewVersion() => SortFieldIndex = 3; + [RelayCommand] private void SortBySource() => SortFieldIndex = 4; + [RelayCommand] private void SetSortAscending() => SortAscending = true; + [RelayCommand] private void SetSortDescending() => SortAscending = false; [RelayCommand] private void SubmitMegaQuery(string query) diff --git a/src/UniGetUI.Avalonia/Views/Controls/Settings/ButtonCard.cs b/src/UniGetUI.Avalonia/Views/Controls/Settings/ButtonCard.cs index 6c7102141..ea9b24a3d 100644 --- a/src/UniGetUI.Avalonia/Views/Controls/Settings/ButtonCard.cs +++ b/src/UniGetUI.Avalonia/Views/Controls/Settings/ButtonCard.cs @@ -1,4 +1,3 @@ -using System.Windows.Input; using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; @@ -8,34 +7,16 @@ namespace UniGetUI.Avalonia.Views.Controls.Settings; public sealed partial class ButtonCard : SettingsCard { - public static readonly StyledProperty CommandProperty = - AvaloniaProperty.Register(nameof(Command)); - - public static readonly StyledProperty CommandParameterProperty = - AvaloniaProperty.Register(nameof(CommandParameter)); - private readonly Button _button = new(); public string ButtonText { - set => _button.Content = CoreTools.Translate(value); + set => _button.Content = value; } public string Text { - set => Header = CoreTools.Translate(value); - } - - public ICommand? Command - { - get => GetValue(CommandProperty); - set => SetValue(CommandProperty, value); - } - - public object? CommandParameter - { - get => GetValue(CommandParameterProperty); - set => SetValue(CommandParameterProperty, value); + set => Header = value; } public new event EventHandler? Click; @@ -51,8 +32,8 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang { base.OnPropertyChanged(change); if (change.Property == CommandProperty) - _button.Command = (ICommand?)change.NewValue; + _button.Command = Command; else if (change.Property == CommandParameterProperty) - _button.CommandParameter = change.NewValue; + _button.CommandParameter = CommandParameter; } } diff --git a/src/UniGetUI.Avalonia/Views/Controls/Settings/CheckboxButtonCard.cs b/src/UniGetUI.Avalonia/Views/Controls/Settings/CheckboxButtonCard.cs index bf04e4ac6..b384d8896 100644 --- a/src/UniGetUI.Avalonia/Views/Controls/Settings/CheckboxButtonCard.cs +++ b/src/UniGetUI.Avalonia/Views/Controls/Settings/CheckboxButtonCard.cs @@ -36,12 +36,12 @@ public CoreSettings.K SettingName public string CheckboxText { - set => _textblock.Text = CoreTools.Translate(value); + set => _textblock.Text = value; } public string ButtonText { - set => Button.Content = CoreTools.Translate(value); + set => Button.Content = value; } private bool _buttonAlwaysOn; diff --git a/src/UniGetUI.Avalonia/Views/Controls/Settings/CheckboxCard.cs b/src/UniGetUI.Avalonia/Views/Controls/Settings/CheckboxCard.cs index f07e1b0ef..73af7686d 100644 --- a/src/UniGetUI.Avalonia/Views/Controls/Settings/CheckboxCard.cs +++ b/src/UniGetUI.Avalonia/Views/Controls/Settings/CheckboxCard.cs @@ -1,3 +1,4 @@ +using System.Windows.Input; using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; @@ -10,6 +11,15 @@ namespace UniGetUI.Avalonia.Views.Controls.Settings; public partial class CheckboxCard : SettingsCard { + public static readonly StyledProperty StateChangedCommandProperty = + AvaloniaProperty.Register(nameof(StateChangedCommand)); + + public ICommand? StateChangedCommand + { + get => GetValue(StateChangedCommandProperty); + set => SetValue(StateChangedCommandProperty, value); + } + public ToggleSwitch _checkbox; public TextBlock _textblock; public TextBlock _warningBlock; @@ -37,14 +47,14 @@ public CoreSettings.K SettingName public string Text { - set => _textblock.Text = CoreTools.Translate(value); + set => _textblock.Text = value; } public string WarningText { set { - _warningBlock.Text = CoreTools.Translate(value); + _warningBlock.Text = value; _warningBlock.IsVisible = value.Any(); } } @@ -94,6 +104,9 @@ protected virtual void _checkbox_Toggled(object? sender, RoutedEventArgs e) CoreSettings.Set(setting_name, (_checkbox.IsChecked ?? false) ^ IS_INVERTED ^ ForceInversion); StateChanged?.Invoke(this, EventArgs.Empty); _textblock.Opacity = (_checkbox.IsChecked ?? false) ? 1 : 0.7; + var cmd = StateChangedCommand; + if (cmd?.CanExecute(null) == true) + cmd.Execute(null); } } diff --git a/src/UniGetUI.Avalonia/Views/Controls/Settings/ComboboxCard.cs b/src/UniGetUI.Avalonia/Views/Controls/Settings/ComboboxCard.cs index 1d6020706..25f545d04 100644 --- a/src/UniGetUI.Avalonia/Views/Controls/Settings/ComboboxCard.cs +++ b/src/UniGetUI.Avalonia/Views/Controls/Settings/ComboboxCard.cs @@ -1,4 +1,6 @@ using System.Collections.ObjectModel; +using System.Windows.Input; +using Avalonia; using Avalonia.Controls; using UniGetUI.Core.Logging; using UniGetUI.Core.Tools; @@ -8,6 +10,15 @@ namespace UniGetUI.Avalonia.Views.Controls.Settings; public sealed partial class ComboboxCard : SettingsCard { + public static readonly StyledProperty ValueChangedCommandProperty = + AvaloniaProperty.Register(nameof(ValueChangedCommand)); + + public ICommand? ValueChangedCommand + { + get => GetValue(ValueChangedCommandProperty); + set => SetValue(ValueChangedCommandProperty, value); + } + private readonly ComboBox _combobox = new(); private readonly ObservableCollection _elements = []; private readonly Dictionary _values_ref = []; @@ -21,7 +32,7 @@ public CoreSettings.K SettingName public string Text { - set => Header = CoreTools.Translate(value); + set => Header = value; } public event EventHandler? ValueChanged; @@ -63,6 +74,9 @@ public void ShowAddedItems() _values_ref[_combobox.SelectedItem?.ToString() ?? ""] ); ValueChanged?.Invoke(this, EventArgs.Empty); + var cmd = ValueChangedCommand; + if (cmd?.CanExecute(null) == true) + cmd.Execute(null); } catch (Exception ex) { diff --git a/src/UniGetUI.Avalonia/Views/Controls/Settings/SecureCheckboxCard.cs b/src/UniGetUI.Avalonia/Views/Controls/Settings/SecureCheckboxCard.cs index 54c12ecf5..12971e1f5 100644 --- a/src/UniGetUI.Avalonia/Views/Controls/Settings/SecureCheckboxCard.cs +++ b/src/UniGetUI.Avalonia/Views/Controls/Settings/SecureCheckboxCard.cs @@ -1,3 +1,4 @@ +using System.Windows.Input; using Avalonia; using Avalonia.Controls; using Avalonia.Layout; @@ -10,6 +11,15 @@ namespace UniGetUI.Avalonia.Views.Controls.Settings; public partial class SecureCheckboxCard : SettingsCard { + public static readonly StyledProperty StateChangedCommandProperty = + AvaloniaProperty.Register(nameof(StateChangedCommand)); + + public ICommand? StateChangedCommand + { + get => GetValue(StateChangedCommandProperty); + set => SetValue(StateChangedCommandProperty, value); + } + public ToggleSwitch _checkbox; public TextBlock _textblock; public TextBlock _warningBlock; @@ -30,16 +40,6 @@ public SecureSettings.K SettingName } } - public new bool IsEnabled - { - set - { - base.IsEnabled = value; - _warningBlock.Opacity = value ? 1 : 0.2; - } - get => base.IsEnabled; - } - public bool ForceInversion { get; set; } public bool Checked => _checkbox.IsChecked ?? false; @@ -47,18 +47,31 @@ public SecureSettings.K SettingName public string Text { - set => _textblock.Text = CoreTools.Translate(value); + set => _textblock.Text = value; } public string WarningText { set { - _warningBlock.Text = CoreTools.Translate(value); + _warningBlock.Text = FormatTwoLine(value); _warningBlock.IsVisible = value.Any(); } } + // Splits translated warning text at the first sentence boundary so it renders + // on two readable lines. Handles both Latin (". ") and CJK ("。") separators. + private static string FormatTwoLine(string text) + { + var idx = text.IndexOf(". ", StringComparison.Ordinal); + if (idx >= 0) + return text[..(idx + 1)] + "\n" + text[(idx + 2)..]; + idx = text.IndexOf('。'); + if (idx >= 0) + return text[..(idx + 1)] + "\n" + text[(idx + 1)..]; + return text; + } + public SecureCheckboxCard() { _checkbox = new ToggleSwitch @@ -104,6 +117,18 @@ public SecureCheckboxCard() }; _checkbox.IsCheckedChanged += (s, e) => _ = _checkbox_Toggled(); + + this.GetObservable(IsEnabledProperty) + .Subscribe(enabled => _warningBlock.Opacity = enabled ? 1 : 0.2); + + // The Devolutions SettingsCard measures the Header with infinite width, so + // TextWrapping alone won't constrain the warning block. We fix it by updating + // MaxWidth after every layout pass, leaving room for the Content (toggle) area. + SizeChanged += (_, e) => + { + var contentWidth = (Content as Control)?.Bounds.Width ?? 0; + _warningBlock.MaxWidth = Math.Max(100, e.NewSize.Width - contentWidth - 48); + }; } protected virtual async Task _checkbox_Toggled() @@ -119,6 +144,9 @@ await SecureSettings.TrySet( (_checkbox.IsChecked ?? false) ^ IS_INVERTED ^ ForceInversion ); StateChanged?.Invoke(this, EventArgs.Empty); + var cmd = StateChangedCommand; + if (cmd?.CanExecute(null) == true) + cmd.Execute(null); _textblock.Opacity = (_checkbox.IsChecked ?? false) ? 1 : 0.7; _checkbox.IsChecked = SecureSettings.Get(setting_name) ^ IS_INVERTED ^ ForceInversion; _loading.IsVisible = false; diff --git a/src/UniGetUI.Avalonia/Views/Controls/Settings/SettingsCard.cs b/src/UniGetUI.Avalonia/Views/Controls/Settings/SettingsCard.cs index 271e27038..4f716b2f8 100644 --- a/src/UniGetUI.Avalonia/Views/Controls/Settings/SettingsCard.cs +++ b/src/UniGetUI.Avalonia/Views/Controls/Settings/SettingsCard.cs @@ -1,3 +1,4 @@ +using System.Windows.Input; using Avalonia; using Avalonia.Controls; using Avalonia.Input; @@ -21,9 +22,20 @@ public class SettingsCard : UserControl private readonly ContentControl _contentPresenter; private readonly StackPanel _descriptionRow; + // ── Styled properties ────────────────────────────────────────────────── + public static readonly StyledProperty HeaderProperty = + AvaloniaProperty.Register(nameof(Header)); + + public static readonly StyledProperty DescriptionProperty = + AvaloniaProperty.Register(nameof(Description)); + + public static readonly StyledProperty CommandProperty = + AvaloniaProperty.Register(nameof(Command)); + + public static readonly StyledProperty CommandParameterProperty = + AvaloniaProperty.Register(nameof(CommandParameter)); + // ── Backing stores ───────────────────────────────────────────────────── - private object? _header; - private object? _description; private Control? _headerIcon; private object? _rightContent; private bool _isClickEnabled; @@ -47,43 +59,26 @@ public class SettingsCard : UserControl public object? Header { - get => _header; - set - { - _header = value; - _headerPresenter.Content = value is string s - ? new TextBlock - { - Text = s, - TextWrapping = TextWrapping.Wrap, - VerticalAlignment = VerticalAlignment.Center, - } - : value; - } + get => GetValue(HeaderProperty); + set => SetValue(HeaderProperty, value); + } + + public ICommand? Command + { + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); + } + + public object? CommandParameter + { + get => GetValue(CommandParameterProperty); + set => SetValue(CommandParameterProperty, value); } public object? Description { - get => _description; - set - { - _description = value; - if (value is null) - { - _descriptionRow.IsVisible = false; - return; - } - _descriptionPresenter.Content = value is string s - ? new TextBlock - { - Text = s, - TextWrapping = TextWrapping.Wrap, - FontSize = 12, - Opacity = 0.7, - } - : value; - _descriptionRow.IsVisible = true; - } + get => GetValue(DescriptionProperty); + set => SetValue(DescriptionProperty, value); } public Control? HeaderIcon @@ -200,9 +195,52 @@ public SettingsCard() PointerPressed += OnPointerPressed; } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == HeaderProperty) + { + var value = change.NewValue; + _headerPresenter.Content = value is string s + ? new TextBlock + { + Text = s, + TextWrapping = TextWrapping.Wrap, + VerticalAlignment = VerticalAlignment.Center, + } + : value; + } + else if (change.Property == DescriptionProperty) + { + var value = change.NewValue; + if (value is null) + { + _descriptionRow.IsVisible = false; + return; + } + _descriptionPresenter.Content = value is string s + ? new TextBlock + { + Text = s, + TextWrapping = TextWrapping.Wrap, + FontSize = 12, + Opacity = 0.7, + } + : value; + _descriptionRow.IsVisible = true; + } + } + private void OnPointerPressed(object? sender, PointerPressedEventArgs e) { - if (_isClickEnabled) - Click?.Invoke(this, new RoutedEventArgs()); + if (!_isClickEnabled) return; + if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return; + + e.Handled = true; + Click?.Invoke(this, new RoutedEventArgs()); + var cmd = Command; + var param = CommandParameter; + if (cmd?.CanExecute(param) == true) + cmd.Execute(param); } } diff --git a/src/UniGetUI.Avalonia/Views/Controls/Settings/SettingsPageButton.cs b/src/UniGetUI.Avalonia/Views/Controls/Settings/SettingsPageButton.cs index e868e4f5e..6d6084322 100644 --- a/src/UniGetUI.Avalonia/Views/Controls/Settings/SettingsPageButton.cs +++ b/src/UniGetUI.Avalonia/Views/Controls/Settings/SettingsPageButton.cs @@ -12,12 +12,12 @@ public partial class SettingsPageButton : SettingsCard { public string Text { - set => Header = CoreTools.Translate(value); + set => Header = value; } public string UnderText { - set => Description = CoreTools.Translate(value); + set => Description = value; } public IconType Icon diff --git a/src/UniGetUI.Avalonia/Views/Controls/Settings/TextboxCard.cs b/src/UniGetUI.Avalonia/Views/Controls/Settings/TextboxCard.cs index 229548209..c7fc187fd 100644 --- a/src/UniGetUI.Avalonia/Views/Controls/Settings/TextboxCard.cs +++ b/src/UniGetUI.Avalonia/Views/Controls/Settings/TextboxCard.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Windows.Input; using Avalonia; using Avalonia.Controls; using Avalonia.Layout; @@ -9,6 +10,15 @@ namespace UniGetUI.Avalonia.Views.Controls.Settings; public sealed partial class TextboxCard : SettingsCard { + public static readonly StyledProperty ValueChangedCommandProperty = + AvaloniaProperty.Register(nameof(ValueChangedCommand)); + + public ICommand? ValueChangedCommand + { + get => GetValue(ValueChangedCommandProperty); + set => SetValue(ValueChangedCommandProperty, value); + } + private readonly TextBox _textbox; private readonly Button _helpbutton; // WinUI HyperlinkButton → plain Button + Process.Start @@ -27,12 +37,12 @@ public CoreSettings.K SettingName public string Placeholder { - set => _textbox.Watermark = CoreTools.Translate(value); + set => _textbox.Watermark = value; } public string Text { - set => Header = CoreTools.Translate(value); + set => Header = value; } public Uri HelpUrl @@ -82,5 +92,8 @@ public void SaveValue() CoreSettings.Set(setting_name, false); ValueChanged?.Invoke(this, EventArgs.Empty); + var cmd = ValueChangedCommand; + if (cmd?.CanExecute(null) == true) + cmd.Execute(null); } } diff --git a/src/UniGetUI.Avalonia/Views/Controls/Settings/TranslatedTextBlock.cs b/src/UniGetUI.Avalonia/Views/Controls/Settings/TranslatedTextBlock.cs index de821ab4d..b0819b305 100644 --- a/src/UniGetUI.Avalonia/Views/Controls/Settings/TranslatedTextBlock.cs +++ b/src/UniGetUI.Avalonia/Views/Controls/Settings/TranslatedTextBlock.cs @@ -1,11 +1,11 @@ using Avalonia.Controls; -using Avalonia.Media; using UniGetUI.Core.Tools; namespace UniGetUI.Avalonia.Views.Controls.Settings; /// /// A TextBlock whose Text property is automatically translated via CoreTools.Translate. +/// Used for section headers set from code-behind; prefer {t:Translate} in AXAML instead. /// public class TranslatedTextBlock : TextBlock { diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/ManageIgnoredUpdatesWindow.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/ManageIgnoredUpdatesWindow.axaml index fdc928c2e..c99ad473c 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/ManageIgnoredUpdatesWindow.axaml +++ b/src/UniGetUI.Avalonia/Views/DialogPages/ManageIgnoredUpdatesWindow.axaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:UniGetUI.Avalonia.ViewModels" xmlns:controls="using:UniGetUI.Avalonia.Views.Controls" + xmlns:t="using:UniGetUI.Avalonia.MarkupExtensions" x:Class="UniGetUI.Avalonia.Views.ManageIgnoredUpdatesWindow" x:DataType="vm:ManageIgnoredUpdatesViewModel" Width="900" MinWidth="520" @@ -21,22 +22,38 @@ - + + + - + + Opacity="0.75"/> - diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/ManageIgnoredUpdatesWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/ManageIgnoredUpdatesWindow.axaml.cs index bfb8754a3..e9ed49210 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/ManageIgnoredUpdatesWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/DialogPages/ManageIgnoredUpdatesWindow.axaml.cs @@ -1,4 +1,5 @@ using Avalonia.Controls; +using Avalonia.Interactivity; using UniGetUI.Avalonia.ViewModels; namespace UniGetUI.Avalonia.Views; @@ -12,4 +13,13 @@ public ManageIgnoredUpdatesWindow() InitializeComponent(); vm.CloseRequested += (_, _) => Close(); } + + private void ResetYes_Click(object? sender, RoutedEventArgs e) + { + ((ManageIgnoredUpdatesViewModel)DataContext!).ResetAllCommand.Execute(null); + ResetButton.Flyout?.Hide(); + } + + private void ResetNo_Click(object? sender, RoutedEventArgs e) => + ResetButton.Flyout?.Hide(); } diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml new file mode 100644 index 000000000..db8fdc66d --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml @@ -0,0 +1,29 @@ + + + + + + + diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml.cs new file mode 100644 index 000000000..3c1efabbe --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia.Controls; +using UniGetUI.Avalonia.ViewModels.DialogPages; +using UniGetUI.PackageOperations; + +namespace UniGetUI.Avalonia.Views.DialogPages; + +public partial class OperationOutputWindow : Window +{ + public OperationOutputWindow(AbstractOperation operation) + { + DataContext = new OperationOutputViewModel(operation); + InitializeComponent(); + } + + protected override void OnOpened(EventArgs e) + { + base.OnOpened(e); + OutputScroll.ScrollToEnd(); + } +} diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.cs b/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.cs deleted file mode 100644 index 76566db64..000000000 --- a/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Layout; -using Avalonia.Media; -using UniGetUI.PackageOperations; - -namespace UniGetUI.Avalonia.Views; - -/// -/// Simple window that shows the full output log of a completed or failed operation. -/// Created in code so no AXAML file is required. -/// -public sealed class OperationOutputWindow : Window -{ - public OperationOutputWindow(AbstractOperation operation) - { - Title = operation.Metadata.Title; - Width = 700; - Height = 500; - MinWidth = 400; - MinHeight = 300; - Background = (Application.Current?.FindResource("AppDialogBackground") as IBrush) ?? new SolidColorBrush(Color.Parse("#1e2025")); - - var lines = string.Join("\n", operation.GetOutput().Select(x => x.Item1)); - - var textBox = new TextBox - { - Text = lines, - IsReadOnly = true, - AcceptsReturn = true, - TextWrapping = TextWrapping.Wrap, - FontFamily = new FontFamily("Cascadia Mono,Consolas,Menlo,monospace"), - FontSize = 12, - Foreground = (Application.Current?.FindResource("SystemControlForegroundBaseHighBrush") as IBrush) ?? new SolidColorBrush(Color.Parse("#d4d4d4")), - Background = Brushes.Transparent, - BorderThickness = new Thickness(0), - VerticalAlignment = VerticalAlignment.Stretch, - }; - - var scroll = new ScrollViewer - { - Content = textBox, - HorizontalScrollBarVisibility = global::Avalonia.Controls.Primitives.ScrollBarVisibility.Auto, - VerticalScrollBarVisibility = global::Avalonia.Controls.Primitives.ScrollBarVisibility.Auto, - Margin = new Thickness(8), - }; - - Content = scroll; - - // Scroll to bottom once shown - Opened += (_, _) => scroll.ScrollToEnd(); - } -} diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml index 8a3ac91ad..be497430f 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml +++ b/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml @@ -28,8 +28,8 @@ - - @@ -361,8 +361,8 @@ - + + + Command="{Binding SubmitGlobalSearchCommand}"> diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs index 025b5fb88..424b75fff 100644 --- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs @@ -1,6 +1,5 @@ using Avalonia.Controls; using Avalonia.Input; -using Avalonia.Interactivity; using UniGetUI.Avalonia.ViewModels; using UniGetUI.Avalonia.Views.Pages; @@ -88,22 +87,6 @@ private void SearchBox_KeyDown(object? sender, KeyEventArgs e) ViewModel.SubmitGlobalSearch(); } - private void SearchButton_Click(object? sender, RoutedEventArgs e) => - ViewModel.SubmitGlobalSearch(); - - // ─── Banner close handlers ──────────────────────────────────────────────── - private void UpdatesBannerCloseButton_Click(object? sender, RoutedEventArgs e) => - ViewModel.UpdatesBannerVisible = false; - - private void ErrorBannerCloseButton_Click(object? sender, RoutedEventArgs e) => - ViewModel.ErrorBannerVisible = false; - - private void WinGetWarningBannerCloseButton_Click(object? sender, RoutedEventArgs e) => - ViewModel.WinGetWarningBannerVisible = false; - - private void TelemetryWarnerCloseButton_Click(object? sender, RoutedEventArgs e) => - ViewModel.TelemetryWarnerVisible = false; - // ─── Public navigation API ──────────────────────────────────────────────── public void Navigate(PageType type) => ViewModel.NavigateTo(type); diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Administrator.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Administrator.axaml index 6174ab2c9..f086046d6 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Administrator.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Administrator.axaml @@ -3,6 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:UniGetUI.Avalonia.ViewModels.Pages.SettingsPages" xmlns:settings="using:UniGetUI.Avalonia.Views.Controls.Settings" + 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" @@ -31,16 +32,16 @@ - - - @@ -51,28 +52,40 @@ FontWeight="SemiBold" Margin="44,32,4,8"/> - - - - - @@ -80,17 +93,26 @@ FontWeight="SemiBold" Margin="44,32,4,8"/> - - - diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Administrator.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Administrator.axaml.cs index c09eb8e9d..cf4a25fd5 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Administrator.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Administrator.axaml.cs @@ -1,7 +1,5 @@ using Avalonia.Controls; using UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; -using UniGetUI.Core.SettingsEngine; -using UniGetUI.Core.SettingsEngine.SecureSettings; using UniGetUI.Core.Tools; namespace UniGetUI.Avalonia.Views.Pages.SettingsPages; @@ -16,99 +14,10 @@ public sealed partial class Administrator : UserControl, ISettingsPage public event EventHandler? RestartRequired; public event EventHandler? NavigationRequested { add { } remove { } } - public void ShowRestartBanner(object? sender, EventArgs e) => - RestartRequired?.Invoke(this, e); - - public void RestartCache(object? sender, EventArgs e) => - _ = CoreTools.ResetUACForCurrentProcess(); - public Administrator() { DataContext = new AdministratorViewModel(); InitializeComponent(); - - // Populate the warning banner - WarningTitleText.Text = CoreTools.Translate("Warning") + "!"; - WarningBodyLine1.Text = - CoreTools.Translate("The following settings may pose a security risk, hence they are disabled by default.") - + " " - + CoreTools.Translate("Enable the settings below IF AND ONLY IF you fully understand what they do, and the implications and dangers they may involve."); - WarningBodyLine2.Text = - CoreTools.Translate("The settings will list, in their descriptions, the potential security issues they may have."); - - // Admin rights section - DoCacheAdminRightsForBatches.SettingName = Settings.K.DoCacheAdminRightsForBatches; - DoCacheAdminRightsForBatches.Text = "Ask for administrator privileges once for each batch of operations"; - DoCacheAdminRightsForBatches.StateChanged += RestartCache; - - DoCacheAdminRights.SettingName = Settings.K.DoCacheAdminRights; - DoCacheAdminRights.Text = "Ask only once for administrator privileges"; - DoCacheAdminRights.StateChanged += RestartCache; - - ProhibitElevator.SettingName = Settings.K.ProhibitElevation; - ProhibitElevator.Text = "Prohibit any kind of Elevation via UniGetUI Elevator or GSudo"; - ProhibitElevator.StateChanged += ShowRestartBanner; - - // Bind IsElevationEnabled to the ProhibitElevator toggle (inverted) - bool initialElevated = !(ProhibitElevator._checkbox.IsChecked ?? false); - VM.IsElevationEnabled = initialElevated; - - ProhibitElevator._checkbox.IsCheckedChanged += (_, _) => - VM.IsElevationEnabled = !(ProhibitElevator._checkbox.IsChecked ?? false); - - // CLI arguments section - AllowCLIArguments.SettingName = SecureSettings.K.AllowCLIArguments; - AllowCLIArguments.Text = "Allow custom command-line arguments"; - AllowCLIArguments._warningBlock.Text = - CoreTools.Translate("Custom command-line arguments can change the way in which programs are installed, upgraded or uninstalled, in a way UniGetUI cannot control.") - + "\n" - + CoreTools.Translate("Using custom command-lines can break packages. Proceed with caution."); - AllowCLIArguments._warningBlock.IsVisible = true; - - AllowPrePostInstallCommands.SettingName = SecureSettings.K.AllowPrePostOpCommand; - AllowPrePostInstallCommands.Text = "Ignore custom pre-install and post-install commands when importing packages from a bundle"; - AllowPrePostInstallCommands._warningBlock.Text = - CoreTools.Translate("Pre and post install commands will be run before and after a package gets installed, upgraded or uninstalled.") - + "\n" - + CoreTools.Translate("Be aware that they may break things unless used carefully."); - AllowPrePostInstallCommands._warningBlock.IsVisible = true; - - // Manager paths section - AllowCustomManagerPaths.SettingName = SecureSettings.K.AllowCustomManagerPaths; - AllowCustomManagerPaths.Text = "Allow changing the paths for package manager executables"; - AllowCustomManagerPaths._warningBlock.Text = - CoreTools.Translate("Turning this on enables changing the executable file used to interact with package managers.") - + "\n" - + CoreTools.Translate("While this allows finer-grained customization of your install processes, it may also be dangerous."); - AllowCustomManagerPaths._warningBlock.IsVisible = true; - AllowCustomManagerPaths.StateChanged += ShowRestartBanner; - - // Bundle import restrictions - AllowImportingCLIArguments.SettingName = SecureSettings.K.AllowImportingCLIArguments; - AllowImportingCLIArguments.Text = "Allow importing custom command-line arguments when importing packages from a bundle"; - AllowImportingCLIArguments._warningBlock.Text = - CoreTools.Translate("Malformed command-line arguments can break packages, or even allow a malicious actor to gain privileged execution.") - + "\n" - + CoreTools.Translate("Therefore, importing custom command-line arguments is disabled by default."); - AllowImportingCLIArguments._warningBlock.IsVisible = true; - - AllowImportingPrePostInstallCommands.SettingName = SecureSettings.K.AllowImportPrePostOpCommands; - AllowImportingPrePostInstallCommands.Text = "Allow importing custom pre-install and post-install commands when importing packages from a bundle"; - AllowImportingPrePostInstallCommands._warningBlock.Text = - CoreTools.Translate("Pre and post install commands can do very nasty things to your device, if designed to do so.") - + "\n" - + CoreTools.Translate("It can be very dangerous to import the commands from a bundle, unless you trust the source of that package bundle."); - AllowImportingPrePostInstallCommands._warningBlock.IsVisible = true; - - // Bind import cards' enabled state directly to parent toggles (imperative, since - // SecureCheckboxCard.IsEnabled overrides the base property and can't use compiled bindings) - AllowImportingCLIArguments.IsEnabled = AllowCLIArguments._checkbox.IsChecked ?? false; - AllowImportingPrePostInstallCommands.IsEnabled = AllowPrePostInstallCommands._checkbox.IsChecked ?? false; - - AllowCLIArguments._checkbox.IsCheckedChanged += (_, _) => - AllowImportingCLIArguments.IsEnabled = AllowCLIArguments._checkbox.IsChecked ?? false; - - AllowPrePostInstallCommands._checkbox.IsCheckedChanged += (_, _) => - AllowImportingPrePostInstallCommands.IsEnabled = AllowPrePostInstallCommands._checkbox.IsChecked ?? false; + VM.RestartRequired += (s, e) => RestartRequired?.Invoke(s, e); } } diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Backup.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Backup.axaml index 3f0f3b1a7..787e0a1e2 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Backup.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Backup.axaml @@ -3,6 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:UniGetUI.Avalonia.ViewModels.Pages.SettingsPages" xmlns:settings="using:UniGetUI.Avalonia.Views.Controls.Settings" + 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" @@ -22,24 +23,26 @@ FontWeight="SemiBold" Margin="44,32,4,8"/> - - @@ -48,12 +51,14 @@ FontWeight="SemiBold" Margin="44,32,4,8"/> - - diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Backup.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Backup.axaml.cs index d44ca1dec..c0c057b36 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Backup.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Backup.axaml.cs @@ -5,51 +5,26 @@ using UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; using UniGetUI.Avalonia.Views.Controls.Settings; using UniGetUI.Core.Tools; -using CoreSettings = global::UniGetUI.Core.SettingsEngine.Settings; namespace UniGetUI.Avalonia.Views.Pages.SettingsPages; public sealed partial class Backup : UserControl, ISettingsPage { - private BackupViewModel VM => (BackupViewModel)DataContext!; - public bool CanGoBack => true; public string ShortTitle => CoreTools.Translate("Backup and Restore"); public event EventHandler? RestartRequired; public event EventHandler? NavigationRequested { add { } remove { } } - private void ShowRestartBanner(object? sender, EventArgs e) => RestartRequired?.Invoke(this, e); - public Backup() { DataContext = new BackupViewModel(); InitializeComponent(); - ChangeBackupDirectory.Description = VM.BackupDirectoryLabel; - VM.PropertyChanged += (_, e) => - { - if (e.PropertyName == nameof(VM.BackupDirectoryLabel)) - ChangeBackupDirectory.Description = VM.BackupDirectoryLabel; - }; + var vm = (BackupViewModel)DataContext; + vm.RestartRequired += (s, e) => RestartRequired?.Invoke(s, e); BuildBackupInfoCard(); - - EnablePackageBackupCheckBox_LOCAL.SettingName = CoreSettings.K.EnablePackageBackup_LOCAL; - EnablePackageBackupCheckBox_LOCAL.Text = "Periodically perform a local backup of the installed packages"; - EnablePackageBackupCheckBox_LOCAL.StateChanged += (_, _) => - { - VM.IsLocalBackupEnabled = EnablePackageBackupCheckBox_LOCAL.Checked; - ShowRestartBanner(this, EventArgs.Empty); - }; - VM.IsLocalBackupEnabled = EnablePackageBackupCheckBox_LOCAL.Checked; - - ChangeBackupFileNameTextBox.SettingName = CoreSettings.K.ChangeBackupFileName; - ChangeBackupFileNameTextBox.Text = "Set a custom backup file name"; - ChangeBackupFileNameTextBox.Placeholder = "Leave empty for default"; - - EnableBackupTimestamping.SettingName = CoreSettings.K.EnableBackupTimestamping; - EnableBackupTimestamping.Text = "Add a timestamp to the backup file names"; } private void BuildBackupInfoCard() diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Experimental.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Experimental.axaml index e24fd418e..6889c6241 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Experimental.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Experimental.axaml @@ -3,6 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:UniGetUI.Avalonia.ViewModels.Pages.SettingsPages" xmlns:settings="using:UniGetUI.Avalonia.Views.Controls.Settings" + 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" @@ -12,49 +13,68 @@ - - - - - - - - diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Experimental.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Experimental.axaml.cs index a0e3caaf7..c9f1a7f35 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Experimental.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Experimental.axaml.cs @@ -1,7 +1,5 @@ using Avalonia.Controls; using UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; -using UniGetUI.Core.SettingsEngine; -using UniGetUI.Core.SettingsEngine.SecureSettings; using UniGetUI.Core.Tools; namespace UniGetUI.Avalonia.Views.Pages.SettingsPages; @@ -14,51 +12,15 @@ public sealed partial class Experimental : UserControl, ISettingsPage public event EventHandler? RestartRequired; public event EventHandler? NavigationRequested { add { } remove { } } - public void ShowRestartBanner(object? sender, EventArgs e) => - RestartRequired?.Invoke(this, e); - public Experimental() { DataContext = new ExperimentalViewModel(); InitializeComponent(); - ShowVersionNumberOnTitlebar.SettingName = Settings.K.ShowVersionNumberOnTitlebar; - ShowVersionNumberOnTitlebar.Text = "Show UniGetUI's version and build number on the titlebar."; - ShowVersionNumberOnTitlebar.StateChanged += ShowRestartBanner; - - DisableWidgetsApi.SettingName = Settings.K.DisableApi; - DisableWidgetsApi.Text = "Enable background api (WingetUI Widgets and Sharing, port 7058)"; - DisableWidgetsApi.StateChanged += ShowRestartBanner; - - DisableWaitForInternetConnection.SettingName = Settings.K.DisableWaitForInternetConnection; - DisableWaitForInternetConnection.Text = "Wait for the device to be connected to the internet before attempting to do tasks that require internet connectivity."; - DisableWaitForInternetConnection.StateChanged += ShowRestartBanner; - - DisableTimeoutOnPackageListingTasks.SettingName = Settings.K.DisableTimeoutOnPackageListingTasks; - DisableTimeoutOnPackageListingTasks.ForceInversion = true; - DisableTimeoutOnPackageListingTasks.Text = "Disable the 1-minute timeout for package-related operations"; - - UseUserGSudoToggle.SettingName = SecureSettings.K.ForceUserGSudo; - UseUserGSudoToggle.Text = "Use installed GSudo instead of UniGetUI Elevator"; - UseUserGSudoToggle.StateChanged += ShowRestartBanner; + var vm = (ExperimentalViewModel)DataContext; + vm.RestartRequired += (s, e) => RestartRequired?.Invoke(s, e); - DisableDownloadingNewTranslations.SettingName = Settings.K.DisableLangAutoUpdater; - DisableDownloadingNewTranslations.Text = "Download updated language files from GitHub automatically"; - DisableDownloadingNewTranslations.StateChanged += ShowRestartBanner; - - IconDatabaseURLCard.SettingName = Settings.K.IconDataBaseURL; + ShowVersionNumberOnTitlebar.Text = CoreTools.Translate("Show UniGetUI's version and build number on the titlebar."); IconDatabaseURLCard.HelpUrl = new Uri("https://www.marticliment.com/unigetui/help/icons-and-screenshots#custom-source"); - IconDatabaseURLCard.Placeholder = "Leave empty for default"; - IconDatabaseURLCard.Text = "Use a custom icon and screenshot database URL"; - IconDatabaseURLCard.ValueChanged += ShowRestartBanner; - - DisableDMWThreadOptimizations.SettingName = Settings.K.DisableDMWThreadOptimizations; - DisableDMWThreadOptimizations.Text = "Enable background CPU Usage optimizations (see Pull Request #3278)"; - - DisableIntegrityChecks.SettingName = Settings.K.DisableIntegrityChecks; - DisableIntegrityChecks.Text = "Perform integrity checks at startup"; - - InstallInstalledPackagesBundlesPage.SettingName = Settings.K.InstallInstalledPackagesBundlesPage; - InstallInstalledPackagesBundlesPage.Text = "When batch installing packages from a bundle, install also packages that are already installed"; } } diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/General.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/General.axaml index 0f7ad8e08..0076b9a61 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/General.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/General.axaml @@ -3,6 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:UniGetUI.Avalonia.ViewModels.Pages.SettingsPages" xmlns:settings="using:UniGetUI.Avalonia.Views.Controls.Settings" + 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" @@ -22,10 +23,14 @@ FontWeight="SemiBold" Margin="44,32,4,8"/> - - @@ -34,8 +39,8 @@ Margin="44,32,4,8"/> @@ -44,21 +49,21 @@ Margin="44,32,4,8"/> @@ -66,12 +71,12 @@ FontWeight="SemiBold" Margin="44,32,4,8"/> - + Text="{t:Translate User interface preferences}" + UnderText="{t:Translate Text='Application theme, startup page, package icons, clear successful installs automatically'}" + Command="{Binding NavigateToInterfaceCommand}"/> diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/General.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/General.axaml.cs index 8cb5c82ca..cc06613c7 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/General.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/General.axaml.cs @@ -18,17 +18,16 @@ public sealed partial class General : UserControl, ISettingsPage public event EventHandler? RestartRequired; public event EventHandler? NavigationRequested; - private void ShowRestartBanner(object? sender, EventArgs e) => RestartRequired?.Invoke(this, e); - public General() { DataContext = new GeneralViewModel(); InitializeComponent(); - // ViewModel events that need to bubble up to the settings shell - ((GeneralViewModel)DataContext).RestartRequired += ShowRestartBanner; + var vm = (GeneralViewModel)DataContext; + vm.RestartRequired += (s, e) => RestartRequired?.Invoke(s, e); + vm.NavigationRequested += (s, t) => NavigationRequested?.Invoke(s, t); - // Populate language selector + // Populate language selector (complex dynamic content) var langDict = new Dictionary(LanguageData.LanguageReference.AsEnumerable()); foreach (string key in langDict.Keys.ToList()) { @@ -38,21 +37,10 @@ public General() foreach (var entry in langDict) LanguageSelector.AddItem(entry.Value, entry.Key, false); LanguageSelector.SettingName = CoreSettings.K.PreferredLanguage; - LanguageSelector.Text = "WingetUI display language:"; + LanguageSelector.Text = CoreTools.Translate("WingetUI display language:"); LanguageSelector.ShowAddedItems(); - LanguageSelector.ValueChanged += ShowRestartBanner; + LanguageSelector.ValueChanged += (s, e) => RestartRequired?.Invoke(s, e); LanguageSelector.Description = BuildTranslatorDescription(); - - DisableAutoUpdateWingetUI.SettingName = CoreSettings.K.DisableAutoUpdateWingetUI; - DisableAutoUpdateWingetUI.CheckboxText = "Update WingetUI automatically"; - DisableAutoUpdateWingetUI.ButtonText = "Check for updates"; - DisableAutoUpdateWingetUI.ButtonAlwaysOn = true; - DisableAutoUpdateWingetUI.Click += (_, _) => { /* auto-updater not available in Avalonia port */ }; - - EnableUniGetUIBeta.SettingName = CoreSettings.K.EnableUniGetUIBeta; - EnableUniGetUIBeta.Text = "Install prerelease versions of UniGetUI"; - - InterfaceSettingsButton.Click += (_, _) => NavigationRequested?.Invoke(this, typeof(Interface_P)); } private static StackPanel BuildTranslatorDescription() @@ -66,7 +54,7 @@ private static StackPanel BuildTranslatorDescription() var link = new TextBlock { - Text = CoreTools.Translate("Become a translator!"), + Text = CoreTools.Translate("Become a translator"), TextDecorations = TextDecorations.Underline, VerticalAlignment = VerticalAlignment.Center, Cursor = new Cursor(StandardCursorType.Hand), diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml index c93e82481..c599711d5 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml @@ -3,6 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:UniGetUI.Avalonia.ViewModels.Pages.SettingsPages" xmlns:settings="using:UniGetUI.Avalonia.Views.Controls.Settings" + 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" @@ -27,14 +28,15 @@ FontWeight="SemiBold" Margin="44,32,4,8"/> - @@ -42,19 +44,24 @@ FontWeight="SemiBold" Margin="44,32,4,8"/> - - diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml.cs index be66f2b93..a25213b76 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml.cs @@ -15,8 +15,6 @@ public sealed partial class Interface_P : UserControl, ISettingsPage public event EventHandler? RestartRequired; public event EventHandler? NavigationRequested { add { } remove { } } - private void ShowRestartBanner(object? sender, EventArgs e) => RestartRequired?.Invoke(this, e); - public Interface_P() { DataContext = new Interface_PViewModel(); @@ -25,44 +23,28 @@ public Interface_P() if (OperatingSystem.IsMacOS()) SystemTraySection.IsVisible = false; - VM.RestartRequired += ShowRestartBanner; - VM.PropertyChanged += (_, e) => - { - if (e.PropertyName == nameof(VM.IconCacheSizeText)) - ResetIconCache.Header = VM.IconCacheSizeText; - }; + VM.RestartRequired += (s, e) => RestartRequired?.Invoke(s, e); _ = VM.LoadIconCacheSize(); if (CoreSettings.GetValue(CoreSettings.K.PreferredTheme) == "") CoreSettings.SetValue(CoreSettings.K.PreferredTheme, "auto"); - ThemeSelector.AddItem(CoreTools.AutoTranslated("Light"), "light"); - ThemeSelector.AddItem(CoreTools.AutoTranslated("Dark"), "dark"); - ThemeSelector.AddItem(CoreTools.AutoTranslated("Follow system color scheme"), "auto"); + ThemeSelector.AddItem(CoreTools.Translate("Light"), "light"); + ThemeSelector.AddItem(CoreTools.Translate("Dark"), "dark"); + ThemeSelector.AddItem(CoreTools.Translate("Follow system color scheme"), "auto"); ThemeSelector.SettingName = CoreSettings.K.PreferredTheme; - ThemeSelector.Text = "Application theme:"; + ThemeSelector.Text = CoreTools.Translate("Application theme:"); ThemeSelector.ShowAddedItems(); ThemeSelector.ValueChanged += (_, _) => App.ApplyTheme(CoreSettings.GetValue(CoreSettings.K.PreferredTheme)); - StartupPageSelector.AddItem(CoreTools.AutoTranslated("Default"), "default"); - StartupPageSelector.AddItem(CoreTools.AutoTranslated("Discover Packages"), "discover"); - StartupPageSelector.AddItem(CoreTools.AutoTranslated("Software Updates"), "updates"); - StartupPageSelector.AddItem(CoreTools.AutoTranslated("Installed Packages"), "installed"); - StartupPageSelector.AddItem(CoreTools.AutoTranslated("Package Bundles"), "bundles"); - StartupPageSelector.AddItem(CoreTools.AutoTranslated("Settings"), "settings"); + StartupPageSelector.AddItem(CoreTools.Translate("Default"), "default"); + StartupPageSelector.AddItem(CoreTools.Translate("Discover Packages"), "discover"); + StartupPageSelector.AddItem(CoreTools.Translate("Software Updates"), "updates"); + StartupPageSelector.AddItem(CoreTools.Translate("Installed Packages"), "installed"); + StartupPageSelector.AddItem(CoreTools.Translate("Package Bundles"), "bundles"); + StartupPageSelector.AddItem(CoreTools.Translate("Settings"), "settings"); StartupPageSelector.SettingName = CoreSettings.K.StartupPage; - StartupPageSelector.Text = "UniGetUI startup page:"; + StartupPageSelector.Text = CoreTools.Translate("UniGetUI startup page:"); StartupPageSelector.ShowAddedItems(); - - DisableSystemTray.SettingName = CoreSettings.K.DisableSystemTray; - DisableSystemTray.Text = "Close UniGetUI to the system tray"; - - DisableIconsOnPackageLists.SettingName = CoreSettings.K.DisableIconsOnPackageLists; - DisableIconsOnPackageLists.Text = "Show package icons on package lists"; - DisableIconsOnPackageLists.StateChanged += ShowRestartBanner; - - DisableSelectingUpdatesByDefault.SettingName = CoreSettings.K.DisableSelectingUpdatesByDefault; - DisableSelectingUpdatesByDefault.Text = "Select upgradable packages by default"; - DisableSelectingUpdatesByDefault.StateChanged += ShowRestartBanner; } } diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Internet.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Internet.axaml index 8c5267716..e4896a2c2 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Internet.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Internet.axaml @@ -3,6 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:UniGetUI.Avalonia.ViewModels.Pages.SettingsPages" xmlns:settings="using:UniGetUI.Avalonia.Views.Controls.Settings" + 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" @@ -15,17 +16,26 @@ FontWeight="SemiBold" Margin="44,32,4,8"/> - - - @@ -40,7 +50,9 @@ FontWeight="SemiBold" Margin="44,32,4,8"/> - diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Internet.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Internet.axaml.cs index f0421c74f..aa473fbe3 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Internet.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Internet.axaml.cs @@ -13,174 +13,21 @@ namespace UniGetUI.Avalonia.Views.Pages.SettingsPages; public sealed partial class Internet : UserControl, ISettingsPage { - private InternetViewModel VM => (InternetViewModel)DataContext!; - public bool CanGoBack => true; public string ShortTitle => CoreTools.Translate("Internet and proxy settings"); public event EventHandler? RestartRequired; public event EventHandler? NavigationRequested { add { } remove { } } - private TextBox? _usernameBox; - private TextBox? _passwordBox; - private ProgressBar? _savingIndicator; - public Internet() { DataContext = new InternetViewModel(); InitializeComponent(); - EnableProxy.SettingName = CoreSettings.K.EnableProxy; - EnableProxy.Text = "Connect the internet using a custom proxy"; - EnableProxy.Description = CoreTools.Translate("Please note that not all package managers may fully support this feature"); - EnableProxy.StateChanged += (_, _) => - { - VM.IsProxyEnabled = EnableProxy.Checked; - InternetViewModel.ApplyProxyToProcess(); - }; - VM.IsProxyEnabled = EnableProxy.Checked; - - ProxyURLCard.SettingName = CoreSettings.K.ProxyURL; - ProxyURLCard.Text = "Proxy URL"; - ProxyURLCard.Placeholder = "Enter proxy URL here"; - ProxyURLCard.ValueChanged += (_, _) => InternetViewModel.ApplyProxyToProcess(); - - EnableProxyAuth.SettingName = CoreSettings.K.EnableProxyAuth; - EnableProxyAuth.Text = "Authenticate to the proxy with a user and a password"; - EnableProxyAuth.Description = CoreTools.Translate("Please note that not all package managers may fully support this feature"); - EnableProxyAuth.StateChanged += (_, _) => - { - VM.IsProxyAuthEnabled = EnableProxyAuth.Checked; - InternetViewModel.ApplyProxyToProcess(); - }; - VM.IsProxyAuthEnabled = EnableProxyAuth.Checked; - - CredentialsHolder.Content = BuildCredentialsCard(); - - ProxyCompatTableHolder.Content = BuildProxyCompatTable(); - - DisableWaitForInternetConnection.SettingName = CoreSettings.K.DisableWaitForInternetConnection; - DisableWaitForInternetConnection.Text = "Wait for the device to be connected to the internet before attempting to do tasks that require internet connectivity."; - DisableWaitForInternetConnection.StateChanged += (_, _) => RestartRequired?.Invoke(this, EventArgs.Empty); - } - - private SettingsCard BuildCredentialsCard() - { - _savingIndicator = new ProgressBar - { - IsIndeterminate = true, - Opacity = 0, - Margin = new Thickness(0, -8, 0, 0), - }; - - _usernameBox = new TextBox - { - Watermark = CoreTools.Translate("Username"), - MinWidth = 200, - Margin = new Thickness(0, 0, 0, 4), - }; - - _passwordBox = new TextBox - { - Watermark = CoreTools.Translate("Password"), - MinWidth = 200, - PasswordChar = '●', - }; - - var creds = CoreSettings.GetProxyCredentials(); - if (creds is not null) - { - _usernameBox.Text = creds.UserName; - _passwordBox.Text = creds.Password; - } - - _usernameBox.TextChanged += (_, _) => _ = SaveCredentialsAsync(); - _passwordBox.TextChanged += (_, _) => _ = SaveCredentialsAsync(); - - var stack = new StackPanel { Orientation = Orientation.Vertical }; - stack.Children.Add(_savingIndicator); - stack.Children.Add(_usernameBox); - stack.Children.Add(_passwordBox); - - return new SettingsCard - { - CornerRadius = new CornerRadius(0, 0, 8, 8), - BorderThickness = new Thickness(1, 0, 1, 1), - Header = CoreTools.Translate("Credentials"), - Description = CoreTools.Translate("It is not guaranteed that the provided credentials will be stored safely"), - Content = stack, - }; - } - - private static Control BuildProxyCompatTable() - { - var noStr = CoreTools.Translate("No"); - 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 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) - { - managerCol.Children.Add(new TextBlock { Text = manager.DisplayName, TextAlignment = TextAlignment.Center }); - - var proxyLevel = manager.Capabilities.SupportsProxy; - proxyCol.Children.Add(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))); - - authCol.Children.Add(StatusBadge( - manager.Capabilities.SupportsProxyAuth ? yesStr : noStr, - manager.Capabilities.SupportsProxyAuth ? Colors.Green : Colors.Red)); - } + var vm = (InternetViewModel)DataContext; + vm.RestartRequired += (s, e) => RestartRequired?.Invoke(s, e); - 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 tableStack = new StackPanel { Orientation = Orientation.Vertical }; - tableStack.Children.Add(headerRow); - tableStack.Children.Add(dataRow); - - return new SettingsCard - { - CornerRadius = new CornerRadius(8), - Header = CoreTools.Translate("Proxy compatibility table"), - Description = tableStack, - }; - } - - private static Border StatusBadge(string text, Color color) => new Border - { - CornerRadius = new CornerRadius(4), - Padding = new Thickness(4, 2), - BorderThickness = new Thickness(1), - 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 async Task SaveCredentialsAsync() - { - if (_usernameBox is null || _passwordBox is null || _savingIndicator is null) return; - _savingIndicator.Opacity = 1; - string u = _usernameBox.Text ?? ""; - string p = _passwordBox.Text ?? ""; - await Task.Delay(500); - if ((_usernameBox.Text ?? "") != u) return; - if ((_passwordBox.Text ?? "") != p) return; - CoreSettings.SetProxyCredentials(u, p); - InternetViewModel.ApplyProxyToProcess(); - _savingIndicator.Opacity = 0; + CredentialsHolder.Content = vm.BuildCredentialsCard(); + ProxyCompatTableHolder.Content = vm.BuildProxyCompatTable(); } } diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Notifications.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Notifications.axaml index b8949f673..614a6a607 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Notifications.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Notifications.axaml @@ -3,6 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:UniGetUI.Avalonia.ViewModels.Pages.SettingsPages" xmlns:settings="using:UniGetUI.Avalonia.Views.Controls.Settings" + 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" @@ -16,13 +17,15 @@ Margin="44,32,4,8"/> - - @@ -30,20 +33,24 @@ FontWeight="SemiBold" Margin="44,32,4,8"/> - - - - diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Notifications.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Notifications.axaml.cs index 014017be6..6ea544fe9 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Notifications.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Notifications.axaml.cs @@ -1,14 +1,11 @@ using Avalonia.Controls; using UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; -using UniGetUI.Core.SettingsEngine; using UniGetUI.Core.Tools; namespace UniGetUI.Avalonia.Views.Pages.SettingsPages; public sealed partial class Notifications : UserControl, ISettingsPage { - private NotificationsViewModel VM => (NotificationsViewModel)DataContext!; - public bool CanGoBack => true; public string ShortTitle => CoreTools.Translate("Notification preferences"); @@ -19,31 +16,5 @@ public Notifications() { DataContext = new NotificationsViewModel(); InitializeComponent(); - - // Assign setting names to named controls - DisableNotifications.SettingName = Settings.K.DisableNotifications; - DisableUpdatesNotifications.SettingName = Settings.K.DisableUpdatesNotifications; - DisableProgressNotifications.SettingName = Settings.K.DisableProgressNotifications; - DisableErrorNotifications.SettingName = Settings.K.DisableUpdatesNotifications; - DisableSuccessNotifications.SettingName = Settings.K.DisableSuccessNotifications; - - DisableNotifications.Text = "Enable WingetUI notifications"; - DisableUpdatesNotifications.Text = "Show a notification when there are available updates"; - DisableProgressNotifications.Text = "Show a silent notification when an operation is running"; - DisableErrorNotifications.Text = "Show a notification when an operation fails"; - DisableSuccessNotifications.Text = "Show a notification when an operation finishes successfully"; - - TrayWarningText.Text = CoreTools.Translate("The system tray icon must be enabled in order for notifications to work"); - - // Mirror WinUI OnNavigatedTo: disable all when tray is off - bool trayEnabled = !Settings.Get(Settings.K.DisableSystemTray); - VM.IsSystemTrayEnabled = trayEnabled; - - // Set initial notifications-enabled state from the master toggle - VM.IsNotificationsEnabled = DisableNotifications._checkbox.IsChecked ?? false; - - // React to changes on the master notifications toggle - DisableNotifications._checkbox.IsCheckedChanged += (_, _) => - VM.IsNotificationsEnabled = DisableNotifications._checkbox.IsChecked ?? false; } } diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Operations.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Operations.axaml index ea32565bf..8649bad5f 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Operations.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Operations.axaml @@ -3,6 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:UniGetUI.Avalonia.ViewModels.Pages.SettingsPages" xmlns:settings="using:UniGetUI.Avalonia.Views.Controls.Settings" + 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" @@ -16,15 +17,23 @@ Margin="44,32,4,8"/> - - - - Package update preferences - Update check frequency, automatically install updates, etc. - + - - Administrator rights and other dangerous settings - Reduce UAC prompts, elevate installations by default, unlock certain dangerous features, etc. - + IsClickEnabled="True" + Command="{Binding NavigateToAdministratorCommand}" + Header="{t:Translate Administrator rights and other dangerous settings}" + Description="{t:Translate Text='Reduce UAC prompts, elevate installations by default, unlock certain dangerous features, etc.'}"/> diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Operations.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Operations.axaml.cs index b0f150c14..042e79b1c 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Operations.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Operations.axaml.cs @@ -1,63 +1,29 @@ using Avalonia.Controls; using UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; -using UniGetUI.Core.SettingsEngine; using UniGetUI.Core.Tools; -using UniGetUI.PackageOperations; namespace UniGetUI.Avalonia.Views.Pages.SettingsPages; public sealed partial class Operations : UserControl, ISettingsPage { + private OperationsViewModel VM => (OperationsViewModel)DataContext!; + public bool CanGoBack => true; public string ShortTitle => CoreTools.Translate("Package operation preferences"); public event EventHandler? RestartRequired; public event EventHandler? NavigationRequested; - public void ShowRestartBanner(object? sender, EventArgs e) => - RestartRequired?.Invoke(this, e); - public Operations() { DataContext = new OperationsViewModel(); InitializeComponent(); - ParallelOperationCount.SettingName = Settings.K.ParallelOperationCount; - ParallelOperationCount.Text = "Choose how many operations should be performed in parallel"; - for (int i = 1; i <= 10; i++) ParallelOperationCount.AddItem(i.ToString(), i.ToString(), false); - foreach (var v in new[] { "15", "20", "30", "50", "75", "100" }) + VM.RestartRequired += (s, e) => RestartRequired?.Invoke(s, e); + VM.NavigationRequested += (s, t) => NavigationRequested?.Invoke(s, t); + + foreach (var v in VM.ParallelOpCounts) ParallelOperationCount.AddItem(v, v, false); ParallelOperationCount.ShowAddedItems(); - ParallelOperationCount.ValueChanged += ParallelOperationCount_OnValueChanged; - - MaintainSuccessfulInstalls.SettingName = Settings.K.MaintainSuccessfulInstalls; - MaintainSuccessfulInstalls.ForceInversion = true; - MaintainSuccessfulInstalls.WarningText = "Download operations are not affected by this setting"; - MaintainSuccessfulInstalls.Text = "Clear successful operations from the operation list after a 5 second delay"; - - KillProcessesThatRefuseToDie.SettingName = Settings.K.KillProcessesThatRefuseToDie; - KillProcessesThatRefuseToDie.Text = "Try to kill the processes that refuse to close when requested to"; - KillProcessesThatRefuseToDie.WarningOpacity = 0.7; - KillProcessesThatRefuseToDie.WarningText = "You may lose unsaved data"; - - AskToDeleteNewDesktopShortcuts.SettingName = Settings.K.AskToDeleteNewDesktopShortcuts; - AskToDeleteNewDesktopShortcuts.CheckboxText = "Ask to delete desktop shortcuts created during an install or upgrade."; - AskToDeleteNewDesktopShortcuts.ButtonText = "Manage shortcuts"; - AskToDeleteNewDesktopShortcuts.Click += (_, _) => - { - // DialogHelper.ManageDesktopShortcuts() — not yet ported; no-op on macOS - }; - - UpdatesSettingsButton.Click += (_, _) => NavigationRequested?.Invoke(this, typeof(Updates)); - AdminButton.Click += (_, _) => NavigationRequested?.Invoke(this, typeof(Administrator)); - } - - private void ParallelOperationCount_OnValueChanged(object? sender, EventArgs e) - { - if (sender is UniGetUI.Avalonia.Views.Controls.Settings.ComboboxCard card && - int.TryParse(card.SelectedValue(), out int value)) - { - AbstractOperation.MAX_OPERATIONS = value; - } } } diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/SettingsBasePage.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/SettingsBasePage.axaml index 210ae3512..edc2eaa13 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/SettingsBasePage.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/SettingsBasePage.axaml @@ -5,6 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" + xmlns:controls="using:UniGetUI.Avalonia.Views.Controls" x:DataType="vm:SettingsBasePageViewModel"> @@ -14,10 +15,14 @@ Orientation="Horizontal" VerticalAlignment="Center" Margin="16,12,16,8"> - - - @@ -159,24 +189,26 @@ Background="Transparent" BorderThickness="0" Padding="12,8" - CornerRadius="6"> + CornerRadius="6" + ToolTip.Tip="{t:Translate More}"> - - + + - - - + + + - - + + - - + + - + diff --git a/src/UniGetUI.Avalonia/Views/SidebarView.axaml.cs b/src/UniGetUI.Avalonia/Views/SidebarView.axaml.cs index b11331329..4d73c5d76 100644 --- a/src/UniGetUI.Avalonia/Views/SidebarView.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/SidebarView.axaml.cs @@ -1,8 +1,5 @@ using Avalonia.Controls; -using Avalonia.Interactivity; using UniGetUI.Avalonia.ViewModels; -using UniGetUI.Core.Data; -using UniGetUI.Core.Tools; namespace UniGetUI.Avalonia.Views; @@ -13,7 +10,6 @@ public partial class SidebarView : BaseView public SidebarView() { InitializeComponent(); - VersionMenuItem.Header = CoreTools.Translate("WingetUI Version {0}", CoreData.VersionName); } protected override void OnDataContextChanged(EventArgs e) @@ -46,33 +42,6 @@ private void NavListBox_SelectionChanged(object? sender, SelectionChangedEventAr if (_lastNavItemSelectionWasAuto) return; if (NavListBox.SelectedItem is ListBoxItem item && item.Tag is string tag && Enum.TryParse(tag, out var pageType)) - ViewModel?.RequestNavigation(pageType); + ViewModel?.RequestNavigation(pageType.ToString()); } - - private void SettingsNavBtn_Click(object? sender, RoutedEventArgs e) => - ViewModel?.RequestNavigation(PageType.Settings); - - private void ManagersNavBtn_Click(object? sender, RoutedEventArgs e) => - ViewModel?.RequestNavigation(PageType.Managers); - - private void UniGetUILogs_Click(object? sender, RoutedEventArgs e) => - ViewModel?.RequestNavigation(PageType.OwnLog); - - private void ManagerLogsMenu_Click(object? sender, RoutedEventArgs e) => - ViewModel?.RequestNavigation(PageType.ManagerLog); - - private void OperationHistoryMenu_Click(object? sender, RoutedEventArgs e) => - ViewModel?.RequestNavigation(PageType.OperationHistory); - - private void ReleaseNotesMenu_Click(object? sender, RoutedEventArgs e) => - ViewModel?.RequestNavigation(PageType.ReleaseNotes); - - private void HelpMenu_Click(object? sender, RoutedEventArgs e) => - ViewModel?.RequestNavigation(PageType.Help); - - private void AboutNavButton_Click(object? sender, RoutedEventArgs e) => - ViewModel?.RequestNavigation(PageType.About); - - private void QuitUniGetUI_Click(object? sender, RoutedEventArgs e) => - ViewModel?.RequestNavigation(PageType.Quit); } diff --git a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml index b5fa43187..173145534 100644 --- a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml +++ b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml @@ -4,6 +4,7 @@ xmlns:vm="using:UniGetUI.Avalonia.ViewModels.Pages" 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" @@ -19,6 +20,33 @@ + + + + + + + + + @@ -72,7 +100,7 @@ Orientation="Horizontal" Spacing="4" VerticalAlignment="Center"> - + @@ -112,10 +140,10 @@ Orientation="Horizontal" Spacing="4" VerticalAlignment="Center"> - + + Margin="0"> - + @@ -170,7 +198,6 @@ @@ -229,16 +255,17 @@ HorizontalContentAlignment="Stretch" IsExpanded="True" CornerRadius="8" - Padding="4,4,4,8"> + Padding="4,4,4,8" + Background="{DynamicResource SettingsCardBackground}"> - + - + + Padding="16,8,16,8" + Background="{DynamicResource SettingsCardBackground}"> - + - + - + - + @@ -323,50 +351,40 @@ HorizontalContentAlignment="Stretch" IsExpanded="True" CornerRadius="8" - Padding="16,8,16,8"> + Padding="16,8,16,8" + Background="{DynamicResource SettingsCardBackground}"> - + - - + IsChecked="{Binding SearchMode_Name, Mode=TwoWay}"> + - - + IsChecked="{Binding SearchMode_Id, Mode=TwoWay}"> + - - + IsChecked="{Binding SearchMode_Both, Mode=TwoWay}"> + - - + IsChecked="{Binding SearchMode_Exact, Mode=TwoWay}"> + - - + @@ -390,10 +408,24 @@ - + BorderThickness="1" + BorderBrush="{DynamicResource AppBorderBrush}" + Background="{DynamicResource SettingsCardBackground}"> + + + + + + - +