Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions src/UniGetUI.Avalonia/Infrastructure/SettingsAnchor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.Threading;
using Avalonia.VisualTree;

namespace UniGetUI.Avalonia.Infrastructure;

/// <summary>
/// Scrolls a named control inside a settings sub-page into view and briefly highlights it,
/// so a search result lands the user on the exact setting they typed.
/// </summary>
public static class SettingsAnchor
{
public static void ScrollToAndHighlight(Control root, string anchorName)
{
// The page may still be sliding into the transitioning frame; wait until it's attached
// and laid out so BringIntoView has real bounds to work with.
if (root.IsLoaded)
{
Dispatcher.UIThread.Post(() => Run(root, anchorName), DispatcherPriority.Loaded);
return;
}

void OnLoaded(object? sender, RoutedEventArgs e)
{
root.Loaded -= OnLoaded;
Dispatcher.UIThread.Post(() => Run(root, anchorName), DispatcherPriority.Loaded);
}

root.Loaded += OnLoaded;
}

private static void Run(Control root, string anchorName)
{
var target = root.GetVisualDescendants()
.OfType<Control>()
.FirstOrDefault(c => c.Name == anchorName);
if (target is null) return;

// A SettingsCard's visible surface is an inner Border.settings-card inset 40px per side;
// adorn that so the outline hugs the card rather than the full-width control wrapper.
Control highlightTarget = target.GetVisualDescendants()
.OfType<Border>()
.FirstOrDefault(b => b.Classes.Contains("settings-card"))
?? target;

highlightTarget.BringIntoView();
Highlight(highlightTarget);
}

private static void Highlight(Control target)
{
var layer = AdornerLayer.GetAdornerLayer(target);
if (layer is null) return;

Color accent = target.TryFindResource("SystemAccentColor", target.ActualThemeVariant, out var res)
&& res is Color c ? c : Colors.DodgerBlue;

var adorner = new Border
{
BorderBrush = new SolidColorBrush(accent),
BorderThickness = new Thickness(2),
CornerRadius = target is Border b ? b.CornerRadius : new CornerRadius(8),
IsHitTestVisible = false,
};
AdornerLayer.SetAdornedElement(adorner, target);
layer.Children.Add(adorner);

void Remove() { if (layer.Children.Contains(adorner)) layer.Children.Remove(adorner); }

// A gentle pulse that holds, then fades out and removes itself.
var fade = new Animation
{
Duration = TimeSpan.FromMilliseconds(1800),
Easing = new CubicEaseOut(),
FillMode = FillMode.Forward,
Children =
{
new KeyFrame { Cue = new Cue(0d), Setters = { new Setter(Visual.OpacityProperty, 1d) } },
new KeyFrame { Cue = new Cue(0.55d), Setters = { new Setter(Visual.OpacityProperty, 1d) } },
new KeyFrame { Cue = new Cue(1d), Setters = { new Setter(Visual.OpacityProperty, 0d) } },
},
};
_ = fade.RunAsync(adorner).ContinueWith(
_ => Dispatcher.UIThread.Post(Remove),
TaskScheduler.Default);
}
}
283 changes: 283 additions & 0 deletions src/UniGetUI.Avalonia/Infrastructure/SettingsSearchIndex.cs

Large diffs are not rendered by default.

67 changes: 65 additions & 2 deletions src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,16 +156,16 @@
private void ToggleOperationsPanel() => OperationsPanelExpanded = !OperationsPanelExpanded;

[RelayCommand]
private void RetryFailedOperations() => AvaloniaOperationRegistry.RetryFailed();

Check warning on line 159 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'RetryFailedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 159 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'RetryFailedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 159 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'RetryFailedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 159 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'RetryFailedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 159 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Windows (NativeAOT)

Member 'RetryFailedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

[RelayCommand]
private void ClearSuccessfulOperations() => AvaloniaOperationRegistry.ClearSuccessful();

Check warning on line 162 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'ClearSuccessfulOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 162 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'ClearSuccessfulOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 162 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'ClearSuccessfulOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 162 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'ClearSuccessfulOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 162 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Windows (NativeAOT)

Member 'ClearSuccessfulOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

[RelayCommand]
private void ClearFinishedOperations() => AvaloniaOperationRegistry.ClearFinished();

Check warning on line 165 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'ClearFinishedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 165 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'ClearFinishedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 165 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'ClearFinishedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 165 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'ClearFinishedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 165 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Windows (NativeAOT)

Member 'ClearFinishedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

[RelayCommand]
private void CancelAllOperations() => AvaloniaOperationRegistry.CancelAll();

Check warning on line 168 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'CancelAllOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 168 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'CancelAllOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 168 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'CancelAllOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 168 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'CancelAllOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 168 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Windows (NativeAOT)

Member 'CancelAllOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

// ─── Sidebar ─────────────────────────────────────────────────────────────
public SidebarViewModel Sidebar { get; } = new();
Expand All @@ -189,6 +189,57 @@
if (_syncingSearch) return;
if (CurrentPageContent is AbstractPackagesPage page)
page.ViewModel.GlobalQueryText = value;
else if (CurrentPageContent is SettingsBasePage)
UpdateSettingsSuggestions(value);
}

// ─── Settings search suggestions ───────────────────────────────────────────
public ObservableCollection<SettingsSearchResult> SettingsSuggestions { get; } = new();

[ObservableProperty]
private bool _isSuggestionsOpen;

[ObservableProperty]
private int _selectedSuggestionIndex = -1;

private void UpdateSettingsSuggestions(string query)
{
SettingsSuggestions.Clear();
foreach (var r in SettingsSearchIndex.Search(query))
SettingsSuggestions.Add(r);

SelectedSuggestionIndex = SettingsSuggestions.Count > 0 ? 0 : -1;
IsSuggestionsOpen = SettingsSuggestions.Count > 0;
}

public void MoveSuggestionSelection(int delta)
{
if (SettingsSuggestions.Count == 0) return;
int next = SelectedSuggestionIndex + delta;
SelectedSuggestionIndex = Math.Clamp(next, 0, SettingsSuggestions.Count - 1);
}

public void CloseSuggestions()
{
IsSuggestionsOpen = false;
SelectedSuggestionIndex = -1;
SettingsSuggestions.Clear();
}

[RelayCommand]
public void SelectSuggestion(SettingsSearchResult? result)
{
if (result is null) return;

_syncingSearch = true;
GlobalSearchText = "";
_syncingSearch = false;
CloseSuggestions();

if (result.Manager is not null)
OpenManagerSettings(result.Manager);
else if (result.PageType is not null)
OpenSettingsPage(result.PageType, result.Anchor);
}

private void SubscribeToPageViewModel(AbstractPackagesPage? page)
Expand Down Expand Up @@ -594,6 +645,8 @@
(newPage as AbstractPackagesPage)?.FilterPackages();
(newPage as IEnterLeaveListener)?.OnEnter();

CloseSuggestions();

if (newPage is ISearchBoxPage newSPage)
{
SubscribeToPageViewModel(newPage as AbstractPackagesPage);
Expand Down Expand Up @@ -660,10 +713,10 @@
if (manager is not null) ManagersPage?.NavigateTo(manager);
}

public void OpenSettingsPage(Type page)
public void OpenSettingsPage(Type page, string? anchor = null)
{
NavigateTo(PageType.Settings);
SettingsPage?.NavigateTo(page);
SettingsPage?.NavigateTo(page, anchor);
}

public void ShowHelp(string uriAttachment = "")
Expand Down Expand Up @@ -713,6 +766,16 @@
[RelayCommand]
public void SubmitGlobalSearch()
{
// On settings/managers the box drives the suggestion dropdown: jump to the highlighted
// result (falling back to the top one).
if (CurrentPageContent is SettingsBasePage)
{
if (SettingsSuggestions.Count == 0) return;
int i = SelectedSuggestionIndex >= 0 ? SelectedSuggestionIndex : 0;
SelectSuggestion(SettingsSuggestions[i]);
return;
}

if (CurrentPageContent is ISearchBoxPage page)
page.SearchBox_QuerySubmitted(this, EventArgs.Empty);
}
Expand Down
39 changes: 39 additions & 0 deletions src/UniGetUI.Avalonia/Views/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,45 @@
</Panel>
</Border>

<!-- Settings search suggestions: dropdown anchored under the search box (only opened
by the view-model while on a settings/managers page). -->
<Popup Grid.Column="1"
PlacementTarget="{Binding #GlobalSearchContainer}"
Placement="BottomEdgeAlignedLeft"
VerticalOffset="4"
IsLightDismissEnabled="True"
IsOpen="{Binding IsSuggestionsOpen}">
<Border Width="358"
MaxHeight="380"
CornerRadius="8"
Padding="4"
Background="{DynamicResource AppWindowBackground}"
BorderBrush="{DynamicResource AppBorderBrush}"
BorderThickness="1"
BoxShadow="0 8 24 0 #40000000">
<ListBox x:Name="SuggestionsList"
ItemsSource="{Binding SettingsSuggestions}"
SelectedIndex="{Binding SelectedSuggestionIndex}"
Background="Transparent"
BorderThickness="0"
Tapped="SuggestionsList_Tapped">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="infra:SettingsSearchResult">
<StackPanel Orientation="Vertical" Spacing="1" Margin="4,2">
<TextBlock Text="{Binding Title}"
FontSize="14"
TextTrimming="CharacterEllipsis"/>
<TextBlock Text="{Binding PageTitle}"
FontSize="12"
Opacity="0.7"
TextTrimming="CharacterEllipsis"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Popup>

<!-- Right column: Linux window buttons on the far right -->
<Panel Grid.Column="2">

Expand Down
22 changes: 21 additions & 1 deletion src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1246,8 +1246,28 @@ private void TitleBar_PointerCaptureLost(object? sender, PointerCaptureLostEvent

private void SearchBox_KeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
// While the settings-search dropdown is open, drive it from the box: arrows move the
// highlight, Enter jumps to it, Escape dismisses.
if (ViewModel.IsSuggestionsOpen)
{
switch (e.Key)
{
case Key.Down: ViewModel.MoveSuggestionSelection(1); e.Handled = true; return;
case Key.Up: ViewModel.MoveSuggestionSelection(-1); e.Handled = true; return;
case Key.Escape: ViewModel.CloseSuggestions(); e.Handled = true; return;
case Key.Enter: ViewModel.SubmitGlobalSearch(); e.Handled = true; return;
}
}
else if (e.Key == Key.Enter)
{
ViewModel.SubmitGlobalSearch();
}
}

private void SuggestionsList_Tapped(object? sender, TappedEventArgs e)
{
if (SuggestionsList.SelectedItem is SettingsSearchResult result)
ViewModel.SelectSuggestion(result);
}

// ─── Public navigation API ────────────────────────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,22 +59,25 @@
Margin="44,32,4,8"
automation:AutomationProperties.HeadingLevel="2"/>

<settings:CheckboxCard SettingName="DoCacheAdminRightsForBatches"
<settings:CheckboxCard x:Name="AdminElevationCard"
SettingName="DoCacheAdminRightsForBatches"
Text="{t:Translate Ask for administrator privileges once for each batch of operations}"
StateChangedCommand="{Binding RestartCacheCommand}"
BorderThickness="1,1,1,0"
CornerRadius="8,8,0,0"
IsEnabled="{Binding IsElevationEnabled}"/>

<settings:CheckboxCard SettingName="DoCacheAdminRights"
<settings:CheckboxCard x:Name="CacheAdminOnceCard"
SettingName="DoCacheAdminRights"
Text="{t:Translate Ask only once for administrator privileges}"
StateChangedCommand="{Binding RestartCacheCommand}"
CornerRadius="0,0,8,8"
IsEnabled="{Binding IsElevationEnabled}"/>

<UserControl Height="16"/>

<settings:CheckboxCard SettingName="ProhibitElevation"
<settings:CheckboxCard x:Name="ProhibitElevationCard"
SettingName="ProhibitElevation"
Text="{t:Translate Prohibit any kind of Elevation via UniGetUI Elevator or GSudo}"
WarningText="{t:Translate Text='This option WILL cause issues. Any operation incapable of elevating itself WILL FAIL. Install/update/uninstall as administrator will NOT WORK.'}"
StateChangedCommand="{Binding RefreshElevationStateCommand}"
Expand All @@ -87,13 +90,15 @@
Margin="44,32,4,8"
automation:AutomationProperties.HeadingLevel="2"/>

<settings:SecureCheckboxCard SettingName="AllowCLIArguments"
<settings:SecureCheckboxCard x:Name="AdminRestrictionsOpsCard"
SettingName="AllowCLIArguments"
Text="{t:Translate Allow custom command-line arguments}"
WarningText="{t:Translate Text='Custom command-line arguments can change the way in which programs are installed, upgraded or uninstalled, in a way UniGetUI cannot control. Using custom command-lines can break packages. Proceed with caution.'}"
StateChangedCommand="{Binding RefreshCLIStateCommand}"
CornerRadius="8,8,0,0"/>

<settings:SecureCheckboxCard SettingName="AllowPrePostOpCommand"
<settings:SecureCheckboxCard x:Name="PrePostCommandCard"
SettingName="AllowPrePostOpCommand"
Text="{t:Translate Text='Ignore custom pre-install and post-install commands when importing packages from a bundle'}"
WarningText="{t:Translate Text='Pre and post install commands will be run before and after a package gets installed, upgraded or uninstalled. Be aware that they may break things unless used carefully'}"
StateChangedCommand="{Binding RefreshPrePostStateCommand}"
Expand All @@ -106,7 +111,8 @@
Margin="44,32,4,8"
automation:AutomationProperties.HeadingLevel="2"/>

<settings:SecureCheckboxCard SettingName="AllowCustomManagerPaths"
<settings:SecureCheckboxCard x:Name="AdminManagerPathsCard"
SettingName="AllowCustomManagerPaths"
Text="{t:Translate Allow changing the paths for package manager executables}"
WarningText="{t:Translate Text='Turning this on enables changing the executable file used to interact with package managers. While this allows finer-grained customization of your install processes, it may also be dangerous'}"
StateChangedCommand="{Binding ShowRestartRequiredCommand}"
Expand All @@ -118,13 +124,15 @@
Margin="44,32,4,8"
automation:AutomationProperties.HeadingLevel="2"/>

<settings:SecureCheckboxCard SettingName="AllowImportingCLIArguments"
<settings:SecureCheckboxCard x:Name="ImportCLIArgsCard"
SettingName="AllowImportingCLIArguments"
Text="{t:Translate Allow importing custom command-line arguments when importing packages from a bundle}"
WarningText="{t:Translate Text='Malformed command-line arguments can break packages, or even allow a malicious actor to gain privileged execution. Therefore, importing custom command-line arguments is disabled by default'}"
IsEnabled="{Binding IsCLIArgumentsEnabled}"
CornerRadius="8,8,0,0"/>

<settings:SecureCheckboxCard SettingName="AllowImportPrePostOpCommands"
<settings:SecureCheckboxCard x:Name="ImportPrePostCard"
SettingName="AllowImportPrePostOpCommands"
Text="{t:Translate Allow importing custom pre-install and post-install commands when importing packages from a bundle}"
WarningText="{t:Translate Text='Pre and post install commands can do very nasty things to your device, if designed to do so. It can be very dangerous to import the commands from a bundle, unless you trust the source of that package bundle'}"
IsEnabled="{Binding IsPrePostCommandsEnabled}"
Expand Down
Loading
Loading