mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Add hotkey recorder with manual modifier tracking
- Add HotkeyRecorderDialog with global keyboard hook for capturing hotkeys - Implement manual modifier state tracking to handle swallowed key events - Add HotkeyControl button that opens the recorder dialog - Add CheckAvailability and RemoveToggleHotkey to HotKeyMapper - Expose GetKeyFromVk helper in GlobalHotkey infrastructure - Add Settings pages (General, Plugin, Theme, Proxy, About) - Add PreviewPanel for result previews in main window - Fix hook reuse issue by clearing callback on close instead of disposing
This commit is contained in:
parent
29b26643cc
commit
0f9b9329dd
38 changed files with 1521 additions and 55 deletions
|
|
@ -1,13 +1,15 @@
|
|||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Flow.Launcher.Avalonia"
|
||||
xmlns:sty="using:FluentAvalonia.Styling"
|
||||
xmlns:i18n="using:Flow.Launcher.Avalonia.Resource"
|
||||
x:Class="Flow.Launcher.Avalonia.App"
|
||||
RequestedThemeVariant="Default">
|
||||
<!-- "Default" ThemeVariant follows system theme variant.
|
||||
"Dark" or "Light" are other available options. -->
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
<sty:FluentAvaloniaTheme />
|
||||
<StyleInclude Source="avares://Flow.Launcher.Avalonia/Themes/Base.axaml"/>
|
||||
</Application.Styles>
|
||||
|
||||
|
|
@ -20,4 +22,21 @@
|
|||
<FontFamily x:Key="SegoeFluentIcons">avares://Flow.Launcher.Avalonia/Resources#Segoe Fluent Icons</FontFamily>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
|
||||
<TrayIcon.Icons>
|
||||
<TrayIcons>
|
||||
<TrayIcon Icon="avares://Flow.Launcher.Avalonia/Images/app.ico"
|
||||
ToolTipText="Flow Launcher"
|
||||
Clicked="TrayIcon_OnClicked">
|
||||
<TrayIcon.Menu>
|
||||
<NativeMenu>
|
||||
<NativeMenuItem Header="{i18n:Localize show}" Click="MenuShow_OnClick" />
|
||||
<NativeMenuItem Header="{i18n:Localize settings}" Click="MenuSettings_OnClick" />
|
||||
<NativeMenuItemSeparator />
|
||||
<NativeMenuItem Header="{i18n:Localize exit}" Click="MenuExit_OnClick" />
|
||||
</NativeMenu>
|
||||
</TrayIcon.Menu>
|
||||
</TrayIcon>
|
||||
</TrayIcons>
|
||||
</TrayIcon.Icons>
|
||||
</Application>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Threading;
|
||||
|
|
@ -6,6 +7,7 @@ using CommunityToolkit.Mvvm.DependencyInjection;
|
|||
using Flow.Launcher.Avalonia.Helper;
|
||||
using Flow.Launcher.Avalonia.Resource;
|
||||
using Flow.Launcher.Avalonia.ViewModel;
|
||||
using Flow.Launcher.Avalonia.Views.SettingPages;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
|
|
@ -26,19 +28,24 @@ public partial class App : Application
|
|||
|
||||
public static IPublicAPI? API { get; private set; }
|
||||
|
||||
public override void Initialize() => AvaloniaXamlLoader.Load(this);
|
||||
public override void Initialize()
|
||||
{
|
||||
// Configure DI before loading XAML so markup extensions can access services
|
||||
LoadSettings();
|
||||
ConfigureDI();
|
||||
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
LoadSettings();
|
||||
ConfigureDI();
|
||||
|
||||
API = Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
_mainVM = Ioc.Default.GetRequiredService<MainViewModel>();
|
||||
|
||||
desktop.MainWindow = new MainWindow();
|
||||
desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown;
|
||||
|
||||
// Initialize hotkeys after window is created
|
||||
HotKeyMapper.Initialize();
|
||||
|
|
@ -51,6 +58,30 @@ public partial class App : Application
|
|||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
|
||||
private void TrayIcon_OnClicked(object? sender, EventArgs e)
|
||||
{
|
||||
_mainVM?.ToggleFlowLauncher();
|
||||
}
|
||||
|
||||
private void MenuShow_OnClick(object? sender, EventArgs e)
|
||||
{
|
||||
_mainVM?.Show();
|
||||
}
|
||||
|
||||
private void MenuSettings_OnClick(object? sender, EventArgs e)
|
||||
{
|
||||
var settingsWindow = new SettingsWindow();
|
||||
settingsWindow.Show();
|
||||
}
|
||||
|
||||
private void MenuExit_OnClick(object? sender, EventArgs e)
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
try
|
||||
|
|
@ -96,6 +127,10 @@ public partial class App : Application
|
|||
await PluginManager.InitializePluginsAsync();
|
||||
Log.Info(ClassName, "Plugins initialized");
|
||||
|
||||
// Update plugin translations after they are initialized
|
||||
var i18n = Ioc.Default.GetRequiredService<Internationalization>();
|
||||
i18n.UpdatePluginMetadataTranslations();
|
||||
|
||||
_mainVM?.OnPluginsReady();
|
||||
}
|
||||
catch (Exception e) { Log.Exception(ClassName, "Plugin init failed", e); }
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ public class AvaloniaPublicAPI : IPublicAPI
|
|||
public void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true) { }
|
||||
public void ShowMsgWithButton(string title, string buttonText, Action buttonAction, string subTitle = "", string iconPath = "") { }
|
||||
public void ShowMsgWithButton(string title, string buttonText, Action buttonAction, string subTitle, string iconPath, bool useMainWindowAsOwner = true) { }
|
||||
public void OpenSettingDialog() { }
|
||||
public void OpenSettingDialog() => _getMainViewModel()?.OpenSettings();
|
||||
public void RegisterGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback) { }
|
||||
public void RemoveGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback) { }
|
||||
public T LoadSettingJsonStorage<T>() where T : new() => new T();
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@
|
|||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<AvaloniaResource Include="Resources\SegoeFluentIcons.ttf" />
|
||||
<AvaloniaResource Include="..\Flow.Launcher\Resources\app.ico" Link="Images\app.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Avalonia.ViewModel;
|
||||
using Flow.Launcher.Infrastructure.Hotkey;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
|
||||
|
|
@ -45,12 +46,7 @@ internal static class HotKeyMapper
|
|||
/// </summary>
|
||||
internal static void SetToggleHotkey(string hotkeyString)
|
||||
{
|
||||
// Unregister existing hotkey
|
||||
if (_toggleHotkeyId >= 0)
|
||||
{
|
||||
GlobalHotkey.Unregister(_toggleHotkeyId);
|
||||
_toggleHotkeyId = -1;
|
||||
}
|
||||
RemoveToggleHotkey();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(hotkeyString))
|
||||
{
|
||||
|
|
@ -78,12 +74,46 @@ internal static class HotKeyMapper
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove the current toggle hotkey.
|
||||
/// </summary>
|
||||
internal static void RemoveToggleHotkey()
|
||||
{
|
||||
if (_toggleHotkeyId >= 0)
|
||||
{
|
||||
GlobalHotkey.Unregister(_toggleHotkeyId);
|
||||
_toggleHotkeyId = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnToggleHotkey()
|
||||
{
|
||||
Log.Info(ClassName, "Toggle hotkey triggered");
|
||||
_mainViewModel?.ToggleFlowLauncher();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a hotkey is available for registration.
|
||||
/// </summary>
|
||||
internal static bool CheckAvailability(HotkeyModel hotkey)
|
||||
{
|
||||
var hotkeyString = hotkey.ToString();
|
||||
var (mods, key) = GlobalHotkey.ParseHotkeyString(hotkeyString);
|
||||
|
||||
if (key == 0)
|
||||
return false;
|
||||
|
||||
// Try to register and immediately unregister
|
||||
int id = GlobalHotkey.Register(mods, key, () => { });
|
||||
if (id >= 0)
|
||||
{
|
||||
GlobalHotkey.Unregister(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleanup and unregister all hotkeys.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@
|
|||
<KeyBinding Gesture="Down" Command="{Binding SelectNextItemCommand}" />
|
||||
<KeyBinding Gesture="Up" Command="{Binding SelectPrevItemCommand}" />
|
||||
<KeyBinding Gesture="Shift+Enter" Command="{Binding LoadContextMenuCommand}" />
|
||||
<KeyBinding Gesture="Ctrl+OemComma" Command="{Binding OpenSettingsCommand}" />
|
||||
<KeyBinding Gesture="F1" Command="{Binding TogglePreviewCommand}" />
|
||||
</Window.KeyBindings>
|
||||
|
||||
<!-- Main container with rounded corners and background -->
|
||||
|
|
@ -89,7 +91,7 @@
|
|||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" MinWidth="80" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="0" /> <!-- Preview panel, hidden for now -->
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Result List (visible when in Results view) -->
|
||||
|
|
@ -106,19 +108,20 @@
|
|||
DataContext="{Binding ContextMenu}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Preview Panel Separator (hidden for now) -->
|
||||
<!-- Preview Panel Separator -->
|
||||
<GridSplitter Name="PreviewSplitter"
|
||||
Grid.Column="1"
|
||||
Width="5"
|
||||
Background="Transparent"
|
||||
IsVisible="False" />
|
||||
IsVisible="{Binding IsPreviewOn}" />
|
||||
|
||||
<!-- Preview Panel (to be implemented) -->
|
||||
<Grid Name="PreviewPanel"
|
||||
Grid.Column="2"
|
||||
IsVisible="False">
|
||||
<!-- Preview content will go here -->
|
||||
</Grid>
|
||||
<!-- Preview Panel -->
|
||||
<Border Name="PreviewPanelContainer"
|
||||
Grid.Column="2"
|
||||
Width="300"
|
||||
IsVisible="{Binding IsPreviewOn}">
|
||||
<views:PreviewPanel DataContext="{Binding PreviewSelectedItem}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@ using System.Threading;
|
|||
using System.Xml.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Avalonia.Resource;
|
||||
|
||||
|
|
@ -71,6 +73,27 @@ public class Internationalization
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update plugin metadata name & description and call OnCultureInfoChanged.
|
||||
/// </summary>
|
||||
public void UpdatePluginMetadataTranslations()
|
||||
{
|
||||
foreach (var p in PluginManager.GetTranslationPlugins())
|
||||
{
|
||||
if (p.Plugin is not IPluginI18n pluginI18N) continue;
|
||||
try
|
||||
{
|
||||
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
|
||||
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
|
||||
pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception(ClassName, $"Failed for <{p.Metadata.Name}>", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetActualLanguageCode()
|
||||
{
|
||||
var languageCode = _settings.Language;
|
||||
|
|
|
|||
|
|
@ -38,15 +38,18 @@ public class LocalizeExtension : MarkupExtension
|
|||
return Fallback ?? "[No Key]";
|
||||
}
|
||||
|
||||
var i18n = Ioc.Default.GetService<Internationalization>();
|
||||
if (i18n == null)
|
||||
try
|
||||
{
|
||||
return Fallback ?? $"[{Key}]";
|
||||
// Try to get I18n service from DI
|
||||
var i18n = Ioc.Default.GetService<Internationalization>();
|
||||
if (i18n != null && i18n.HasTranslation(Key))
|
||||
{
|
||||
return i18n.GetTranslation(Key);
|
||||
}
|
||||
}
|
||||
|
||||
if (i18n.HasTranslation(Key))
|
||||
catch
|
||||
{
|
||||
return i18n.GetTranslation(Key);
|
||||
// Ioc.Default might throw if not configured yet
|
||||
}
|
||||
|
||||
return Fallback ?? $"[{Key}]";
|
||||
|
|
@ -65,13 +68,20 @@ public static class Translator
|
|||
/// <returns>The translated string or the key in brackets if not found</returns>
|
||||
public static string GetString(string key)
|
||||
{
|
||||
var i18n = Ioc.Default.GetService<Internationalization>();
|
||||
if (i18n == null)
|
||||
try
|
||||
{
|
||||
var i18n = Ioc.Default.GetService<Internationalization>();
|
||||
if (i18n == null)
|
||||
{
|
||||
return $"[{key}]";
|
||||
}
|
||||
|
||||
return i18n.GetTranslation(key);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return $"[{key}]";
|
||||
}
|
||||
|
||||
return i18n.GetTranslation(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -144,19 +144,13 @@
|
|||
<Style Selector="Image.resultIcon">
|
||||
<Setter Property="Width" Value="32" />
|
||||
<Setter Property="Height" Value="32" />
|
||||
<Setter Property="Margin" Value="12,0,0,0" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<!-- Result Glyph -->
|
||||
<Style Selector="TextBlock.resultGlyph">
|
||||
<Setter Property="Width" Value="32" />
|
||||
<Setter Property="Height" Value="32" />
|
||||
<Setter Property="Margin" Value="12,0,0,0" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||
<Setter Property="TextAlignment" Value="Center" />
|
||||
<Setter Property="FontSize" Value="20" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
<Setter Property="TextAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<!-- Result Title -->
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Flow.Launcher.Avalonia.Resource;
|
||||
using Flow.Launcher.Avalonia.Views.SettingPages;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
|
|
@ -57,6 +58,12 @@ public partial class MainViewModel : ObservableObject
|
|||
[ObservableProperty]
|
||||
private ActiveView _activeView = ActiveView.Results;
|
||||
|
||||
[ObservableProperty]
|
||||
private ResultViewModel? _previewSelectedItem;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isPreviewOn;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the results view is currently active.
|
||||
/// </summary>
|
||||
|
|
@ -79,6 +86,22 @@ public partial class MainViewModel : ObservableObject
|
|||
_settings = settings;
|
||||
_results = new ResultsViewModel(settings);
|
||||
_contextMenu = new ResultsViewModel(settings);
|
||||
|
||||
_results.PropertyChanged += (s, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(ResultsViewModel.SelectedItem) && IsResultsViewActive)
|
||||
{
|
||||
PreviewSelectedItem = _results.SelectedItem;
|
||||
}
|
||||
};
|
||||
|
||||
_contextMenu.PropertyChanged += (s, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(ResultsViewModel.SelectedItem) && IsContextMenuViewActive)
|
||||
{
|
||||
PreviewSelectedItem = _contextMenu.SelectedItem;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
partial void OnActiveViewChanged(ActiveView value)
|
||||
|
|
@ -86,6 +109,14 @@ public partial class MainViewModel : ObservableObject
|
|||
OnPropertyChanged(nameof(IsResultsViewActive));
|
||||
OnPropertyChanged(nameof(IsContextMenuViewActive));
|
||||
OnPropertyChanged(nameof(ShowResultsArea));
|
||||
|
||||
PreviewSelectedItem = value == ActiveView.Results ? Results.SelectedItem : ContextMenu.SelectedItem;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void TogglePreview()
|
||||
{
|
||||
IsPreviewOn = !IsPreviewOn;
|
||||
}
|
||||
|
||||
partial void OnHasResultsChanged(bool value)
|
||||
|
|
@ -291,6 +322,17 @@ public partial class MainViewModel : ObservableObject
|
|||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void OpenSettings()
|
||||
{
|
||||
global::Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var settingsWindow = new SettingsWindow();
|
||||
settingsWindow.Show();
|
||||
Hide();
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OpenResultAsync()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Flow.Launcher.Avalonia.ViewModel.SettingPages;
|
||||
|
||||
public partial class AboutSettingsViewModel : ObservableObject
|
||||
{
|
||||
public string Version => Constant.Version;
|
||||
public string Website => "https://www.flowlauncher.com";
|
||||
public string GitHub => "https://github.com/Flow-Launcher/Flow.Launcher";
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OpenWebsite()
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(Website) { UseShellExecute = true });
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OpenGitHub()
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(GitHub) { UseShellExecute = true });
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AvaloniaI18n = Flow.Launcher.Avalonia.Resource.Internationalization;
|
||||
|
||||
namespace Flow.Launcher.Avalonia.ViewModel.SettingPages;
|
||||
|
||||
public partial class GeneralSettingsViewModel : ObservableObject
|
||||
{
|
||||
private readonly Flow.Launcher.Infrastructure.UserSettings.Settings _settings;
|
||||
private readonly AvaloniaI18n _i18n;
|
||||
|
||||
public GeneralSettingsViewModel()
|
||||
{
|
||||
_settings = Ioc.Default.GetRequiredService<Flow.Launcher.Infrastructure.UserSettings.Settings>();
|
||||
_i18n = Ioc.Default.GetRequiredService<AvaloniaI18n>();
|
||||
|
||||
LoadLanguages();
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private List<Language> _languages = new();
|
||||
|
||||
public Language? SelectedLanguage
|
||||
{
|
||||
get => Languages.FirstOrDefault(l => l.LanguageCode == _settings.Language);
|
||||
set
|
||||
{
|
||||
if (value != null && value.LanguageCode != _settings.Language)
|
||||
{
|
||||
_settings.Language = value.LanguageCode;
|
||||
_i18n.ChangeLanguage(value.LanguageCode);
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool StartOnStartup
|
||||
{
|
||||
get => _settings.StartFlowLauncherOnSystemStartup;
|
||||
set
|
||||
{
|
||||
_settings.StartFlowLauncherOnSystemStartup = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool HideWhenDeactivated
|
||||
{
|
||||
get => _settings.HideWhenDeactivated;
|
||||
set
|
||||
{
|
||||
_settings.HideWhenDeactivated = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowAtTopmost
|
||||
{
|
||||
get => _settings.ShowAtTopmost;
|
||||
set
|
||||
{
|
||||
_settings.ShowAtTopmost = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string PythonPath => _settings.PluginSettings.PythonExecutablePath ?? "Not set";
|
||||
public string NodePath => _settings.PluginSettings.NodeExecutablePath ?? "Not set";
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SelectPython()
|
||||
{
|
||||
// TODO: Implement file picker
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SelectNode()
|
||||
{
|
||||
// TODO: Implement file picker
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void LoadLanguages()
|
||||
{
|
||||
// Minimal set of languages for now, can be expanded by loading from directory later
|
||||
Languages = new List<Language>
|
||||
{
|
||||
new Language("en", "English"),
|
||||
new Language("zh-cn", "中文 (简体)"),
|
||||
new Language("zh-tw", "中文 (繁體)"),
|
||||
new Language("ko", "한국어"),
|
||||
new Language("ja", "日本語")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Avalonia.Helper;
|
||||
using System;
|
||||
|
||||
namespace Flow.Launcher.Avalonia.ViewModel.SettingPages;
|
||||
|
||||
public partial class HotkeySettingsViewModel : ObservableObject
|
||||
{
|
||||
private readonly Settings _settings;
|
||||
|
||||
public HotkeySettingsViewModel()
|
||||
{
|
||||
_settings = Ioc.Default.GetRequiredService<Settings>();
|
||||
}
|
||||
|
||||
public string ToggleHotkey
|
||||
{
|
||||
get => _settings.Hotkey;
|
||||
set
|
||||
{
|
||||
if (_settings.Hotkey != value)
|
||||
{
|
||||
_settings.Hotkey = value;
|
||||
HotKeyMapper.SetToggleHotkey(value);
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace Flow.Launcher.Avalonia.ViewModel.SettingPages;
|
||||
|
||||
public partial class PluginsSettingsViewModel : ObservableObject
|
||||
{
|
||||
public PluginsSettingsViewModel()
|
||||
{
|
||||
LoadPlugins();
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<PluginItemViewModel> _plugins = new();
|
||||
|
||||
[ObservableProperty]
|
||||
private string _searchText = string.Empty;
|
||||
|
||||
public IEnumerable<PluginItemViewModel> FilteredPlugins =>
|
||||
string.IsNullOrWhiteSpace(SearchText)
|
||||
? Plugins
|
||||
: Plugins.Where(p => p.Name.Contains(SearchText, System.StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
partial void OnSearchTextChanged(string value) => OnPropertyChanged(nameof(FilteredPlugins));
|
||||
|
||||
private void LoadPlugins()
|
||||
{
|
||||
var allPlugins = PluginManager.AllPlugins;
|
||||
foreach (var plugin in allPlugins.OrderBy(p => p.Metadata.Name))
|
||||
{
|
||||
Plugins.Add(new PluginItemViewModel(plugin));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public partial class PluginItemViewModel : ObservableObject
|
||||
{
|
||||
private readonly PluginPair _plugin;
|
||||
|
||||
public PluginItemViewModel(PluginPair plugin)
|
||||
{
|
||||
_plugin = plugin;
|
||||
}
|
||||
|
||||
public string Name => _plugin.Metadata.Name;
|
||||
public string Description => _plugin.Metadata.Description;
|
||||
public string Author => _plugin.Metadata.Author;
|
||||
public string Version => _plugin.Metadata.Version;
|
||||
public string IconPath => _plugin.Metadata.IcoPath;
|
||||
|
||||
public bool IsDisabled
|
||||
{
|
||||
get => _plugin.Metadata.Disabled;
|
||||
set
|
||||
{
|
||||
if (_plugin.Metadata.Disabled != value)
|
||||
{
|
||||
_plugin.Metadata.Disabled = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using System;
|
||||
|
||||
namespace Flow.Launcher.Avalonia.ViewModel.SettingPages;
|
||||
|
||||
public partial class ProxySettingsViewModel : ObservableObject
|
||||
{
|
||||
private readonly Settings _settings;
|
||||
|
||||
public ProxySettingsViewModel()
|
||||
{
|
||||
_settings = Ioc.Default.GetRequiredService<Settings>();
|
||||
}
|
||||
|
||||
public bool ProxyEnabled
|
||||
{
|
||||
get => _settings.Proxy.Enabled;
|
||||
set
|
||||
{
|
||||
if (_settings.Proxy.Enabled != value)
|
||||
{
|
||||
_settings.Proxy.Enabled = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ProxyServer
|
||||
{
|
||||
get => _settings.Proxy.Server;
|
||||
set
|
||||
{
|
||||
if (_settings.Proxy.Server != value)
|
||||
{
|
||||
_settings.Proxy.Server = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ProxyPort
|
||||
{
|
||||
get => _settings.Proxy.Port;
|
||||
set
|
||||
{
|
||||
if (_settings.Proxy.Port != value)
|
||||
{
|
||||
_settings.Proxy.Port = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ProxyUserName
|
||||
{
|
||||
get => _settings.Proxy.UserName;
|
||||
set
|
||||
{
|
||||
if (_settings.Proxy.UserName != value)
|
||||
{
|
||||
_settings.Proxy.UserName = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ProxyPassword
|
||||
{
|
||||
get => _settings.Proxy.Password;
|
||||
set
|
||||
{
|
||||
if (_settings.Proxy.Password != value)
|
||||
{
|
||||
_settings.Proxy.Password = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Avalonia.Resource;
|
||||
using FluentAvalonia.Styling;
|
||||
using Avalonia;
|
||||
using Avalonia.Styling;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Flow.Launcher.Avalonia.ViewModel.SettingPages;
|
||||
|
||||
public partial class ThemeSettingsViewModel : ObservableObject
|
||||
{
|
||||
private readonly Settings _settings;
|
||||
|
||||
public ThemeSettingsViewModel()
|
||||
{
|
||||
_settings = Ioc.Default.GetRequiredService<Settings>();
|
||||
}
|
||||
|
||||
public List<string> ThemeVariants => new() { "System", "Light", "Dark" };
|
||||
|
||||
public string SelectedThemeVariant
|
||||
{
|
||||
get => _settings.Theme switch
|
||||
{
|
||||
"Light" => "Light",
|
||||
"Dark" => "Dark",
|
||||
_ => "System"
|
||||
};
|
||||
set
|
||||
{
|
||||
if (value != SelectedThemeVariant)
|
||||
{
|
||||
_settings.Theme = value;
|
||||
ApplyTheme(value);
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyTheme(string variant)
|
||||
{
|
||||
if (Application.Current == null) return;
|
||||
|
||||
Application.Current.RequestedThemeVariant = variant switch
|
||||
{
|
||||
"Light" => ThemeVariant.Light,
|
||||
"Dark" => ThemeVariant.Dark,
|
||||
_ => ThemeVariant.Default
|
||||
};
|
||||
}
|
||||
|
||||
public int MaxResults
|
||||
{
|
||||
get => _settings.MaxResultsToShow;
|
||||
set
|
||||
{
|
||||
if (_settings.MaxResultsToShow != value)
|
||||
{
|
||||
_settings.MaxResultsToShow = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<int> MaxResultsRange => Enumerable.Range(1, 20).ToList();
|
||||
|
||||
public bool UseGlyphIcons
|
||||
{
|
||||
get => _settings.UseGlyphIcons;
|
||||
set
|
||||
{
|
||||
if (_settings.UseGlyphIcons != value)
|
||||
{
|
||||
_settings.UseGlyphIcons = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public double QueryBoxFontSize
|
||||
{
|
||||
get => _settings.QueryBoxFontSize;
|
||||
set
|
||||
{
|
||||
_settings.QueryBoxFontSize = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public double ResultItemFontSize
|
||||
{
|
||||
get => _settings.ResultItemFontSize;
|
||||
set
|
||||
{
|
||||
_settings.ResultItemFontSize = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Flow.Launcher.Avalonia/Views/Controls/HotkeyControl.axaml
Normal file
36
Flow.Launcher.Avalonia/Views/Controls/HotkeyControl.axaml
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="300" d:DesignHeight="50"
|
||||
x:Class="Flow.Launcher.Avalonia.Views.Controls.HotkeyControl"
|
||||
x:Name="root">
|
||||
|
||||
<Button Command="{Binding #root.RecordHotkeyCommand}"
|
||||
Padding="8,4"
|
||||
Background="{DynamicResource ControlFillColorDefaultBrush}"
|
||||
BorderBrush="{DynamicResource ControlElevationBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4"
|
||||
HorizontalAlignment="Left">
|
||||
<ItemsControl ItemsSource="{Binding #root.KeysToDisplay}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Padding="6,2"
|
||||
Background="{DynamicResource AccentFillColorDefaultBrush}"
|
||||
CornerRadius="4">
|
||||
<TextBlock Text="{Binding}"
|
||||
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold" />
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Button>
|
||||
</UserControl>
|
||||
89
Flow.Launcher.Avalonia/Views/Controls/HotkeyControl.axaml.cs
Normal file
89
Flow.Launcher.Avalonia/Views/Controls/HotkeyControl.axaml.cs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Flow.Launcher.Infrastructure.Hotkey;
|
||||
using Flow.Launcher.Avalonia.Helper;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace Flow.Launcher.Avalonia.Views.Controls
|
||||
{
|
||||
public partial class HotkeyControl : UserControl
|
||||
{
|
||||
public static readonly DirectProperty<HotkeyControl, string> HotkeyProperty =
|
||||
AvaloniaProperty.RegisterDirect<HotkeyControl, string>(
|
||||
nameof(Hotkey),
|
||||
o => o.Hotkey,
|
||||
(o, v) => o.Hotkey = v);
|
||||
|
||||
private string _hotkey = string.Empty;
|
||||
public string Hotkey
|
||||
{
|
||||
get => _hotkey;
|
||||
set
|
||||
{
|
||||
if (SetAndRaise(HotkeyProperty, ref _hotkey, value))
|
||||
{
|
||||
UpdateKeysDisplay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<string> KeysToDisplay { get; } = new();
|
||||
|
||||
public IAsyncRelayCommand RecordHotkeyCommand { get; }
|
||||
|
||||
public HotkeyControl()
|
||||
{
|
||||
RecordHotkeyCommand = new AsyncRelayCommand(OpenHotkeyRecorderDialog);
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
private void UpdateKeysDisplay()
|
||||
{
|
||||
KeysToDisplay.Clear();
|
||||
if (string.IsNullOrEmpty(Hotkey))
|
||||
{
|
||||
KeysToDisplay.Add("None");
|
||||
return;
|
||||
}
|
||||
|
||||
var model = new HotkeyModel(Hotkey);
|
||||
foreach (var key in model.EnumerateDisplayKeys())
|
||||
{
|
||||
KeysToDisplay.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenHotkeyRecorderDialog()
|
||||
{
|
||||
var originalHotkey = Hotkey;
|
||||
|
||||
// Temporarily unregister so it doesn't conflict with availability check
|
||||
HotKeyMapper.RemoveToggleHotkey();
|
||||
|
||||
var dialog = new HotkeyRecorderDialog(Hotkey);
|
||||
var result = await dialog.ShowAsync();
|
||||
|
||||
if (result == HotkeyRecorderDialog.EResultType.Save)
|
||||
{
|
||||
Hotkey = dialog.ResultValue;
|
||||
}
|
||||
else if (result == HotkeyRecorderDialog.EResultType.Delete)
|
||||
{
|
||||
Hotkey = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Restore original hotkey if cancelled
|
||||
HotKeyMapper.SetToggleHotkey(originalHotkey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
<ui:ContentDialog xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:i18n="using:Flow.Launcher.Avalonia.Resource"
|
||||
x:Class="Flow.Launcher.Avalonia.Views.Controls.HotkeyRecorderDialog"
|
||||
x:Name="root"
|
||||
Title="{i18n:Localize hotkeyRegTitle}"
|
||||
PrimaryButtonText="{i18n:Localize commonSave}"
|
||||
SecondaryButtonText="{i18n:Localize commonDelete}"
|
||||
CloseButtonText="{i18n:Localize commonCancel}">
|
||||
|
||||
<StackPanel Spacing="20" Width="400">
|
||||
<TextBlock Text="{i18n:Localize hotkeyRegGuide}" TextWrapping="Wrap" />
|
||||
|
||||
<Border Height="100"
|
||||
Background="{DynamicResource ControlFillColorDefaultBrush}"
|
||||
BorderBrush="{DynamicResource ControlElevationBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8">
|
||||
<ItemsControl ItemsSource="{Binding #root.KeysToDisplay}" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Padding="12,8"
|
||||
Background="{DynamicResource AccentFillColorDefaultBrush}"
|
||||
CornerRadius="6">
|
||||
<TextBlock Text="{Binding}"
|
||||
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
|
||||
FontSize="20"
|
||||
FontWeight="Bold" />
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="Alert"
|
||||
Padding="12"
|
||||
Background="{DynamicResource SystemFillColorCautionBackgroundBrush}"
|
||||
BorderBrush="{DynamicResource SystemFillColorCautionBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4"
|
||||
IsVisible="False">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<ui:FontIcon Glyph=""
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
Margin="0,0,12,0" />
|
||||
<TextBlock x:Name="tbMsg" Grid.Column="1" TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ui:ContentDialog>
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Flow.Launcher.Avalonia.Helper;
|
||||
using Flow.Launcher.Infrastructure.Hotkey;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using InfrastructureGlobalHotkey = Flow.Launcher.Infrastructure.Hotkey.GlobalHotkey;
|
||||
|
||||
namespace Flow.Launcher.Avalonia.Views.Controls
|
||||
{
|
||||
public partial class HotkeyRecorderDialog : ContentDialog
|
||||
{
|
||||
public enum EResultType
|
||||
{
|
||||
Cancel,
|
||||
Save,
|
||||
Delete
|
||||
}
|
||||
|
||||
public EResultType ResultType { get; private set; } = EResultType.Cancel;
|
||||
public string ResultValue { get; private set; } = string.Empty;
|
||||
public ObservableCollection<string> KeysToDisplay { get; } = new();
|
||||
|
||||
private bool _altDown;
|
||||
private bool _ctrlDown;
|
||||
private bool _shiftDown;
|
||||
private bool _winDown;
|
||||
|
||||
public HotkeyRecorderDialog(string currentHotkey)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
var model = new HotkeyModel(currentHotkey);
|
||||
UpdateKeysDisplay(model);
|
||||
|
||||
Opened += HotkeyRecorderDialog_Opened;
|
||||
Closing += HotkeyRecorderDialog_Closing;
|
||||
|
||||
PrimaryButtonClick += (s, e) => { ResultType = EResultType.Save; ResultValue = string.Join("+", KeysToDisplay); };
|
||||
SecondaryButtonClick += (s, e) => { ResultType = EResultType.Delete; };
|
||||
}
|
||||
|
||||
protected override Type StyleKeyOverride => typeof(ContentDialog);
|
||||
|
||||
private void HotkeyRecorderDialog_Opened(object? sender, EventArgs args)
|
||||
{
|
||||
this.Focus();
|
||||
|
||||
// Initialize Global Hotkey Hook when dialog opens
|
||||
try
|
||||
{
|
||||
// Sync initial modifier state
|
||||
var state = InfrastructureGlobalHotkey.CheckModifiers();
|
||||
_altDown = state.AltPressed;
|
||||
_ctrlDown = state.CtrlPressed;
|
||||
_shiftDown = state.ShiftPressed;
|
||||
_winDown = state.WinPressed;
|
||||
|
||||
InfrastructureGlobalHotkey.hookedKeyboardCallback = GlobalKeyHook;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Hook Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void HotkeyRecorderDialog_Closing(object? sender, EventArgs args)
|
||||
{
|
||||
// Clear the callback but DON'T dispose the static hook
|
||||
InfrastructureGlobalHotkey.hookedKeyboardCallback = null;
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
private bool GlobalKeyHook(KeyEvent keyEvent, int vkCode, SpecialKeyState state)
|
||||
{
|
||||
var wpfKey = InfrastructureGlobalHotkey.GetKeyFromVk(vkCode);
|
||||
bool isKeyDown = (keyEvent == KeyEvent.WM_KEYDOWN || keyEvent == KeyEvent.WM_SYSKEYDOWN);
|
||||
bool isKeyUp = (keyEvent == KeyEvent.WM_KEYUP || keyEvent == KeyEvent.WM_SYSKEYUP);
|
||||
|
||||
// Track modifier state manually (since we swallow keys, OS state is stale)
|
||||
if (wpfKey == System.Windows.Input.Key.LeftAlt || wpfKey == System.Windows.Input.Key.RightAlt)
|
||||
_altDown = isKeyDown;
|
||||
else if (wpfKey == System.Windows.Input.Key.LeftCtrl || wpfKey == System.Windows.Input.Key.RightCtrl)
|
||||
_ctrlDown = isKeyDown;
|
||||
else if (wpfKey == System.Windows.Input.Key.LeftShift || wpfKey == System.Windows.Input.Key.RightShift)
|
||||
_shiftDown = isKeyDown;
|
||||
else if (wpfKey == System.Windows.Input.Key.LWin || wpfKey == System.Windows.Input.Key.RWin)
|
||||
_winDown = isKeyDown;
|
||||
|
||||
// Only process key down events for UI updates
|
||||
if (isKeyDown)
|
||||
{
|
||||
// Capture current modifier state for the UI thread
|
||||
bool alt = _altDown;
|
||||
bool ctrl = _ctrlDown;
|
||||
bool shift = _shiftDown;
|
||||
bool win = _winDown;
|
||||
|
||||
// Marshal to UI Thread
|
||||
global::Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
HandleGlobalKey(vkCode, alt, ctrl, shift, win);
|
||||
});
|
||||
}
|
||||
|
||||
// Return false to SWALLOW the key (prevents other apps from receiving it)
|
||||
return false;
|
||||
}
|
||||
|
||||
private void HandleGlobalKey(int vkCode, bool altDown, bool ctrlDown, bool shiftDown, bool winDown)
|
||||
{
|
||||
var wpfKey = InfrastructureGlobalHotkey.GetKeyFromVk(vkCode);
|
||||
|
||||
// Don't treat modifiers as main keys
|
||||
if (wpfKey == System.Windows.Input.Key.LeftAlt || wpfKey == System.Windows.Input.Key.RightAlt ||
|
||||
wpfKey == System.Windows.Input.Key.LeftCtrl || wpfKey == System.Windows.Input.Key.RightCtrl ||
|
||||
wpfKey == System.Windows.Input.Key.LeftShift || wpfKey == System.Windows.Input.Key.RightShift ||
|
||||
wpfKey == System.Windows.Input.Key.LWin || wpfKey == System.Windows.Input.Key.RWin)
|
||||
{
|
||||
wpfKey = System.Windows.Input.Key.None;
|
||||
}
|
||||
|
||||
var model = new HotkeyModel(
|
||||
altDown,
|
||||
shiftDown,
|
||||
winDown,
|
||||
ctrlDown,
|
||||
wpfKey);
|
||||
|
||||
UpdateKeysDisplay(model);
|
||||
|
||||
// Update Save button enablement based on validity and availability
|
||||
var isValid = model.Validate();
|
||||
var isAvailable = isValid && HotKeyMapper.CheckAvailability(model);
|
||||
|
||||
IsPrimaryButtonEnabled = isAvailable;
|
||||
|
||||
var alert = this.FindControl<Border>("Alert");
|
||||
var tbMsg = this.FindControl<TextBlock>("tbMsg");
|
||||
if (alert != null && tbMsg != null)
|
||||
{
|
||||
if (isValid && !isAvailable)
|
||||
{
|
||||
// TODO: Get actual translation
|
||||
tbMsg.Text = "Hotkey already in use";
|
||||
alert.IsVisible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
alert.IsVisible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateKeysDisplay(HotkeyModel model)
|
||||
{
|
||||
KeysToDisplay.Clear();
|
||||
foreach (var key in model.EnumerateDisplayKeys())
|
||||
{
|
||||
KeysToDisplay.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
public new async Task<EResultType> ShowAsync()
|
||||
{
|
||||
var result = await base.ShowAsync();
|
||||
if (result == ContentDialogResult.Primary)
|
||||
return EResultType.Save;
|
||||
if (result == ContentDialogResult.Secondary)
|
||||
return EResultType.Delete;
|
||||
return EResultType.Cancel;
|
||||
}
|
||||
}
|
||||
}
|
||||
65
Flow.Launcher.Avalonia/Views/PreviewPanel.axaml
Normal file
65
Flow.Launcher.Avalonia/Views/PreviewPanel.axaml
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:Flow.Launcher.Avalonia.ViewModel"
|
||||
mc:Ignorable="d" d:DesignWidth="300" d:DesignHeight="400"
|
||||
x:Class="Flow.Launcher.Avalonia.Views.PreviewPanel"
|
||||
x:DataType="vm:ResultViewModel">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*" Margin="20">
|
||||
|
||||
<!-- Large Icon -->
|
||||
<Border Width="128" Height="128"
|
||||
CornerRadius="8"
|
||||
Background="{DynamicResource ControlFillColorDefaultBrush}"
|
||||
Margin="0,0,0,20">
|
||||
<Panel>
|
||||
<!-- Image Icon -->
|
||||
<Image Source="{Binding Image^}"
|
||||
Stretch="Uniform"
|
||||
IsVisible="{Binding !ShowGlyph}" />
|
||||
|
||||
<!-- Glyph Icon -->
|
||||
<TextBlock Text="{Binding Glyph.Glyph}"
|
||||
FontFamily="{Binding GlyphFontFamily}"
|
||||
FontSize="64"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
IsVisible="{Binding ShowGlyph}" />
|
||||
</Panel>
|
||||
</Border>
|
||||
|
||||
<!-- Title -->
|
||||
<SelectableTextBlock Grid.Row="1"
|
||||
Text="{Binding Title}"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center"
|
||||
Margin="0,0,0,10" />
|
||||
|
||||
<!-- SubTitle and Details -->
|
||||
<ScrollViewer Grid.Row="2">
|
||||
<StackPanel Spacing="10">
|
||||
<SelectableTextBlock Text="{Binding SubTitle}"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center" />
|
||||
|
||||
<!-- Separator -->
|
||||
<Border Height="1" Background="{DynamicResource DividerStrokeColorDefaultBrush}" Margin="0,10" />
|
||||
|
||||
<!-- Additional Metadata could go here -->
|
||||
<TextBlock Text="Score" FontWeight="Bold" FontSize="12" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
|
||||
<TextBlock Text="{Binding Score}" FontSize="12" />
|
||||
|
||||
<TextBlock Text="Plugin" FontWeight="Bold" FontSize="12" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Margin="0,10,0,0" />
|
||||
<TextBlock Text="{Binding PluginResult.PluginDirectory}" FontSize="12" TextWrapping="Wrap" />
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
17
Flow.Launcher.Avalonia/Views/PreviewPanel.axaml.cs
Normal file
17
Flow.Launcher.Avalonia/Views/PreviewPanel.axaml.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Flow.Launcher.Avalonia.Views;
|
||||
|
||||
public partial class PreviewPanel : UserControl
|
||||
{
|
||||
public PreviewPanel()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -34,22 +34,27 @@
|
|||
Classes="resultBullet"
|
||||
VerticalAlignment="Stretch" />
|
||||
|
||||
<!-- Icon and Text -->
|
||||
<!-- Icon Area -->
|
||||
<Grid Grid.Column="1" ColumnDefinitions="Auto,*" Margin="6,0,0,0">
|
||||
|
||||
<!-- Glyph Icon (shown when UseGlyphIcons is enabled and glyph is available) -->
|
||||
<TextBlock Grid.Column="0"
|
||||
Classes="resultGlyph"
|
||||
Text="{Binding Glyph.Glyph}"
|
||||
IsVisible="{Binding ShowGlyph}"
|
||||
FontFamily="{Binding GlyphFontFamily}" />
|
||||
|
||||
<!-- Image Icon (shown when no glyph or UseGlyphIcons is disabled) -->
|
||||
<Image Grid.Column="0"
|
||||
Classes="resultIcon"
|
||||
Source="{Binding Image^, FallbackValue={x:Static helper:ImageLoader.DefaultImage}}"
|
||||
IsVisible="{Binding !ShowGlyph}"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality" />
|
||||
<!-- Icon Container (fixed size for perfect centering) -->
|
||||
<Panel Grid.Column="0" Width="32" Height="32" Margin="12,0,0,0" VerticalAlignment="Center">
|
||||
<!-- Glyph Icon -->
|
||||
<TextBlock Classes="resultGlyph"
|
||||
Text="{Binding Glyph.Glyph}"
|
||||
IsVisible="{Binding ShowGlyph}"
|
||||
FontFamily="{Binding GlyphFontFamily}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
|
||||
<!-- Image Icon -->
|
||||
<Image Classes="resultIcon"
|
||||
Source="{Binding Image^, FallbackValue={x:Static helper:ImageLoader.DefaultImage}}"
|
||||
IsVisible="{Binding !ShowGlyph}"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Panel>
|
||||
|
||||
<!-- Title and SubTitle -->
|
||||
<Grid Grid.Column="1"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:vm="using:Flow.Launcher.Avalonia.ViewModel.SettingPages"
|
||||
xmlns:i18n="using:Flow.Launcher.Avalonia.Resource"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
|
||||
x:Class="Flow.Launcher.Avalonia.Views.SettingPages.AboutSettingsPage"
|
||||
x:DataType="vm:AboutSettingsViewModel">
|
||||
|
||||
<StackPanel Spacing="20" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Image Source="avares://Flow.Launcher.Avalonia/Images/app.ico" Width="128" Height="128" />
|
||||
|
||||
<TextBlock Text="Flow Launcher" FontSize="32" FontWeight="Bold" HorizontalAlignment="Center" />
|
||||
<TextBlock Text="{Binding Version, StringFormat='Version: {0}'}" Foreground="Gray" HorizontalAlignment="Center" />
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="20" HorizontalAlignment="Center" Margin="0,20,0,0">
|
||||
<Button Content="{i18n:Localize website}" Command="{Binding OpenWebsiteCommand}" Width="120" />
|
||||
<Button Content="GitHub" Command="{Binding OpenGitHubCommand}" Width="120" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Copyright © 2024 Flow Launcher" Foreground="Gray" FontSize="12" HorizontalAlignment="Center" Margin="0,50,0,0" />
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
using Avalonia.Controls;
|
||||
using Flow.Launcher.Avalonia.ViewModel.SettingPages;
|
||||
|
||||
namespace Flow.Launcher.Avalonia.Views.SettingPages;
|
||||
|
||||
public partial class AboutSettingsPage : UserControl
|
||||
{
|
||||
public AboutSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new AboutSettingsViewModel();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:vm="using:Flow.Launcher.Avalonia.ViewModel.SettingPages"
|
||||
xmlns:i18n="using:Flow.Launcher.Avalonia.Resource"
|
||||
xmlns:core="using:Flow.Launcher.Core.Resource"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Flow.Launcher.Avalonia.Views.SettingPages.GeneralSettingsPage"
|
||||
x:DataType="vm:GeneralSettingsViewModel">
|
||||
|
||||
<StackPanel Spacing="20">
|
||||
<TextBlock Text="{i18n:Localize general}" FontSize="28" FontWeight="SemiBold" />
|
||||
|
||||
<!-- Language Selection -->
|
||||
<ui:SettingsExpander Header="{i18n:Localize language}"
|
||||
IconSource="Earth">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ComboBox ItemsSource="{Binding Languages}"
|
||||
SelectedItem="{Binding SelectedLanguage}"
|
||||
MinWidth="200">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="core:Language">
|
||||
<TextBlock Text="{Binding Display}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<!-- Startup Settings -->
|
||||
<ui:SettingsExpander Header="{i18n:Localize startFlowLauncherOnSystemStartup}"
|
||||
IconSource="Save">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ToggleSwitch IsChecked="{Binding StartOnStartup}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<!-- Behavior Settings -->
|
||||
<ui:SettingsExpander Header="{i18n:Localize hideFlowLauncherWhenLoseFocus}"
|
||||
IconSource="Hide">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ToggleSwitch IsChecked="{Binding HideWhenDeactivated}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="{i18n:Localize showAtTopmost}"
|
||||
IconSource="Up">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ToggleSwitch IsChecked="{Binding ShowAtTopmost}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<!-- Python & Node Paths -->
|
||||
<ui:SettingsExpander Header="{i18n:Localize pythonFilePath}"
|
||||
Description="{Binding PythonPath}">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<Button Content="{i18n:Localize select}" Command="{Binding SelectPythonCommand}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="{i18n:Localize nodeFilePath}"
|
||||
Description="{Binding NodePath}">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<Button Content="{i18n:Localize select}" Command="{Binding SelectNodeCommand}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
using Avalonia.Controls;
|
||||
using Flow.Launcher.Avalonia.ViewModel.SettingPages;
|
||||
|
||||
namespace Flow.Launcher.Avalonia.Views.SettingPages;
|
||||
|
||||
public partial class GeneralSettingsPage : UserControl
|
||||
{
|
||||
public GeneralSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new GeneralSettingsViewModel();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:vm="using:Flow.Launcher.Avalonia.ViewModel.SettingPages"
|
||||
xmlns:i18n="using:Flow.Launcher.Avalonia.Resource"
|
||||
xmlns:controls="using:Flow.Launcher.Avalonia.Views.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Flow.Launcher.Avalonia.Views.SettingPages.HotkeySettingsPage"
|
||||
x:DataType="vm:HotkeySettingsViewModel">
|
||||
|
||||
<StackPanel Spacing="20">
|
||||
<TextBlock Text="{i18n:Localize hotkey}" FontSize="28" FontWeight="SemiBold" />
|
||||
|
||||
<ui:SettingsExpander Header="{i18n:Localize toggleFlowLauncher}"
|
||||
Description="{i18n:Localize toggleFlowLauncherTip}"
|
||||
IconSource="Keyboard">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<controls:HotkeyControl Hotkey="{Binding ToggleHotkey, Mode=TwoWay}" MinWidth="150" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<TextBlock Text="{i18n:Localize hotkeyFormatTip}" Foreground="Gray" FontSize="12" />
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
using Avalonia.Controls;
|
||||
using Flow.Launcher.Avalonia.ViewModel.SettingPages;
|
||||
|
||||
namespace Flow.Launcher.Avalonia.Views.SettingPages;
|
||||
|
||||
public partial class HotkeySettingsPage : UserControl
|
||||
{
|
||||
public HotkeySettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new HotkeySettingsViewModel();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:vm="using:Flow.Launcher.Avalonia.ViewModel.SettingPages"
|
||||
xmlns:i18n="using:Flow.Launcher.Avalonia.Resource"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
|
||||
x:Class="Flow.Launcher.Avalonia.Views.SettingPages.PluginsSettingsPage"
|
||||
x:DataType="vm:PluginsSettingsViewModel">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*">
|
||||
<TextBlock Grid.Row="0" Text="{i18n:Localize plugin}" FontSize="28" FontWeight="SemiBold" Margin="0,0,0,10" />
|
||||
|
||||
<TextBox Grid.Row="1"
|
||||
Text="{Binding SearchText, Mode=TwoWay}"
|
||||
Watermark="{i18n:Localize search}"
|
||||
Margin="0,0,0,20" />
|
||||
|
||||
<ListBox Grid.Row="2"
|
||||
ItemsSource="{Binding FilteredPlugins}"
|
||||
Background="Transparent"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:PluginItemViewModel">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,5">
|
||||
<Image Grid.Column="0" Source="{Binding IconPath}" Width="32" Height="32" Margin="0,0,15,0" VerticalAlignment="Center" />
|
||||
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
|
||||
<TextBlock Text="{Binding Version}" Foreground="Gray" FontSize="12" VerticalAlignment="Bottom" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding Description}" Foreground="Gray" FontSize="12" TextWrapping="Wrap" />
|
||||
<TextBlock Text="{Binding Author}" Foreground="LightGray" FontSize="11" />
|
||||
</StackPanel>
|
||||
|
||||
<ToggleSwitch Grid.Column="2"
|
||||
IsChecked="{Binding !IsDisabled}"
|
||||
VerticalAlignment="Center"
|
||||
OnContent="" OffContent="" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
using Avalonia.Controls;
|
||||
using Flow.Launcher.Avalonia.ViewModel.SettingPages;
|
||||
|
||||
namespace Flow.Launcher.Avalonia.Views.SettingPages;
|
||||
|
||||
public partial class PluginsSettingsPage : UserControl
|
||||
{
|
||||
public PluginsSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new PluginsSettingsViewModel();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:vm="using:Flow.Launcher.Avalonia.ViewModel.SettingPages"
|
||||
xmlns:i18n="using:Flow.Launcher.Avalonia.Resource"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
|
||||
x:Class="Flow.Launcher.Avalonia.Views.SettingPages.ProxySettingsPage"
|
||||
x:DataType="vm:ProxySettingsViewModel">
|
||||
|
||||
<StackPanel Spacing="20">
|
||||
<TextBlock Text="{i18n:Localize proxy}" FontSize="28" FontWeight="SemiBold" />
|
||||
|
||||
<ui:SettingsExpander Header="{i18n:Localize proxyEnabled}"
|
||||
IconSource="Globe">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ToggleSwitch IsChecked="{Binding ProxyEnabled}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="{i18n:Localize proxyServer}">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<TextBox Text="{Binding ProxyServer}" MinWidth="200" IsEnabled="{Binding ProxyEnabled}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="{i18n:Localize proxyPort}">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<NumericUpDown Value="{Binding ProxyPort}" Minimum="0" Maximum="65535" IsEnabled="{Binding ProxyEnabled}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="{i18n:Localize proxyUserName}">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<TextBox Text="{Binding ProxyUserName}" MinWidth="200" IsEnabled="{Binding ProxyEnabled}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="{i18n:Localize proxyPassword}">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<TextBox Text="{Binding ProxyPassword}" PasswordChar="*" MinWidth="200" IsEnabled="{Binding ProxyEnabled}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
using Avalonia.Controls;
|
||||
using Flow.Launcher.Avalonia.ViewModel.SettingPages;
|
||||
|
||||
namespace Flow.Launcher.Avalonia.Views.SettingPages;
|
||||
|
||||
public partial class ProxySettingsPage : UserControl
|
||||
{
|
||||
public ProxySettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new ProxySettingsViewModel();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:i18n="using:Flow.Launcher.Avalonia.Resource"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Flow.Launcher.Avalonia.Views.SettingPages.SettingsWindow"
|
||||
Title="Flow Launcher Settings"
|
||||
Width="900" Height="650"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
|
||||
<ui:NavigationView Name="NavView"
|
||||
PaneDisplayMode="Left"
|
||||
OpenPaneLength="200"
|
||||
IsSettingsVisible="False">
|
||||
<ui:NavigationView.MenuItems>
|
||||
<ui:NavigationViewItem Content="{i18n:Localize general}" Tag="General" IconSource="Setting" IsSelected="True" />
|
||||
<ui:NavigationViewItem Content="{i18n:Localize plugin}" Tag="Plugins" IconSource="Library" />
|
||||
<ui:NavigationViewItem Content="{i18n:Localize theme}" Tag="Theme" IconSource="DarkTheme" />
|
||||
<ui:NavigationViewItem Content="{i18n:Localize hotkey}" Tag="Hotkey" IconSource="Keyboard" />
|
||||
<ui:NavigationViewItem Content="{i18n:Localize proxy}" Tag="Proxy" IconSource="Globe" />
|
||||
<ui:NavigationViewItem Content="{i18n:Localize about}" Tag="About" IconSource="Help" />
|
||||
</ui:NavigationView.MenuItems>
|
||||
|
||||
<ScrollViewer Name="ContentFrame" Padding="20">
|
||||
<!-- Content will be loaded here -->
|
||||
</ScrollViewer>
|
||||
</ui:NavigationView>
|
||||
</Window>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
using Avalonia.Controls;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Flow.Launcher.Avalonia.ViewModel.SettingPages;
|
||||
using System;
|
||||
|
||||
namespace Flow.Launcher.Avalonia.Views.SettingPages;
|
||||
|
||||
public partial class SettingsWindow : Window
|
||||
{
|
||||
public SettingsWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
NavView.SelectionChanged += NavView_SelectionChanged;
|
||||
|
||||
// Load default page
|
||||
LoadPage("General");
|
||||
}
|
||||
|
||||
private void NavView_SelectionChanged(object? sender, NavigationViewSelectionChangedEventArgs e)
|
||||
{
|
||||
if (e.SelectedItem is NavigationViewItem item && item.Tag is string tag)
|
||||
{
|
||||
LoadPage(tag);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadPage(string tag)
|
||||
{
|
||||
Control? page = tag switch
|
||||
{
|
||||
"General" => new GeneralSettingsPage(),
|
||||
"Plugins" => new PluginsSettingsPage(),
|
||||
"Theme" => new ThemeSettingsPage(),
|
||||
"Hotkey" => new HotkeySettingsPage(),
|
||||
"Proxy" => new ProxySettingsPage(),
|
||||
"About" => new AboutSettingsPage(),
|
||||
_ => new TextBlock { Text = $"Page {tag} not implemented yet", HorizontalAlignment = global::Avalonia.Layout.HorizontalAlignment.Center, VerticalAlignment = global::Avalonia.Layout.VerticalAlignment.Center }
|
||||
};
|
||||
|
||||
if (page != null)
|
||||
{
|
||||
ContentFrame.Content = page;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:vm="using:Flow.Launcher.Avalonia.ViewModel.SettingPages"
|
||||
xmlns:i18n="using:Flow.Launcher.Avalonia.Resource"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
|
||||
x:Class="Flow.Launcher.Avalonia.Views.SettingPages.ThemeSettingsPage"
|
||||
x:DataType="vm:ThemeSettingsViewModel">
|
||||
|
||||
<StackPanel Spacing="20">
|
||||
<TextBlock Text="{i18n:Localize appearance}" FontSize="28" FontWeight="SemiBold" />
|
||||
|
||||
<!-- Theme Selection -->
|
||||
<ui:SettingsExpander Header="{i18n:Localize theme}"
|
||||
IconSource="DarkTheme">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ComboBox ItemsSource="{Binding ThemeVariants}"
|
||||
SelectedItem="{Binding SelectedThemeVariant}"
|
||||
MinWidth="150" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<!-- Glyph Icons -->
|
||||
<ui:SettingsExpander Header="{i18n:Localize useGlyphUI}"
|
||||
Description="{i18n:Localize useGlyphUIEffect}"
|
||||
IconSource="Font">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ToggleSwitch IsChecked="{Binding UseGlyphIcons}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<!-- Max Results -->
|
||||
<ui:SettingsExpander Header="{i18n:Localize maxShowResults}"
|
||||
IconSource="List">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ComboBox ItemsSource="{Binding MaxResultsRange}"
|
||||
SelectedItem="{Binding MaxResults}"
|
||||
MinWidth="100" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<!-- Font Sizes -->
|
||||
<ui:SettingsExpander Header="{i18n:Localize queryBoxFont}"
|
||||
IconSource="FontSize">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Text="{Binding QueryBoxFontSize}" VerticalAlignment="Center" Width="30" />
|
||||
<Slider Minimum="10" Maximum="60" Value="{Binding QueryBoxFontSize}" Width="200" />
|
||||
</StackPanel>
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="{i18n:Localize resultItemFont}"
|
||||
IconSource="FontSize">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Text="{Binding ResultItemFontSize}" VerticalAlignment="Center" Width="30" />
|
||||
<Slider Minimum="8" Maximum="40" Value="{Binding ResultItemFontSize}" Width="200" />
|
||||
</StackPanel>
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
using Avalonia.Controls;
|
||||
using Flow.Launcher.Avalonia.ViewModel.SettingPages;
|
||||
|
||||
namespace Flow.Launcher.Avalonia.Views.SettingPages;
|
||||
|
||||
public partial class ThemeSettingsPage : UserControl
|
||||
{
|
||||
public ThemeSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new ThemeSettingsViewModel();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
|
@ -19,9 +19,15 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
private static readonly UnhookWindowsHookExSafeHandle hookId;
|
||||
|
||||
public delegate bool KeyboardCallback(KeyEvent keyEvent, int vkCode, SpecialKeyState state);
|
||||
internal static Func<KeyEvent, int, SpecialKeyState, bool> hookedKeyboardCallback;
|
||||
public static Func<KeyEvent, int, SpecialKeyState, bool> hookedKeyboardCallback;
|
||||
|
||||
public static System.Windows.Input.Key GetKeyFromVk(int vkCode)
|
||||
{
|
||||
return System.Windows.Input.KeyInterop.KeyFromVirtualKey(vkCode);
|
||||
}
|
||||
|
||||
static GlobalHotkey()
|
||||
|
||||
{
|
||||
// Set the hook
|
||||
hookId = SetHook(_procKeyboard, WINDOWS_HOOK_ID.WH_KEYBOARD_LL);
|
||||
|
|
|
|||
Loading…
Reference in a new issue