diff --git a/Flow.Launcher.Avalonia/App.axaml b/Flow.Launcher.Avalonia/App.axaml
index a43fc0b4f..2d23fa515 100644
--- a/Flow.Launcher.Avalonia/App.axaml
+++ b/Flow.Launcher.Avalonia/App.axaml
@@ -1,13 +1,15 @@
-
+
@@ -20,4 +22,21 @@
avares://Flow.Launcher.Avalonia/Resources#Segoe Fluent Icons
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher.Avalonia/App.axaml.cs b/Flow.Launcher.Avalonia/App.axaml.cs
index fae4c6a64..2a0f45aa2 100644
--- a/Flow.Launcher.Avalonia/App.axaml.cs
+++ b/Flow.Launcher.Avalonia/App.axaml.cs
@@ -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();
_mainVM = Ioc.Default.GetRequiredService();
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();
+ i18n.UpdatePluginMetadataTranslations();
+
_mainVM?.OnPluginsReady();
}
catch (Exception e) { Log.Exception(ClassName, "Plugin init failed", e); }
diff --git a/Flow.Launcher.Avalonia/AvaloniaPublicAPI.cs b/Flow.Launcher.Avalonia/AvaloniaPublicAPI.cs
index deac2575c..d05a5733d 100644
--- a/Flow.Launcher.Avalonia/AvaloniaPublicAPI.cs
+++ b/Flow.Launcher.Avalonia/AvaloniaPublicAPI.cs
@@ -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 callback) { }
public void RemoveGlobalKeyboardCallback(Func callback) { }
public T LoadSettingJsonStorage() where T : new() => new T();
diff --git a/Flow.Launcher.Avalonia/Flow.Launcher.Avalonia.csproj b/Flow.Launcher.Avalonia/Flow.Launcher.Avalonia.csproj
index 19dc2acbb..740443bc3 100644
--- a/Flow.Launcher.Avalonia/Flow.Launcher.Avalonia.csproj
+++ b/Flow.Launcher.Avalonia/Flow.Launcher.Avalonia.csproj
@@ -67,6 +67,7 @@
PreserveNewest
+
diff --git a/Flow.Launcher.Avalonia/Helper/HotKeyMapper.cs b/Flow.Launcher.Avalonia/Helper/HotKeyMapper.cs
index 4a7791b23..18e0db7f0 100644
--- a/Flow.Launcher.Avalonia/Helper/HotKeyMapper.cs
+++ b/Flow.Launcher.Avalonia/Helper/HotKeyMapper.cs
@@ -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
///
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
}
}
+ ///
+ /// Remove the current toggle hotkey.
+ ///
+ internal static void RemoveToggleHotkey()
+ {
+ if (_toggleHotkeyId >= 0)
+ {
+ GlobalHotkey.Unregister(_toggleHotkeyId);
+ _toggleHotkeyId = -1;
+ }
+ }
+
private static void OnToggleHotkey()
{
Log.Info(ClassName, "Toggle hotkey triggered");
_mainViewModel?.ToggleFlowLauncher();
}
+ ///
+ /// Checks if a hotkey is available for registration.
+ ///
+ 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;
+ }
+
///
/// Cleanup and unregister all hotkeys.
///
diff --git a/Flow.Launcher.Avalonia/MainWindow.axaml b/Flow.Launcher.Avalonia/MainWindow.axaml
index 017564871..43baeb64f 100644
--- a/Flow.Launcher.Avalonia/MainWindow.axaml
+++ b/Flow.Launcher.Avalonia/MainWindow.axaml
@@ -37,6 +37,8 @@
+
+
@@ -89,7 +91,7 @@
-
+
@@ -106,19 +108,20 @@
DataContext="{Binding ContextMenu}" />
-
+
+ IsVisible="{Binding IsPreviewOn}" />
-
-
-
-
+
+
+
+
diff --git a/Flow.Launcher.Avalonia/Resource/Internationalization.cs b/Flow.Launcher.Avalonia/Resource/Internationalization.cs
index b5cc19ecd..0705eca14 100644
--- a/Flow.Launcher.Avalonia/Resource/Internationalization.cs
+++ b/Flow.Launcher.Avalonia/Resource/Internationalization.cs
@@ -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
}
}
+ ///
+ /// Update plugin metadata name & description and call OnCultureInfoChanged.
+ ///
+ 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;
diff --git a/Flow.Launcher.Avalonia/Resource/LocalizeExtension.cs b/Flow.Launcher.Avalonia/Resource/LocalizeExtension.cs
index e88ec87e2..cbac2454a 100644
--- a/Flow.Launcher.Avalonia/Resource/LocalizeExtension.cs
+++ b/Flow.Launcher.Avalonia/Resource/LocalizeExtension.cs
@@ -38,15 +38,18 @@ public class LocalizeExtension : MarkupExtension
return Fallback ?? "[No Key]";
}
- var i18n = Ioc.Default.GetService();
- if (i18n == null)
+ try
{
- return Fallback ?? $"[{Key}]";
+ // Try to get I18n service from DI
+ var i18n = Ioc.Default.GetService();
+ 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
/// The translated string or the key in brackets if not found
public static string GetString(string key)
{
- var i18n = Ioc.Default.GetService();
- if (i18n == null)
+ try
+ {
+ var i18n = Ioc.Default.GetService();
+ if (i18n == null)
+ {
+ return $"[{key}]";
+ }
+
+ return i18n.GetTranslation(key);
+ }
+ catch
{
return $"[{key}]";
}
-
- return i18n.GetTranslation(key);
}
///
diff --git a/Flow.Launcher.Avalonia/Themes/Base.axaml b/Flow.Launcher.Avalonia/Themes/Base.axaml
index c73dbd221..740e01ae2 100644
--- a/Flow.Launcher.Avalonia/Themes/Base.axaml
+++ b/Flow.Launcher.Avalonia/Themes/Base.axaml
@@ -144,19 +144,13 @@
diff --git a/Flow.Launcher.Avalonia/ViewModel/MainViewModel.cs b/Flow.Launcher.Avalonia/ViewModel/MainViewModel.cs
index 0b35f37bf..df7e83483 100644
--- a/Flow.Launcher.Avalonia/ViewModel/MainViewModel.cs
+++ b/Flow.Launcher.Avalonia/ViewModel/MainViewModel.cs
@@ -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;
+
///
/// Whether the results view is currently active.
///
@@ -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()
{
diff --git a/Flow.Launcher.Avalonia/ViewModel/SettingPages/AboutSettingsViewModel.cs b/Flow.Launcher.Avalonia/ViewModel/SettingPages/AboutSettingsViewModel.cs
new file mode 100644
index 000000000..97e739b4d
--- /dev/null
+++ b/Flow.Launcher.Avalonia/ViewModel/SettingPages/AboutSettingsViewModel.cs
@@ -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;
+ }
+}
diff --git a/Flow.Launcher.Avalonia/ViewModel/SettingPages/GeneralSettingsViewModel.cs b/Flow.Launcher.Avalonia/ViewModel/SettingPages/GeneralSettingsViewModel.cs
new file mode 100644
index 000000000..b718870a9
--- /dev/null
+++ b/Flow.Launcher.Avalonia/ViewModel/SettingPages/GeneralSettingsViewModel.cs
@@ -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();
+ _i18n = Ioc.Default.GetRequiredService();
+
+ LoadLanguages();
+ }
+
+ [ObservableProperty]
+ private List _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
+ {
+ new Language("en", "English"),
+ new Language("zh-cn", "中文 (简体)"),
+ new Language("zh-tw", "中文 (繁體)"),
+ new Language("ko", "한국어"),
+ new Language("ja", "日本語")
+ };
+ }
+}
diff --git a/Flow.Launcher.Avalonia/ViewModel/SettingPages/HotkeySettingsViewModel.cs b/Flow.Launcher.Avalonia/ViewModel/SettingPages/HotkeySettingsViewModel.cs
new file mode 100644
index 000000000..1aa20d633
--- /dev/null
+++ b/Flow.Launcher.Avalonia/ViewModel/SettingPages/HotkeySettingsViewModel.cs
@@ -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();
+ }
+
+ public string ToggleHotkey
+ {
+ get => _settings.Hotkey;
+ set
+ {
+ if (_settings.Hotkey != value)
+ {
+ _settings.Hotkey = value;
+ HotKeyMapper.SetToggleHotkey(value);
+ OnPropertyChanged();
+ }
+ }
+ }
+}
diff --git a/Flow.Launcher.Avalonia/ViewModel/SettingPages/PluginsSettingsViewModel.cs b/Flow.Launcher.Avalonia/ViewModel/SettingPages/PluginsSettingsViewModel.cs
new file mode 100644
index 000000000..53c581ece
--- /dev/null
+++ b/Flow.Launcher.Avalonia/ViewModel/SettingPages/PluginsSettingsViewModel.cs
@@ -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 _plugins = new();
+
+ [ObservableProperty]
+ private string _searchText = string.Empty;
+
+ public IEnumerable 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();
+ }
+ }
+ }
+}
diff --git a/Flow.Launcher.Avalonia/ViewModel/SettingPages/ProxySettingsViewModel.cs b/Flow.Launcher.Avalonia/ViewModel/SettingPages/ProxySettingsViewModel.cs
new file mode 100644
index 000000000..ad420e1f1
--- /dev/null
+++ b/Flow.Launcher.Avalonia/ViewModel/SettingPages/ProxySettingsViewModel.cs
@@ -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();
+ }
+
+ 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();
+ }
+ }
+ }
+}
diff --git a/Flow.Launcher.Avalonia/ViewModel/SettingPages/ThemeSettingsViewModel.cs b/Flow.Launcher.Avalonia/ViewModel/SettingPages/ThemeSettingsViewModel.cs
new file mode 100644
index 000000000..f27ec70f6
--- /dev/null
+++ b/Flow.Launcher.Avalonia/ViewModel/SettingPages/ThemeSettingsViewModel.cs
@@ -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();
+ }
+
+ public List 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 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();
+ }
+ }
+}
diff --git a/Flow.Launcher.Avalonia/Views/Controls/HotkeyControl.axaml b/Flow.Launcher.Avalonia/Views/Controls/HotkeyControl.axaml
new file mode 100644
index 000000000..26a7ee706
--- /dev/null
+++ b/Flow.Launcher.Avalonia/Views/Controls/HotkeyControl.axaml
@@ -0,0 +1,36 @@
+
+
+
+
diff --git a/Flow.Launcher.Avalonia/Views/Controls/HotkeyControl.axaml.cs b/Flow.Launcher.Avalonia/Views/Controls/HotkeyControl.axaml.cs
new file mode 100644
index 000000000..22b3a6971
--- /dev/null
+++ b/Flow.Launcher.Avalonia/Views/Controls/HotkeyControl.axaml.cs
@@ -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 HotkeyProperty =
+ AvaloniaProperty.RegisterDirect(
+ 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 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);
+ }
+ }
+ }
+}
diff --git a/Flow.Launcher.Avalonia/Views/Controls/HotkeyRecorderDialog.axaml b/Flow.Launcher.Avalonia/Views/Controls/HotkeyRecorderDialog.axaml
new file mode 100644
index 000000000..14b25d6a2
--- /dev/null
+++ b/Flow.Launcher.Avalonia/Views/Controls/HotkeyRecorderDialog.axaml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher.Avalonia/Views/Controls/HotkeyRecorderDialog.axaml.cs b/Flow.Launcher.Avalonia/Views/Controls/HotkeyRecorderDialog.axaml.cs
new file mode 100644
index 000000000..f4c95cc51
--- /dev/null
+++ b/Flow.Launcher.Avalonia/Views/Controls/HotkeyRecorderDialog.axaml.cs
@@ -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 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("Alert");
+ var tbMsg = this.FindControl("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 ShowAsync()
+ {
+ var result = await base.ShowAsync();
+ if (result == ContentDialogResult.Primary)
+ return EResultType.Save;
+ if (result == ContentDialogResult.Secondary)
+ return EResultType.Delete;
+ return EResultType.Cancel;
+ }
+ }
+}
diff --git a/Flow.Launcher.Avalonia/Views/PreviewPanel.axaml b/Flow.Launcher.Avalonia/Views/PreviewPanel.axaml
new file mode 100644
index 000000000..3008771bb
--- /dev/null
+++ b/Flow.Launcher.Avalonia/Views/PreviewPanel.axaml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher.Avalonia/Views/PreviewPanel.axaml.cs b/Flow.Launcher.Avalonia/Views/PreviewPanel.axaml.cs
new file mode 100644
index 000000000..24bb32259
--- /dev/null
+++ b/Flow.Launcher.Avalonia/Views/PreviewPanel.axaml.cs
@@ -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);
+ }
+}
diff --git a/Flow.Launcher.Avalonia/Views/ResultListBox.axaml b/Flow.Launcher.Avalonia/Views/ResultListBox.axaml
index 376a3f31b..b07f98abe 100644
--- a/Flow.Launcher.Avalonia/Views/ResultListBox.axaml
+++ b/Flow.Launcher.Avalonia/Views/ResultListBox.axaml
@@ -34,22 +34,27 @@
Classes="resultBullet"
VerticalAlignment="Stretch" />
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher.Avalonia/Views/SettingPages/AboutSettingsPage.axaml.cs b/Flow.Launcher.Avalonia/Views/SettingPages/AboutSettingsPage.axaml.cs
new file mode 100644
index 000000000..b3b3ad38c
--- /dev/null
+++ b/Flow.Launcher.Avalonia/Views/SettingPages/AboutSettingsPage.axaml.cs
@@ -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();
+ }
+}
diff --git a/Flow.Launcher.Avalonia/Views/SettingPages/GeneralSettingsPage.axaml b/Flow.Launcher.Avalonia/Views/SettingPages/GeneralSettingsPage.axaml
new file mode 100644
index 000000000..0d3615e87
--- /dev/null
+++ b/Flow.Launcher.Avalonia/Views/SettingPages/GeneralSettingsPage.axaml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher.Avalonia/Views/SettingPages/GeneralSettingsPage.axaml.cs b/Flow.Launcher.Avalonia/Views/SettingPages/GeneralSettingsPage.axaml.cs
new file mode 100644
index 000000000..982a97aea
--- /dev/null
+++ b/Flow.Launcher.Avalonia/Views/SettingPages/GeneralSettingsPage.axaml.cs
@@ -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();
+ }
+}
diff --git a/Flow.Launcher.Avalonia/Views/SettingPages/HotkeySettingsPage.axaml b/Flow.Launcher.Avalonia/Views/SettingPages/HotkeySettingsPage.axaml
new file mode 100644
index 000000000..5218eb832
--- /dev/null
+++ b/Flow.Launcher.Avalonia/Views/SettingPages/HotkeySettingsPage.axaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher.Avalonia/Views/SettingPages/HotkeySettingsPage.axaml.cs b/Flow.Launcher.Avalonia/Views/SettingPages/HotkeySettingsPage.axaml.cs
new file mode 100644
index 000000000..7f6bf6e38
--- /dev/null
+++ b/Flow.Launcher.Avalonia/Views/SettingPages/HotkeySettingsPage.axaml.cs
@@ -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();
+ }
+}
diff --git a/Flow.Launcher.Avalonia/Views/SettingPages/PluginsSettingsPage.axaml b/Flow.Launcher.Avalonia/Views/SettingPages/PluginsSettingsPage.axaml
new file mode 100644
index 000000000..10be3cf52
--- /dev/null
+++ b/Flow.Launcher.Avalonia/Views/SettingPages/PluginsSettingsPage.axaml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher.Avalonia/Views/SettingPages/PluginsSettingsPage.axaml.cs b/Flow.Launcher.Avalonia/Views/SettingPages/PluginsSettingsPage.axaml.cs
new file mode 100644
index 000000000..3355c66f1
--- /dev/null
+++ b/Flow.Launcher.Avalonia/Views/SettingPages/PluginsSettingsPage.axaml.cs
@@ -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();
+ }
+}
diff --git a/Flow.Launcher.Avalonia/Views/SettingPages/ProxySettingsPage.axaml b/Flow.Launcher.Avalonia/Views/SettingPages/ProxySettingsPage.axaml
new file mode 100644
index 000000000..9e9162142
--- /dev/null
+++ b/Flow.Launcher.Avalonia/Views/SettingPages/ProxySettingsPage.axaml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher.Avalonia/Views/SettingPages/ProxySettingsPage.axaml.cs b/Flow.Launcher.Avalonia/Views/SettingPages/ProxySettingsPage.axaml.cs
new file mode 100644
index 000000000..4b43fe18f
--- /dev/null
+++ b/Flow.Launcher.Avalonia/Views/SettingPages/ProxySettingsPage.axaml.cs
@@ -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();
+ }
+}
diff --git a/Flow.Launcher.Avalonia/Views/SettingPages/SettingsWindow.axaml b/Flow.Launcher.Avalonia/Views/SettingPages/SettingsWindow.axaml
new file mode 100644
index 000000000..6d68018e4
--- /dev/null
+++ b/Flow.Launcher.Avalonia/Views/SettingPages/SettingsWindow.axaml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher.Avalonia/Views/SettingPages/SettingsWindow.axaml.cs b/Flow.Launcher.Avalonia/Views/SettingPages/SettingsWindow.axaml.cs
new file mode 100644
index 000000000..b3a2d6c1f
--- /dev/null
+++ b/Flow.Launcher.Avalonia/Views/SettingPages/SettingsWindow.axaml.cs
@@ -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;
+ }
+ }
+}
diff --git a/Flow.Launcher.Avalonia/Views/SettingPages/ThemeSettingsPage.axaml b/Flow.Launcher.Avalonia/Views/SettingPages/ThemeSettingsPage.axaml
new file mode 100644
index 000000000..b6fce9585
--- /dev/null
+++ b/Flow.Launcher.Avalonia/Views/SettingPages/ThemeSettingsPage.axaml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher.Avalonia/Views/SettingPages/ThemeSettingsPage.axaml.cs b/Flow.Launcher.Avalonia/Views/SettingPages/ThemeSettingsPage.axaml.cs
new file mode 100644
index 000000000..bfdfb86e6
--- /dev/null
+++ b/Flow.Launcher.Avalonia/Views/SettingPages/ThemeSettingsPage.axaml.cs
@@ -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();
+ }
+}
diff --git a/Flow.Launcher.Infrastructure/Hotkey/GlobalHotkey.cs b/Flow.Launcher.Infrastructure/Hotkey/GlobalHotkey.cs
index b2a140755..71ca1420c 100644
--- a/Flow.Launcher.Infrastructure/Hotkey/GlobalHotkey.cs
+++ b/Flow.Launcher.Infrastructure/Hotkey/GlobalHotkey.cs
@@ -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 hookedKeyboardCallback;
+ public static Func 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);