Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ test-data/
.env.*
!.env.example
*.pem
!core/privstack-cloud/data/cacert.pem
*.key
*.pfx
*.p12
Expand Down
3,879 changes: 3,879 additions & 0 deletions core/privstack-cloud/data/cacert.pem

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion desktop/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<PrivStackSdkVersion>1.46.1</PrivStackSdkVersion>
<PrivStackSdkVersion>1.47.1</PrivStackSdkVersion>
</PropertyGroup>
</Project>

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,120 +1,111 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Threading;
using Avalonia.VisualTree;
using PrivStack.Desktop.ViewModels;

namespace PrivStack.Desktop.Views;

public partial class CommandPalette : UserControl
{
public CommandPalette()
{
InitializeComponent();

// Use tunneling route so we intercept keys before ListBox/TextBox consume them
AddHandler(KeyDownEvent, OnTunnelKeyDown, RoutingStrategies.Tunnel);
}

protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);

// Focus the search box when the palette opens
if (DataContext is CommandPaletteViewModel vm)
{
vm.PropertyChanged += (_, args) =>
{
if (args.PropertyName == nameof(CommandPaletteViewModel.IsOpen) && vm.IsOpen)
{
Dispatcher.UIThread.Post(() =>
{
SearchBox.Focus();
SearchBox.SelectAll();
}, DispatcherPriority.Input);
}
};
}
}

private void OnTunnelKeyDown(object? sender, KeyEventArgs e)
{
if (DataContext is not CommandPaletteViewModel vm) return;

switch (e.Key)
{
case Key.Escape:
vm.CloseCommand.Execute(null);
e.Handled = true;
break;

case Key.Enter:
vm.ExecuteSelectedCommand.Execute(null);
e.Handled = true;
break;

case Key.Down:
vm.SelectNextCommand.Execute(null);
e.Handled = true;
break;

case Key.Up:
vm.SelectPreviousCommand.Execute(null);
e.Handled = true;
break;

case Key.Back:
// Backspace on empty search box clears the plugin scope filter
if (vm.HasPluginFilter && string.IsNullOrEmpty(vm.SearchQuery))
{
vm.ClearPluginFilter();
e.Handled = true;
}
break;
}
}

private void OnBackdropPressed(object? sender, PointerPressedEventArgs e)
{
if (DataContext is CommandPaletteViewModel vm)
{
vm.CloseCommand.Execute(null);
}
}

private void OnItemTapped(object? sender, TappedEventArgs e)
{
// Only execute if the tap originated from within a ListBoxItem
if (e.Source is not Control source) return;
var listBoxItem = source.FindAncestorOfType<ListBoxItem>();
if (listBoxItem is null) return;

if (DataContext is CommandPaletteViewModel vm && vm.SelectedCommand is not null)
{
// Small delay to let SelectedItem binding update first
Dispatcher.UIThread.Post(() =>
{
vm.ExecuteSelectedCommand.Execute(null);
}, DispatcherPriority.Input);
}
}

private void OnItemDoubleTapped(object? sender, TappedEventArgs e)
{
if (e.Source is not Control source) return;
var listBoxItem = source.FindAncestorOfType<ListBoxItem>();
if (listBoxItem is null) return;

if (DataContext is CommandPaletteViewModel vm && vm.SelectedCommand is not null)
{
vm.ExecuteSelectedCommand.Execute(null);
}
}

private void OnFilterPillPressed(object? sender, PointerPressedEventArgs e)
{
if (DataContext is CommandPaletteViewModel vm)
vm.ClearPluginFilter();
}
}
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Threading;
using Avalonia.VisualTree;
using PrivStack.Desktop.ViewModels;

namespace PrivStack.Desktop.Controls;

public partial class UniversalSearchDropdown : UserControl
{
public UniversalSearchDropdown()
{
InitializeComponent();
}

/// <summary>
/// Positions the dropdown card below the given anchor control.
/// </summary>
public void UpdatePosition(Control anchor)
{
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel == null) return;

var point = anchor.TranslatePoint(new Point(0, anchor.Bounds.Height + 4), topLevel);
if (point == null) return;

DropdownCard.Margin = new Thickness(point.Value.X, point.Value.Y, 0, 0);
}

/// <summary>
/// Handles keyboard navigation forwarded from the search TextBox.
/// </summary>
public void HandleKeyDown(KeyEventArgs e)
{
if (DataContext is not CommandPaletteViewModel vm) return;

switch (e.Key)
{
case Key.Escape:
vm.CloseCommand.Execute(null);
e.Handled = true;
break;

case Key.Enter:
vm.ExecuteSelectedCommand.Execute(null);
e.Handled = true;
break;

case Key.Down:
vm.SelectNextCommand.Execute(null);
e.Handled = true;
break;

case Key.Up:
vm.SelectPreviousCommand.Execute(null);
e.Handled = true;
break;

case Key.Back:
if (vm.HasPluginFilter && string.IsNullOrEmpty(vm.SearchQuery))
{
vm.ClearPluginFilter();
e.Handled = true;
}
break;
}
}

private void OnBackdropPressed(object? sender, PointerPressedEventArgs e)
{
if (DataContext is CommandPaletteViewModel vm)
{
vm.CloseCommand.Execute(null);
}
}

private void OnItemTapped(object? sender, TappedEventArgs e)
{
if (e.Source is not Control source) return;
var listBoxItem = source.FindAncestorOfType<ListBoxItem>();
if (listBoxItem is null) return;

if (DataContext is CommandPaletteViewModel vm && vm.SelectedCommand is not null)
{
Dispatcher.UIThread.Post(() =>
{
vm.ExecuteSelectedCommand.Execute(null);
}, DispatcherPriority.Input);
}
}

private void OnItemDoubleTapped(object? sender, TappedEventArgs e)
{
if (e.Source is not Control source) return;
var listBoxItem = source.FindAncestorOfType<ListBoxItem>();
if (listBoxItem is null) return;

if (DataContext is CommandPaletteViewModel vm && vm.SelectedCommand is not null)
{
vm.ExecuteSelectedCommand.Execute(null);
}
}

private void OnFilterPillPressed(object? sender, PointerPressedEventArgs e)
{
if (DataContext is CommandPaletteViewModel vm)
vm.ClearPluginFilter();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public sealed class DashboardPlugin : PluginBase<DashboardViewModel>
Id = "privstack.dashboard",
Name = "Dashboard",
Description = "System overview, plugin marketplace, and management dashboard",
Version = new Version(1, 2, 1),
Version = new Version(1, 3, 0),
Author = "PrivStack",
Icon = "LayoutDashboard",
NavigationOrder = 50,
Expand Down
Loading