From 5e117c05320f7c510e9e37134c2615e21c68393c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Fri, 23 Oct 2026 11:56:59 -0400 Subject: [PATCH] Move the Catalog Editor out of the sidebar and behind its own setting The Catalog Editor is an administrator tool, but it sat in the main navigation where any curious user could open it and edit the deployed catalog. It is now off by default and enabled with a new "Enable the Catalog Editor" sub-option under Enable Software Catalog. When enabled, it opens from a Catalog Editor button in the Software Catalog toolbar, or from Create a catalog on a device with no catalog. The editor links back to the Software Catalog above its title, and it starts on an untitled document instead of loading the machine-wide SoftwareCatalog.json, so the deployed catalog is only edited when an administrator opens it on purpose. Co-Authored-By: Claude Opus 5.5 --- src/Languages/lang_en.json | 6 ++++ .../Infrastructure/AvaloniaBootstrapper.cs | 13 +++++-- .../Infrastructure/NavigationPageOrder.cs | 2 +- .../DialogPages/CatalogEditorViewModel.cs | 11 +++--- .../ViewModels/MainWindowViewModel.cs | 20 +++++++++-- .../SettingsPages/ExperimentalViewModel.cs | 13 +++++++ .../ViewModels/SidebarViewModel.cs | 4 +++ .../SoftwarePages/PackagesPageViewModel.cs | 16 ++++++++- .../SoftwarePages/SoftwareCatalogViewModel.cs | 28 +++++++-------- .../Views/MainWindow.axaml.cs | 1 + .../Pages/SettingsPages/Experimental.axaml | 9 ++++- .../Pages/SettingsPages/Interface_P.axaml.cs | 2 +- src/UniGetUI.Avalonia/Views/SidebarView.axaml | 12 ------- .../Views/SidebarView.axaml.cs | 2 -- .../SoftwarePages/AbstractPackagesPage.axaml | 16 +++++++++ .../Views/SoftwarePages/CatalogEditorPage.cs | 35 +++++++++++++++++-- .../SoftwarePages/SoftwareCatalogPage.axaml | 14 ++++++++ .../SettingsEngine_Names.cs | 2 ++ src/UniGetUI.Tests/CatalogEditorTests.cs | 12 +++++++ src/UniGetUI.Tests/SoftwareCatalogTests.cs | 20 +++++------ 20 files changed, 181 insertions(+), 57 deletions(-) diff --git a/src/Languages/lang_en.json b/src/Languages/lang_en.json index ec5f9c7249..b8dd1edf07 100644 --- a/src/Languages/lang_en.json +++ b/src/Languages/lang_en.json @@ -113,6 +113,12 @@ "Enable Software Catalog in Experimental settings and developer options, then restart UniGetUI.": "Enable Software Catalog in Experimental settings and developer options, then restart UniGetUI.", "The software catalog could not be loaded. Ask an administrator to check {0}, then try again.": "The software catalog could not be loaded. Ask an administrator to check {0}, then try again.", "Edit software catalog": "Edit software catalog", + "Back to {0}": "Back to {0}", + "Create a catalog": "Create a catalog", + "Create a new catalog or open a catalog file": "Create a new catalog or open a catalog file", + "Enable the Catalog Editor": "Enable the Catalog Editor", + "Catalog Editor is disabled": "Catalog Editor is disabled", + "Enable the Catalog Editor in Experimental settings and developer options, then restart UniGetUI.": "Enable the Catalog Editor in Experimental settings and developer options, then restart UniGetUI.", "Software Catalog": "Software Catalog", "Software selected for you, ready to install.": "Software selected for you, ready to install.", "Refresh software catalog": "Refresh software catalog", diff --git a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs index 953c751455..1025d5e04f 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs @@ -257,9 +257,14 @@ private static IpcAppInfo GetAppInfo() CanNavigate = window is not null, CanQuit = true, CurrentPage = window is null ? "" : IpcAppPages.ToPageName(window.CurrentPage.ToString()), - SupportedPages = window is { SoftwareCatalogEnabled: true } - ? IpcAppPages.SupportedPages - : IpcAppPages.SupportedPages.Where(page => page is not ("catalog" or "catalog-editor")).ToArray(), + SupportedPages = IpcAppPages.SupportedPages + .Where(page => page switch + { + "catalog" => window is { SoftwareCatalogEnabled: true }, + "catalog-editor" => window is { CatalogEditorEnabled: true }, + _ => true, + }) + .ToArray(), SystemRestartPending = PendingRebootStore.HasPending, SystemRestartPendingPackages = PendingRebootStore.PendingCount, }; @@ -298,6 +303,8 @@ private static IpcCommandResult NavigateApp(IpcAppNavigateRequest request) case "catalog-editor": if (!window.SoftwareCatalogEnabled) throw new InvalidOperationException("Software Catalog is disabled. Enable it in Experimental settings and developer options, then restart UniGetUI."); + if (page == "catalog-editor" && !window.CatalogEditorEnabled) + throw new InvalidOperationException("Catalog Editor is disabled. Enable it in Experimental settings and developer options, then restart UniGetUI."); window.Navigate(page == "catalog-editor" ? PageType.CatalogEditor : PageType.Catalog); break; case "settings": diff --git a/src/UniGetUI.Avalonia/Infrastructure/NavigationPageOrder.cs b/src/UniGetUI.Avalonia/Infrastructure/NavigationPageOrder.cs index acdb1901cf..7c376e7906 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/NavigationPageOrder.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/NavigationPageOrder.cs @@ -6,7 +6,7 @@ namespace UniGetUI.Avalonia.Infrastructure; internal static class NavigationPageOrder { public static PageType[] DefaultPages(bool catalogEnabled) => catalogEnabled - ? [PageType.Catalog, PageType.Discover, PageType.Updates, PageType.Installed, PageType.Bundles, PageType.CatalogEditor] + ? [PageType.Catalog, PageType.Discover, PageType.Updates, PageType.Installed, PageType.Bundles] : [PageType.Discover, PageType.Updates, PageType.Installed, PageType.Bundles]; public static PageType[] Get(bool catalogEnabled) diff --git a/src/UniGetUI.Avalonia/ViewModels/DialogPages/CatalogEditorViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/DialogPages/CatalogEditorViewModel.cs index 58a4667d95..3db59f35c9 100644 --- a/src/UniGetUI.Avalonia/ViewModels/DialogPages/CatalogEditorViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/DialogPages/CatalogEditorViewModel.cs @@ -55,7 +55,7 @@ public partial class CatalogEditorViewModel : ViewModelBase private string _query = ""; [ObservableProperty] - private string _filePath = SoftwareCatalog.FilePath; + private string _filePath = ""; private string _message = ""; @@ -110,14 +110,15 @@ public Task EnsureLoadedAsync() return _initialLoad ??= LoadOrStartAsync(FilePath); } - // No catalog deployed yet: start an empty document that saves to that path, so a first - // catalog can be created there instead of the editor opening disabled. + // The editor starts on an untitled, empty document rather than the deployed catalog, so nobody + // edits what this device installs from by accident; New catalog or Open file is the first step. + // A path set beforehand that does not exist yet also starts empty, and saves there. private Task LoadOrStartAsync(string path) { - if (!IsMissing(path)) return LoadAsync(path); + if (path.Length > 0 && !IsMissing(path)) return LoadAsync(path); SelectedCatalog = null; Catalogs.Clear(); - FilePath = Path.GetFullPath(path); + FilePath = path.Length > 0 ? Path.GetFullPath(path) : ""; _savedCatalogs = []; IsLoaded = true; IsDirty = false; diff --git a/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs index f90b016d11..e350b0e42c 100644 --- a/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs @@ -433,7 +433,7 @@ public MainWindowViewModel() RegisterBannerToast(TelemetryWarner); RegisterBannerToast(PortableImportBanner); - CatalogEditor = Sidebar.SoftwareCatalogEnabled ? new CatalogEditorViewModel() : null; + CatalogEditor = Sidebar.CatalogEditorEnabled ? new CatalogEditorViewModel() : null; DiscoverPage = new DiscoverSoftwarePage(CatalogEditor); UpdatesPage = new SoftwareUpdatesPage(); InstalledPage = new InstalledPackagesPage(); @@ -669,7 +669,7 @@ public void LoadDefaultPage() "installed" => PageType.Installed, "bundles" => PageType.Bundles, "catalog" when Sidebar.SoftwareCatalogEnabled => PageType.Catalog, - "catalog-editor" when Sidebar.SoftwareCatalogEnabled => PageType.CatalogEditor, + "catalog-editor" when Sidebar.CatalogEditorEnabled => PageType.CatalogEditor, "settings" => PageType.Settings, // A usable admin-curated catalog is the simplest starting point, so it wins over the default pages. _ when Sidebar.HasUsableCatalog => PageType.Catalog, @@ -743,6 +743,15 @@ public async Task NavigateToAsync( MainWindow.RuntimeNotificationLevel.Error); return false; } + if (newPage_t is PageType.CatalogEditor && !Sidebar.CatalogEditorEnabled) + { + Logger.Warn("Catalog Editor navigation was rejected because the editor is disabled."); + MainWindow.Instance?.ShowBanner( + CoreTools.Translate("Catalog Editor is disabled"), + CoreTools.Translate("Enable the Catalog Editor in Experimental settings and developer options, then restart UniGetUI."), + MainWindow.RuntimeNotificationLevel.Error); + return false; + } if (newPage_t is PageType.About) { _ = ShowAboutDialog(); return true; } if (newPage_t is PageType.Quit) { MainWindow.Instance?.QuitApplication(); return true; } @@ -840,6 +849,13 @@ public async Task NavigateToAsync( _ => CoreTools.Translate("UniGetUI"), }; + /// Opens the Catalog Editor on a new catalog file, for the Software Catalog's New catalog actions. + public async Task StartNewCatalogAsync() + { + if (!await NavigateToAsync(PageType.CatalogEditor)) return; + if (_catalogEditorPage is { } editor) await editor.StartNewCatalogAsync(); + } + public void NavigateBack() => _ = NavigateBackAsync(); public async Task NavigateBackAsync(CancellationToken cancellationToken = default) diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/ExperimentalViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/ExperimentalViewModel.cs index 47f103135c..113c7f120f 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/ExperimentalViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/ExperimentalViewModel.cs @@ -1,5 +1,7 @@ +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; @@ -7,8 +9,19 @@ public partial class ExperimentalViewModel : ViewModelBase { public bool IsWindows { get; } = OperatingSystem.IsWindows(); + // The Catalog Editor is a sub-option of the Software Catalog. + [ObservableProperty] + private bool _isSoftwareCatalogEnabled = CoreSettings.Get(CoreSettings.K.EnableSoftwareCatalog); + public event EventHandler? RestartRequired; [RelayCommand] private void ShowRestartRequired() => RestartRequired?.Invoke(this, EventArgs.Empty); + + [RelayCommand] + private void SoftwareCatalogChanged() + { + IsSoftwareCatalogEnabled = CoreSettings.Get(CoreSettings.K.EnableSoftwareCatalog); + ShowRestartRequired(); + } } diff --git a/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs index 2c7bad65ab..31da33e069 100644 --- a/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs @@ -13,6 +13,10 @@ public partial class SidebarViewModel : ViewModelBase { public bool SoftwareCatalogEnabled { get; } = Settings.Get(Settings.K.EnableSoftwareCatalog); + // An administrator tool, opened from the catalog page rather than the sidebar; off unless asked for. + public bool CatalogEditorEnabled { get; } = + Settings.Get(Settings.K.EnableSoftwareCatalog) && Settings.Get(Settings.K.EnableCatalogEditor); + // Only a catalog with packages to offer becomes the startup page; an unset or broken one would // open the app on an empty page. Checked once at startup, like the setting itself. public bool HasUsableCatalog { get; } = diff --git a/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs index d217a93dfa..df137752ae 100644 --- a/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs @@ -154,7 +154,8 @@ partial void OnIsFilterPaneOpenChanged(bool value) public bool SimilarSearchEnabled { get; private set; } public bool InstallerHostColumnVisible { get; } public bool DownloadSizeColumnVisible { get; } - public readonly string NoPackagesText; + // Settable so a page can say why it is empty; takes effect on the next list update. + public string NoPackagesText { get; set; } public readonly string NoMatchesText; public readonly string SearchBoxPlaceholder; private readonly string _noPackagesSubtitleBase; @@ -170,6 +171,19 @@ partial void OnIsFilterPaneOpenChanged(bool value) [ObservableProperty] private string _pageIconPath = ""; [ObservableProperty] private string _subtitle = ""; + // A page opened from another one (the Catalog Editor from the Software Catalog) links back to it above its title. + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasParentPage))] + [NotifyPropertyChangedFor(nameof(ParentPageLinkName))] + private string _parentPageTitle = ""; + + public bool HasParentPage => ParentPageTitle.Length > 0; + public string ParentPageLinkName => CoreTools.Translate("Back to {0}", ParentPageTitle); + public Action? ParentPageRequested { get; set; } + + [RelayCommand] + private void GoToParentPage() => ParentPageRequested?.Invoke(); + // Shown after the subtitle in a secondary tone: when the list was last checked, then the page suffix. [ObservableProperty] private string _subtitleDetail = ""; diff --git a/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/SoftwareCatalogViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/SoftwareCatalogViewModel.cs index 4e3ea3322f..598ec5c619 100644 --- a/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/SoftwareCatalogViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/SoftwareCatalogViewModel.cs @@ -168,6 +168,7 @@ private void NotifyBackgroundTextChanged() OnPropertyChanged(nameof(BackgroundText)); OnPropertyChanged(nameof(HasBackgroundText)); OnPropertyChanged(nameof(HasBackgroundDetail)); + OnPropertyChanged(nameof(ShowCreateCatalog)); } private bool IsSelectedByFilters(CatalogTileViewModel tile) => @@ -267,22 +268,21 @@ partial void OnErrorMessageChanged(string value) value, global::Avalonia.Automation.AutomationLiveSetting.Assertive); } + // Administrators who enabled the Catalog Editor can start a catalog from here; everyone else only installs. + public bool IsEditorEnabled { get; } = + Settings.Get(Settings.K.EnableSoftwareCatalog) && Settings.Get(Settings.K.EnableCatalogEditor); + + // On a device with no catalog yet, the empty page offers to create one. + public bool ShowCreateCatalog => IsEditorEnabled && _notConfigured && IsEmpty; + + [RelayCommand] + private void OpenEditor() => MainWindow.Instance?.Navigate(PageType.CatalogEditor); + [RelayCommand] - private async Task EditAsync() + private async Task NewCatalogAsync() { - try - { - var owner = MainWindow.Instance - ?? throw new InvalidOperationException("The catalog editor requires an application window."); - owner.Navigate(PageType.CatalogEditor); - await Task.CompletedTask; - } - catch (Exception ex) - { - Logger.Error("Could not open the software catalog editor."); - Logger.Error(ex); - ErrorMessage = CoreTools.Translate("The catalog editor could not be opened: {0}", ex.Message); - } + if (MainWindow.Instance?.DataContext is MainWindowViewModel main) + await main.StartNewCatalogAsync(); } private bool _waitingForManagers; diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs index b70066f19c..db4e357eb8 100644 --- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs @@ -194,6 +194,7 @@ public enum RuntimeNotificationLevel private MainWindowViewModel ViewModel => (MainWindowViewModel)DataContext!; public PageType CurrentPage => ViewModel.CurrentPage_t; public bool SoftwareCatalogEnabled => ViewModel.Sidebar.SoftwareCatalogEnabled; + public bool CatalogEditorEnabled => ViewModel.Sidebar.CatalogEditorEnabled; public MainWindow() { diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Experimental.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Experimental.axaml index 08b4a9d0fa..5b41963dbd 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Experimental.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Experimental.axaml @@ -84,8 +84,15 @@ + + + IsEnabled="{Binding IsSoftwareCatalogEnabled}" + BorderThickness="1,0,1,1" + CornerRadius="0,0,8,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 27f310621a..6fbd56ed29 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml.cs @@ -46,7 +46,7 @@ public Interface_P() StartupPageSelector.AddItem(CoreTools.Translate("Software Updates"), "updates"); StartupPageSelector.AddItem(CoreTools.Translate("Installed Packages"), "installed"); StartupPageSelector.AddItem(CoreTools.Translate("Package Bundles"), "bundles"); - if (catalogEnabled) + if (catalogEnabled && CoreSettings.Get(CoreSettings.K.EnableCatalogEditor)) StartupPageSelector.AddItem(CoreTools.Translate("Catalog Editor"), "catalog-editor"); StartupPageSelector.AddItem(CoreTools.Translate("Settings"), "settings"); StartupPageSelector.SettingName = CoreSettings.K.StartupPage; diff --git a/src/UniGetUI.Avalonia/Views/SidebarView.axaml b/src/UniGetUI.Avalonia/Views/SidebarView.axaml index e97bf88a5f..f961777e5d 100644 --- a/src/UniGetUI.Avalonia/Views/SidebarView.axaml +++ b/src/UniGetUI.Avalonia/Views/SidebarView.axaml @@ -160,18 +160,6 @@ - - - - - - - diff --git a/src/UniGetUI.Avalonia/Views/SidebarView.axaml.cs b/src/UniGetUI.Avalonia/Views/SidebarView.axaml.cs index 30c3ea8a3b..aa6b340027 100644 --- a/src/UniGetUI.Avalonia/Views/SidebarView.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/SidebarView.axaml.cs @@ -426,7 +426,6 @@ private void AssignShortcutKeys(PageType[] pages) PageType.Updates => UpdatesNavBtn, PageType.Installed => InstalledNavBtn, PageType.Bundles => BundlesNavBtn, - PageType.CatalogEditor => CatalogEditorNavBtn, PageType.Settings => SettingsNavBtn, PageType.Managers => ManagersNavBtn, _ => null, @@ -446,7 +445,6 @@ private void SyncListBoxSelection(PageType page) PageType.Installed => InstalledNavBtn, PageType.Bundles => BundlesNavBtn, PageType.Catalog => CatalogNavBtn, - PageType.CatalogEditor => CatalogEditorNavBtn, _ => null, }; FooterNavListBox.SelectedItem = page switch diff --git a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml index 811880c1d2..7a5efccf66 100644 --- a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml +++ b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml @@ -262,6 +262,22 @@ Path="{Binding PageIconPath}"/> + + + { + if (GetMainWindow()?.DataContext is MainWindowViewModel main) main.NavigateTo(PageType.Catalog); + }; + document.PropertyChanged += DocumentChanged; document.ErrorRaised += message => Dispatcher.UIThread.Post(() => { @@ -148,6 +155,9 @@ private void SyncCatalog() _observedCatalog?.Packages.CollectionChanged -= PackagesChanged; _observedCatalog = _document.SelectedCatalog; _observedCatalog?.Packages.CollectionChanged += PackagesChanged; + ViewModel.NoPackagesText = _observedCatalog is null + ? CoreTools.Translate("Create a new catalog or open a catalog file") + : CoreTools.Translate("This catalog contains no packages."); _loader.Replace(_observedCatalog?.Packages ?? []); } @@ -349,6 +359,13 @@ public async Task ConfirmDiscardAsync() return dialog.Result is true; } + /// Starts a new catalog file, as New file does, once the document has loaded. + public async Task StartNewCatalogAsync() + { + await _document.EnsureLoadedAsync(); + await NewAsync(); + } + private async Task NewAsync() { if (_document.IsBusy || _pickingFile) return; @@ -374,11 +391,17 @@ private async Task OpenAsync() SyncStatus(); try { - var files = await MainWindow.Instance!.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions + var storage = MainWindow.Instance!.StorageProvider; + string deployedFolder = Path.GetDirectoryName(SoftwareCatalog.FilePath)!; + var files = await storage.OpenFilePickerAsync(new FilePickerOpenOptions { Title = CoreTools.Translate("Open catalog file"), AllowMultiple = false, FileTypeFilter = [new FilePickerFileType("JSON") { Patterns = ["*.json"] }], + // Start where the deployed catalog lives, so opening it is one click away. + SuggestedStartLocation = Directory.Exists(deployedFolder) + ? await storage.TryGetFolderFromPathAsync(deployedFolder) + : null, }); if (files.Count > 0) await _document.LoadAsync(files[0].TryGetLocalPath() @@ -398,9 +421,15 @@ private async Task SaveAsync(bool saveAs) string path = _document.FilePath; if (saveAs || string.IsNullOrEmpty(path)) { - var file = await MainWindow.Instance!.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions + var storage = MainWindow.Instance!.StorageProvider; + string deployedFolder = Path.GetDirectoryName(SoftwareCatalog.FilePath)!; + var file = await storage.SaveFilePickerAsync(new FilePickerSaveOptions { Title = CoreTools.Translate("Save catalog as"), + // An untitled catalog starts where it would be deployed. + SuggestedStartLocation = string.IsNullOrEmpty(path) && Directory.Exists(deployedFolder) + ? await storage.TryGetFolderFromPathAsync(deployedFolder) + : null, SuggestedFileName = string.IsNullOrEmpty(path) ? "SoftwareCatalog.json" : Path.GetFileName(path), DefaultExtension = "json", ShowOverwritePrompt = true, diff --git a/src/UniGetUI.Avalonia/Views/SoftwarePages/SoftwareCatalogPage.axaml b/src/UniGetUI.Avalonia/Views/SoftwarePages/SoftwareCatalogPage.axaml index a256d85b99..1649f86a37 100644 --- a/src/UniGetUI.Avalonia/Views/SoftwarePages/SoftwareCatalogPage.axaml +++ b/src/UniGetUI.Avalonia/Views/SoftwarePages/SoftwareCatalogPage.axaml @@ -217,6 +217,17 @@ + + @@ -572,6 +583,9 @@ TextAlignment="Center" TextWrapping="Wrap" Text="{Binding BackgroundDetail}" IsVisible="{Binding HasBackgroundDetail}"/> +