-
-
Notifications
You must be signed in to change notification settings - Fork 548
Avalonia migration #4214
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Avalonia migration #4214
Changes from 6 commits
4120407
e961dc5
2079531
065dc19
f3d3f80
2466b90
192eb4d
36e3530
5d80816
c68a03e
ed668f3
29b2664
0f9b932
8a99881
1d94565
1209ca5
0e1af22
fb9721c
dda5874
19913fa
2b328e5
32aa3ff
d9bfe49
0c2f026
c0d1767
5e1411f
ae0bdbc
82390b5
e726c44
e337f9e
0681332
8ee32df
8300235
9c99c7c
c507bb9
fbcd700
c51c029
5e1ec2b
54827a1
0e90bc1
b0dfd0b
45e2ce4
11eefa4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| <Application xmlns="https://github.com/avaloniaui" | ||
|
Check failure on line 1 in Flow.Launcher.Avalonia/App.axaml
|
||
| xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | ||
| xmlns:local="using:Flow.Launcher.Avalonia" | ||
| x:Class="Flow.Launcher.Avalonia.App" | ||
| RequestedThemeVariant="Default"> | ||
| <!-- "Default" ThemeVariant follows system theme variant. | ||
| "Dark" or "Light" are other available options. --> | ||
|
|
||
| <Application.Styles> | ||
| <FluentTheme /> | ||
| <StyleInclude Source="avares://Flow.Launcher.Avalonia/Themes/Base.axaml"/> | ||
| </Application.Styles> | ||
|
|
||
| <Application.Resources> | ||
| <ResourceDictionary> | ||
| <ResourceDictionary.MergedDictionaries> | ||
| <!-- Additional resources can be added here --> | ||
| </ResourceDictionary.MergedDictionaries> | ||
| </ResourceDictionary> | ||
| </Application.Resources> | ||
| </Application> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| using Avalonia; | ||
|
Check failure on line 1 in Flow.Launcher.Avalonia/App.axaml.cs
|
||
| using Avalonia.Controls.ApplicationLifetimes; | ||
| using Avalonia.Markup.Xaml; | ||
| using Avalonia.Threading; | ||
| using CommunityToolkit.Mvvm.DependencyInjection; | ||
| using Flow.Launcher.Avalonia.Helper; | ||
| using Flow.Launcher.Avalonia.ViewModel; | ||
| using Flow.Launcher.Core.Plugin; | ||
| using Flow.Launcher.Infrastructure; | ||
| using Flow.Launcher.Infrastructure.Logger; | ||
| using Flow.Launcher.Infrastructure.Storage; | ||
| using Flow.Launcher.Infrastructure.UserSettings; | ||
| using Flow.Launcher.Plugin; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using System; | ||
| using System.Threading.Tasks; | ||
|
|
||
| namespace Flow.Launcher.Avalonia; | ||
|
|
||
| public partial class App : Application | ||
| { | ||
| private static readonly string ClassName = nameof(App); | ||
| private Settings? _settings; | ||
| private MainViewModel? _mainVM; | ||
|
|
||
| public static IPublicAPI? API { get; private set; } | ||
|
|
||
| public override void Initialize() => 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(); | ||
|
|
||
| // Initialize hotkeys after window is created | ||
| HotKeyMapper.Initialize(); | ||
|
|
||
| Dispatcher.UIThread.Post(async () => await InitializePluginsAsync(), DispatcherPriority.Background); | ||
|
|
||
| // Cleanup on exit | ||
| desktop.Exit += (_, _) => HotKeyMapper.Shutdown(); | ||
| } | ||
| base.OnFrameworkInitializationCompleted(); | ||
| } | ||
|
|
||
| private void LoadSettings() | ||
| { | ||
| try | ||
| { | ||
| var storage = new FlowLauncherJsonStorage<Settings>(); | ||
| _settings = storage.Load(); | ||
| _settings.SetStorage(storage); | ||
| } | ||
| catch (Exception e) | ||
| { | ||
| Log.Exception(ClassName, "Settings load failed", e); | ||
| _settings = new Settings | ||
| { | ||
| WindowSize = 580, WindowHeightSize = 42, QueryBoxFontSize = 24, | ||
| ItemHeightSize = 50, ResultItemFontSize = 14, ResultSubItemFontSize = 12, MaxResultsToShow = 6 | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| private void ConfigureDI() | ||
| { | ||
| var services = new ServiceCollection(); | ||
| services.AddSingleton(_settings!); | ||
| services.AddSingleton<IAlphabet, PinyinAlphabet>(); | ||
| services.AddSingleton<StringMatcher>(); | ||
| services.AddSingleton<MainViewModel>(); | ||
| services.AddSingleton<IPublicAPI>(sp => new AvaloniaPublicAPI( | ||
| sp.GetRequiredService<Settings>(), | ||
| () => sp.GetRequiredService<MainViewModel>())); | ||
| Ioc.Default.ConfigureServices(services.BuildServiceProvider()); | ||
| } | ||
|
|
||
| private async Task InitializePluginsAsync() | ||
| { | ||
| try | ||
| { | ||
| Log.Info(ClassName, "Loading plugins..."); | ||
| PluginManager.LoadPlugins(_settings!.PluginSettings); | ||
| Log.Info(ClassName, $"Loaded {PluginManager.AllPlugins.Count} plugins"); | ||
|
|
||
| await PluginManager.InitializePluginsAsync(); | ||
| Log.Info(ClassName, "Plugins initialized"); | ||
|
|
||
| _mainVM?.OnPluginsReady(); | ||
| } | ||
| catch (Exception e) { Log.Exception(ClassName, "Plugin init failed", e); } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,141 @@ | ||||||||||||||||||||
| using System; | ||||||||||||||||||||
|
Check failure on line 1 in Flow.Launcher.Avalonia/AvaloniaPublicAPI.cs
|
||||||||||||||||||||
| using System.Collections.Generic; | ||||||||||||||||||||
| using System.IO; | ||||||||||||||||||||
| using System.Runtime.CompilerServices; | ||||||||||||||||||||
| using System.Threading; | ||||||||||||||||||||
| using System.Threading.Tasks; | ||||||||||||||||||||
| using System.Windows; | ||||||||||||||||||||
| using System.Windows.Media; | ||||||||||||||||||||
| using Flow.Launcher.Infrastructure; | ||||||||||||||||||||
| using Flow.Launcher.Infrastructure.Logger; | ||||||||||||||||||||
| using Flow.Launcher.Infrastructure.UserSettings; | ||||||||||||||||||||
| using Flow.Launcher.Plugin; | ||||||||||||||||||||
| using Flow.Launcher.Plugin.SharedModels; | ||||||||||||||||||||
| using Flow.Launcher.Core.Plugin; | ||||||||||||||||||||
| using Flow.Launcher.Avalonia.ViewModel; | ||||||||||||||||||||
| using CommunityToolkit.Mvvm.DependencyInjection; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| namespace Flow.Launcher.Avalonia; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||
| /// Minimal IPublicAPI for Avalonia - just enough for plugin queries to work. | ||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||
| public class AvaloniaPublicAPI : IPublicAPI | ||||||||||||||||||||
|
Check failure on line 23 in Flow.Launcher.Avalonia/AvaloniaPublicAPI.cs
|
||||||||||||||||||||
| { | ||||||||||||||||||||
| private readonly Settings _settings; | ||||||||||||||||||||
| private readonly Func<MainViewModel> _getMainViewModel; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| public AvaloniaPublicAPI(Settings settings, Func<MainViewModel> getMainViewModel) | ||||||||||||||||||||
| { | ||||||||||||||||||||
| _settings = settings; | ||||||||||||||||||||
| _getMainViewModel = getMainViewModel; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #pragma warning disable CS0067 | ||||||||||||||||||||
| public event VisibilityChangedEventHandler? VisibilityChanged; | ||||||||||||||||||||
| public event ActualApplicationThemeChangedEventHandler? ActualApplicationThemeChanged; | ||||||||||||||||||||
| #pragma warning restore CS0067 | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Essential for plugins | ||||||||||||||||||||
| public void ChangeQuery(string query, bool requery = false) => _getMainViewModel().QueryText = query; | ||||||||||||||||||||
| public string GetTranslation(string key) => key; | ||||||||||||||||||||
| public List<PluginPair> GetAllPlugins() => PluginManager.AllPlugins; | ||||||||||||||||||||
| public MatchResult FuzzySearch(string query, string stringToCompare) => | ||||||||||||||||||||
| Ioc.Default.GetRequiredService<StringMatcher>().FuzzyMatch(query, stringToCompare); | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
Comment on lines
+27
to
+52
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: rg -n "ChangeQuery\s*\(|ReQuery\s*\(|Requery" -g '*.cs' -A 3Repository: Flow-Launcher/Flow.Launcher Length of output: 14843 🏁 Script executed: # Also search for QueryText property and how it's used
rg -n "QueryText\s*=" -g '*.cs' -A 2 -B 2Repository: Flow-Launcher/Flow.Launcher Length of output: 6533 🏁 Script executed: # Look for any methods that might indicate requery intent
rg -n "class MainViewModel|QueryText|ExecuteQuery" -g '*.cs' -A 2Repository: Flow-Launcher/Flow.Launcher Length of output: 30993 Respect the The 🤖 Prompt for AI Agents |
||||||||||||||||||||
| // Logging | ||||||||||||||||||||
| public void LogDebug(string className, string message, [CallerMemberName] string methodName = "") => Log.Debug(className, message, methodName); | ||||||||||||||||||||
| public void LogInfo(string className, string message, [CallerMemberName] string methodName = "") => Log.Info(className, message, methodName); | ||||||||||||||||||||
| public void LogWarn(string className, string message, [CallerMemberName] string methodName = "") => Log.Warn(className, message, methodName); | ||||||||||||||||||||
| public void LogError(string className, string message, [CallerMemberName] string methodName = "") => Log.Error(className, message, methodName); | ||||||||||||||||||||
| public void LogException(string className, string message, Exception e, [CallerMemberName] string methodName = "") => Log.Exception(className, message, e, methodName); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Shell/URL operations | ||||||||||||||||||||
| public void ShellRun(string cmd, string filename = "cmd.exe") => | ||||||||||||||||||||
| System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = filename, Arguments = $"/c {cmd}", UseShellExecute = true }); | ||||||||||||||||||||
|
Comment on lines
+61
to
+62
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential command injection vulnerability in The Proposed safer implementation public void ShellRun(string cmd, string filename = "cmd.exe") =>
- System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = filename, Arguments = $"/c {cmd}", UseShellExecute = true });
+ System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = filename,
+ Arguments = filename == "cmd.exe" ? $"/c \"{cmd.Replace("\"", "\\\"")}\"" : cmd,
+ UseShellExecute = true
+ });Note: The WPF implementation may have similar logic - consider aligning the behavior or documenting the security implications. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| public void OpenUrl(string url, bool? inPrivate = null) => | ||||||||||||||||||||
| System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = url, UseShellExecute = true }); | ||||||||||||||||||||
| public void OpenUrl(Uri url, bool? inPrivate = null) => OpenUrl(url.ToString(), inPrivate); | ||||||||||||||||||||
| public void OpenWebUrl(string url, bool? inPrivate = null) => OpenUrl(url, inPrivate); | ||||||||||||||||||||
| public void OpenWebUrl(Uri url, bool? inPrivate = null) => OpenUrl(url.ToString(), inPrivate); | ||||||||||||||||||||
| public void OpenAppUri(Uri appUri) => OpenUrl(appUri); | ||||||||||||||||||||
| public void OpenAppUri(string appUri) => OpenUrl(appUri); | ||||||||||||||||||||
| public void OpenDirectory(string DirectoryPath, string? FileNameOrFilePath = null) => | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The Prompt for AI agents |
||||||||||||||||||||
| System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = DirectoryPath, UseShellExecute = true }); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Clipboard | ||||||||||||||||||||
| public void CopyToClipboard(string text, bool directCopy = false, bool showDefaultNotification = true) | ||||||||||||||||||||
| { | ||||||||||||||||||||
| if (global::Avalonia.Application.Current?.ApplicationLifetime is global::Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop) | ||||||||||||||||||||
| desktop.MainWindow?.Clipboard?.SetTextAsync(text); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // HTTP (delegate to Infrastructure) | ||||||||||||||||||||
| public Task<string> HttpGetStringAsync(string url, CancellationToken token = default) => Infrastructure.Http.Http.GetAsync(url, token); | ||||||||||||||||||||
| public Task<Stream> HttpGetStreamAsync(string url, CancellationToken token = default) => Infrastructure.Http.Http.GetStreamAsync(url, token); | ||||||||||||||||||||
| public Task HttpDownloadAsync(string url, string filePath, Action<double>? reportProgress = null, CancellationToken token = default) => | ||||||||||||||||||||
| Infrastructure.Http.Http.DownloadAsync(url, filePath, reportProgress, token); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Plugin management | ||||||||||||||||||||
| public void AddActionKeyword(string pluginId, string newActionKeyword) => PluginManager.AddActionKeyword(pluginId, newActionKeyword); | ||||||||||||||||||||
| public void RemoveActionKeyword(string pluginId, string oldActionKeyword) => PluginManager.RemoveActionKeyword(pluginId, oldActionKeyword); | ||||||||||||||||||||
| public bool ActionKeywordAssigned(string actionKeyword) => PluginManager.ActionKeywordRegistered(actionKeyword); | ||||||||||||||||||||
| public bool PluginModified(string id) => PluginManager.PluginModified(id); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Paths | ||||||||||||||||||||
| public string GetDataDirectory() => DataLocation.DataDirectory(); | ||||||||||||||||||||
| public string GetLogDirectory() => Log.CurrentLogDirectory; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Stubs - not critical for basic queries | ||||||||||||||||||||
| public void RestartApp() { } | ||||||||||||||||||||
| public void SaveAppAllSettings() { } | ||||||||||||||||||||
| public void SavePluginSettings() { } | ||||||||||||||||||||
| public Task ReloadAllPluginData() => Task.CompletedTask; | ||||||||||||||||||||
| public void CheckForNewUpdate() { } | ||||||||||||||||||||
| public void ShowMsgError(string title, string subTitle = "") => LogError("API", $"{title}: {subTitle}"); | ||||||||||||||||||||
| public void ShowMsgErrorWithButton(string title, string buttonText, Action buttonAction, string subTitle = "") { } | ||||||||||||||||||||
| public void ShowMainWindow() { } | ||||||||||||||||||||
| public void FocusQueryTextBox() { } | ||||||||||||||||||||
| public void HideMainWindow() => _getMainViewModel()?.RequestHide(); | ||||||||||||||||||||
| public bool IsMainWindowVisible() => true; | ||||||||||||||||||||
| public void ShowMsg(string title, string subTitle = "", string iconPath = "") { } | ||||||||||||||||||||
| 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 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(); | ||||||||||||||||||||
| public void SaveSettingJsonStorage<T>() where T : new() { } | ||||||||||||||||||||
|
Comment on lines
+115
to
+116
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Returning Consider delegating to the Infrastructure layer's JSON storage implementation or clearly documenting that settings persistence is not yet supported in Avalonia mode. 🤖 Prompt for AI Agents |
||||||||||||||||||||
| public void ToggleGameMode() { } | ||||||||||||||||||||
| public void SetGameMode(bool value) { } | ||||||||||||||||||||
| public bool IsGameModeOn() => false; | ||||||||||||||||||||
| public void ReQuery(bool reselect = true) { var q = _getMainViewModel().QueryText; _getMainViewModel().QueryText = ""; _getMainViewModel().QueryText = q; } | ||||||||||||||||||||
| public void BackToQueryResults() { } | ||||||||||||||||||||
| public MessageBoxResult ShowMsgBox(string messageBoxText, string caption = "", MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None, MessageBoxResult defaultResult = MessageBoxResult.OK) => defaultResult; | ||||||||||||||||||||
| public Task ShowProgressBoxAsync(string caption, Func<Action<double>, Task> reportProgressAsync, Action? cancelProgress = null) => reportProgressAsync(_ => { }); | ||||||||||||||||||||
| public void StartLoadingBar() => _getMainViewModel().IsQueryRunning = true; | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Inconsistent null handling - Prompt for AI agents |
||||||||||||||||||||
| public void StopLoadingBar() => _getMainViewModel().IsQueryRunning = false; | ||||||||||||||||||||
| public List<ThemeData> GetAvailableThemes() => new(); | ||||||||||||||||||||
| public ThemeData? GetCurrentTheme() => null; | ||||||||||||||||||||
| public bool SetCurrentTheme(ThemeData theme) => false; | ||||||||||||||||||||
| public void SavePluginCaches() { } | ||||||||||||||||||||
| public Task<T> LoadCacheBinaryStorageAsync<T>(string cacheName, string cacheDirectory, T defaultData) where T : new() => Task.FromResult(defaultData); | ||||||||||||||||||||
| public Task SaveCacheBinaryStorageAsync<T>(string cacheName, string cacheDirectory) where T : new() => Task.CompletedTask; | ||||||||||||||||||||
| public ValueTask<ImageSource> LoadImageAsync(string path, bool loadFullImage = false, bool cacheImage = true) => new((ImageSource)null!); | ||||||||||||||||||||
| public Task<bool> UpdatePluginManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default) => Task.FromResult(true); | ||||||||||||||||||||
| public IReadOnlyList<UserPlugin> GetPluginManifest() => new List<UserPlugin>(); | ||||||||||||||||||||
| public Task<bool> UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath) => Task.FromResult(false); | ||||||||||||||||||||
| public bool InstallPlugin(UserPlugin plugin, string zipFilePath) => false; | ||||||||||||||||||||
| public Task<bool> UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false) => Task.FromResult(false); | ||||||||||||||||||||
| public bool IsApplicationDarkTheme() => true; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| public long StopwatchLogDebug(string className, string message, Action action, [CallerMemberName] string methodName = "") | ||||||||||||||||||||
| { var sw = System.Diagnostics.Stopwatch.StartNew(); action(); sw.Stop(); LogDebug(className, $"{message}: {sw.ElapsedMilliseconds}ms", methodName); return sw.ElapsedMilliseconds; } | ||||||||||||||||||||
| public async Task<long> StopwatchLogDebugAsync(string className, string message, Func<Task> action, [CallerMemberName] string methodName = "") | ||||||||||||||||||||
| { var sw = System.Diagnostics.Stopwatch.StartNew(); await action(); sw.Stop(); LogDebug(className, $"{message}: {sw.ElapsedMilliseconds}ms", methodName); return sw.ElapsedMilliseconds; } | ||||||||||||||||||||
| public long StopwatchLogInfo(string className, string message, Action action, [CallerMemberName] string methodName = "") | ||||||||||||||||||||
| { var sw = System.Diagnostics.Stopwatch.StartNew(); action(); sw.Stop(); LogInfo(className, $"{message}: {sw.ElapsedMilliseconds}ms", methodName); return sw.ElapsedMilliseconds; } | ||||||||||||||||||||
| public async Task<long> StopwatchLogInfoAsync(string className, string message, Func<Task> action, [CallerMemberName] string methodName = "") | ||||||||||||||||||||
| { var sw = System.Diagnostics.Stopwatch.StartNew(); await action(); sw.Stop(); LogInfo(className, $"{message}: {sw.ElapsedMilliseconds}ms", methodName); return sw.ElapsedMilliseconds; } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| using System; | ||
| using System.Globalization; | ||
| using Avalonia.Data.Converters; | ||
|
|
||
| namespace Flow.Launcher.Avalonia.Converters; | ||
|
|
||
| /// <summary> | ||
| /// Converts a boolean value to IsVisible (Avalonia uses bool for visibility, not Visibility enum) | ||
| /// </summary> | ||
| public class BoolToIsVisibleConverter : IValueConverter | ||
| { | ||
| /// <summary> | ||
| /// If true, inverts the boolean value (true becomes false, false becomes true) | ||
| /// </summary> | ||
| public bool Invert { get; set; } | ||
|
|
||
| public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) | ||
| { | ||
| if (value is bool boolValue) | ||
| { | ||
| return Invert ? !boolValue : boolValue; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) | ||
| { | ||
| if (value is bool boolValue) | ||
| { | ||
| return Invert ? !boolValue : boolValue; | ||
| } | ||
| return false; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ensure fallback settings still have storage attached.
Lines 95–106 create a new
Settingsinstance on load failure but never callSetStorage, which can prevent saves or trigger null usage later. Consider attaching a fresh storage in the catch block.🛠️ Suggested fix
catch (Exception e) { Log.Exception(ClassName, "Settings load failed", e); - _settings = new Settings + var storage = new FlowLauncherJsonStorage<Settings>(); + _settings = new Settings { WindowSize = 580, WindowHeightSize = 42, QueryBoxFontSize = 24, ItemHeightSize = 50, ResultItemFontSize = 14, ResultSubItemFontSize = 12, MaxResultsToShow = 6 }; + _settings.SetStorage(storage); }🤖 Prompt for AI Agents