diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 07c447d05..52d544cd8 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -308,10 +308,62 @@ jobs: run: | Remove-Item "unigetui_bin" -Recurse -Force -ErrorAction SilentlyContinue + build-avalonia: + name: Build Avalonia (${{ matrix.platform }}) + runs-on: macos-latest + needs: [preflight] + environment: ${{ needs.preflight.outputs.package-env }} + permissions: + contents: read + env: + UNIGETUI_GITHUB_CLIENT_ID: ${{ secrets.UNIGETUI_GITHUB_CLIENT_ID }} + UNIGETUI_GITHUB_CLIENT_SECRET: ${{ secrets.UNIGETUI_GITHUB_CLIENT_SECRET }} + NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages + strategy: + fail-fast: false + matrix: + platform: [arm64, x64] + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install .NET + uses: actions/setup-dotnet@v5 + with: + global-json-file: global.json + + - name: Cache NuGet packages + uses: actions/cache@v5 + with: + path: ${{ env.NUGET_PACKAGES }} + key: ${{ runner.os }}-nuget-${{ hashFiles('global.json', 'src/**/*.csproj', 'src/**/*.props', 'src/**/*.targets', 'src/**/*.sln') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Restore dependencies + working-directory: src + run: dotnet restore UniGetUI.Avalonia/UniGetUI.Avalonia.csproj + + - name: Publish + working-directory: src + run: | + dotnet publish UniGetUI.Avalonia/UniGetUI.Avalonia.csproj \ + --configuration Release \ + --runtime osx-${{ matrix.platform }} \ + --self-contained true \ + --output ../avalonia_bin/${{ matrix.platform }} + + - name: Upload artifacts + uses: actions/upload-artifact@v7 + with: + name: UniGetUI-Avalonia-${{ matrix.platform }} + path: avalonia_bin/${{ matrix.platform }}/* + publish: name: Publish GitHub Release runs-on: ubuntu-latest - needs: [preflight, build] + needs: [preflight, build, build-avalonia] if: ${{ fromJSON(needs.preflight.outputs.skip-publish) == false }} environment: ${{ needs.preflight.outputs.package-env }} permissions: diff --git a/.gitignore b/.gitignore index 946a5595c..9616f431d 100644 --- a/.gitignore +++ b/.gitignore @@ -80,6 +80,7 @@ src/UniGetUI/choco-cli/extensions/chocolatey-dotnetfx/ *.user /src/UniGetUI/Generated Files +/src/UniGetUI.Avalonia/Infrastructure/Generated Files /InstallerExtras/MsiCreator/.vs/MsiInstallerWrapper/CopilotIndices /InstallerExtras/MsiCreator/.vs InstallerExtras/MsiCreator/setup.exe @@ -93,3 +94,4 @@ src/UniGetUI.v3.ncrunchsolution # macOS Finder metadata .DS_Store +/src/UniGetUI.Avalonia/Generated Files diff --git a/src/UniGetUI.Avalonia/.vscode/launch.json b/src/UniGetUI.Avalonia/.vscode/launch.json new file mode 100644 index 000000000..9e5ba13f0 --- /dev/null +++ b/src/UniGetUI.Avalonia/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + "configurations": [ + { + "name": "UniGetUI.Avalonia (Debug)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build UniGetUI.Avalonia (Debug)", + "program": "${workspaceFolder}/bin/arm64/Debug/net10.0/UniGetUI.Avalonia", + "args": [], + "cwd": "${workspaceFolder}/bin/arm64/Debug/net10.0/", + "console": "integratedTerminal", + "stopAtEntry": false, + "env": { + "UNIGETUI_GITHUB_CLIENT_ID": "", + "UNIGETUI_GITHUB_CLIENT_SECRET": "" + } + } + ] +} diff --git a/src/UniGetUI.Avalonia/App.axaml.cs b/src/UniGetUI.Avalonia/App.axaml.cs index d3ef1afde..8ea526ca4 100644 --- a/src/UniGetUI.Avalonia/App.axaml.cs +++ b/src/UniGetUI.Avalonia/App.axaml.cs @@ -1,10 +1,11 @@ using System.Diagnostics; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Data.Core.Plugins; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Styling; +using Avalonia.Platform; using Avalonia.Styling; +using UniGetUI.Avalonia.Infrastructure; using UniGetUI.Avalonia.Views; using UniGetUI.PackageEngine; using CoreSettings = global::UniGetUI.Core.SettingsEngine.Settings; @@ -16,6 +17,9 @@ public partial class App : Application public override void Initialize() { AvaloniaXamlLoader.Load(this); +#if AVALONIA_DIAGNOSTICS_ENABLED + this.AttachDeveloperTools(); +#endif string platform = OperatingSystem.IsWindows() ? "Windows" : OperatingSystem.IsMacOS() ? "macOS" @@ -31,17 +35,17 @@ public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - // Avoid duplicate validations from both Avalonia and the CommunityToolkit. - // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins - DisableAvaloniaDataAnnotationValidation(); if (OperatingSystem.IsMacOS()) + { ExpandMacOSPath(); + using var stream = AssetLoader.Open(new Uri("avares://UniGetUI.Avalonia/Assets/icon.png")); + using var ms = new MemoryStream(); + stream.CopyTo(ms); + MacOsNotificationBridge.SetDockIcon(ms.ToArray()); + } PEInterface.LoadLoaders(); ApplyTheme(CoreSettings.GetValue(CoreSettings.K.PreferredTheme)); var mainWindow = new MainWindow(); -#if DEBUG - mainWindow.AttachDevTools(); -#endif desktop.MainWindow = mainWindow; _ = Task.Run(PEInterface.LoadManagers); } @@ -86,16 +90,4 @@ public static void ApplyTheme(string value) }; } - private static void DisableAvaloniaDataAnnotationValidation() - { - // Get an array of plugins to remove - var dataValidationPluginsToRemove = - BindingPlugins.DataValidators.OfType().ToArray(); - - // remove each entry found - foreach (var plugin in dataValidationPluginsToRemove) - { - BindingPlugins.DataValidators.Remove(plugin); - } - } } diff --git a/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml b/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml index 0ffcf9ff6..ad73738f7 100644 --- a/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml +++ b/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml @@ -27,6 +27,13 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + @@ -148,16 +204,21 @@ - + + - - - + - + + + diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs index 424b75fff..9bc2da9b9 100644 --- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs @@ -1,7 +1,10 @@ using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Input; using UniGetUI.Avalonia.ViewModels; using UniGetUI.Avalonia.Views.Pages; +using UniGetUI.Core.Logging; +using UniGetUI.Core.SettingsEngine; namespace UniGetUI.Avalonia.Views; @@ -32,10 +35,13 @@ public enum RuntimeNotificationLevel Error, } + public static MainWindow? Instance { get; private set; } + private MainWindowViewModel ViewModel => (MainWindowViewModel)DataContext!; public MainWindow() { + Instance = this; DataContext = new MainWindowViewModel(); InitializeComponent(); @@ -93,7 +99,20 @@ private void SearchBox_KeyDown(object? sender, KeyEventArgs e) // ─── Public API (legacy compat) ─────────────────────────────────────────── public void ShowBanner(string title, string message, RuntimeNotificationLevel level) { - // TODO: implement in-app notification display + if (level == RuntimeNotificationLevel.Progress) return; + + var severity = level switch + { + RuntimeNotificationLevel.Error => InfoBarSeverity.Error, + RuntimeNotificationLevel.Success => InfoBarSeverity.Success, + _ => InfoBarSeverity.Informational, + }; + ViewModel.ErrorBanner.ActionButtonText = ""; + ViewModel.ErrorBanner.ActionButtonCommand = null; + ViewModel.ErrorBanner.Title = title; + ViewModel.ErrorBanner.Message = message; + ViewModel.ErrorBanner.Severity = severity; + ViewModel.ErrorBanner.IsOpen = true; } public void UpdateSystemTrayStatus() @@ -103,4 +122,65 @@ public void UpdateSystemTrayStatus() public void ShowRuntimeNotification(string title, string message, RuntimeNotificationLevel level) => ShowBanner(title, message, level); + + // ─── BackgroundAPI integration ──────────────────────────────────────────── + public void ShowFromTray() + { + Show(); + WindowState = WindowState.Normal; + Activate(); + } + + public void QuitApplication() + { + (global::Avalonia.Application.Current?.ApplicationLifetime + as IClassicDesktopStyleApplicationLifetime)?.Shutdown(); + } + + public void OpenSharedPackage(string managerName, string packageId) + { + // TODO: open package details for the shared package + Logger.Info($"OpenSharedPackage: {managerName}/{packageId}"); + Navigate(PageType.Discover); + } + + public static void ApplyProxyVariableToProcess() + { + try + { + var proxyUri = Settings.GetProxyUrl(); + if (proxyUri is null || !Settings.Get(Settings.K.EnableProxy)) + { + Environment.SetEnvironmentVariable("HTTP_PROXY", "", EnvironmentVariableTarget.Process); + return; + } + + string content; + if (!Settings.Get(Settings.K.EnableProxyAuth)) + { + content = proxyUri.ToString(); + } + else + { + var creds = Settings.GetProxyCredentials(); + if (creds is null) + { + content = proxyUri.ToString(); + } + else + { + content = $"{proxyUri.Scheme}://{Uri.EscapeDataString(creds.UserName)}" + + $":{Uri.EscapeDataString(creds.Password)}" + + $"@{proxyUri.AbsoluteUri.Replace($"{proxyUri.Scheme}://", "")}"; + } + } + + Environment.SetEnvironmentVariable("HTTP_PROXY", content, EnvironmentVariableTarget.Process); + } + catch (Exception ex) + { + Logger.Error("Failed to apply proxy settings:"); + Logger.Error(ex); + } + } } diff --git a/src/UniGetUI.Avalonia/Views/Pages/AboutPages/AboutPage.axaml b/src/UniGetUI.Avalonia/Views/Pages/AboutPages/AboutPage.axaml new file mode 100644 index 000000000..44ffba3c1 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/AboutPages/AboutPage.axaml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Views/Pages/AboutPages/AboutPage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/AboutPages/AboutPage.axaml.cs new file mode 100644 index 000000000..a844faea2 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/AboutPages/AboutPage.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia.Controls; +using UniGetUI.Avalonia.ViewModels.Pages.AboutPages; + +namespace UniGetUI.Avalonia.Views.Pages.AboutPages; + +public partial class AboutPage : UserControl +{ + public AboutPage() + { + DataContext = new AboutPageViewModel(); + InitializeComponent(); + } +} diff --git a/src/UniGetUI.Avalonia/Views/Pages/AboutPages/Contributors.axaml b/src/UniGetUI.Avalonia/Views/Pages/AboutPages/Contributors.axaml new file mode 100644 index 000000000..194411659 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/AboutPages/Contributors.axaml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Views/Pages/AboutPages/Contributors.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/AboutPages/Contributors.axaml.cs new file mode 100644 index 000000000..2cdb6d11f --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/AboutPages/Contributors.axaml.cs @@ -0,0 +1,21 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using UniGetUI.Avalonia.ViewModels.Pages.AboutPages; +using UniGetUI.Core.Tools; + +namespace UniGetUI.Avalonia.Views.Pages.AboutPages; + +public partial class Contributors : UserControl +{ + public Contributors() + { + DataContext = new ContributorsViewModel(); + InitializeComponent(); + } + + private void GitHubButton_Click(object? sender, RoutedEventArgs e) + { + if (sender is Button { Tag: Uri url }) + CoreTools.Launch(url.ToString()); + } +} diff --git a/src/UniGetUI.Avalonia/Views/Pages/AboutPages/ThirdPartyLicenses.axaml b/src/UniGetUI.Avalonia/Views/Pages/AboutPages/ThirdPartyLicenses.axaml new file mode 100644 index 000000000..c4cc02d80 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/AboutPages/ThirdPartyLicenses.axaml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Views/Pages/AboutPages/ThirdPartyLicenses.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/AboutPages/ThirdPartyLicenses.axaml.cs new file mode 100644 index 000000000..96fd45353 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/AboutPages/ThirdPartyLicenses.axaml.cs @@ -0,0 +1,27 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using UniGetUI.Avalonia.ViewModels.Pages.AboutPages; +using UniGetUI.Core.Tools; + +namespace UniGetUI.Avalonia.Views.Pages.AboutPages; + +public partial class ThirdPartyLicenses : UserControl +{ + public ThirdPartyLicenses() + { + DataContext = new ThirdPartyLicensesViewModel(); + InitializeComponent(); + } + + private void LicenseButton_Click(object? sender, RoutedEventArgs e) + { + if (sender is Button { Tag: Uri url }) + CoreTools.Launch(url.ToString()); + } + + private void HomepageButton_Click(object? sender, RoutedEventArgs e) + { + if (sender is Button { Tag: Uri url }) + CoreTools.Launch(url.ToString()); + } +} diff --git a/src/UniGetUI.Avalonia/Views/Pages/AboutPages/Translators.axaml b/src/UniGetUI.Avalonia/Views/Pages/AboutPages/Translators.axaml new file mode 100644 index 000000000..b30c3c515 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/AboutPages/Translators.axaml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Views/Pages/AboutPages/Translators.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/AboutPages/Translators.axaml.cs new file mode 100644 index 000000000..4ddd591fe --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/AboutPages/Translators.axaml.cs @@ -0,0 +1,24 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using UniGetUI.Avalonia.ViewModels.Pages.AboutPages; +using UniGetUI.Core.Tools; + +namespace UniGetUI.Avalonia.Views.Pages.AboutPages; + +public partial class Translators : UserControl +{ + public Translators() + { + DataContext = new TranslatorsViewModel(); + InitializeComponent(); + } + + private void BecomeTranslatorButton_Click(object? sender, RoutedEventArgs e) => + CoreTools.Launch("https://github.com/Devolutions/UniGetUI/wiki#translating-wingetui"); + + private void GitHubButton_Click(object? sender, RoutedEventArgs e) + { + if (sender is Button { Tag: Uri url }) + CoreTools.Launch(url.ToString()); + } +} diff --git a/src/UniGetUI.Avalonia/Views/Pages/HelpPage.axaml b/src/UniGetUI.Avalonia/Views/Pages/HelpPage.axaml new file mode 100644 index 000000000..a2cf70ce5 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/HelpPage.axaml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Views/Pages/HelpPage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/HelpPage.axaml.cs new file mode 100644 index 000000000..268ef295d --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/HelpPage.axaml.cs @@ -0,0 +1,81 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using UniGetUI.Avalonia.ViewModels.Pages; +using UniGetUI.Avalonia.Views.Pages; + +namespace UniGetUI.Avalonia.Views.Pages; + +public partial class HelpPage : UserControl, IEnterLeaveListener +{ + private readonly HelpPageViewModel _viewModel; + private string _pendingNavigation = HelpPageViewModel.HelpBaseUrl; + + public HelpPage() + { + _viewModel = new HelpPageViewModel(); + DataContext = _viewModel; + InitializeComponent(); + + WebViewControl.NavigationStarted += OnNavigationStarted; + WebViewControl.NavigationCompleted += OnNavigationCompleted; + } + + private void OnNavigationStarted(object? sender, WebViewNavigationStartingEventArgs e) + { + NavProgressBar.IsVisible = true; + + // Add iframe query param so the help site shows the embedded view + string url = e.Request?.ToString() ?? ""; + if (url.Contains("marticliment.com") && !url.Contains("isWingetUIIframe")) + { + e.Cancel = true; + string modified = url.Contains('?') + ? url + "&isWingetUIIframe" + : url + "?isWingetUIIframe"; + WebViewControl.Navigate(new Uri(modified)); + } + } + + private void OnNavigationCompleted(object? sender, WebViewNavigationCompletedEventArgs e) + { + NavProgressBar.IsVisible = false; + _viewModel.CurrentUrl = WebViewControl.Source?.ToString() ?? HelpPageViewModel.HelpBaseUrl; + + BackButton.IsEnabled = WebViewControl.CanGoBack; + ForwardButton.IsEnabled = WebViewControl.CanGoForward; + } + + public void NavigateTo(string uriAttachment) + { + string url = _viewModel.GetInitialUrl(uriAttachment); + if (WebViewControl.IsLoaded) + WebViewControl.Navigate(new Uri(url)); + else + _pendingNavigation = url; + } + + public void OnEnter() + { + WebViewControl.Navigate(new Uri(_pendingNavigation)); + } + + public void OnLeave() { } + + private void BackButton_Click(object? sender, RoutedEventArgs e) + { + if (WebViewControl.CanGoBack) + WebViewControl.GoBack(); + } + + private void ForwardButton_Click(object? sender, RoutedEventArgs e) + { + if (WebViewControl.CanGoForward) + WebViewControl.GoForward(); + } + + private void HomeButton_Click(object? sender, RoutedEventArgs e) => + WebViewControl.Navigate(new Uri(HelpPageViewModel.HelpBaseUrl)); + + private void ReloadButton_Click(object? sender, RoutedEventArgs e) => + WebViewControl.Refresh(); +} diff --git a/src/UniGetUI.Avalonia/Views/Pages/LogPages/BaseLogPage.axaml b/src/UniGetUI.Avalonia/Views/Pages/LogPages/BaseLogPage.axaml new file mode 100644 index 000000000..4ec47e528 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/LogPages/BaseLogPage.axaml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Views/Pages/LogPages/BaseLogPage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/LogPages/BaseLogPage.axaml.cs new file mode 100644 index 000000000..b9d3fa70f --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/LogPages/BaseLogPage.axaml.cs @@ -0,0 +1,69 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input.Platform; +using Avalonia.Platform.Storage; +using Avalonia.Threading; +using UniGetUI.Avalonia.ViewModels.Pages.LogPages; +using UniGetUI.Core.Tools; + +namespace UniGetUI.Avalonia.Views.Pages.LogPages; + +public partial class BaseLogPage : UserControl, IEnterLeaveListener, IKeyboardShortcutListener +{ + protected readonly BaseLogPageViewModel ViewModel; + + protected BaseLogPage(BaseLogPageViewModel viewModel) + { + ViewModel = viewModel; + DataContext = ViewModel; + InitializeComponent(); + + ViewModel.CopyTextRequested += OnCopyTextRequested; + ViewModel.ExportTextRequested += OnExportTextRequested; + ViewModel.ScrollToBottomRequested += OnScrollToBottomRequested; + } + + private async void OnCopyTextRequested(object? sender, string text) + { + var clipboard = TopLevel.GetTopLevel(this)?.Clipboard; + if (clipboard is not null) + await clipboard.SetTextAsync(text); + } + + private async void OnExportTextRequested(object? sender, string text) + { + var topLevel = TopLevel.GetTopLevel(this); + if (topLevel is null) return; + + var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions + { + Title = CoreTools.Translate("Export log"), + SuggestedFileName = CoreTools.Translate("UniGetUI Log"), + FileTypeChoices = + [ + new FilePickerFileType(CoreTools.Translate("Text")) { Patterns = ["*.txt"] }, + ], + }); + + if (file is not null) + await File.WriteAllTextAsync(file.Path.LocalPath, text); + } + + private void OnScrollToBottomRequested(object? sender, EventArgs e) + { + Dispatcher.UIThread.Post(() => + { + MainScroller.Offset = new Vector(MainScroller.Offset.X, double.MaxValue); + }, DispatcherPriority.Background); + } + + public void OnEnter() => ViewModel.LoadLog(); + + public void OnLeave() => ViewModel.ClearLog(); + + public void ReloadTriggered() => ViewModel.LoadLog(isReload: true); + + public void SelectAllTriggered() { } + + public void SearchTriggered() { } +} diff --git a/src/UniGetUI.Avalonia/Views/Pages/LogPages/ManagerLogsPage.cs b/src/UniGetUI.Avalonia/Views/Pages/LogPages/ManagerLogsPage.cs new file mode 100644 index 000000000..60a465fa4 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/LogPages/ManagerLogsPage.cs @@ -0,0 +1,18 @@ +using UniGetUI.Avalonia.ViewModels.Pages.LogPages; +using UniGetUI.PackageEngine.Interfaces; + +namespace UniGetUI.Avalonia.Views.Pages; + +public class ManagerLogsPage : LogPages.BaseLogPage +{ + private readonly ManagerLogsPageViewModel _viewModel; + + public ManagerLogsPage() : this(new ManagerLogsPageViewModel()) { } + + private ManagerLogsPage(ManagerLogsPageViewModel vm) : base(vm) + { + _viewModel = vm; + } + + public void LoadForManager(IPackageManager manager) => _viewModel.LoadForManager(manager); +} diff --git a/src/UniGetUI.Avalonia/Views/Pages/LogPages/OperationHistoryPage.cs b/src/UniGetUI.Avalonia/Views/Pages/LogPages/OperationHistoryPage.cs new file mode 100644 index 000000000..2abfbd630 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/LogPages/OperationHistoryPage.cs @@ -0,0 +1,8 @@ +using UniGetUI.Avalonia.ViewModels.Pages.LogPages; + +namespace UniGetUI.Avalonia.Views.Pages; + +public class OperationHistoryPage : LogPages.BaseLogPage +{ + public OperationHistoryPage() : base(new OperationHistoryPageViewModel()) { } +} diff --git a/src/UniGetUI.Avalonia/Views/Pages/LogPages/UniGetUILogPage.cs b/src/UniGetUI.Avalonia/Views/Pages/LogPages/UniGetUILogPage.cs new file mode 100644 index 000000000..6492e787a --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/LogPages/UniGetUILogPage.cs @@ -0,0 +1,8 @@ +using UniGetUI.Avalonia.ViewModels.Pages.LogPages; + +namespace UniGetUI.Avalonia.Views.Pages; + +public class UniGetUILogPage : LogPages.BaseLogPage +{ + public UniGetUILogPage() : base(new UniGetUILogPageViewModel()) { } +} diff --git a/src/UniGetUI.Avalonia/Views/Pages/ReleaseNotesPage.axaml b/src/UniGetUI.Avalonia/Views/Pages/ReleaseNotesPage.axaml new file mode 100644 index 000000000..e20ef4c90 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/ReleaseNotesPage.axaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Views/Pages/ReleaseNotesPage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/ReleaseNotesPage.axaml.cs new file mode 100644 index 000000000..842cc529b --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/ReleaseNotesPage.axaml.cs @@ -0,0 +1,35 @@ +using Avalonia.Controls; +using UniGetUI.Avalonia.ViewModels.Pages; + +namespace UniGetUI.Avalonia.Views.Pages; + +public partial class ReleaseNotesPage : UserControl, IEnterLeaveListener +{ + private readonly ReleaseNotesPageViewModel _viewModel; + private bool _loaded; + + public ReleaseNotesPage() + { + _viewModel = new ReleaseNotesPageViewModel(); + DataContext = _viewModel; + InitializeComponent(); + + WebViewControl.NavigationStarted += (_, _) => NavProgressBar.IsVisible = true; + WebViewControl.NavigationCompleted += (_, e) => + { + NavProgressBar.IsVisible = false; + _viewModel.CurrentUrl = WebViewControl.Source?.ToString() ?? _viewModel.ReleaseNotesUrl; + }; + } + + public void OnEnter() + { + if (!_loaded) + { + WebViewControl.Navigate(new Uri(_viewModel.ReleaseNotesUrl)); + _loaded = true; + } + } + + public void OnLeave() { } +} diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Administrator.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Administrator.axaml index f086046d6..63cb666cc 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Administrator.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Administrator.axaml @@ -48,29 +48,33 @@ - - - - - - - - - + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RestartRequired?.Invoke(s, e); - - BuildBackupInfoCard(); - } - - private void BuildBackupInfoCard() - { - var stack = new StackPanel { Orientation = Orientation.Vertical }; - foreach (var line in new[] - { - "The backup will include the complete list of the installed packages and their installation options. Ignored updates and skipped versions will also be saved.", - "The backup will NOT include any binary file nor any program's saved data.", - "The size of the backup is estimated to be less than 1MB.", - "The backup will be performed after login.", - }) - { - stack.Children.Add(new TextBlock - { - Text = " \u25cf " + CoreTools.Translate(line), - TextWrapping = TextWrapping.Wrap, - }); - } - BackupInfoCardHolder.Content = new SettingsCard - { - CornerRadius = new CornerRadius(8), - Description = stack, - }; + ((BackupViewModel)DataContext).RestartRequired += (s, e) => RestartRequired?.Invoke(s, e); } } diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Experimental.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Experimental.axaml index 6889c6241..35a3343cc 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Experimental.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Experimental.axaml @@ -37,12 +37,13 @@ Text="{t:Translate Disable the 1-minute timeout for package-related operations}" CornerRadius="8"/> - + + CornerRadius="8" + IsVisible="{Binding IsWindows}"/> @@ -56,6 +57,7 @@ SettingName="IconDataBaseURL" Text="{t:Translate Use a custom icon and screenshot database URL}" Placeholder="{t:Translate Leave empty for default}" + HelpUrl="https://www.marticliment.com/unigetui/help/icons-and-screenshots#custom-source" ValueChangedCommand="{Binding ShowRestartRequiredCommand}" CornerRadius="0,0,8,8"/> diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/InstallOptionsPanel.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/InstallOptionsPanel.axaml new file mode 100644 index 000000000..52b590632 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/InstallOptionsPanel.axaml @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/InstallOptionsPanel.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/InstallOptionsPanel.axaml.cs new file mode 100644 index 000000000..fa233cb05 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/InstallOptionsPanel.axaml.cs @@ -0,0 +1,41 @@ +using Avalonia.Controls; +using UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; +using UniGetUI.PackageEngine.Interfaces; + +namespace UniGetUI.Avalonia.Views.Pages.SettingsPages; + +public sealed partial class InstallOptionsPanel : UserControl +{ + private InstallOptionsPanelViewModel ViewModel => (InstallOptionsPanelViewModel)DataContext!; + + public event EventHandler? NavigateToAdministratorRequested; + + public InstallOptionsPanel(IPackageManager manager) + { + DataContext = new InstallOptionsPanelViewModel(manager); + InitializeComponent(); + + ViewModel.NavigateToAdministratorRequested += (s, e) => + NavigateToAdministratorRequested?.Invoke(s, e); + + ViewModel.PropertyChanged += (_, e) => + { + if (e.PropertyName == nameof(InstallOptionsPanelViewModel.HasChanges)) + { + if (ViewModel.HasChanges) + ApplyButton.Classes.Add("accent"); + else + ApplyButton.Classes.Remove("accent"); + } + }; + + // Wire location picker (needs Visual reference for StorageProvider) + SelectDirButton.Click += (_, _) => + _ = ViewModel.SelectLocationCommand.ExecuteAsync(this); + + // Mark changed whenever the user edits a CLI textbox + CustomInstallBox.TextChanged += (_, _) => ViewModel.MarkChanged(); + CustomUpdateBox.TextChanged += (_, _) => ViewModel.MarkChanged(); + CustomUninstallBox.TextChanged += (_, _) => ViewModel.MarkChanged(); + } +} diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml index c599711d5..37b7943c1 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml @@ -23,7 +23,7 @@ CornerRadius="0,0,8,8" BorderThickness="1,0,1,1"/> - + diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Internet.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Internet.axaml index e4896a2c2..13baa7eaa 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Internet.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Internet.axaml @@ -8,7 +8,6 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:DataType="vm:InternetViewModel"> - @@ -54,7 +53,6 @@ Text="{t:Translate Wait for the device to be connected to the internet before attempting to do tasks that require internet connectivity.}" StateChangedCommand="{Binding ShowRestartRequiredCommand}" CornerRadius="8"/> - diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/ManagersHomepage.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/ManagersHomepage.axaml index e6533238e..328fca3dc 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/ManagersHomepage.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/ManagersHomepage.axaml @@ -11,8 +11,7 @@ + Margin="0,8"/> diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/ManagersHomepage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/ManagersHomepage.axaml.cs index 495ebb71f..8cae92f5d 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/ManagersHomepage.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/ManagersHomepage.axaml.cs @@ -1,34 +1,153 @@ +using Avalonia; using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; using UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; using UniGetUI.Avalonia.Views.Controls.Settings; using UniGetUI.Core.Tools; using UniGetUI.PackageEngine; +using UniGetUI.PackageEngine.Interfaces; +using CoreSettings = UniGetUI.Core.SettingsEngine.Settings; namespace UniGetUI.Avalonia.Views.Pages.SettingsPages; public sealed partial class ManagersHomepage : UserControl, ISettingsPage { public bool CanGoBack => false; - public string ShortTitle => CoreTools.Translate("Package Managers"); + public string ShortTitle => CoreTools.Translate("Package manager preferences"); public event EventHandler? RestartRequired { add { } remove { } } public event EventHandler? NavigationRequested { add { } remove { } } + public event EventHandler? ManagerNavigationRequested; + + private readonly List<(ToggleSwitch Toggle, IPackageManager Manager, Border Badge, TextBlock BadgeText)> _rows = []; + private bool _isLoadingToggles; public ManagersHomepage() { DataContext = new ManagersHomepageViewModel(); InitializeComponent(); - // Build the manager buttons dynamically (manager list is only known at runtime) - foreach (var manager in PEInterface.Managers) + int count = PEInterface.Managers.Length; + for (int i = 0; i < count; i++) { + var manager = PEInterface.Managers[i]; + bool isFirst = i == 0; + bool isLast = i == count - 1; + + CornerRadius radius = isFirst && isLast ? new CornerRadius(8) + : isFirst ? new CornerRadius(8, 8, 0, 0) + : isLast ? new CornerRadius(0, 0, 8, 8) + : new CornerRadius(0); + var thickness = isFirst ? new Thickness(1) : new Thickness(1, 0, 1, 1); + + // ── Status badge ───────────────────────────────────────────────── + var badgeText = new TextBlock + { + FontSize = 12, + FontWeight = FontWeight.SemiBold, + VerticalAlignment = VerticalAlignment.Center, + }; + var badge = new Border + { + CornerRadius = new CornerRadius(4), + Padding = new Thickness(6, 3, 6, 3), + Child = badgeText, + }; + + // ── Enable/disable toggle ──────────────────────────────────────── + var toggle = new ToggleSwitch + { + OnContent = "", + OffContent = "", + VerticalAlignment = VerticalAlignment.Center, + }; + toggle.Loaded += (_, _) => + { + _isLoadingToggles = true; + toggle.IsChecked = manager.IsEnabled(); + _isLoadingToggles = false; + ApplyStatusBadge(manager, badge, badgeText); + }; + toggle.IsCheckedChanged += async (_, _) => + { + if (_isLoadingToggles) return; + CoreSettings.SetDictionaryItem(CoreSettings.K.DisabledManagers, manager.Name, toggle.IsChecked != true); + await Task.Run(manager.Initialize); + ApplyStatusBadge(manager, badge, badgeText); + }; + + var toggleAndBadge = new StackPanel + { + Orientation = Orientation.Vertical, + Spacing = 4, + VerticalAlignment = VerticalAlignment.Center, + }; + toggleAndBadge.Children.Add(toggle); + toggleAndBadge.Children.Add(badge); + + var rightContent = toggleAndBadge; + var btn = new SettingsPageButton { Text = manager.DisplayName, - UnderText = CoreTools.Translate(manager.IsEnabled() ? "Enabled" : "Disabled"), + UnderText = manager.Properties.Description.Split("
")[0], + Icon = manager.Properties.IconId, + CornerRadius = radius, + BorderThickness = thickness, + Content = rightContent, }; - // TODO: navigate to PackageManagerPage for this manager (Phase 5) + + var capturedManager = manager; + btn.Click += (_, _) => ManagerNavigationRequested?.Invoke(this, capturedManager); + ManagersPanel.Children.Add(btn); + _rows.Add((toggle, manager, badge, badgeText)); + } + } + + /// Re-sync toggle states after returning from a sub-page. + public void RefreshToggles() + { + _isLoadingToggles = true; + foreach (var (toggle, manager, badge, badgeText) in _rows) + { + toggle.IsChecked = manager.IsEnabled(); + ApplyStatusBadge(manager, badge, badgeText); } + _isLoadingToggles = false; + } + + private void ApplyStatusBadge(IPackageManager manager, Border badge, TextBlock text) + { + string bgKey, fgKey, label; + if (!manager.IsEnabled()) + { + bgKey = "WarningBannerBackground"; + fgKey = "StatusWarningForeground"; + label = CoreTools.Translate("Disabled"); + } + else if (manager.Status.Found) + { + bgKey = "StatusSuccessBackground"; + fgKey = "StatusSuccessForeground"; + label = CoreTools.Translate("Ready"); + } + else + { + bgKey = "StatusErrorBackground"; + fgKey = "StatusErrorForeground"; + label = CoreTools.Translate("Not found"); + } + badge.Background = LookupBrush(bgKey); + text.Foreground = LookupBrush(fgKey); + text.Text = label; + } + + private IBrush LookupBrush(string key) + { + if (this.TryFindResource(key, ActualThemeVariant, out var res) && res is IBrush brush) + return brush; + return Brushes.Transparent; } } diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Notifications.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Notifications.axaml index 614a6a607..366ad9e23 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Notifications.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Notifications.axaml @@ -16,18 +16,18 @@ FontWeight="SemiBold" Margin="44,32,4,8"/> - + + IsVisible="{Binding IsSystemTrayWarningVisible}"/> + IsEnabled="{Binding IsSystemTrayWarningVisible, Converter={x:Static BoolConverters.Not}}"/> + { + if (Application.Current?.ApplicationLifetime + is IClassicDesktopStyleApplicationLifetime { MainWindow: { } win }) + await new ManageDesktopShortcutsWindow().ShowDialog(win); + }; } } diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml new file mode 100644 index 000000000..41ce2f5dc --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs new file mode 100644 index 000000000..bcac9ab48 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs @@ -0,0 +1,440 @@ +using Avalonia.Controls; +using Avalonia.Input.Platform; +using Avalonia.Layout; +using Avalonia.Media; +using UniGetUI.Avalonia.ViewModels; +using UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; +using UniGetUI.Avalonia.Views.Controls; +using UniGetUI.Avalonia.Views.Controls.Settings; +using UniGetUI.Core.SettingsEngine.SecureSettings; +using UniGetUI.Core.Tools; +using UniGetUI.Interface.Enums; +using UniGetUI.PackageEngine.Interfaces; +using UniGetUI.PackageEngine.Managers.VcpkgManager; +using CoreSettings = UniGetUI.Core.SettingsEngine.Settings; +using CornerRadius = global::Avalonia.CornerRadius; +using Thickness = global::Avalonia.Thickness; + +namespace UniGetUI.Avalonia.Views.Pages.SettingsPages; + +public sealed partial class PackageManagerPage : UserControl, ISettingsPage +{ + private PackageManagerViewModel ViewModel => (PackageManagerViewModel)DataContext!; + + public bool CanGoBack => true; + public string ShortTitle => ViewModel.PageTitle; + + public event EventHandler? RestartRequired; + public event EventHandler? NavigationRequested; + + public PackageManagerPage(IPackageManager manager) + { + DataContext = new PackageManagerViewModel(manager); + InitializeComponent(); + + ViewModel.PropertyChanged += (_, e) => + { + if (e.PropertyName is nameof(PackageManagerViewModel.Severity) + or nameof(PackageManagerViewModel.StatusTitle) + or nameof(PackageManagerViewModel.StatusMessage)) + ApplyStatusBrushes(); + }; + + ViewModel.RestartRequired += (s, e) => RestartRequired?.Invoke(s, e); + ViewModel.NavigateToAdministratorRequested += (_, _) => NavigationRequested?.Invoke(this, typeof(Administrator)); + + BuildPage(); + ApplyStatusBrushes(); + } + + // ── Dynamic UI construction ─────────────────────────────────────────────── + + private void BuildPage() + { + var manager = ViewModel.Manager; + + // ── Enable/Disable toggle + EnableManager.DictionaryName = CoreSettings.K.DisabledManagers; + EnableManager.KeyName = manager.Name; + EnableManager.Text = CoreTools.Translate("Enable {pm}").Replace("{pm}", manager.DisplayName); + ExtraControls.IsEnabled = manager.IsEnabled(); + EnableManager.StateChanged += (_, _) => + { + ExtraControls.IsEnabled = manager.IsEnabled(); + _ = ViewModel.ReloadManagerCommand.ExecuteAsync(null); + }; + + // ── Executable picker card + bool customPathsAllowed = SecureSettings.Get(SecureSettings.K.AllowCustomManagerPaths); + var execGrid = new Grid + { + ColumnDefinitions = new ColumnDefinitions("*,Auto"), + RowDefinitions = new RowDefinitions("Auto,Auto,Auto"), + HorizontalAlignment = HorizontalAlignment.Stretch, + }; + + var execHint = new TextBlock + { + Text = CoreTools.Translate("Not finding the file you are looking for? Make sure it has been added to path."), + FontSize = 12, + FontWeight = FontWeight.SemiBold, + Opacity = 0.7, + TextWrapping = TextWrapping.Wrap, + }; + Grid.SetColumnSpan(execHint, 2); + execGrid.Children.Add(execHint); + + var execCombo = new ComboBox { HorizontalAlignment = HorizontalAlignment.Stretch }; + foreach (var path in manager.FindCandidateExecutableFiles()) + execCombo.Items.Add(path); + + string savedPath = CoreSettings.GetDictionaryItem(CoreSettings.K.ManagerPaths, manager.Name) ?? ""; + if (string.IsNullOrEmpty(savedPath)) + { + var (found, path) = manager.GetExecutableFile(); + savedPath = found ? path : ""; + } + execCombo.SelectedItem = savedPath; + execCombo.IsEnabled = customPathsAllowed; + execCombo.SelectionChanged += (s, _) => + { + if (s is ComboBox combo && combo.SelectedItem?.ToString() is { Length: > 0 } selected) + ViewModel.OnExecutableSelected(selected); + }; + Grid.SetRow(execCombo, 1); + Grid.SetColumnSpan(execCombo, 2); + execGrid.Children.Add(execCombo); + + if (!customPathsAllowed) + { + var securityWarning = new TextBlock + { + Text = CoreTools.Translate("For security reasons, changing the executable file is disabled by default"), + FontSize = 12, + FontWeight = FontWeight.SemiBold, + Opacity = 0.7, + TextWrapping = TextWrapping.Wrap, + Classes = { "setting-warning-text" }, + }; + Grid.SetRow(securityWarning, 2); + execGrid.Children.Add(securityWarning); + + var goToSecureBtn = new Button + { + Content = new TextBlock { Text = CoreTools.Translate("Change this"), FontSize = 12, Classes = { "hyperlink" } }, + Background = Brushes.Transparent, + BorderThickness = new Thickness(0), + Padding = new Thickness(0), + }; + goToSecureBtn.Click += (_, _) => ViewModel.NavigateToAdministratorCommand.Execute(null); + Grid.SetRow(goToSecureBtn, 2); + Grid.SetColumn(goToSecureBtn, 1); + execGrid.Children.Add(goToSecureBtn); + } + + ExecutableHolder.Content = new SettingsCard + { + BorderThickness = new Thickness(1, 0, 1, 0), + CornerRadius = new CornerRadius(0), + Header = CoreTools.Translate("Select the executable to be used. The following list shows the executables found by UniGetUI"), + Description = execGrid, + Margin = new Thickness(0, 2, 0, 2), + }; + + // ── Current path card + var copyIcon = new SvgIcon + { + Path = "avares://UniGetUI.Avalonia/Assets/Symbols/copy.svg", + Width = 24, + Height = 24, + }; + var copyBtn = new Button + { + Content = copyIcon, + Padding = new Thickness(8), + VerticalAlignment = VerticalAlignment.Center, + Background = Brushes.Transparent, + BorderThickness = new Thickness(0), + }; + var pathCard = new SettingsCard + { + BorderThickness = new Thickness(1, 0, 1, 1), + CornerRadius = new CornerRadius(0, 0, 8, 8), + Header = CoreTools.Translate("Current executable file:"), + Content = copyBtn, + }; + var pathLabel = new TextBlock + { + FontFamily = new FontFamily("Courier New"), + FontSize = 14, + TextWrapping = TextWrapping.Wrap, + }; + pathLabel.Text = ViewModel.PathLabelText; + ViewModel.PropertyChanged += (_, e) => + { + if (e.PropertyName == nameof(PackageManagerViewModel.PathLabelText)) + pathLabel.Text = ViewModel.PathLabelText; + }; + pathCard.Description = pathLabel; + copyBtn.Click += (_, _) => _ = CopyPathAndFlashIcon(pathLabel.Text, copyBtn, copyIcon); + PathHolder.Content = pathCard; + + // ── Install options panel + var installOptions = new InstallOptionsPanel(manager); + installOptions.NavigateToAdministratorRequested += (_, _) => + NavigationRequested?.Invoke(this, typeof(Administrator)); + InstallOptionsHolder.Content = installOptions; + + // ── Disable notifications card + var disableNotifsCard = new CheckboxCard_Dict + { + Text = CoreTools.Translate("Ignore packages from {pm} when showing a notification about updates") + .Replace("{pm}", manager.DisplayName), + DictionaryName = CoreSettings.K.DisabledPackageManagerNotifications, + ForceInversion = true, + KeyName = manager.Name, + }; + + BuildExtraControls(disableNotifsCard); + + // ── Logs card + ManagerLogs.Text = CoreTools.Translate("View {0} logs", manager.DisplayName); + ManagerLogs.Icon = UniGetUI.Interface.Enums.IconType.Console; + ManagerLogs.Click += (_, _) => + { + if (TopLevel.GetTopLevel(this) is Window { DataContext: MainWindowViewModel vm }) + vm.OpenManagerLogs(manager); + }; + + // ── Pip AppExecution Alias warning + if (manager.Name == "Pip") + { + ManagerLogs.CornerRadius = new CornerRadius(8, 8, 0, 0); + AppExecutionAliasWarning.IsVisible = true; + AppExecutionAliasLabel.Text = CoreTools.Translate( + "If Python cannot be found or is not listing packages but is installed on the system, " + + "you may need to disable the \"python.exe\" App Execution Alias in the settings."); + } + } + + private void BuildExtraControls(CheckboxCard_Dict disableNotifsCard) + { + ExtraControls.Children.Clear(); + var manager = ViewModel.Manager; + bool managerHasSources = manager.Capabilities.SupportsCustomSources && manager.Name != "Vcpkg"; + + if (managerHasSources) + { + ExtraControls.Children.Add(new SourceManagerCard(manager) + { + Margin = new Thickness(0, 0, 0, 16), + }); + ExtraControls.Children.Add(new TextBlock + { + Margin = new Thickness(44, 24, 4, 8), + FontWeight = FontWeight.SemiBold, + Text = CoreTools.Translate("Advanced options"), + }); + } + + switch (manager.Name) + { + case "WinGet": + disableNotifsCard.CornerRadius = new CornerRadius(8, 8, 0, 0); + disableNotifsCard.BorderThickness = new Thickness(1, 1, 1, 0); + ExtraControls.Children.Add(disableNotifsCard); + + var wingetResetBtn = new ButtonCard + { + Text = CoreTools.Translate("Reset WinGet") + + $" ({CoreTools.Translate("This may help if no packages are listed")})", + ButtonText = CoreTools.AutoTranslated("Reset"), + CornerRadius = new CornerRadius(0), + }; + wingetResetBtn.Click += (_, _) => { /* TODO: HandleBrokenWinGet */ }; + ExtraControls.Children.Add(wingetResetBtn); + + ExtraControls.Children.Add(new CheckboxCard + { + Text = CoreTools.Translate("Force install location parameter when updating packages with custom locations"), + SettingName = CoreSettings.K.WinGetForceLocationOnUpdate, + CornerRadius = new CornerRadius(0), + BorderThickness = new Thickness(1, 0, 1, 1), + }); + + var wingetUseBundled = new CheckboxCard + { + Text = $"{CoreTools.Translate("Use bundled WinGet instead of system WinGet")} ({CoreTools.Translate("This may help if WinGet packages are not shown")})", + SettingName = CoreSettings.K.ForceLegacyBundledWinGet, + CornerRadius = new CornerRadius(0, 0, 8, 8), + BorderThickness = new Thickness(1, 0, 1, 1), + }; + wingetUseBundled.StateChanged += (_, _) => _ = ViewModel.ReloadManagerCommand.ExecuteAsync(null); + ExtraControls.Children.Add(wingetUseBundled); + break; + + case "Scoop": + disableNotifsCard.CornerRadius = new CornerRadius(8, 8, 0, 0); + disableNotifsCard.BorderThickness = new Thickness(1, 1, 1, 0); + ExtraControls.Children.Add(disableNotifsCard); + + var scoopInstall = new ButtonCard + { + Text = CoreTools.AutoTranslated("Install Scoop"), + ButtonText = CoreTools.AutoTranslated("Install"), + CornerRadius = new CornerRadius(0), + }; + scoopInstall.Click += (_, _) => ViewModel.ScoopInstallCommand.Execute(null); + ExtraControls.Children.Add(scoopInstall); + + var scoopUninstall = new ButtonCard + { + Text = CoreTools.AutoTranslated("Uninstall Scoop (and its packages)"), + ButtonText = CoreTools.AutoTranslated("Uninstall"), + CornerRadius = new CornerRadius(0), + BorderThickness = new Thickness(1, 0, 1, 0), + }; + scoopUninstall.Click += (_, _) => ViewModel.ScoopUninstallCommand.Execute(null); + ExtraControls.Children.Add(scoopUninstall); + + var scoopCleanup = new ButtonCard + { + Text = CoreTools.AutoTranslated("Run cleanup and clear cache"), + ButtonText = CoreTools.AutoTranslated("Run"), + CornerRadius = new CornerRadius(0), + }; + scoopCleanup.Click += (_, _) => ViewModel.ScoopCleanupCommand.Execute(null); + ExtraControls.Children.Add(scoopCleanup); + + ExtraControls.Children.Add(new CheckboxCard + { + CornerRadius = new CornerRadius(0, 0, 8, 8), + BorderThickness = new Thickness(1, 0, 1, 1), + SettingName = CoreSettings.K.EnableScoopCleanup, + Text = CoreTools.AutoTranslated("Enable Scoop cleanup on launch"), + }); + break; + + case "Chocolatey": + disableNotifsCard.CornerRadius = new CornerRadius(8, 8, 0, 0); + disableNotifsCard.BorderThickness = new Thickness(1, 1, 1, 0); + ExtraControls.Children.Add(disableNotifsCard); + + var chocoSysChoco = new CheckboxCard + { + Text = CoreTools.AutoTranslated("Use system Chocolatey"), + SettingName = CoreSettings.K.UseSystemChocolatey, + CornerRadius = new CornerRadius(0, 0, 8, 8), + }; + chocoSysChoco.StateChanged += (_, _) => _ = ViewModel.ReloadManagerCommand.ExecuteAsync(null); + ExtraControls.Children.Add(chocoSysChoco); + break; + + case "Vcpkg": + disableNotifsCard.CornerRadius = new CornerRadius(8, 8, 0, 0); + disableNotifsCard.BorderThickness = new Thickness(1, 1, 1, 0); + ExtraControls.Children.Add(disableNotifsCard); + + CoreSettings.SetValue(CoreSettings.K.DefaultVcpkgTriplet, Vcpkg.GetDefaultTriplet()); + var vcpkgTriplet = new ComboboxCard + { + Text = CoreTools.Translate("Default vcpkg triplet"), + SettingName = CoreSettings.K.DefaultVcpkgTriplet, + CornerRadius = new CornerRadius(0), + }; + foreach (string triplet in Vcpkg.GetSystemTriplets()) + vcpkgTriplet.AddItem(triplet, triplet, false); + vcpkgTriplet.ShowAddedItems(); + ExtraControls.Children.Add(vcpkgTriplet); + ExtraControls.Children.Add(BuildVcpkgRootCard()); + break; + + default: + disableNotifsCard.CornerRadius = new CornerRadius(8); + disableNotifsCard.BorderThickness = new Thickness(1); + ExtraControls.Children.Add(disableNotifsCard); + break; + } + } + + private ButtonCard BuildVcpkgRootCard() + { + var vcpkgRootCard = new ButtonCard + { + Text = CoreTools.AutoTranslated("Change vcpkg root location"), + ButtonText = CoreTools.AutoTranslated("Select"), + CornerRadius = new CornerRadius(0, 0, 8, 8), + BorderThickness = new Thickness(1, 0, 1, 1), + }; + + var rootLabel = new TextBlock + { + VerticalAlignment = VerticalAlignment.Center, + Text = ViewModel.VcpkgRootPath, + }; + var resetBtn = new Button + { + Content = CoreTools.Translate("Reset"), + IsEnabled = ViewModel.IsCustomVcpkgRootSet, + Margin = new Thickness(4, 0), + }; + var openBtn = new Button + { + Content = CoreTools.Translate("Open"), + IsEnabled = ViewModel.IsCustomVcpkgRootSet, + Margin = new Thickness(4, 0), + }; + + ViewModel.PropertyChanged += (_, e) => + { + if (e.PropertyName == nameof(PackageManagerViewModel.VcpkgRootPath)) + rootLabel.Text = ViewModel.VcpkgRootPath; + if (e.PropertyName == nameof(PackageManagerViewModel.IsCustomVcpkgRootSet)) + { + resetBtn.IsEnabled = ViewModel.IsCustomVcpkgRootSet; + openBtn.IsEnabled = ViewModel.IsCustomVcpkgRootSet; + } + }; + + resetBtn.Click += (_, _) => ViewModel.ResetVcpkgRootCommand.Execute(null); + openBtn.Click += (_, _) => ViewModel.OpenVcpkgRootCommand.Execute(null); + + var descPanel = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 4 }; + descPanel.Children.Add(rootLabel); + descPanel.Children.Add(resetBtn); + descPanel.Children.Add(openBtn); + vcpkgRootCard.Description = descPanel; + + vcpkgRootCard.Click += (_, _) => ViewModel.PickVcpkgRootCommand.Execute(vcpkgRootCard); + return vcpkgRootCard; + } + + // ── View-only: brush lookup (needs ActualThemeVariant) ─────────────────── + + private void ApplyStatusBrushes() + { + StatusBar.Classes.Remove("status-success"); + StatusBar.Classes.Remove("status-warning"); + StatusBar.Classes.Remove("status-error"); + StatusBar.Classes.Remove("status-info"); + + string cls = ViewModel.Severity switch + { + ManagerStatusSeverity.Success => "status-success", + ManagerStatusSeverity.Warning => "status-warning", + ManagerStatusSeverity.Error => "status-error", + _ => "status-info", + }; + StatusBar.Classes.Add(cls); + } + + private async Task CopyPathAndFlashIcon(string? text, Button btn, SvgIcon icon) + { + if (string.IsNullOrEmpty(text)) return; + if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard) + await clipboard.SetTextAsync(text); + btn.Content = new TextBlock { Text = "✓", FontSize = 20, VerticalAlignment = VerticalAlignment.Center }; + await Task.Delay(1000); + btn.Content = icon; + } +} diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/SettingsBasePage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/SettingsBasePage.axaml.cs index 5703ebaf0..82816cfec 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/SettingsBasePage.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/SettingsBasePage.axaml.cs @@ -69,6 +69,10 @@ private void NavigateToPage(UserControl page) sp.RestartRequired += Page_RestartRequired; VM.Title = sp.ShortTitle; } + + // Refresh toggle states when returning to the managers list + if (page is ManagersHomepage mh) + mh.RefreshToggles(); } private void NavigateBack() @@ -117,8 +121,15 @@ private void Page_RestartRequired(object? sender, EventArgs e) => private SettingsHomepage GetSettingsHomepage() => _settingsHomepage ??= new SettingsHomepage(); - private ManagersHomepage GetManagersHomepage() => - _managersHomepage ??= new ManagersHomepage(); + private ManagersHomepage GetManagersHomepage() + { + if (_managersHomepage is null) + { + _managersHomepage = new ManagersHomepage(); + _managersHomepage.ManagerNavigationRequested += (_, manager) => NavigateTo(manager); + } + return _managersHomepage; + } // ── IInnerNavigationPage ────────────────────────────────────────────── @@ -150,7 +161,10 @@ public void OnLeave() { } public void NavigateTo(IPackageManager manager) { - // TODO: navigate to PackageManagerPage for this manager (Phase 5) + if (_currentContent is not null) + _history.Push(_currentContent); + + NavigateToPage(new PackageManagerPage(manager)); } public void NavigateTo(Type page) diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/SourceManagerCard.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/SourceManagerCard.axaml new file mode 100644 index 000000000..e601156de --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/SourceManagerCard.axaml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +