diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs index 1228cf75ff..5f0c54e948 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs @@ -3,6 +3,7 @@ using Avalonia.Input.Platform; using Avalonia.Layout; using Avalonia.Media; +using Avalonia.Platform.Storage; using UniGetUI.Avalonia.Infrastructure; using UniGetUI.Avalonia.ViewModels; using UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; @@ -78,7 +79,7 @@ private void BuildPage() var execHint = new TextBlock { - Text = CoreTools.Translate("Not finding the file you are looking for? Make sure it has been added to path."), + Text = CoreTools.Translate("Not finding the file you are looking for? Browse to it or make sure it has been added to PATH."), FontSize = 12, FontWeight = FontWeight.SemiBold, Opacity = 0.7, @@ -90,14 +91,21 @@ private void BuildPage() var execCombo = new ComboBox { HorizontalAlignment = HorizontalAlignment.Stretch }; AutomationProperties.SetName(execCombo, CoreTools.Translate("Select the executable to be used. The following list shows the executables found by UniGetUI")); foreach (var path in manager.FindCandidateExecutableFiles()) - execCombo.Items.Add(path); + AddExecutablePathItem(execCombo, path); string savedPath = CoreSettings.GetDictionaryItem(CoreSettings.K.ManagerPaths, manager.Name) ?? ""; + if (!string.IsNullOrEmpty(savedPath) && File.Exists(savedPath)) + AddExecutablePathItem(execCombo, savedPath); if (string.IsNullOrEmpty(savedPath)) { var (found, path) = manager.GetExecutableFile(); savedPath = found ? path : ""; } + else if (!File.Exists(savedPath)) + { + var (found, path) = manager.GetExecutableFile(); + savedPath = found ? path : ""; + } execCombo.SelectedItem = savedPath; execCombo.IsEnabled = customPathsAllowed; execCombo.SelectionChanged += (s, _) => @@ -106,9 +114,37 @@ private void BuildPage() ViewModel.OnExecutableSelected(selected); }; Grid.SetRow(execCombo, 1); - Grid.SetColumnSpan(execCombo, 2); + Grid.SetColumn(execCombo, 0); execGrid.Children.Add(execCombo); + var browseExecutableButton = new Button + { + Content = CoreTools.Translate("Browse..."), + IsEnabled = customPathsAllowed, + Margin = new Thickness(8, 0, 0, 0), + }; + browseExecutableButton.Click += async (_, _) => + { + if (TopLevel.GetTopLevel(this) is not { } topLevel) return; + var files = await topLevel.StorageProvider.OpenFilePickerAsync( + new FilePickerOpenOptions + { + AllowMultiple = false, + Title = CoreTools.Translate("Select executable"), + FileTypeFilter = GetExecutableFileTypeFilter(), + }); + if (files is not [{ } file]) return; + + string? path = file.TryGetLocalPath(); + if (string.IsNullOrWhiteSpace(path)) return; + + AddExecutablePathItem(execCombo, path); + execCombo.SelectedItem = path; + }; + Grid.SetRow(browseExecutableButton, 1); + Grid.SetColumn(browseExecutableButton, 1); + execGrid.Children.Add(browseExecutableButton); + if (!customPathsAllowed) { var securityWarning = new TextBlock @@ -342,6 +378,34 @@ private void BuildPage() } } + private static IReadOnlyList GetExecutableFileTypeFilter() + { + if (!OperatingSystem.IsWindows()) + { + return [new FilePickerFileType(CoreTools.Translate("All files")) { Patterns = ["*"] }]; + } + + return + [ + new FilePickerFileType(CoreTools.Translate("Executable")) { Patterns = ["*.exe"] }, + new FilePickerFileType(CoreTools.Translate("All files")) { Patterns = ["*"] }, + ]; + } + + private static void AddExecutablePathItem(ComboBox comboBox, string path) + { + if (string.IsNullOrWhiteSpace(path)) + return; + + foreach (object? item in comboBox.Items) + { + if (string.Equals(item?.ToString(), path, StringComparison.OrdinalIgnoreCase)) + return; + } + + comboBox.Items.Add(path); + } + private void BuildExtraControls(CheckboxCard_Dict disableNotifsCard) { ExtraControls.Children.Clear(); diff --git a/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs b/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs index 40b861ef77..c08ef9a594 100644 --- a/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs +++ b/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs @@ -194,15 +194,16 @@ private void _logManagerInfo() public Tuple GetExecutableFile() { var candidates = FindCandidateExecutableFiles(); - if (candidates.Count == 0) - { - // No paths were found - return new(false, ""); - } // If custom package manager paths are DISABLED, get the first one (as old UniGetUI did) and return it. if (!SecureSettings.Get(SecureSettings.K.AllowCustomManagerPaths)) { + if (candidates.Count == 0) + { + // No paths were found + return new(false, ""); + } + return new(true, candidates[0]); } else @@ -214,32 +215,33 @@ public Tuple GetExecutableFile() // If there is no executable selection for this package manager if (string.IsNullOrEmpty(exeSelection)) { + if (candidates.Count == 0) + { + // No paths were found + return new(false, ""); + } + return new(true, candidates[0]); } else if (!File.Exists(exeSelection)) { Logger.Error( - $"The selected executable path {exeSelection} for manager {Name} does not exist, the default one will be used..." + $"The selected executable path {exeSelection} for manager {Name} does not exist, the default one will be used if available..." ); - return new(true, candidates[0]); } - // While technically executables that are not in the path should work, - // since detection of executables will be performed on the found paths, it is more consistent - // to throw an error when a non-found executable is used. Furthermore, doing this we can filter out - // any invalid paths or files. - if (candidates.Select(x => x.ToLower()).Contains(exeSelection.ToLower())) + else { return new(true, exeSelection); } - else + + if (candidates.Count == 0) { - Logger.Error( - $"The selected executable path {exeSelection} for manager {Name} was not found among the candidates " - + $"(executables found are [{string.Join(',', candidates)}]), the default will be used..." - ); - return new(true, candidates[0]); + // No paths were found + return new(false, ""); } + + return new(true, candidates[0]); } } diff --git a/src/UniGetUI.PackageEngine.Tests/PackageManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/PackageManagerTests.cs index b9d8a7a249..0aab8e4b23 100644 --- a/src/UniGetUI.PackageEngine.Tests/PackageManagerTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/PackageManagerTests.cs @@ -145,6 +145,21 @@ public void GetExecutableFileReturnsFalseWhenNoCandidatesExist() Assert.Equal(string.Empty, executable.Item2); } + [Fact] + public void GetExecutableFileUsesSavedPathWhenNoCandidatesExistAndCustomPathsAreEnabled() + { + var manager = CreateManager(); + var customPath = CreateExecutable("custom-only.exe"); + manager.SetCandidateExecutableFiles(); + EnableCustomManagerPaths(); + Settings.SetDictionaryItem(Settings.K.ManagerPaths, manager.Name, customPath); + + var executable = manager.GetExecutableFile(); + + Assert.True(executable.Item1); + Assert.Equal(customPath, executable.Item2); + } + [Fact] public void GetExecutableFileIgnoresSavedPathWhenCustomPathsAreDisabled() { @@ -197,7 +212,7 @@ public void GetExecutableFileFallsBackWhenSavedPathDoesNotExist() } [Fact] - public void GetExecutableFileFallsBackWhenSavedPathIsNotACandidate() + public void GetExecutableFileUsesSavedPathWhenSavedPathIsNotACandidate() { var manager = CreateManager(); var first = CreateExecutable("outside-a.exe"); @@ -214,7 +229,7 @@ public void GetExecutableFileFallsBackWhenSavedPathIsNotACandidate() var executable = manager.GetExecutableFile(); Assert.True(executable.Item1); - Assert.Equal(first, executable.Item2); + Assert.Equal(outsideCandidateList, executable.Item2); } [Fact] diff --git a/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml b/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml index 3a79e68dfe..3bebba72bd 100644 --- a/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml +++ b/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml @@ -75,14 +75,23 @@ FontSize="12" FontWeight="SemiBold" Opacity="0.7" - Text="Not finding the file you are looking for? Make sure it has been added to path." + Text="Not finding the file you are looking for? Browse to it or make sure it has been added to PATH." /> + (Settings.K.ManagerPaths, Manager.Name) ?? ""; + string selectedValue = configuredValue; + if (!string.IsNullOrEmpty(configuredValue) && File.Exists(configuredValue)) + { + AddExecutableComboBoxItem(configuredValue); + } if (string.IsNullOrEmpty(selectedValue)) { var exe = Manager.GetExecutableFile(); selectedValue = exe.Item1 ? exe.Item2 : ""; } + else if (!File.Exists(selectedValue)) + { + var exe = Manager.GetExecutableFile(); + selectedValue = exe.Item1 ? exe.Item2 : ""; + } ExecutableComboBox.SelectedValue = selectedValue; ExecutableComboBox.SelectionChanged += ExecutableComboBox_SelectionChanged; @@ -699,17 +712,50 @@ private void ManagerLogs_Click(object sender, RoutedEventArgs e) private void ExecutableComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { - if (string.IsNullOrEmpty(ExecutableComboBox.SelectedValue.ToString())) + if (ExecutableComboBox.SelectedValue?.ToString() is not { Length: > 0 } selectedValue) return; Settings.SetDictionaryItem( Settings.K.ManagerPaths, Manager!.Name, - ExecutableComboBox.SelectedValue.ToString() + selectedValue ); _ = ReloadPackageManager(); } + private void BrowseExecutableButton_Click(object sender, RoutedEventArgs e) => + _ = _browseExecutableButton_Click(); + + private async Task _browseExecutableButton_Click() + { + if (Manager is null) + return; + + ExternalLibraries.Pickers.FileOpenPicker picker = new( + MainApp.Instance.MainWindow.GetWindowHandle() + ); + string file = picker.Show(["*.exe"]); + if (file == string.Empty) + return; + + Settings.SetDictionaryItem(Settings.K.ManagerPaths, Manager.Name, file); + await ReloadPackageManager(); + } + + private void AddExecutableComboBoxItem(string path) + { + if (string.IsNullOrWhiteSpace(path)) + return; + + foreach (object? item in ExecutableComboBox.Items) + { + if (string.Equals(item?.ToString(), path, StringComparison.OrdinalIgnoreCase)) + return; + } + + ExecutableComboBox.Items.Add(path); + } + private void GoToSecureSettingsBtn_Click(object sender, RoutedEventArgs e) { MainApp.Instance.MainWindow.NavigationPage.OpenSettingsPage(typeof(Administrator));