From 7883e7a0a95c6609c5cf5622dfc7d4ac810c18fc Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sat, 18 Oct 2025 10:35:23 +1100 Subject: [PATCH 01/77] add result IcoPath & Glyph + handling show badge switching --- Flow.Launcher/Storage/LastOpenedHistoryItem.cs | 2 ++ Flow.Launcher/Storage/QueryHistory.cs | 4 +++- Flow.Launcher/ViewModel/MainViewModel.cs | 10 +++++++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher/Storage/LastOpenedHistoryItem.cs b/Flow.Launcher/Storage/LastOpenedHistoryItem.cs index 47647066c..c7f9144a7 100644 --- a/Flow.Launcher/Storage/LastOpenedHistoryItem.cs +++ b/Flow.Launcher/Storage/LastOpenedHistoryItem.cs @@ -10,6 +10,8 @@ public class LastOpenedHistoryItem public string PluginID { get; set; } = string.Empty; public string Query { get; set; } = string.Empty; public string RecordKey { get; set; } = string.Empty; + public string IcoPath { get; set; } = string.Empty; + public GlyphInfo Glyph { get; init; } = null; public DateTime ExecutedDateTime { get; set; } public bool Equals(Result r) diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs index 8284f7eea..bcf080669 100644 --- a/Flow.Launcher/Storage/QueryHistory.cs +++ b/Flow.Launcher/Storage/QueryHistory.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; @@ -59,6 +59,8 @@ namespace Flow.Launcher.Storage PluginID = result.PluginID, Query = result.OriginQuery.RawQuery, RecordKey = result.RecordKey, + IcoPath = result.IcoPath, + Glyph = result.Glyph, ExecutedDateTime = DateTime.Now }); } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index f0f4b257a..f26e0f60f 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; @@ -1349,7 +1349,9 @@ namespace Flow.Launcher.ViewModel Localize.executeQuery(h.Query) : h.Title, SubTitle = Localize.lastExecuteTime(h.ExecutedDateTime), - IcoPath = Constant.HistoryIcon, + IcoPath = Settings.ShowBadges ? h.IcoPath : Constant.HistoryIcon, + BadgeIcoPath = Settings.ShowBadges ? Constant.HistoryIcon : h.IcoPath, + ShowBadge = Settings.ShowBadges, OriginQuery = new Query { RawQuery = h.Query }, AsyncAction = async c => { @@ -1369,7 +1371,9 @@ namespace Flow.Launcher.ViewModel App.API.ChangeQuery(h.Query); return false; }, - Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C") + Glyph = Settings.ShowBadges ? + h.Glyph is not null ? h.Glyph : null + : new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C") }; results.Add(result); } From 10d3ba268dd216b41113c762604668b3dd276bcc Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 19 Oct 2025 20:22:03 +1100 Subject: [PATCH 02/77] update history result icon display logic & remove badge icon logic --- Flow.Launcher/ViewModel/MainViewModel.cs | 46 +++++++++++++----------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index f26e0f60f..b8d56a652 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; @@ -1318,15 +1318,24 @@ namespace Flow.Launcher.ViewModel private List GetHistoryItems(IEnumerable historyItems) { var results = new List(); - if (Settings.HistoryStyle == HistoryStyle.Query) + foreach (var h in historyItems) { - foreach (var h in historyItems) + Result result = null; + var glyph = h.Glyph is null && !string.IsNullOrEmpty(h.IcoPath) // Some plugins won't have Glyph, then prefer IcoPath + ? null + : h.Glyph is not null + ? h.Glyph + : new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C"); // Default fallback + + var icoPath = !string.IsNullOrEmpty(h.IcoPath) ? h.IcoPath : Constant.HistoryIcon; + + if (Settings.HistoryStyle == HistoryStyle.Query) { - var result = new Result + result = new Result { Title = Localize.executeQuery(h.Query), SubTitle = Localize.lastExecuteTime(h.ExecutedDateTime), - IcoPath = Constant.HistoryIcon, + IcoPath = icoPath, OriginQuery = new Query { RawQuery = h.Query }, Action = _ => { @@ -1334,24 +1343,19 @@ namespace Flow.Launcher.ViewModel App.API.ChangeQuery(h.Query); return false; }, - Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C") + Glyph = glyph }; - results.Add(result); } - } - else - { - foreach (var h in historyItems) + else { - var result = new Result + + result = new Result { Title = string.IsNullOrEmpty(h.Title) ? // Old migrated history items have no title - Localize.executeQuery(h.Query) : - h.Title, + Localize.executeQuery(h.Query) : + h.Title, SubTitle = Localize.lastExecuteTime(h.ExecutedDateTime), - IcoPath = Settings.ShowBadges ? h.IcoPath : Constant.HistoryIcon, - BadgeIcoPath = Settings.ShowBadges ? Constant.HistoryIcon : h.IcoPath, - ShowBadge = Settings.ShowBadges, + IcoPath = icoPath, OriginQuery = new Query { RawQuery = h.Query }, AsyncAction = async c => { @@ -1371,13 +1375,13 @@ namespace Flow.Launcher.ViewModel App.API.ChangeQuery(h.Query); return false; }, - Glyph = Settings.ShowBadges ? - h.Glyph is not null ? h.Glyph : null - : new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C") + Glyph = glyph }; - results.Add(result); } + + results.Add(result); } + return results; } From 26ad47359203c120829af9c5816d9cddee67ea95 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 19 Oct 2025 21:46:02 +1100 Subject: [PATCH 03/77] change getting last history item to get the same result if exists --- Flow.Launcher/Storage/QueryHistory.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs index bcf080669..fde74bf17 100644 --- a/Flow.Launcher/Storage/QueryHistory.cs +++ b/Flow.Launcher/Storage/QueryHistory.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; @@ -44,11 +44,11 @@ namespace Flow.Launcher.Storage LastOpenedHistoryItems.RemoveAt(0); } - // If the last item is the same as the current result, just update the timestamp - if (LastOpenedHistoryItems.Count > 0 && - LastOpenedHistoryItems.Last().Equals(result)) + if (LastOpenedHistoryItems.Count > 0 && + TryGetLastOpenedHistoryResult(result, out var existingHistoryItem)) { - LastOpenedHistoryItems.Last().ExecutedDateTime = DateTime.Now; + existingHistoryItem.IcoPath = result.IcoPath; + existingHistoryItem.ExecutedDateTime = DateTime.Now; } else { @@ -65,5 +65,11 @@ namespace Flow.Launcher.Storage }); } } + + private bool TryGetLastOpenedHistoryResult(Result result, out LastOpenedHistoryItem historyItem) + { + historyItem = LastOpenedHistoryItems.FirstOrDefault(x => x.Equals(result)); + return historyItem is not null; + } } } From 92551c58a5d65b2d7ff6563ee9fdf75a22959043 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 21 Oct 2025 21:53:32 +1100 Subject: [PATCH 04/77] show distinct records when history style is last opened --- Flow.Launcher/ViewModel/MainViewModel.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index b8d56a652..b70d6e5e2 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; @@ -1319,6 +1319,13 @@ namespace Flow.Launcher.ViewModel { var results = new List(); foreach (var h in historyItems) + if (Settings.HistoryStyle == HistoryStyle.LastOpened) + { + historyItems = historyItems + .GroupBy(r => new { r.Title, r.SubTitle, r.PluginID, r.RecordKey }) + .Select(g => g.First()); + } + { Result result = null; var glyph = h.Glyph is null && !string.IsNullOrEmpty(h.IcoPath) // Some plugins won't have Glyph, then prefer IcoPath From 25aa5bf2af1cd683211b9ea1829c1d80aa98a141 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 21 Oct 2025 21:54:38 +1100 Subject: [PATCH 05/77] show history result sorted descending by execution time --- Flow.Launcher/ViewModel/MainViewModel.cs | 37 ++++++++++++------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index b70d6e5e2..eeb15d74e 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; @@ -1318,7 +1318,7 @@ namespace Flow.Launcher.ViewModel private List GetHistoryItems(IEnumerable historyItems) { var results = new List(); - foreach (var h in historyItems) + if (Settings.HistoryStyle == HistoryStyle.LastOpened) { historyItems = historyItems @@ -1326,28 +1326,29 @@ namespace Flow.Launcher.ViewModel .Select(g => g.First()); } + foreach (var item in historyItems.OrderByDescending(h => h.ExecutedDateTime)) { Result result = null; - var glyph = h.Glyph is null && !string.IsNullOrEmpty(h.IcoPath) // Some plugins won't have Glyph, then prefer IcoPath + var glyph = item.Glyph is null && !string.IsNullOrEmpty(item.IcoPath) // Some plugins won't have Glyph, then prefer IcoPath ? null - : h.Glyph is not null - ? h.Glyph + : item.Glyph is not null + ? item.Glyph : new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C"); // Default fallback - var icoPath = !string.IsNullOrEmpty(h.IcoPath) ? h.IcoPath : Constant.HistoryIcon; + var icoPath = !string.IsNullOrEmpty(item.IcoPath) ? item.IcoPath : Constant.HistoryIcon; if (Settings.HistoryStyle == HistoryStyle.Query) { result = new Result { - Title = Localize.executeQuery(h.Query), - SubTitle = Localize.lastExecuteTime(h.ExecutedDateTime), + Title = Localize.executeQuery(item.Query), + SubTitle = Localize.lastExecuteTime(item.ExecutedDateTime), IcoPath = icoPath, - OriginQuery = new Query { RawQuery = h.Query }, + OriginQuery = new Query { RawQuery = item.Query }, Action = _ => { App.API.BackToQueryResults(); - App.API.ChangeQuery(h.Query); + App.API.ChangeQuery(item.Query); return false; }, Glyph = glyph @@ -1358,15 +1359,15 @@ namespace Flow.Launcher.ViewModel result = new Result { - Title = string.IsNullOrEmpty(h.Title) ? // Old migrated history items have no title - Localize.executeQuery(h.Query) : - h.Title, - SubTitle = Localize.lastExecuteTime(h.ExecutedDateTime), + Title = string.IsNullOrEmpty(item.Title) ? // Old migrated history items have no title + Localize.executeQuery(item.Query) : + item.Title, + SubTitle = Localize.lastExecuteTime(item.ExecutedDateTime), IcoPath = icoPath, - OriginQuery = new Query { RawQuery = h.Query }, + OriginQuery = new Query { RawQuery = item.Query }, AsyncAction = async c => { - var reflectResult = await ResultHelper.PopulateResultsAsync(h); + var reflectResult = await ResultHelper.PopulateResultsAsync(item); if (reflectResult != null) { // Record the user selected record for result ranking @@ -1379,7 +1380,7 @@ namespace Flow.Launcher.ViewModel // If we cannot get the result, fallback to re-query App.API.BackToQueryResults(); - App.API.ChangeQuery(h.Query); + App.API.ChangeQuery(item.Query); return false; }, Glyph = glyph @@ -1388,7 +1389,7 @@ namespace Flow.Launcher.ViewModel results.Add(result); } - + return results; } From 0f269455b1c9fac303d11bed238bd1ec14e92d44 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 21 Oct 2025 22:18:29 +1100 Subject: [PATCH 06/77] remove duplicate history item sort call --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index eeb15d74e..08d4e0d03 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1326,7 +1326,7 @@ namespace Flow.Launcher.ViewModel .Select(g => g.First()); } - foreach (var item in historyItems.OrderByDescending(h => h.ExecutedDateTime)) + foreach (var item in historyItems) { Result result = null; var glyph = item.Glyph is null && !string.IsNullOrEmpty(item.IcoPath) // Some plugins won't have Glyph, then prefer IcoPath From 7958b17968ac1936ea171037e96c446efd058c93 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 20 Nov 2025 21:02:19 +1100 Subject: [PATCH 07/77] wip show result icon --- Flow.Launcher.Plugin/Result.cs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index 2c9b8d4fd..0f118e36d 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -90,6 +90,34 @@ namespace Flow.Launcher.Plugin } } + /// + /// Returns the plugin directory relative IcoPath or URL. This path is useful for storage where the it needs + /// to be resistant to changes in plugin location from update or portable mode change. + /// + /// Must be a local file path. + /// This will be empty string if IcoPath is a URL, so must check for empty string during usage + public string IcoPathRelative + { + get => _icoPath; + set + { + // As a standard this property will handle prepping and converting to absolute local path for icon image processing + if (!string.IsNullOrEmpty(value) + && !string.IsNullOrEmpty(PluginDirectory) + && !Path.IsPathRooted(value) + && !value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) + && !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase) + && !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase)) + { + _icoPath = Path.Combine(PluginDirectory, value); + } + else + { + _icoPath = value; + } + } + } + /// /// The image to be displayed for the badge of the result. /// From d78d313372dd07aa2df5bd8fc3c27228fa2220a6 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 26 Dec 2025 21:37:30 +1100 Subject: [PATCH 08/77] update to use IcoAbsoluteLocalPath --- Flow.Launcher.Plugin/Result.cs | 50 +++++++++++-------- .../Views/SettingsPanePluginStore.xaml | 2 +- Flow.Launcher/ViewModel/ResultViewModel.cs | 6 +-- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index 0f118e36d..e5b3155b6 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Security.Policy; using System.Threading.Tasks; using System.Windows.Controls; using System.Windows.Media; @@ -21,6 +22,8 @@ namespace Flow.Launcher.Plugin private string _icoPath; + private string _icoAbsoluteFullPath; + private string _copyText = string.Empty; private string _badgeIcoPath; @@ -67,12 +70,17 @@ namespace Flow.Launcher.Plugin /// The image to be displayed for the result. /// /// Can be a local file path or a URL. - /// GlyphInfo is prioritized if not null + /// + /// GlyphInfo is prioritized if not null. + /// Use IcoPathRelative for storage where it needs to be resistant to plugin location change. + /// public string IcoPath { get => _icoPath; set { + _icoPath = value; + // As a standard this property will handle prepping and converting to absolute local path for icon image processing if (!string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(PluginDirectory) @@ -81,40 +89,40 @@ namespace Flow.Launcher.Plugin && !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase) && !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase)) { - _icoPath = Path.Combine(PluginDirectory, value); + _icoAbsoluteFullPath = Path.Combine(PluginDirectory, value); } else { - _icoPath = value; + _icoAbsoluteFullPath = value; } } } /// - /// Returns the plugin directory relative IcoPath or URL. This path is useful for storage where the it needs - /// to be resistant to changes in plugin location from update or portable mode change. + /// TODO COMMENT + /// + public string IcoAbsoluteFullPath => _icoAbsoluteFullPath; + + /// + /// Returns IcoPath's relative path based on the plugin directory or original value if not file path. + /// This property is useful for storage where it needs to be resistant to changes in plugin location from update + /// or portable mode change. /// - /// Must be a local file path. - /// This will be empty string if IcoPath is a URL, so must check for empty string during usage public string IcoPathRelative { - get => _icoPath; - set + get { - // As a standard this property will handle prepping and converting to absolute local path for icon image processing - if (!string.IsNullOrEmpty(value) - && !string.IsNullOrEmpty(PluginDirectory) - && !Path.IsPathRooted(value) - && !value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) - && !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase) - && !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(IcoAbsoluteFullPath) + || string.IsNullOrEmpty(PluginDirectory) + || !Path.IsPathRooted(IcoAbsoluteFullPath) + || IcoAbsoluteFullPath.StartsWith("http://", StringComparison.OrdinalIgnoreCase) + || IcoAbsoluteFullPath.StartsWith("https://", StringComparison.OrdinalIgnoreCase) + || IcoAbsoluteFullPath.StartsWith("data:image", StringComparison.OrdinalIgnoreCase)) { - _icoPath = Path.Combine(PluginDirectory, value); - } - else - { - _icoPath = value; + return IcoAbsoluteFullPath; } + + return Path.GetRelativePath(PluginDirectory, IcoAbsoluteFullPath); } } diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml index aa027e19e..5d8837891 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml @@ -333,7 +333,7 @@ Margin="18 24 0 0" HorizontalAlignment="Left" RenderOptions.BitmapScalingMode="Fant" - Source="{Binding IcoPath, IsAsync=True}" /> + Source="{Binding IcoAbsoluteFullPath, IsAsync=True}" /> Glyph is not null; - private bool ImgIconAvailable => !string.IsNullOrEmpty(Result.IcoPath) || Result.Icon is not null; + private bool ImgIconAvailable => !string.IsNullOrEmpty(Result.IcoAbsoluteFullPath) || Result.Icon is not null; private bool BadgeIconAvailable => !string.IsNullOrEmpty(Result.BadgeIcoPath) || Result.BadgeIcon is not null; @@ -236,7 +236,7 @@ namespace Flow.Launcher.ViewModel private async Task LoadImageAsync() { - var imagePath = Result.IcoPath; + var imagePath = Result.IcoAbsoluteFullPath; var iconDelegate = Result.Icon; if (ImageLoader.TryGetValue(imagePath, false, out var img)) { @@ -266,7 +266,7 @@ namespace Flow.Launcher.ViewModel private async Task LoadPreviewImageAsync() { - var imagePath = Result.Preview.PreviewImagePath ?? Result.IcoPath; + var imagePath = Result.Preview.PreviewImagePath ?? Result.IcoAbsoluteFullPath; var iconDelegate = Result.Preview.PreviewDelegate ?? Result.Icon; if (ImageLoader.TryGetValue(imagePath, true, out var img)) { From de0d022268dd0e8ee682042444e357a5e83e2547 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 31 Dec 2025 12:14:07 +1100 Subject: [PATCH 09/77] rename last opened class; use inheritance; initialise at startup --- Flow.Launcher.Plugin/Result.cs | 8 + Flow.Launcher/App.xaml.cs | 2 + Flow.Launcher/Helper/ResultHelper.cs | 2 +- .../Storage/LastOpenedHistoryItem.cs | 33 --- .../Storage/LastOpenedHistoryResult.cs | 106 +++++++++ Flow.Launcher/Storage/QueryHistory.cs | 52 +++-- Flow.Launcher/ViewModel/MainViewModel.cs | 201 ++++++++++++------ 7 files changed, 293 insertions(+), 111 deletions(-) delete mode 100644 Flow.Launcher/Storage/LastOpenedHistoryItem.cs create mode 100644 Flow.Launcher/Storage/LastOpenedHistoryResult.cs diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index e5b3155b6..2ad19effe 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -5,6 +5,7 @@ using System.Security.Policy; using System.Threading.Tasks; using System.Windows.Controls; using System.Windows.Media; +using System.Text.Json.Serialization; namespace Flow.Launcher.Plugin { @@ -167,11 +168,13 @@ namespace Flow.Launcher.Plugin /// /// Delegate to load an icon for this result. /// + [JsonIgnore] public IconDelegate Icon = null; /// /// Delegate to load an icon for the badge of this result. /// + [JsonIgnore] public IconDelegate BadgeIcon = null; /// @@ -187,6 +190,7 @@ namespace Flow.Launcher.Plugin /// Its result determines what happens to Flow Launcher's query form: /// when true, the form will be hidden; when false, it will stay in focus. /// + [JsonIgnore] public Func Action { get; set; } /// @@ -197,6 +201,7 @@ namespace Flow.Launcher.Plugin /// Its result determines what happens to Flow Launcher's query form: /// when true, the form will be hidden; when false, it will stay in focus. /// + [JsonIgnore] public Func> AsyncAction { get; set; } /// @@ -239,6 +244,7 @@ namespace Flow.Launcher.Plugin /// /// As external information for ContextMenu /// + [JsonIgnore] public object ContextData { get; set; } /// @@ -259,6 +265,7 @@ namespace Flow.Launcher.Plugin /// /// Customized Preview Panel /// + [JsonIgnore] public Lazy PreviewPanel { get; set; } /// @@ -388,6 +395,7 @@ namespace Flow.Launcher.Plugin /// /// Delegate to get the preview panel's image /// + [JsonIgnore] public IconDelegate PreviewDelegate { get; set; } = null; /// diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index b45bbc549..1f1b3c546 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -259,6 +259,8 @@ namespace Flow.Launcher await PluginManager.InitializePluginsAsync(_mainVM); + _mainVM.InitializeQueryHistoryItems(); + // Refresh home page after plugins are initialized because users may open main window during plugin initialization // And home page is created without full plugin list if (_settings.ShowHomePage && _mainVM.QueryResultsSelected() && string.IsNullOrEmpty(_mainVM.QueryText)) diff --git a/Flow.Launcher/Helper/ResultHelper.cs b/Flow.Launcher/Helper/ResultHelper.cs index 5f9a69f28..d0cf2dddc 100644 --- a/Flow.Launcher/Helper/ResultHelper.cs +++ b/Flow.Launcher/Helper/ResultHelper.cs @@ -11,7 +11,7 @@ namespace Flow.Launcher.Helper; public static class ResultHelper { - public static async Task PopulateResultsAsync(LastOpenedHistoryItem item) + public static async Task PopulateResultsAsync(LastOpenedHistoryResult item) { return await PopulateResultsAsync(item.PluginID, item.Query, item.Title, item.SubTitle, item.RecordKey); } diff --git a/Flow.Launcher/Storage/LastOpenedHistoryItem.cs b/Flow.Launcher/Storage/LastOpenedHistoryItem.cs deleted file mode 100644 index c7f9144a7..000000000 --- a/Flow.Launcher/Storage/LastOpenedHistoryItem.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using Flow.Launcher.Plugin; - -namespace Flow.Launcher.Storage; - -public class LastOpenedHistoryItem -{ - public string Title { get; set; } = string.Empty; - public string SubTitle { get; set; } = string.Empty; - public string PluginID { get; set; } = string.Empty; - public string Query { get; set; } = string.Empty; - public string RecordKey { get; set; } = string.Empty; - public string IcoPath { get; set; } = string.Empty; - public GlyphInfo Glyph { get; init; } = null; - public DateTime ExecutedDateTime { get; set; } - - public bool Equals(Result r) - { - if (string.IsNullOrEmpty(RecordKey) || string.IsNullOrEmpty(r.RecordKey)) - { - return Title == r.Title - && SubTitle == r.SubTitle - && PluginID == r.PluginID - && Query == r.OriginQuery.RawQuery; - } - else - { - return RecordKey == r.RecordKey - && PluginID == r.PluginID - && Query == r.OriginQuery.RawQuery; - } - } -} diff --git a/Flow.Launcher/Storage/LastOpenedHistoryResult.cs b/Flow.Launcher/Storage/LastOpenedHistoryResult.cs new file mode 100644 index 000000000..98fdc1a74 --- /dev/null +++ b/Flow.Launcher/Storage/LastOpenedHistoryResult.cs @@ -0,0 +1,106 @@ +using System; +using System.DirectoryServices.ActiveDirectory; +using Flow.Launcher.Helper; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Storage; + +public class LastOpenedHistoryResult : Result +{ + public string Query { get; set; } = string.Empty; + + public DateTime ExecutedDateTime { get; set; } + + public LastOpenedHistoryResult() + { + } + + public LastOpenedHistoryResult(Result result) + { + Title = result.Title; + SubTitle = result.SubTitle; + PluginID = result.PluginID; + Query = result.OriginQuery.RawQuery; + RecordKey = result.RecordKey; + IcoPath = result.IcoPath; + PluginDirectory = result.PluginDirectory; + Glyph = result.Glyph; + ExecutedDateTime = DateTime.Now; + } + + //public Result ToResult(bool isQueryHistoryStyle) + //{ + // Result result = null; + + // if (isQueryHistoryStyle) + // { + // result = new Result + // { + // Action = _ => + // { + // App.API.BackToQueryResults(); + // App.API.ChangeQuery(Query); + // return false; + // }, + // Glyph = Glyph, + // }; + // } + // else + // { + // result = new Result + // { + // AsyncAction = async c => + // { + // var reflectResult = await ResultHelper.PopulateResultsAsync(item); + // if (reflectResult != null) + // { + // // Record the user selected record for result ranking + // _userSelectedRecord.Add(reflectResult); + + // // Since some actions may need to hide the Flow window to execute + // // So let us populate the results of them + // return await reflectResult.ExecuteAsync(c); + // } + + // // If we cannot get the result, fallback to re-query + // App.API.BackToQueryResults(); + // App.API.ChangeQuery(item.Query); + // return false; + // }, + // Glyph = Glyph, + // }; + // } + + // var result = new Result + // { + // Title = Title, + // SubTitle = Localize.lastExecuteTime(ExecutedDateTime), + // IcoPath = IcoPath, + // OriginQuery = new Query { RawQuery = Query }, + // Action = _ => + // { + // App.API.BackToQueryResults(); + // App.API.ChangeQuery(Query); + // return false; + // }, + // Glyph = Glyph, + // }; + //} + + public bool Equals(Result r) + { + if (string.IsNullOrEmpty(RecordKey) || string.IsNullOrEmpty(r.RecordKey)) + { + return Title == r.Title + && SubTitle == r.SubTitle + && PluginID == r.PluginID + && Query == r.OriginQuery.RawQuery; + } + else + { + return RecordKey == r.RecordKey + && PluginID == r.PluginID + && Query == r.OriginQuery.RawQuery; + } + } +} diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs index fde74bf17..c95a9308b 100644 --- a/Flow.Launcher/Storage/QueryHistory.cs +++ b/Flow.Launcher/Storage/QueryHistory.cs @@ -1,8 +1,13 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text.Json.Serialization; +using Flow.Launcher.Core.Plugin; +using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin; +using Windows.Devices.Geolocation; +using YamlDotNet.Core.Tokens; namespace Flow.Launcher.Storage { @@ -14,7 +19,7 @@ namespace Flow.Launcher.Storage #pragma warning restore CS0618 // Type or member is obsolete [JsonInclude] - public List LastOpenedHistoryItems { get; private set; } = []; + public List LastOpenedHistoryItems { get; private set; } = []; private readonly int _maxHistory = 300; @@ -24,8 +29,12 @@ namespace Flow.Launcher.Storage // Migrate old history items to new LastOpenedHistoryItems foreach (var item in Items) { - LastOpenedHistoryItems.Add(new LastOpenedHistoryItem + LastOpenedHistoryItems.Add(new LastOpenedHistoryResult { + Title = Localize.executeQuery(item.Query), + IcoPath = Constant.HistoryIcon, + OriginQuery = new Query { RawQuery = item.Query }, + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C"), Query = item.Query, ExecutedDateTime = item.ExecutedDateTime }); @@ -47,29 +56,42 @@ namespace Flow.Launcher.Storage if (LastOpenedHistoryItems.Count > 0 && TryGetLastOpenedHistoryResult(result, out var existingHistoryItem)) { - existingHistoryItem.IcoPath = result.IcoPath; + //existingHistoryItem.IcoPath = result.IcoPath; existingHistoryItem.ExecutedDateTime = DateTime.Now; } else { - LastOpenedHistoryItems.Add(new LastOpenedHistoryItem - { - Title = result.Title, - SubTitle = result.SubTitle, - PluginID = result.PluginID, - Query = result.OriginQuery.RawQuery, - RecordKey = result.RecordKey, - IcoPath = result.IcoPath, - Glyph = result.Glyph, - ExecutedDateTime = DateTime.Now - }); + LastOpenedHistoryItems.Add(new LastOpenedHistoryResult(result)); } } - private bool TryGetLastOpenedHistoryResult(Result result, out LastOpenedHistoryItem historyItem) + private bool TryGetLastOpenedHistoryResult(Result result, out LastOpenedHistoryResult historyItem) { historyItem = LastOpenedHistoryItems.FirstOrDefault(x => x.Equals(result)); return historyItem is not null; } + + /// + /// Refresh stored PluginDirectory (and optionally normalize relative ico paths) + /// using current plugin metadata. Call this after plugins are loaded/initialized. + /// + public void UpdateIcoAbsoluteFullPath() + { + if (LastOpenedHistoryItems.Count == 0) return; + + foreach (var item in LastOpenedHistoryItems) + { + if (string.IsNullOrEmpty(item.PluginID)) + continue; + + var pluginPair = PluginManager.GetPluginForId(item.PluginID); + if (pluginPair == null) + continue; + + //item.IcoPath = Path.Combine(pluginPair.Metadata.PluginDirectory, item.IcoPath); + + item.PluginDirectory = pluginPair.Metadata.PluginDirectory; + } + } } } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 08d4e0d03..2ff571f19 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; @@ -41,10 +42,32 @@ namespace Flow.Launcher.ViewModel private string _queryTextBeforeLeaveResults; private string _ignoredQueryText; // Used to ignore query text change when switching between context menu and query results - private readonly FlowLauncherJsonStorage _historyItemsStorage; + private int QueryHistoryItemsInitialized = 0; + private FlowLauncherJsonStorage _historyItemsStorage; + private History _queryHistoryItems; + private History QueryHistoryItems + { + get + { + if (QueryHistoryItemsInitialized == 0) + { + App.API.LogException(ClassName, + "QueryHistoryItems is not initialized. Call InitializeQueryHistoryItems() before accessing QueryHistoryItems.", + new InvalidOperationException(), + "QueryHistoryItems"); + throw new InvalidOperationException("QueryHistoryItems is not initialized. Call InitializeQueryHistoryItems() before accessing QueryHistoryItems."); + } + + return _queryHistoryItems; + } + set + { + _queryHistoryItems = value; + } + } + private readonly FlowLauncherJsonStorage _userSelectedRecordStorage; private readonly FlowLauncherJsonStorageTopMostRecord _topMostRecord; - private readonly History _history; private int lastHistoryIndex = 1; private readonly UserSelectedRecord _userSelectedRecord; @@ -151,8 +174,6 @@ namespace Flow.Launcher.ViewModel _historyItemsStorage = new FlowLauncherJsonStorage(); _userSelectedRecordStorage = new FlowLauncherJsonStorage(); _topMostRecord = new FlowLauncherJsonStorageTopMostRecord(); - _history = _historyItemsStorage.Load(); - _history.PopulateHistoryFromLegacyHistory(); _userSelectedRecord = _userSelectedRecordStorage.Load(); ContextMenu = new ResultsViewModel(Settings, this) @@ -352,7 +373,7 @@ namespace Flow.Launcher.ViewModel if (QueryResultsSelected()) { SelectedResults = History; - History.SelectedIndex = _history.LastOpenedHistoryItems.Count - 1; + History.SelectedIndex = QueryHistoryItems.LastOpenedHistoryItems.Count - 1; } else { @@ -380,7 +401,7 @@ namespace Flow.Launcher.ViewModel [RelayCommand] public void ReverseHistory() { - var historyItems = _history.LastOpenedHistoryItems; + var historyItems = QueryHistoryItems.LastOpenedHistoryItems; if (historyItems.Count > 0) { ChangeQueryText(historyItems[^lastHistoryIndex].Query); @@ -394,7 +415,7 @@ namespace Flow.Launcher.ViewModel [RelayCommand] public void ForwardHistory() { - var historyItems = _history.LastOpenedHistoryItems; + var historyItems = QueryHistoryItems.LastOpenedHistoryItems; if (historyItems.Count > 0) { ChangeQueryText(historyItems[^lastHistoryIndex].Query); @@ -536,7 +557,7 @@ namespace Flow.Launcher.ViewModel // Add item to history only if it is from results but not context menu or history if (queryResultsSelected) { - _history.Add(result); + QueryHistoryItems.Add(result); lastHistoryIndex = 1; } } @@ -612,7 +633,7 @@ namespace Flow.Launcher.ViewModel [RelayCommand] private void SelectPrevItem() { - var historyItems = _history.LastOpenedHistoryItems; + var historyItems = QueryHistoryItems.LastOpenedHistoryItems; if (QueryResultsSelected() // Results selected && string.IsNullOrEmpty(QueryText) // No input && Results.Visibility != Visibility.Visible // No items in result list, e.g. when home page is off and no query text is entered, therefore the view is collapsed. @@ -1298,7 +1319,7 @@ namespace Flow.Launcher.ViewModel var query = QueryText.ToLower().Trim(); History.Clear(); - var results = GetHistoryItems(_history.LastOpenedHistoryItems); + var results = GetHistoryItems(QueryHistoryItems.LastOpenedHistoryItems); if (!string.IsNullOrEmpty(query)) { @@ -1315,7 +1336,7 @@ namespace Flow.Launcher.ViewModel } } - private List GetHistoryItems(IEnumerable historyItems) + private List GetHistoryItems(IEnumerable historyItems) { var results = new List(); @@ -1329,70 +1350,126 @@ namespace Flow.Launcher.ViewModel foreach (var item in historyItems) { Result result = null; - var glyph = item.Glyph is null && !string.IsNullOrEmpty(item.IcoPath) // Some plugins won't have Glyph, then prefer IcoPath - ? null - : item.Glyph is not null - ? item.Glyph - : new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C"); // Default fallback + //var glyph = item.Glyph is null && !string.IsNullOrEmpty(item.IcoPath) // Some plugins won't have Glyph, then prefer IcoPath + // ? null + // : item.Glyph is not null + // ? item.Glyph + // : new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C"); // Default fallback - var icoPath = !string.IsNullOrEmpty(item.IcoPath) ? item.IcoPath : Constant.HistoryIcon; + //var icoPath = !string.IsNullOrEmpty(item.IcoPath) ? item.IcoPath : Constant.HistoryIcon; - if (Settings.HistoryStyle == HistoryStyle.Query) + + + + + + result = new Result { - result = new Result + Title = Settings.HistoryStyle == HistoryStyle.Query + ? Localize.executeQuery(item.Query) + : item.Title, + SubTitle = Localize.lastExecuteTime(item.ExecutedDateTime), + IcoPath = item.IcoAbsoluteFullPath, + OriginQuery = new Query { RawQuery = item.Query }, + Action = _ => { - Title = Localize.executeQuery(item.Query), - SubTitle = Localize.lastExecuteTime(item.ExecutedDateTime), - IcoPath = icoPath, - OriginQuery = new Query { RawQuery = item.Query }, - Action = _ => - { - App.API.BackToQueryResults(); - App.API.ChangeQuery(item.Query); - return false; - }, - Glyph = glyph - }; - } - else - { - - result = new Result + App.API.BackToQueryResults(); + App.API.ChangeQuery(item.Query); + return false; + }, + AsyncAction = async c => { - Title = string.IsNullOrEmpty(item.Title) ? // Old migrated history items have no title - Localize.executeQuery(item.Query) : - item.Title, - SubTitle = Localize.lastExecuteTime(item.ExecutedDateTime), - IcoPath = icoPath, - OriginQuery = new Query { RawQuery = item.Query }, - AsyncAction = async c => + var reflectResult = await ResultHelper.PopulateResultsAsync(item); + if (reflectResult != null) { - var reflectResult = await ResultHelper.PopulateResultsAsync(item); - if (reflectResult != null) - { - // Record the user selected record for result ranking - _userSelectedRecord.Add(reflectResult); + // Record the user selected record for result ranking + _userSelectedRecord.Add(reflectResult); - // Since some actions may need to hide the Flow window to execute - // So let us populate the results of them - return await reflectResult.ExecuteAsync(c); - } + // Since some actions may need to hide the Flow window to execute + // So let us populate the results of them + return await reflectResult.ExecuteAsync(c); + } - // If we cannot get the result, fallback to re-query - App.API.BackToQueryResults(); - App.API.ChangeQuery(item.Query); - return false; - }, - Glyph = glyph - }; - } + // If we cannot get the result, fallback to re-query + App.API.BackToQueryResults(); + App.API.ChangeQuery(item.Query); + return false; + }, + Glyph = item.Glyph + }; + + + + //if (Settings.HistoryStyle == HistoryStyle.Query) + //{ + // result = new Result + // { + // Title = Localize.executeQuery(item.Query), + // SubTitle = Localize.lastExecuteTime(item.ExecutedDateTime), + // IcoPath = icoPath, + // OriginQuery = new Query { RawQuery = item.Query }, + // Action = _ => + // { + // App.API.BackToQueryResults(); + // App.API.ChangeQuery(item.Query); + // return false; + // }, + // Glyph = glyph + // }; + //} + //else + //{ + // result = new Result + // { + // Title = string.IsNullOrEmpty(item.Title) ? // Old migrated history items have no title + // Localize.executeQuery(item.Query) : + // item.Title, + // SubTitle = Localize.lastExecuteTime(item.ExecutedDateTime), + // IcoPath = icoPath, + // OriginQuery = new Query { RawQuery = item.Query }, + // AsyncAction = async c => + // { + // var reflectResult = await ResultHelper.PopulateResultsAsync(item); + // if (reflectResult != null) + // { + // // Record the user selected record for result ranking + // _userSelectedRecord.Add(reflectResult); + + // // Since some actions may need to hide the Flow window to execute + // // So let us populate the results of them + // return await reflectResult.ExecuteAsync(c); + // } + + // // If we cannot get the result, fallback to re-query + // App.API.BackToQueryResults(); + // App.API.ChangeQuery(item.Query); + // return false; + // }, + // Glyph = glyph + // }; + //} results.Add(result); } - + return results; } + /// + /// TODO COMMENT- Requires the plugins to have initialized first because + /// it needs the plugin directory paths for initialization + /// + public void InitializeQueryHistoryItems() + { + // ensure single-run even if called from multiple threads + if (Interlocked.Exchange(ref QueryHistoryItemsInitialized, 1) == 1) + return; + + QueryHistoryItems = _historyItemsStorage.Load(); + QueryHistoryItems.PopulateHistoryFromLegacyHistory(); + QueryHistoryItems.UpdateIcoAbsoluteFullPath(); + } + private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true) { _updateSource?.Cancel(); @@ -1620,7 +1697,7 @@ namespace Flow.Launcher.ViewModel void QueryHistoryTask(CancellationToken token) { // Select last history results and revert its order to make sure last history results are on top - var historyItems = _history.LastOpenedHistoryItems.TakeLast(Settings.MaxHistoryResultsToShowForHomePage).Reverse(); + var historyItems = QueryHistoryItems.LastOpenedHistoryItems.TakeLast(Settings.MaxHistoryResultsToShowForHomePage).Reverse(); var results = GetHistoryItems(historyItems); From 6b6a9a9935aee30ad7c2d34b8dde0ed11903bf09 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 1 Jan 2026 14:30:28 +1100 Subject: [PATCH 10/77] simplify GetHistoryItems --- Flow.Launcher.Plugin/Result.cs | 27 +--- .../Storage/LastOpenedHistoryResult.cs | 86 +++++-------- Flow.Launcher/Storage/QueryHistory.cs | 9 +- Flow.Launcher/ViewModel/MainViewModel.cs | 116 +++--------------- 4 files changed, 57 insertions(+), 181 deletions(-) diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index 2ad19effe..f1dd53b22 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Security.Policy; using System.Threading.Tasks; using System.Windows.Controls; using System.Windows.Media; @@ -10,7 +9,8 @@ using System.Text.Json.Serialization; namespace Flow.Launcher.Plugin { /// - /// Describes a result of a executed by a plugin + /// Describes a result of a executed by a plugin. + /// This or its child classses is serializable. /// public class Result { @@ -104,29 +104,6 @@ namespace Flow.Launcher.Plugin /// public string IcoAbsoluteFullPath => _icoAbsoluteFullPath; - /// - /// Returns IcoPath's relative path based on the plugin directory or original value if not file path. - /// This property is useful for storage where it needs to be resistant to changes in plugin location from update - /// or portable mode change. - /// - public string IcoPathRelative - { - get - { - if (string.IsNullOrEmpty(IcoAbsoluteFullPath) - || string.IsNullOrEmpty(PluginDirectory) - || !Path.IsPathRooted(IcoAbsoluteFullPath) - || IcoAbsoluteFullPath.StartsWith("http://", StringComparison.OrdinalIgnoreCase) - || IcoAbsoluteFullPath.StartsWith("https://", StringComparison.OrdinalIgnoreCase) - || IcoAbsoluteFullPath.StartsWith("data:image", StringComparison.OrdinalIgnoreCase)) - { - return IcoAbsoluteFullPath; - } - - return Path.GetRelativePath(PluginDirectory, IcoAbsoluteFullPath); - } - } - /// /// The image to be displayed for the badge of the result. /// diff --git a/Flow.Launcher/Storage/LastOpenedHistoryResult.cs b/Flow.Launcher/Storage/LastOpenedHistoryResult.cs index 98fdc1a74..4eb76da1c 100644 --- a/Flow.Launcher/Storage/LastOpenedHistoryResult.cs +++ b/Flow.Launcher/Storage/LastOpenedHistoryResult.cs @@ -13,6 +13,7 @@ public class LastOpenedHistoryResult : Result public LastOpenedHistoryResult() { + this.OriginQuery = new Query { RawQuery = Query }; } public LastOpenedHistoryResult(Result result) @@ -21,71 +22,42 @@ public class LastOpenedHistoryResult : Result SubTitle = result.SubTitle; PluginID = result.PluginID; Query = result.OriginQuery.RawQuery; + OriginQuery = result.OriginQuery; RecordKey = result.RecordKey; IcoPath = result.IcoPath; PluginDirectory = result.PluginDirectory; Glyph = result.Glyph; ExecutedDateTime = DateTime.Now; + // Used for Query History style reopening + Action = _ => + { + App.API.BackToQueryResults(); + App.API.ChangeQuery(result.OriginQuery.RawQuery); + return false; + }; + //Used for last history style reopening, currently need to be assigned at MainViewModel.cs + AsyncAction = null; } - //public Result ToResult(bool isQueryHistoryStyle) - //{ - // Result result = null; + public LastOpenedHistoryResult Copy() + { - // if (isQueryHistoryStyle) - // { - // result = new Result - // { - // Action = _ => - // { - // App.API.BackToQueryResults(); - // App.API.ChangeQuery(Query); - // return false; - // }, - // Glyph = Glyph, - // }; - // } - // else - // { - // result = new Result - // { - // AsyncAction = async c => - // { - // var reflectResult = await ResultHelper.PopulateResultsAsync(item); - // if (reflectResult != null) - // { - // // Record the user selected record for result ranking - // _userSelectedRecord.Add(reflectResult); - - // // Since some actions may need to hide the Flow window to execute - // // So let us populate the results of them - // return await reflectResult.ExecuteAsync(c); - // } - - // // If we cannot get the result, fallback to re-query - // App.API.BackToQueryResults(); - // App.API.ChangeQuery(item.Query); - // return false; - // }, - // Glyph = Glyph, - // }; - // } - - // var result = new Result - // { - // Title = Title, - // SubTitle = Localize.lastExecuteTime(ExecutedDateTime), - // IcoPath = IcoPath, - // OriginQuery = new Query { RawQuery = Query }, - // Action = _ => - // { - // App.API.BackToQueryResults(); - // App.API.ChangeQuery(Query); - // return false; - // }, - // Glyph = Glyph, - // }; - //} + return new LastOpenedHistoryResult + { + Title = this.Title, + SubTitle = this.SubTitle, + PluginID = this.PluginID, + Query = this.Query, + OriginQuery = this.OriginQuery, + RecordKey = this.RecordKey, + IcoPath = this.IcoPath, + PluginDirectory = this.PluginDirectory, + Action = this.Action, + AsyncAction = this.AsyncAction, + Glyph = this.Glyph, + ExecutedDateTime = this.ExecutedDateTime + }; + } public bool Equals(Result r) { diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs index c95a9308b..cbde3e9aa 100644 --- a/Flow.Launcher/Storage/QueryHistory.cs +++ b/Flow.Launcher/Storage/QueryHistory.cs @@ -1,13 +1,10 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Text.Json.Serialization; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin; -using Windows.Devices.Geolocation; -using YamlDotNet.Core.Tokens; namespace Flow.Launcher.Storage { @@ -36,6 +33,12 @@ namespace Flow.Launcher.Storage OriginQuery = new Query { RawQuery = item.Query }, Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C"), Query = item.Query, + Action = _ => + { + App.API.BackToQueryResults(); + App.API.ChangeQuery(item.Query); + return false; + }, ExecutedDateTime = item.ExecutedDateTime }); } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 2ff571f19..9a32b0d03 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1349,107 +1349,31 @@ namespace Flow.Launcher.ViewModel foreach (var item in historyItems) { - Result result = null; - //var glyph = item.Glyph is null && !string.IsNullOrEmpty(item.IcoPath) // Some plugins won't have Glyph, then prefer IcoPath - // ? null - // : item.Glyph is not null - // ? item.Glyph - // : new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C"); // Default fallback - - //var icoPath = !string.IsNullOrEmpty(item.IcoPath) ? item.IcoPath : Constant.HistoryIcon; - - - - - - - result = new Result + var copiedItem = item.Copy(); + // Subtitle has datetime which can cause duplicates when saving. + copiedItem.SubTitle = Localize.lastExecuteTime(item.ExecutedDateTime); + // Empty PluginID so the source of last opened history results won't be updated, these results are meant to be temporary copy. + copiedItem.PluginID = string.Empty; + copiedItem.AsyncAction = async c => { - Title = Settings.HistoryStyle == HistoryStyle.Query - ? Localize.executeQuery(item.Query) - : item.Title, - SubTitle = Localize.lastExecuteTime(item.ExecutedDateTime), - IcoPath = item.IcoAbsoluteFullPath, - OriginQuery = new Query { RawQuery = item.Query }, - Action = _ => + var reflectResult = await ResultHelper.PopulateResultsAsync(item); + if (reflectResult != null) { - App.API.BackToQueryResults(); - App.API.ChangeQuery(item.Query); - return false; - }, - AsyncAction = async c => - { - var reflectResult = await ResultHelper.PopulateResultsAsync(item); - if (reflectResult != null) - { - // Record the user selected record for result ranking - _userSelectedRecord.Add(reflectResult); + // Record the user selected record for result ranking + _userSelectedRecord.Add(reflectResult); - // Since some actions may need to hide the Flow window to execute - // So let us populate the results of them - return await reflectResult.ExecuteAsync(c); - } + // Since some actions may need to hide the Flow window to execute + // So let us populate the results of them + return await reflectResult.ExecuteAsync(c); + } - // If we cannot get the result, fallback to re-query - App.API.BackToQueryResults(); - App.API.ChangeQuery(item.Query); - return false; - }, - Glyph = item.Glyph - }; + // If we cannot get the result, fallback to re-query + App.API.BackToQueryResults(); + App.API.ChangeQuery(item.Query); + return false; + }; - - - //if (Settings.HistoryStyle == HistoryStyle.Query) - //{ - // result = new Result - // { - // Title = Localize.executeQuery(item.Query), - // SubTitle = Localize.lastExecuteTime(item.ExecutedDateTime), - // IcoPath = icoPath, - // OriginQuery = new Query { RawQuery = item.Query }, - // Action = _ => - // { - // App.API.BackToQueryResults(); - // App.API.ChangeQuery(item.Query); - // return false; - // }, - // Glyph = glyph - // }; - //} - //else - //{ - // result = new Result - // { - // Title = string.IsNullOrEmpty(item.Title) ? // Old migrated history items have no title - // Localize.executeQuery(item.Query) : - // item.Title, - // SubTitle = Localize.lastExecuteTime(item.ExecutedDateTime), - // IcoPath = icoPath, - // OriginQuery = new Query { RawQuery = item.Query }, - // AsyncAction = async c => - // { - // var reflectResult = await ResultHelper.PopulateResultsAsync(item); - // if (reflectResult != null) - // { - // // Record the user selected record for result ranking - // _userSelectedRecord.Add(reflectResult); - - // // Since some actions may need to hide the Flow window to execute - // // So let us populate the results of them - // return await reflectResult.ExecuteAsync(c); - // } - - // // If we cannot get the result, fallback to re-query - // App.API.BackToQueryResults(); - // App.API.ChangeQuery(item.Query); - // return false; - // }, - // Glyph = glyph - // }; - //} - - results.Add(result); + results.Add(copiedItem); } return results; From 8d7fa1d397f2e3f590c294bef345bf99290b2435 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 1 Jan 2026 15:14:05 +1100 Subject: [PATCH 11/77] revert QueryHistoryItems property --- Flow.Launcher/App.xaml.cs | 2 +- Flow.Launcher/ViewModel/MainViewModel.cs | 56 +++++++----------------- 2 files changed, 16 insertions(+), 42 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 1f1b3c546..30036cb10 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -259,7 +259,7 @@ namespace Flow.Launcher await PluginManager.InitializePluginsAsync(_mainVM); - _mainVM.InitializeQueryHistoryItems(); + _mainVM.RefreshLastOpenedHistoryResult(); // Refresh home page after plugins are initialized because users may open main window during plugin initialization // And home page is created without full plugin list diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 9a32b0d03..47183489f 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -42,33 +42,11 @@ namespace Flow.Launcher.ViewModel private string _queryTextBeforeLeaveResults; private string _ignoredQueryText; // Used to ignore query text change when switching between context menu and query results - private int QueryHistoryItemsInitialized = 0; - private FlowLauncherJsonStorage _historyItemsStorage; - private History _queryHistoryItems; - private History QueryHistoryItems - { - get - { - if (QueryHistoryItemsInitialized == 0) - { - App.API.LogException(ClassName, - "QueryHistoryItems is not initialized. Call InitializeQueryHistoryItems() before accessing QueryHistoryItems.", - new InvalidOperationException(), - "QueryHistoryItems"); - throw new InvalidOperationException("QueryHistoryItems is not initialized. Call InitializeQueryHistoryItems() before accessing QueryHistoryItems."); - } - - return _queryHistoryItems; - } - set - { - _queryHistoryItems = value; - } - } - + private readonly FlowLauncherJsonStorage _historyItemsStorage; + private readonly History _history; + private int lastHistoryIndex = 1; private readonly FlowLauncherJsonStorage _userSelectedRecordStorage; private readonly FlowLauncherJsonStorageTopMostRecord _topMostRecord; - private int lastHistoryIndex = 1; private readonly UserSelectedRecord _userSelectedRecord; private CancellationTokenSource _updateSource; // Used to cancel old query flows @@ -172,9 +150,10 @@ namespace Flow.Launcher.ViewModel }; _historyItemsStorage = new FlowLauncherJsonStorage(); + _history = _historyItemsStorage.Load(); _userSelectedRecordStorage = new FlowLauncherJsonStorage(); - _topMostRecord = new FlowLauncherJsonStorageTopMostRecord(); _userSelectedRecord = _userSelectedRecordStorage.Load(); + _topMostRecord = new FlowLauncherJsonStorageTopMostRecord(); ContextMenu = new ResultsViewModel(Settings, this) { @@ -373,7 +352,7 @@ namespace Flow.Launcher.ViewModel if (QueryResultsSelected()) { SelectedResults = History; - History.SelectedIndex = QueryHistoryItems.LastOpenedHistoryItems.Count - 1; + History.SelectedIndex = _history.LastOpenedHistoryItems.Count - 1; } else { @@ -401,7 +380,7 @@ namespace Flow.Launcher.ViewModel [RelayCommand] public void ReverseHistory() { - var historyItems = QueryHistoryItems.LastOpenedHistoryItems; + var historyItems = _history.LastOpenedHistoryItems; if (historyItems.Count > 0) { ChangeQueryText(historyItems[^lastHistoryIndex].Query); @@ -415,7 +394,7 @@ namespace Flow.Launcher.ViewModel [RelayCommand] public void ForwardHistory() { - var historyItems = QueryHistoryItems.LastOpenedHistoryItems; + var historyItems = _history.LastOpenedHistoryItems; if (historyItems.Count > 0) { ChangeQueryText(historyItems[^lastHistoryIndex].Query); @@ -557,7 +536,7 @@ namespace Flow.Launcher.ViewModel // Add item to history only if it is from results but not context menu or history if (queryResultsSelected) { - QueryHistoryItems.Add(result); + _history.Add(result); lastHistoryIndex = 1; } } @@ -633,7 +612,7 @@ namespace Flow.Launcher.ViewModel [RelayCommand] private void SelectPrevItem() { - var historyItems = QueryHistoryItems.LastOpenedHistoryItems; + var historyItems = _history.LastOpenedHistoryItems; if (QueryResultsSelected() // Results selected && string.IsNullOrEmpty(QueryText) // No input && Results.Visibility != Visibility.Visible // No items in result list, e.g. when home page is off and no query text is entered, therefore the view is collapsed. @@ -1319,7 +1298,7 @@ namespace Flow.Launcher.ViewModel var query = QueryText.ToLower().Trim(); History.Clear(); - var results = GetHistoryItems(QueryHistoryItems.LastOpenedHistoryItems); + var results = GetHistoryItems(_history.LastOpenedHistoryItems); if (!string.IsNullOrEmpty(query)) { @@ -1383,15 +1362,10 @@ namespace Flow.Launcher.ViewModel /// TODO COMMENT- Requires the plugins to have initialized first because /// it needs the plugin directory paths for initialization /// - public void InitializeQueryHistoryItems() + internal void RefreshLastOpenedHistoryResult() { - // ensure single-run even if called from multiple threads - if (Interlocked.Exchange(ref QueryHistoryItemsInitialized, 1) == 1) - return; - - QueryHistoryItems = _historyItemsStorage.Load(); - QueryHistoryItems.PopulateHistoryFromLegacyHistory(); - QueryHistoryItems.UpdateIcoAbsoluteFullPath(); + _history.PopulateHistoryFromLegacyHistory(); + _history.UpdateIcoAbsoluteFullPath(); } private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true) @@ -1621,7 +1595,7 @@ namespace Flow.Launcher.ViewModel void QueryHistoryTask(CancellationToken token) { // Select last history results and revert its order to make sure last history results are on top - var historyItems = QueryHistoryItems.LastOpenedHistoryItems.TakeLast(Settings.MaxHistoryResultsToShowForHomePage).Reverse(); + var historyItems = _history.LastOpenedHistoryItems.TakeLast(Settings.MaxHistoryResultsToShowForHomePage).Reverse(); var results = GetHistoryItems(historyItems); From 810725cc6b8930ffd284e6d5a6074fa98fe02a1a Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 1 Jan 2026 15:43:23 +1100 Subject: [PATCH 12/77] add explicit JsonInclude for PluginID internal set --- Flow.Launcher.Plugin/Result.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index f1dd53b22..edfa5bb99 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -227,6 +227,7 @@ namespace Flow.Launcher.Plugin /// /// Plugin ID that generated this result /// + [JsonInclude] public string PluginID { get; internal set; } /// From 20854ba5d72d62db13a6b980c1963f7efb595290 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 1 Jan 2026 18:20:29 +1100 Subject: [PATCH 13/77] formatting --- Flow.Launcher/Storage/QueryHistory.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs index c1ea4b520..25ab521b6 100644 --- a/Flow.Launcher/Storage/QueryHistory.cs +++ b/Flow.Launcher/Storage/QueryHistory.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; @@ -48,6 +48,9 @@ namespace Flow.Launcher.Storage public void Add(Result result) { if (string.IsNullOrEmpty(result.OriginQuery.TrimmedQuery)) return; + // History results triggered from homepage do not contain PluginID, + // these are intentionally not saved otherwise cause duplicates due to subtitle + // containing datetime string. if (string.IsNullOrEmpty(result.PluginID)) return; // Maintain the max history limit @@ -59,7 +62,6 @@ namespace Flow.Launcher.Storage if (LastOpenedHistoryItems.Count > 0 && TryGetLastOpenedHistoryResult(result, out var existingHistoryItem)) { - //existingHistoryItem.IcoPath = result.IcoPath; existingHistoryItem.ExecutedDateTime = DateTime.Now; } else @@ -91,8 +93,6 @@ namespace Flow.Launcher.Storage if (pluginPair == null) continue; - //item.IcoPath = Path.Combine(pluginPair.Metadata.PluginDirectory, item.IcoPath); - item.PluginDirectory = pluginPair.Metadata.PluginDirectory; } } From 7c670de7c2d548ed60675399b07056f9e04fe4d4 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 1 Jan 2026 18:23:12 +1100 Subject: [PATCH 14/77] fix history result highlight and scroll when history mode activated --- Flow.Launcher/ViewModel/MainViewModel.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index e7b4023c2..c3c9d8fc8 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; @@ -355,7 +355,9 @@ namespace Flow.Launcher.ViewModel if (QueryResultsSelected()) { SelectedResults = History; - History.SelectedIndex = _history.LastOpenedHistoryItems.Count - 1; + + SelectedResults.SelectedIndex = 0; + SelectedResults.SelectedItem = SelectedResults.Results[0]; } else { From f9df44a2cdca26ea92a83ea3e08f65075fbead67 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 1 Jan 2026 22:32:39 +1100 Subject: [PATCH 15/77] rename to IcoPathAbsolute --- Flow.Launcher.Plugin/Result.cs | 12 +++++------- .../SettingPages/Views/SettingsPanePluginStore.xaml | 2 +- Flow.Launcher/Storage/QueryHistory.cs | 2 +- Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- Flow.Launcher/ViewModel/ResultViewModel.cs | 6 +++--- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index edfa5bb99..a34608454 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -23,7 +23,7 @@ namespace Flow.Launcher.Plugin private string _icoPath; - private string _icoAbsoluteFullPath; + private string _icoPathAbsolute; private string _copyText = string.Empty; @@ -72,9 +72,7 @@ namespace Flow.Launcher.Plugin /// /// Can be a local file path or a URL. /// - /// GlyphInfo is prioritized if not null. - /// Use IcoPathRelative for storage where it needs to be resistant to plugin location change. - /// + /// GlyphInfo is prioritized if not null public string IcoPath { get => _icoPath; @@ -90,11 +88,11 @@ namespace Flow.Launcher.Plugin && !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase) && !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase)) { - _icoAbsoluteFullPath = Path.Combine(PluginDirectory, value); + _icoPathAbsolute = Path.Combine(PluginDirectory, value); } else { - _icoAbsoluteFullPath = value; + _icoPathAbsolute = value; } } } @@ -102,7 +100,7 @@ namespace Flow.Launcher.Plugin /// /// TODO COMMENT /// - public string IcoAbsoluteFullPath => _icoAbsoluteFullPath; + public string IcoPathAbsolute => _icoPathAbsolute; /// /// The image to be displayed for the badge of the result. diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml index 5d8837891..4d37dc93a 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml @@ -333,7 +333,7 @@ Margin="18 24 0 0" HorizontalAlignment="Left" RenderOptions.BitmapScalingMode="Fant" - Source="{Binding IcoAbsoluteFullPath, IsAsync=True}" /> + Source="{Binding IcoPathAbsolute, IsAsync=True}" /> - public void UpdateIcoAbsoluteFullPath() + public void UpdateIcoPathAbsolute() { if (LastOpenedHistoryItems.Count == 0) return; diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index c3c9d8fc8..650c0a34c 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1370,7 +1370,7 @@ namespace Flow.Launcher.ViewModel internal void RefreshLastOpenedHistoryResult() { _history.PopulateHistoryFromLegacyHistory(); - _history.UpdateIcoAbsoluteFullPath(); + _history.UpdateIcoPathAbsolute(); } private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true) diff --git a/Flow.Launcher/ViewModel/ResultViewModel.cs b/Flow.Launcher/ViewModel/ResultViewModel.cs index 974b2e83b..077acfce7 100644 --- a/Flow.Launcher/ViewModel/ResultViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultViewModel.cs @@ -141,7 +141,7 @@ namespace Flow.Launcher.ViewModel private bool GlyphAvailable => Glyph is not null; - private bool ImgIconAvailable => !string.IsNullOrEmpty(Result.IcoAbsoluteFullPath) || Result.Icon is not null; + private bool ImgIconAvailable => !string.IsNullOrEmpty(Result.IcoPathAbsolute) || Result.Icon is not null; private bool BadgeIconAvailable => !string.IsNullOrEmpty(Result.BadgeIcoPath) || Result.BadgeIcon is not null; @@ -236,7 +236,7 @@ namespace Flow.Launcher.ViewModel private async Task LoadImageAsync() { - var imagePath = Result.IcoAbsoluteFullPath; + var imagePath = Result.IcoPathAbsolute; var iconDelegate = Result.Icon; if (ImageLoader.TryGetValue(imagePath, false, out var img)) { @@ -266,7 +266,7 @@ namespace Flow.Launcher.ViewModel private async Task LoadPreviewImageAsync() { - var imagePath = Result.Preview.PreviewImagePath ?? Result.IcoAbsoluteFullPath; + var imagePath = Result.Preview.PreviewImagePath ?? Result.IcoPathAbsolute; var iconDelegate = Result.Preview.PreviewDelegate ?? Result.Icon; if (ImageLoader.TryGetValue(imagePath, true, out var img)) { From 6d6003fd8051efb19df7e740a35e9e020739bce2 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 1 Jan 2026 22:45:52 +1100 Subject: [PATCH 16/77] switch to using TrimmedQuery --- Flow.Launcher/App.xaml.cs | 2 +- Flow.Launcher/Storage/LastOpenedHistoryResult.cs | 12 ++++++------ Flow.Launcher/Storage/QueryHistory.cs | 2 +- Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 30036cb10..1d6130c48 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -259,7 +259,7 @@ namespace Flow.Launcher await PluginManager.InitializePluginsAsync(_mainVM); - _mainVM.RefreshLastOpenedHistoryResult(); + _mainVM.RefreshLastOpenedHistoryResults(); // Refresh home page after plugins are initialized because users may open main window during plugin initialization // And home page is created without full plugin list diff --git a/Flow.Launcher/Storage/LastOpenedHistoryResult.cs b/Flow.Launcher/Storage/LastOpenedHistoryResult.cs index 4eb76da1c..7d49df44b 100644 --- a/Flow.Launcher/Storage/LastOpenedHistoryResult.cs +++ b/Flow.Launcher/Storage/LastOpenedHistoryResult.cs @@ -13,7 +13,7 @@ public class LastOpenedHistoryResult : Result public LastOpenedHistoryResult() { - this.OriginQuery = new Query { RawQuery = Query }; + this.OriginQuery = new Query { TrimmedQuery = Query }; } public LastOpenedHistoryResult(Result result) @@ -21,7 +21,7 @@ public class LastOpenedHistoryResult : Result Title = result.Title; SubTitle = result.SubTitle; PluginID = result.PluginID; - Query = result.OriginQuery.RawQuery; + Query = result.OriginQuery.TrimmedQuery; OriginQuery = result.OriginQuery; RecordKey = result.RecordKey; IcoPath = result.IcoPath; @@ -32,10 +32,10 @@ public class LastOpenedHistoryResult : Result Action = _ => { App.API.BackToQueryResults(); - App.API.ChangeQuery(result.OriginQuery.RawQuery); + App.API.ChangeQuery(result.OriginQuery.TrimmedQuery); return false; }; - //Used for last history style reopening, currently need to be assigned at MainViewModel.cs + //Used for Last Opened History style reopening, currently need to be assigned at MainViewModel.cs AsyncAction = null; } @@ -66,13 +66,13 @@ public class LastOpenedHistoryResult : Result return Title == r.Title && SubTitle == r.SubTitle && PluginID == r.PluginID - && Query == r.OriginQuery.RawQuery; + && Query == r.OriginQuery.TrimmedQuery; } else { return RecordKey == r.RecordKey && PluginID == r.PluginID - && Query == r.OriginQuery.RawQuery; + && Query == r.OriginQuery.TrimmedQuery; } } } diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs index 8b8cbdd90..3c5c7000c 100644 --- a/Flow.Launcher/Storage/QueryHistory.cs +++ b/Flow.Launcher/Storage/QueryHistory.cs @@ -30,7 +30,7 @@ namespace Flow.Launcher.Storage { Title = Localize.executeQuery(item.Query), IcoPath = Constant.HistoryIcon, - OriginQuery = new Query { RawQuery = item.Query }, + OriginQuery = new Query { TrimmedQuery = item.Query }, Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C"), Query = item.Query, Action = _ => diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 650c0a34c..844adf4c4 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1367,7 +1367,7 @@ namespace Flow.Launcher.ViewModel /// TODO COMMENT- Requires the plugins to have initialized first because /// it needs the plugin directory paths for initialization /// - internal void RefreshLastOpenedHistoryResult() + internal void RefreshLastOpenedHistoryResults() { _history.PopulateHistoryFromLegacyHistory(); _history.UpdateIcoPathAbsolute(); From 9c2f2398f1bf1d77430b7790de43ef3d189df28a Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 1 Jan 2026 23:11:28 +1100 Subject: [PATCH 17/77] add method comments for LastOpenedHistoryResult --- .../Storage/LastOpenedHistoryResult.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Flow.Launcher/Storage/LastOpenedHistoryResult.cs b/Flow.Launcher/Storage/LastOpenedHistoryResult.cs index 7d49df44b..90d47a4d7 100644 --- a/Flow.Launcher/Storage/LastOpenedHistoryResult.cs +++ b/Flow.Launcher/Storage/LastOpenedHistoryResult.cs @@ -5,17 +5,37 @@ using Flow.Launcher.Plugin; namespace Flow.Launcher.Storage; +/// +/// A serializable result used to record the last opened history for reopening results. +/// Inherits common result fields from and adds the original query and execution time. +/// public class LastOpenedHistoryResult : Result { + /// + /// The query string from Query.TrimmedQuery property, it is stored as a string instead of the entire Query class . + /// This is used so results can be reopened or re-run using the serialized query string. + /// public string Query { get; set; } = string.Empty; + /// + /// The UTC date and time when this result was executed/opened. + /// public DateTime ExecutedDateTime { get; set; } + /// + /// Initializes a new instance of . + /// Sets using the current value, because OriginQuery is not serialized. + /// public LastOpenedHistoryResult() { this.OriginQuery = new Query { TrimmedQuery = Query }; } + /// + /// Creates a from an existing . + /// Copies required fields and sets up default reopening actions. + /// + /// The original result to create history from. public LastOpenedHistoryResult(Result result) { Title = result.Title; @@ -39,6 +59,10 @@ public class LastOpenedHistoryResult : Result AsyncAction = null; } + /// + /// Creates a deep copy of . + /// + /// A new containing the same required data. public LastOpenedHistoryResult Copy() { @@ -59,6 +83,12 @@ public class LastOpenedHistoryResult : Result }; } + /// + /// Determines whether the specified is equivalent to this history result. + /// Comparison uses when available; otherwise falls back to title/subtitle/plugin id and query. + /// + /// The result to compare to. + /// true if the results are considered equal; otherwise false. public bool Equals(Result r) { if (string.IsNullOrEmpty(RecordKey) || string.IsNullOrEmpty(r.RecordKey)) From 024eeaf6bfa40428a67035a8a1f91c8f6501d94e Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 2 Jan 2026 22:30:09 +1100 Subject: [PATCH 18/77] changed LastOpenHistoryResult's Copy to DeepCopy to clarify intent --- .../Storage/LastOpenedHistoryResult.cs | 25 +++++++++++++------ Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/Flow.Launcher/Storage/LastOpenedHistoryResult.cs b/Flow.Launcher/Storage/LastOpenedHistoryResult.cs index 90d47a4d7..4a4257048 100644 --- a/Flow.Launcher/Storage/LastOpenedHistoryResult.cs +++ b/Flow.Launcher/Storage/LastOpenedHistoryResult.cs @@ -60,26 +60,37 @@ public class LastOpenedHistoryResult : Result } /// - /// Creates a deep copy of . + /// Selectively creates a deep copy of the required properties for . + /// This copy should be independent of original and full isolated. /// /// A new containing the same required data. - public LastOpenedHistoryResult Copy() + public LastOpenedHistoryResult DeepCopy() { - + // queryValue and glyphValue are captured to ensure they are correctly referenced in the Action delegate. + var queryValue = Query; + var glyphValue = this.Glyph; return new LastOpenedHistoryResult { Title = this.Title, SubTitle = this.SubTitle, PluginID = this.PluginID, Query = this.Query, - OriginQuery = this.OriginQuery, + OriginQuery = new Query { TrimmedQuery = Query }, RecordKey = this.RecordKey, IcoPath = this.IcoPath, PluginDirectory = this.PluginDirectory, - Action = this.Action, - AsyncAction = this.AsyncAction, - Glyph = this.Glyph, + // Used for Query History style reopening + Action = _ => + { + App.API.BackToQueryResults(); + App.API.ChangeQuery(queryValue); + return false; + }, + //Used for Last Opened History style reopening, currently need to be assigned at MainViewModel.cs + AsyncAction = null, + Glyph = new GlyphInfo(this.Glyph.FontFamily, this.Glyph.Glyph), ExecutedDateTime = this.ExecutedDateTime + // Note: Other properties are left as default — copy if needed. }; } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 844adf4c4..53ee68e01 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1333,7 +1333,7 @@ namespace Flow.Launcher.ViewModel foreach (var item in historyItems) { - var copiedItem = item.Copy(); + var copiedItem = item.DeepCopy(); // Subtitle has datetime which can cause duplicates when saving. copiedItem.SubTitle = Localize.lastExecuteTime(item.ExecutedDateTime); // Empty PluginID so the source of last opened history results won't be updated, these results are meant to be temporary copy. From 2176553b26f4e4f97f95d107295aee8555456888 Mon Sep 17 00:00:00 2001 From: Nilvan Lopes Date: Fri, 2 Jan 2026 14:17:29 -0300 Subject: [PATCH 19/77] Add max suggestions setting for web search plugins Introduced a MaxSuggestions property (min 1, max 10) to control the number of autocomplete suggestions shown in web search plugins. Updated the UI to allow user configuration and added localized labels for the new setting in all supported languages. Suggestions in results are now limited according to this setting. --- .../Languages/de.xaml | 1 + .../Languages/en.xaml | 1 + .../Languages/es.xaml | 1 + .../Languages/fr.xaml | 1 + .../Languages/pt-br.xaml | 1 + Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs | 5 +++-- Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs | 14 ++++++++++++++ .../SettingsControl.xaml | 14 ++++++++++++++ 8 files changed, 36 insertions(+), 2 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/de.xaml b/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/de.xaml index ed9f35454..6a8a3e471 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/de.xaml +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/de.xaml @@ -19,6 +19,7 @@ URL Suche Autovervollständigung von Suchanfragen verwenden + Max. Vorschläge: Autovervollständigung der Daten aus: Bitte wählen Sie eine Websuche aus Sind Sie sicher, dass Sie {0} löschen wollen? diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/en.xaml index 5d65e4462..b3d19d8ae 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/en.xaml @@ -21,6 +21,7 @@ URL Search Use Search Query Autocomplete + Max Suggestions: Autocomplete Data from: Please select a web search Are you sure you want to delete {0}? diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/es.xaml b/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/es.xaml index 41a6325b3..c41ae2413 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/es.xaml +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/es.xaml @@ -19,6 +19,7 @@ URL Busca en Usar autocompletado en consultas de búsqueda + Max sugerencias: Autocompletar datos desde: Por favor, seleccione una búsqueda web ¿Está seguro de que desea eliminar {0}? diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/fr.xaml b/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/fr.xaml index f185a4b50..e9ba6b205 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/fr.xaml +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/fr.xaml @@ -19,6 +19,7 @@ URL Rechercher sur Utiliser la fonction d'auto-complétion des requêtes de recherche + Max suggestions : Saisir automatiquement les données à partir de : Veuillez sélectionner une recherche web Êtes-vous sûr de vouloir supprimer {0} ? diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/pt-br.xaml b/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/pt-br.xaml index 6a6704b95..2d4384e8c 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/pt-br.xaml +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/pt-br.xaml @@ -19,6 +19,7 @@ URL Search Use Search Query Autocomplete + Qtd Sugestões: Autocomplete Data from: Please select a web search Are you sure you want to delete {0}? diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs index b9b8a0b19..299e27499 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -91,6 +91,7 @@ namespace Flow.Launcher.Plugin.WebSearch if (token.IsCancellationRequested) return null; + } return results; @@ -126,7 +127,7 @@ namespace Flow.Launcher.Plugin.WebSearch token.ThrowIfCancellationRequested(); - var resultsFromSuggestion = suggestions?.Select(o => new Result + var resultsFromSuggestion = suggestions?.Take(_settings.MaxSuggestions).Select(o => new Result { Title = o, SubTitle = subtitle, diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs index 0c0ac4b84..d6f7b3012 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs @@ -205,6 +205,20 @@ namespace Flow.Launcher.Plugin.WebSearch } } + private int maxSuggestions = 1; + public int MaxSuggestions + { + get => maxSuggestions; + set + { + if (maxSuggestions != value) + { + maxSuggestions = Math.Max(1, Math.Min(value, 10)); + OnPropertyChanged(nameof(MaxSuggestions)); + } + } + } + [JsonIgnore] public SuggestionSource[] Suggestions { get; set; } = { new Google(), diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml b/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml index 152558e81..d046f101b 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml @@ -154,6 +154,20 @@ + [Obsolete("For backwards comaptibility. Remove after release v2.3.0")] public void PopulateHistoryFromLegacyHistory() { if (Items.Count == 0) return; diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 68ddada00..7c0cadb67 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1386,7 +1386,6 @@ namespace Flow.Launcher.ViewModel /// internal void RefreshLastOpenedHistoryResults() { - // TODO: remove after release v2.3.0 _history.PopulateHistoryFromLegacyHistory(); _history.UpdateIcoPathAbsolute(); From 19fa107feab66d4d3eea15268300fff21d8834c8 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 4 Jan 2026 22:05:39 +1100 Subject: [PATCH 28/77] minor fixes and adjustments for code quality --- Flow.Launcher.Plugin/Result.cs | 2 +- Flow.Launcher/Storage/LastOpenedHistoryResult.cs | 4 +--- Flow.Launcher/Storage/QueryHistory.cs | 2 +- Flow.Launcher/ViewModel/MainViewModel.cs | 7 +++++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index 1894e0eda..78fa31974 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -10,7 +10,7 @@ namespace Flow.Launcher.Plugin { /// /// Describes a result of a executed by a plugin. - /// This or its child classses is serializable. + /// This or its child classes is serializable. /// public class Result { diff --git a/Flow.Launcher/Storage/LastOpenedHistoryResult.cs b/Flow.Launcher/Storage/LastOpenedHistoryResult.cs index 51fba5849..032d24498 100644 --- a/Flow.Launcher/Storage/LastOpenedHistoryResult.cs +++ b/Flow.Launcher/Storage/LastOpenedHistoryResult.cs @@ -18,17 +18,15 @@ public class LastOpenedHistoryResult : Result public string Query { get; set; } = string.Empty; /// - /// The UTC date and time when this result was executed/opened. + /// The local date and time when this result was executed/opened. /// public DateTime ExecutedDateTime { get; set; } /// /// Initializes a new instance of . - /// Sets using the current value, because OriginQuery is not serialized. /// public LastOpenedHistoryResult() { - this.OriginQuery = new Query { TrimmedQuery = Query }; } /// diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs index 015b33399..21e78f8c7 100644 --- a/Flow.Launcher/Storage/QueryHistory.cs +++ b/Flow.Launcher/Storage/QueryHistory.cs @@ -25,7 +25,7 @@ namespace Flow.Launcher.Storage /// format and append them to /// . /// - [Obsolete("For backwards comaptibility. Remove after release v2.3.0")] + [Obsolete("For backwards compatibility. Remove after release v2.3.0")] public void PopulateHistoryFromLegacyHistory() { if (Items.Count == 0) return; diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 7c0cadb67..e91533dfe 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -356,8 +356,11 @@ namespace Flow.Launcher.ViewModel { SelectedResults = History; - SelectedResults.SelectedIndex = 0; - SelectedResults.SelectedItem = SelectedResults.Results[0]; + if (SelectedResults.Results.Count > 0) + { + SelectedResults.SelectedIndex = 0; + SelectedResults.SelectedItem = SelectedResults.Results[0]; + } } else { From 590b4c82d2c0d935a3c2657a307264e864d61775 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 4 Jan 2026 23:00:24 +0800 Subject: [PATCH 29/77] Try NumberBox --- .../Flow.Launcher.Plugin.WebSearch.csproj | 6 ++ .../Settings.cs | 4 +- .../SettingsControl.xaml | 57 ++++++++++--------- 3 files changed, 39 insertions(+), 28 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj b/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj index 42176376b..a743c929f 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj @@ -50,6 +50,12 @@ + + + all + + + diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs index d6f7b3012..d08e14aaf 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs @@ -213,8 +213,8 @@ namespace Flow.Launcher.Plugin.WebSearch { if (maxSuggestions != value) { - maxSuggestions = Math.Max(1, Math.Min(value, 10)); - OnPropertyChanged(nameof(MaxSuggestions)); + maxSuggestions = value; + OnPropertyChanged(); } } } diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml b/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml index d046f101b..963652b13 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml @@ -3,7 +3,9 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:ikw="http://schemas.inkore.net/lib/ui/wpf" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern" xmlns:vm="clr-namespace:Flow.Launcher.Plugin.WebSearch" d:DataContext="{d:DesignInstance vm:SettingsViewModel}" d:DesignHeight="300" @@ -45,17 +47,17 @@ x:Name="SearchSourcesListView" Grid.Row="0" Margin="{StaticResource SettingPanelItemTopBottomMargin}" + AllowDrop="True" BorderBrush="DarkGray" BorderThickness="1" + Drop="ListView_Drop" GridViewColumnHeader.Click="SortByColumn" ItemsSource="{Binding Settings.SearchSources}" MouseDoubleClick="MouseDoubleClickItem" - SelectedItem="{Binding Settings.SelectedSearchSource}" - SizeChanged="ListView_SizeChanged" PreviewMouseLeftButtonDown="ListView_PreviewMouseLeftButtonDown" PreviewMouseMove="ListView_PreviewMouseMove" - AllowDrop="True" - Drop="ListView_Drop" + SelectedItem="{Binding Settings.SelectedSearchSource}" + SizeChanged="ListView_SizeChanged" Style="{StaticResource {x:Static GridView.GridViewStyleKey}}"> @@ -146,45 +148,48 @@ - - - + \ No newline at end of file From 4c46e161609e661d7c2c62df735c03cd3f54ce72 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 4 Jan 2026 23:19:12 +0800 Subject: [PATCH 30/77] Fix possible convert issue --- .../Flow.Launcher.Plugin.WebSearch.csproj | 1 + Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs | 9 ++++++--- .../SettingsControl.xaml | 3 ++- .../SettingsControl.xaml.cs | 10 ++++++++++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj b/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj index a743c929f..edf6fe9d6 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj @@ -51,6 +51,7 @@ + all diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs index d08e14aaf..31540ad92 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs @@ -211,10 +211,13 @@ namespace Flow.Launcher.Plugin.WebSearch get => maxSuggestions; set { - if (maxSuggestions != value) + if (value > 0 && value <= 1000) { - maxSuggestions = value; - OnPropertyChanged(); + if (maxSuggestions != value) + { + maxSuggestions = value; + OnPropertyChanged(); + } } } } diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml b/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml index 963652b13..06afd72f5 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml @@ -167,7 +167,8 @@ SmallChange="10" SpinButtonPlacementMode="Compact" ValidationMode="InvalidInputOverwritten" - Value="{Binding Settings.MaxSuggestions, Mode=TwoWay}" /> + ValueChanged="NumberBox_ValueChanged" + Value="{Binding Settings.MaxSuggestions, Mode=OneWay}" /> Date: Sun, 4 Jan 2026 23:19:41 +0800 Subject: [PATCH 31/77] Test no PrivateAssets --- .../Flow.Launcher.Plugin.WebSearch.csproj | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj b/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj index edf6fe9d6..3b3add106 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj @@ -51,10 +51,7 @@ - - - all - + From 6c1c9fe6008760c4caa60bbc44a3b3f6c819c231 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 4 Jan 2026 23:45:13 +0800 Subject: [PATCH 32/77] Use wrap panel --- .../SettingsControl.xaml | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml b/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml index 06afd72f5..e4f3485cd 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml @@ -148,20 +148,16 @@ - + + - - + \ No newline at end of file From a9e6a68369c5e9298cb9659de10f6148283347a9 Mon Sep 17 00:00:00 2001 From: Jack Ye Date: Sun, 4 Jan 2026 23:47:42 +0800 Subject: [PATCH 33/77] Revert "Upgrade iNKORE.UI.WPF.Modern and refactor scroll logic" --- Flow.Launcher/Flow.Launcher.csproj | 2 +- .../Controls/CustomScrollViewerEx.cs | 253 ++++++++++++++++++ Flow.Launcher/Themes/Base.xaml | 12 +- Flow.Launcher/packages.lock.json | 10 +- 4 files changed, 264 insertions(+), 13 deletions(-) create mode 100644 Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index f3c614702..576bf6f2f 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -138,7 +138,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs b/Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs new file mode 100644 index 000000000..78985108c --- /dev/null +++ b/Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs @@ -0,0 +1,253 @@ +using iNKORE.UI.WPF.Modern.Controls; +using iNKORE.UI.WPF.Modern.Controls.Helpers; +using iNKORE.UI.WPF.Modern.Controls.Primitives; +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +namespace Flow.Launcher.Resources.Controls +{ + // TODO: Use IsScrollAnimationEnabled property in future: https://github.com/iNKORE-NET/UI.WPF.Modern/pull/347 + public class CustomScrollViewerEx : ScrollViewer + { + private double LastVerticalLocation = 0; + private double LastHorizontalLocation = 0; + + public CustomScrollViewerEx() + { + Loaded += OnLoaded; + var valueSource = DependencyPropertyHelper.GetValueSource(this, AutoPanningMode.IsEnabledProperty).BaseValueSource; + if (valueSource == BaseValueSource.Default) + { + AutoPanningMode.SetIsEnabled(this, true); + } + } + + #region Orientation + + public static readonly DependencyProperty OrientationProperty = + DependencyProperty.Register( + nameof(Orientation), + typeof(Orientation), + typeof(CustomScrollViewerEx), + new PropertyMetadata(Orientation.Vertical)); + + public Orientation Orientation + { + get => (Orientation)GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + #endregion + + #region AutoHideScrollBars + + public static readonly DependencyProperty AutoHideScrollBarsProperty = + ScrollViewerHelper.AutoHideScrollBarsProperty + .AddOwner( + typeof(CustomScrollViewerEx), + new PropertyMetadata(true, OnAutoHideScrollBarsChanged)); + + public bool AutoHideScrollBars + { + get => (bool)GetValue(AutoHideScrollBarsProperty); + set => SetValue(AutoHideScrollBarsProperty, value); + } + + private static void OnAutoHideScrollBarsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is CustomScrollViewerEx sv) + { + sv.UpdateVisualState(); + } + } + + #endregion + + private void OnLoaded(object sender, RoutedEventArgs e) + { + LastVerticalLocation = VerticalOffset; + LastHorizontalLocation = HorizontalOffset; + UpdateVisualState(false); + } + + /// + protected override void OnInitialized(EventArgs e) + { + base.OnInitialized(e); + + if (Style == null && ReadLocalValue(StyleProperty) == DependencyProperty.UnsetValue) + { + SetResourceReference(StyleProperty, typeof(ScrollViewer)); + } + } + + /// + protected override void OnMouseWheel(MouseWheelEventArgs e) + { + var Direction = GetDirection(); + ScrollViewerBehavior.SetIsAnimating(this, true); + + if (Direction == Orientation.Vertical) + { + if (ScrollableHeight > 0) + { + e.Handled = true; + } + + var WheelChange = e.Delta * (ViewportHeight / 1.5) / ActualHeight; + var newOffset = LastVerticalLocation - WheelChange; + + if (newOffset < 0) + { + newOffset = 0; + } + + if (newOffset > ScrollableHeight) + { + newOffset = ScrollableHeight; + } + + if (newOffset == LastVerticalLocation) + { + return; + } + + ScrollToVerticalOffset(LastVerticalLocation); + + ScrollToValue(newOffset, Direction); + LastVerticalLocation = newOffset; + } + else + { + if (ScrollableWidth > 0) + { + e.Handled = true; + } + + var WheelChange = e.Delta * (ViewportWidth / 1.5) / ActualWidth; + var newOffset = LastHorizontalLocation - WheelChange; + + if (newOffset < 0) + { + newOffset = 0; + } + + if (newOffset > ScrollableWidth) + { + newOffset = ScrollableWidth; + } + + if (newOffset == LastHorizontalLocation) + { + return; + } + + ScrollToHorizontalOffset(LastHorizontalLocation); + + ScrollToValue(newOffset, Direction); + LastHorizontalLocation = newOffset; + } + } + + /// + protected override void OnScrollChanged(ScrollChangedEventArgs e) + { + base.OnScrollChanged(e); + if (!ScrollViewerBehavior.GetIsAnimating(this)) + { + LastVerticalLocation = VerticalOffset; + LastHorizontalLocation = HorizontalOffset; + } + } + + private Orientation GetDirection() + { + var isShiftDown = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift); + + if (Orientation == Orientation.Horizontal) + { + return isShiftDown ? Orientation.Vertical : Orientation.Horizontal; + } + else + { + return isShiftDown ? Orientation.Horizontal : Orientation.Vertical; + } + } + + /// + /// Causes the to load a new view into the viewport using the specified offsets and zoom factor. + /// + /// A value between 0 and that specifies the distance the content should be scrolled horizontally. + /// A value between 0 and that specifies the distance the content should be scrolled vertically. + /// A value between MinZoomFactor and MaxZoomFactor that specifies the required target ZoomFactor. + /// if the view is changed; otherwise, . + public bool ChangeView(double? horizontalOffset, double? verticalOffset, float? zoomFactor) + { + return ChangeView(horizontalOffset, verticalOffset, zoomFactor, false); + } + + /// + /// Causes the to load a new view into the viewport using the specified offsets and zoom factor, and optionally disables scrolling animation. + /// + /// A value between 0 and that specifies the distance the content should be scrolled horizontally. + /// A value between 0 and that specifies the distance the content should be scrolled vertically. + /// A value between MinZoomFactor and MaxZoomFactor that specifies the required target ZoomFactor. + /// to disable zoom/pan animations while changing the view; otherwise, . The default is false. + /// if the view is changed; otherwise, . + public bool ChangeView(double? horizontalOffset, double? verticalOffset, float? zoomFactor, bool disableAnimation) + { + if (disableAnimation) + { + if (horizontalOffset.HasValue) + { + ScrollToHorizontalOffset(horizontalOffset.Value); + } + + if (verticalOffset.HasValue) + { + ScrollToVerticalOffset(verticalOffset.Value); + } + } + else + { + if (horizontalOffset.HasValue) + { + ScrollToHorizontalOffset(LastHorizontalLocation); + ScrollToValue(Math.Min(ScrollableWidth, horizontalOffset.Value), Orientation.Horizontal); + LastHorizontalLocation = horizontalOffset.Value; + } + + if (verticalOffset.HasValue) + { + ScrollToVerticalOffset(LastVerticalLocation); + ScrollToValue(Math.Min(ScrollableHeight, verticalOffset.Value), Orientation.Vertical); + LastVerticalLocation = verticalOffset.Value; + } + } + + return true; + } + + private void ScrollToValue(double value, Orientation Direction) + { + if (Direction == Orientation.Vertical) + { + ScrollToVerticalOffset(value); + } + else + { + ScrollToHorizontalOffset(value); + } + + ScrollViewerBehavior.SetIsAnimating(this, false); + } + + private void UpdateVisualState(bool useTransitions = true) + { + var stateName = AutoHideScrollBars ? "NoIndicator" : "MouseIndicator"; + VisualStateManager.GoToState(this, stateName, useTransitions); + } + } +} diff --git a/Flow.Launcher/Themes/Base.xaml b/Flow.Launcher/Themes/Base.xaml index c3831e68f..c5b45890b 100644 --- a/Flow.Launcher/Themes/Base.xaml +++ b/Flow.Launcher/Themes/Base.xaml @@ -252,14 +252,12 @@ - - - - + - + diff --git a/Flow.Launcher/packages.lock.json b/Flow.Launcher/packages.lock.json index 8c3a16e5e..b4b929d19 100644 --- a/Flow.Launcher/packages.lock.json +++ b/Flow.Launcher/packages.lock.json @@ -28,9 +28,9 @@ }, "iNKORE.UI.WPF.Modern": { "type": "Direct", - "requested": "[0.10.2.1, )", - "resolved": "0.10.2.1", - "contentHash": "nGwuuVul+TcLCTgPmaAZCc0fYFqUpCNZ8PiulVT3gZnsWt/AvxMZ0DSPpuyI/iRPc/NhFIg9lSIR7uaHWV0I/Q==", + "requested": "[0.10.1, )", + "resolved": "0.10.1", + "contentHash": "nRYmBosiL+42eUpLbHeqP7qJqtp5EpzuIMZTpvq4mFV33VB/JjkFg1y82gk50pjkXlAQWDvRyrfSAmPR5AM+3g==", "dependencies": { "iNKORE.UI.WPF": "1.2.8" } @@ -1619,7 +1619,7 @@ "FSharp.Core": "[9.0.303, )", "Flow.Launcher.Infrastructure": "[1.0.0, )", "Flow.Launcher.Localization": "[0.0.6, )", - "Flow.Launcher.Plugin": "[5.1.0, )", + "Flow.Launcher.Plugin": "[5.0.0, )", "Meziantou.Framework.Win32.Jobs": "[3.4.5, )", "Microsoft.IO.RecyclableMemoryStream": "[3.0.1, )", "SemanticVersioning": "[3.0.0, )", @@ -1634,7 +1634,7 @@ "BitFaster.Caching": "[2.5.4, )", "CommunityToolkit.Mvvm": "[8.4.0, )", "Flow.Launcher.Localization": "[0.0.6, )", - "Flow.Launcher.Plugin": "[5.1.0, )", + "Flow.Launcher.Plugin": "[5.0.0, )", "InputSimulator": "[1.0.4, )", "MemoryPack": "[1.21.4, )", "Microsoft.VisualStudio.Threading": "[17.14.15, )", From 216b6f536f6d748e004d14fedacf7762ed0f63d0 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 5 Jan 2026 13:07:02 +0800 Subject: [PATCH 34/77] Code cleanup --- .../Storage/LastOpenedHistoryResult.cs | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/Flow.Launcher/Storage/LastOpenedHistoryResult.cs b/Flow.Launcher/Storage/LastOpenedHistoryResult.cs index 032d24498..482502776 100644 --- a/Flow.Launcher/Storage/LastOpenedHistoryResult.cs +++ b/Flow.Launcher/Storage/LastOpenedHistoryResult.cs @@ -1,6 +1,4 @@ using System; -using System.DirectoryServices.ActiveDirectory; -using Flow.Launcher.Helper; using Flow.Launcher.Plugin; namespace Flow.Launcher.Storage; @@ -53,7 +51,7 @@ public class LastOpenedHistoryResult : Result App.API.ChangeQuery(result.OriginQuery.TrimmedQuery); return false; }; - //Used for Last Opened History style reopening, currently need to be assigned at MainViewModel.cs + // Used for Last Opened History style reopening, currently need to be assigned at MainViewModel.cs AsyncAction = null; } @@ -66,17 +64,17 @@ public class LastOpenedHistoryResult : Result { // queryValue and glyphValue are captured to ensure they are correctly referenced in the Action delegate. var queryValue = Query; - var glyphValue = this.Glyph; + var glyphValue = Glyph; return new LastOpenedHistoryResult { - Title = this.Title, - SubTitle = this.SubTitle, - PluginID = this.PluginID, - Query = this.Query, + Title = Title, + SubTitle = SubTitle, + PluginID = PluginID, + Query = Query, OriginQuery = new Query { TrimmedQuery = Query }, - RecordKey = this.RecordKey, - IcoPath = this.IcoPath, - PluginDirectory = this.PluginDirectory, + RecordKey = RecordKey, + IcoPath = IcoPath, + PluginDirectory = PluginDirectory, // Used for Query History style reopening Action = _ => { @@ -84,12 +82,12 @@ public class LastOpenedHistoryResult : Result App.API.ChangeQuery(queryValue); return false; }, - //Used for Last Opened History style reopening, currently need to be assigned at MainViewModel.cs + // Used for Last Opened History style reopening, currently need to be assigned at MainViewModel.cs AsyncAction = null, Glyph = glyphValue != null - ? new GlyphInfo(this.Glyph.FontFamily, this.Glyph.Glyph) + ? new GlyphInfo(Glyph.FontFamily, Glyph.Glyph) : null, - ExecutedDateTime = this.ExecutedDateTime + ExecutedDateTime = ExecutedDateTime // Note: Other properties are left as default — copy if needed. }; } From 5f992352eec0028f65c54042697af8020591a5cc Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 5 Jan 2026 13:48:58 +0800 Subject: [PATCH 35/77] Code cleanup --- Flow.Launcher/Storage/QueryHistory.cs | 13 ++++++------- Flow.Launcher/ViewModel/MainViewModel.cs | 16 ++++++++++------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs index 21e78f8c7..d612c81d0 100644 --- a/Flow.Launcher/Storage/QueryHistory.cs +++ b/Flow.Launcher/Storage/QueryHistory.cs @@ -70,13 +70,15 @@ namespace Flow.Launcher.Storage LastOpenedHistoryItems.RemoveAt(0); } + // If the last item is the same as the current result, just update the timestamp and the icon path if (LastOpenedHistoryItems.Count > 0 && TryGetLastOpenedHistoryResult(result, out var existingHistoryItem)) { existingHistoryItem.ExecutedDateTime = DateTime.Now; - if (existingHistoryItem.IcoPath != result.IcoPath) + { existingHistoryItem.IcoPath = result.IcoPath; + } } else { @@ -102,17 +104,14 @@ namespace Flow.Launcher.Storage /// Call this after plugins are loaded/initialized. public void UpdateIcoPathAbsolute() { - if (LastOpenedHistoryItems.Count == 0) - return; + if (LastOpenedHistoryItems.Count == 0) return; foreach (var item in LastOpenedHistoryItems) { - if (string.IsNullOrEmpty(item.PluginID)) - continue; + if (string.IsNullOrEmpty(item.PluginID)) continue; var pluginPair = PluginManager.GetPluginForId(item.PluginID); - if (pluginPair == null) - continue; + if (pluginPair == null) continue; item.PluginDirectory = pluginPair.Metadata.PluginDirectory; } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index e91533dfe..c3eeada11 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -355,11 +355,10 @@ namespace Flow.Launcher.ViewModel if (QueryResultsSelected()) { SelectedResults = History; - - if (SelectedResults.Results.Count > 0) + if (History.Results.Count > 0) { - SelectedResults.SelectedIndex = 0; - SelectedResults.SelectedItem = SelectedResults.Results[0]; + History.SelectedIndex = 0; + History.SelectedItem = History.Results[0]; } } else @@ -1331,8 +1330,8 @@ namespace Flow.Launcher.ViewModel { // Items saved to disk are differentiated by Query also, but LastOpened style only cares about unique results historyItems = historyItems - .GroupBy(r => new { r.Title, r.SubTitle, r.PluginID, r.RecordKey }) - .Select(g => g.First()); + .GroupBy(r => new { r.Title, r.SubTitle, r.PluginID, r.RecordKey }) + .Select(g => g.First()); } foreach (var item in historyItems) @@ -1340,11 +1339,16 @@ namespace Flow.Launcher.ViewModel var copiedItem = item.DeepCopy(); if (Settings.HistoryStyle == HistoryStyle.Query) + { copiedItem.Title = Localize.executeQuery(copiedItem.Query); + } + // Subtitle has datetime which can cause duplicates when saving. copiedItem.SubTitle = Localize.lastExecuteTime(copiedItem.ExecutedDateTime); + // Empty PluginID so the source of last opened history results won't be updated, these results are meant to be temporary copy. copiedItem.PluginID = string.Empty; + if (Settings.HistoryStyle == HistoryStyle.LastOpened) { copiedItem.AsyncAction = async c => From 177e60739c7a65536e38387872ea1d00fa63a776 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 5 Jan 2026 14:34:57 +0800 Subject: [PATCH 36/77] Add code comments --- Flow.Launcher/App.xaml.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 1d6130c48..da11380b8 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -259,6 +259,7 @@ namespace Flow.Launcher await PluginManager.InitializePluginsAsync(_mainVM); + // Refresh the history results after plugins are initialized so that we can parse the absolute icon paths _mainVM.RefreshLastOpenedHistoryResults(); // Refresh home page after plugins are initialized because users may open main window during plugin initialization From 11645fdc9a9e22ef39f19a63cf1733bd5835bfb8 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 5 Jan 2026 19:27:11 +1100 Subject: [PATCH 37/77] ensure history results ordered by ExecutedDateTime --- Flow.Launcher/ViewModel/MainViewModel.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index c3eeada11..6be88ced5 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -357,8 +357,8 @@ namespace Flow.Launcher.ViewModel SelectedResults = History; if (History.Results.Count > 0) { - History.SelectedIndex = 0; - History.SelectedItem = History.Results[0]; + SelectedResults.SelectedIndex = 0; + SelectedResults.SelectedItem = History.Results[0]; } } else @@ -1326,6 +1326,8 @@ namespace Flow.Launcher.ViewModel { var results = new List(); + historyItems = historyItems.OrderByDescending(x => x.ExecutedDateTime); + if (Settings.HistoryStyle == HistoryStyle.LastOpened) { // Items saved to disk are differentiated by Query also, but LastOpened style only cares about unique results From 58d910dc572a399a7fd10604f3327c7a92ed8f05 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 5 Jan 2026 19:37:05 +1100 Subject: [PATCH 38/77] fix deep copy Glyph on LastOpenHistoryResult --- Flow.Launcher/Storage/LastOpenedHistoryResult.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/Storage/LastOpenedHistoryResult.cs b/Flow.Launcher/Storage/LastOpenedHistoryResult.cs index 482502776..b71282f14 100644 --- a/Flow.Launcher/Storage/LastOpenedHistoryResult.cs +++ b/Flow.Launcher/Storage/LastOpenedHistoryResult.cs @@ -85,7 +85,7 @@ public class LastOpenedHistoryResult : Result // Used for Last Opened History style reopening, currently need to be assigned at MainViewModel.cs AsyncAction = null, Glyph = glyphValue != null - ? new GlyphInfo(Glyph.FontFamily, Glyph.Glyph) + ? new GlyphInfo(glyphValue.FontFamily, glyphValue.Glyph) : null, ExecutedDateTime = ExecutedDateTime // Note: Other properties are left as default — copy if needed. From f22b6449d4a929950d608fccd07aafc7188ef415 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 5 Jan 2026 19:18:47 +0800 Subject: [PATCH 39/77] Fix issue when querying history items in home page --- Flow.Launcher/ViewModel/MainViewModel.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 6be88ced5..65a382447 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1322,12 +1322,19 @@ namespace Flow.Launcher.ViewModel } } - private List GetHistoryItems(IEnumerable historyItems) + private List GetHistoryItems(IEnumerable historyItems, int? selectLast = null) { var results = new List(); + // Order by executed time descending: Latest -> Oldest historyItems = historyItems.OrderByDescending(x => x.ExecutedDateTime); + // Select the last N items if specified + if (selectLast.HasValue) + { + historyItems = historyItems.Take(selectLast.Value); + } + if (Settings.HistoryStyle == HistoryStyle.LastOpened) { // Items saved to disk are differentiated by Query also, but LastOpened style only cares about unique results @@ -1637,10 +1644,8 @@ namespace Flow.Launcher.ViewModel void QueryHistoryTask(CancellationToken token) { - // Select last history results and revert its order to make sure last history results are on top - var historyItems = _history.LastOpenedHistoryItems.TakeLast(Settings.MaxHistoryResultsToShowForHomePage).Reverse(); - - var results = GetHistoryItems(historyItems); + // Select last history results + var results = GetHistoryItems(_history.LastOpenedHistoryItems, Settings.MaxHistoryResultsToShowForHomePage); if (token.IsCancellationRequested) return; From e6a91a9959d3e07d6d8f315f1ee781b93f6935f1 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 5 Jan 2026 20:17:12 +0800 Subject: [PATCH 40/77] Ensure history items have valid icons in QueryHistory Added CheckIcoPathValidity to QueryHistory to set default icons for history items missing IcoPath, addressing legacy data issues. Integrated this check into MainViewModel's RefreshLastOpenedHistoryResults to guarantee icon validity before updating absolute paths. --- Flow.Launcher/Storage/QueryHistory.cs | 18 ++++++++++++++++++ Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs index d612c81d0..0622248fb 100644 --- a/Flow.Launcher/Storage/QueryHistory.cs +++ b/Flow.Launcher/Storage/QueryHistory.cs @@ -51,6 +51,24 @@ namespace Flow.Launcher.Storage Items.Clear(); } + /// + /// Checks all items in for empty IcoPath. + /// If found, updates it using the history icon. + /// + /// + /// We need this because some prereleased version of Flow did not set the IcoPath when adding. + /// + public void CheckIcoPathValidity() + { + foreach (var item in LastOpenedHistoryItems) + { + if (string.IsNullOrEmpty(item.IcoPath)) + { + item.IcoPath = Constant.HistoryIcon; + } + } + } + /// /// Records a result into the last-opened history list (). /// This will also update the IcoPath if existing history item has one that is different. diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 65a382447..4a1a826ce 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1403,7 +1403,7 @@ namespace Flow.Launcher.ViewModel internal void RefreshLastOpenedHistoryResults() { _history.PopulateHistoryFromLegacyHistory(); - + _history.CheckIcoPathValidity(); _history.UpdateIcoPathAbsolute(); } From cf4268d1c32ba70860d6e5a1a762b8910bd45ac4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 5 Jan 2026 21:30:06 +0800 Subject: [PATCH 41/77] Fix preview panel blank issue --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 4a1a826ce..e9f902f13 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -646,6 +646,8 @@ namespace Flow.Launcher.ViewModel if (!QueryResultsSelected()) { SelectedResults = Results; + PreviewSelectedItem = Results.SelectedItem; + _ = UpdatePreviewAsync(); } else { From 5b2fb1fb3875900e8bb692d604f5319e20b190be Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 5 Jan 2026 22:26:39 +0800 Subject: [PATCH 42/77] Revert "Ensure history items have valid icons in QueryHistory" This reverts commit e6a91a9959d3e07d6d8f315f1ee781b93f6935f1. --- Flow.Launcher/Storage/QueryHistory.cs | 18 ------------------ Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs index 0622248fb..d612c81d0 100644 --- a/Flow.Launcher/Storage/QueryHistory.cs +++ b/Flow.Launcher/Storage/QueryHistory.cs @@ -51,24 +51,6 @@ namespace Flow.Launcher.Storage Items.Clear(); } - /// - /// Checks all items in for empty IcoPath. - /// If found, updates it using the history icon. - /// - /// - /// We need this because some prereleased version of Flow did not set the IcoPath when adding. - /// - public void CheckIcoPathValidity() - { - foreach (var item in LastOpenedHistoryItems) - { - if (string.IsNullOrEmpty(item.IcoPath)) - { - item.IcoPath = Constant.HistoryIcon; - } - } - } - /// /// Records a result into the last-opened history list (). /// This will also update the IcoPath if existing history item has one that is different. diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index e9f902f13..7de29232e 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1405,7 +1405,7 @@ namespace Flow.Launcher.ViewModel internal void RefreshLastOpenedHistoryResults() { _history.PopulateHistoryFromLegacyHistory(); - _history.CheckIcoPathValidity(); + _history.UpdateIcoPathAbsolute(); } From e403c41530a94267fd4a90037a6a473333290170 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 5 Jan 2026 22:55:09 +0800 Subject: [PATCH 43/77] Change copied icon logic as origin --- Flow.Launcher/Storage/QueryHistory.cs | 4 ++-- Flow.Launcher/ViewModel/MainViewModel.cs | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs index d612c81d0..6af2a5908 100644 --- a/Flow.Launcher/Storage/QueryHistory.cs +++ b/Flow.Launcher/Storage/QueryHistory.cs @@ -35,9 +35,9 @@ namespace Flow.Launcher.Storage LastOpenedHistoryItems.Add(new LastOpenedHistoryResult { Title = Localize.executeQuery(item.Query), - IcoPath = Constant.HistoryIcon, + IcoPath = null, OriginQuery = new Query { TrimmedQuery = item.Query }, - Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C"), + Glyph = null, Query = item.Query, Action = _ => { diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 7de29232e..7e5869b65 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1352,6 +1352,18 @@ namespace Flow.Launcher.ViewModel if (Settings.HistoryStyle == HistoryStyle.Query) { copiedItem.Title = Localize.executeQuery(copiedItem.Query); + copiedItem.IcoPath = Constant.HistoryIcon; + // TODO: Add Glyph here + // copiedItem.Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C"); + } + else + { + if (string.IsNullOrEmpty(copiedItem.IcoPath)) // Must manually set missing image icon here + { + copiedItem.IcoPath = Constant.MissingImgIcon; + } + copiedItem.ShowBadge = true; + copiedItem.BadgeIcoPath = Constant.HistoryIcon; } // Subtitle has datetime which can cause duplicates when saving. From 77b631be87f188feba00833b8c05ebe0c94d89d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 22:08:44 +0000 Subject: [PATCH 44/77] Bump Svg.Skia from 3.2.1 to 3.4.0 --- updated-dependencies: - dependency-name: Svg.Skia dependency-version: 3.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .../Flow.Launcher.Plugin.BrowserBookmark.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj index ed121375b..1df65c2eb 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj @@ -106,7 +106,7 @@ - + From c610100c13fb50f74522d5d7c6a874922ed154e1 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 Jan 2026 15:07:00 +0800 Subject: [PATCH 45/77] Use QueryManager function --- Flow.Launcher/Helper/ResultHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/Helper/ResultHelper.cs b/Flow.Launcher/Helper/ResultHelper.cs index 82211242b..017651fdf 100644 --- a/Flow.Launcher/Helper/ResultHelper.cs +++ b/Flow.Launcher/Helper/ResultHelper.cs @@ -24,7 +24,7 @@ public static class ResultHelper if (query == null) return null; try { - var freshResults = await plugin.Plugin.QueryAsync(query, CancellationToken.None); + var freshResults = await PluginManager.QueryForPluginAsync(plugin, query, CancellationToken.None); // Try to match by record key first if it is valid, otherwise fall back to title + subtitle match if (string.IsNullOrEmpty(recordKey)) { From e404d02965abeea1cccf522577a841d56fd54d52 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 Jan 2026 15:13:37 +0800 Subject: [PATCH 46/77] Update the result when executing history item --- Flow.Launcher/Storage/QueryHistory.cs | 22 ++++++++++++++++++++++ Flow.Launcher/ViewModel/MainViewModel.cs | 3 +++ 2 files changed, 25 insertions(+) diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs index 6af2a5908..20e295f02 100644 --- a/Flow.Launcher/Storage/QueryHistory.cs +++ b/Flow.Launcher/Storage/QueryHistory.cs @@ -86,6 +86,28 @@ namespace Flow.Launcher.Storage } } + /// + /// Update a result into the last-opened history list (). + /// + public void Update(Result result) + { + if (string.IsNullOrEmpty(result.OriginQuery.TrimmedQuery)) return; + // History results triggered from homepage do not contain PluginID, + // these are intentionally not saved otherwise cause duplicates due to subtitle + // containing datetime string. + if (string.IsNullOrEmpty(result.PluginID)) return; + + // If the last item is the same as the current result, just update the icon path + if (LastOpenedHistoryItems.Count > 0 && + TryGetLastOpenedHistoryResult(result, out var existingHistoryItem)) + { + if (existingHistoryItem.IcoPath != result.IcoPath) + { + existingHistoryItem.IcoPath = result.IcoPath; + } + } + } + /// /// Attempts to find an existing in /// that is considered equal to the supplied . diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 7e5869b65..1aee20e5b 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1383,6 +1383,9 @@ namespace Flow.Launcher.ViewModel // Record the user selected record for result ranking _userSelectedRecord.Add(reflectResult); + // Update the history with the reflected result + _history.Update(reflectResult); + // Since some actions may need to hide the Flow window to execute // So let us populate the results of them return await reflectResult.ExecuteAsync(c); From 26e4529f27b493b878401a517c95c1e557877e81 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 Jan 2026 15:45:49 +0800 Subject: [PATCH 47/77] Update the result when executing history item --- Flow.Launcher/Storage/QueryHistory.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs index 20e295f02..38bfe0c49 100644 --- a/Flow.Launcher/Storage/QueryHistory.cs +++ b/Flow.Launcher/Storage/QueryHistory.cs @@ -101,6 +101,7 @@ namespace Flow.Launcher.Storage if (LastOpenedHistoryItems.Count > 0 && TryGetLastOpenedHistoryResult(result, out var existingHistoryItem)) { + existingHistoryItem.ExecutedDateTime = DateTime.Now; if (existingHistoryItem.IcoPath != result.IcoPath) { existingHistoryItem.IcoPath = result.IcoPath; From 583bf74724fe9a94ac634f9564495e5090ab5b20 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 Jan 2026 15:46:08 +0800 Subject: [PATCH 48/77] Update code comments --- Flow.Launcher/Storage/QueryHistory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs index 38bfe0c49..803591662 100644 --- a/Flow.Launcher/Storage/QueryHistory.cs +++ b/Flow.Launcher/Storage/QueryHistory.cs @@ -97,7 +97,7 @@ namespace Flow.Launcher.Storage // containing datetime string. if (string.IsNullOrEmpty(result.PluginID)) return; - // If the last item is the same as the current result, just update the icon path + // If the last item is the same as the current result, just update the timestamp and the icon path if (LastOpenedHistoryItems.Count > 0 && TryGetLastOpenedHistoryResult(result, out var existingHistoryItem)) { From 938a87ceca9973ea8dceeb95edc39ac434c4c3eb Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 Jan 2026 18:31:15 +0800 Subject: [PATCH 49/77] Fix preview panel blank issue --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 1aee20e5b..876026e55 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -443,6 +443,8 @@ namespace Flow.Launcher.ViewModel else { SelectedResults = Results; + PreviewSelectedItem = Results.SelectedItem; + _ = UpdatePreviewAsync(); } } From 9d9ab1ffd2030998a09c944ab71ad1156b175cf3 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 Jan 2026 18:35:53 +0800 Subject: [PATCH 50/77] Do not allow context menu for history items --- Flow.Launcher/ViewModel/MainViewModel.cs | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 876026e55..3d5c99edc 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -435,7 +435,8 @@ namespace Flow.Launcher.ViewModel { // When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing // i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing - if (SelectedResults.SelectedItem != null) + if (SelectedResults.SelectedItem?.Result != null && + !string.IsNullOrEmpty(SelectedResults.SelectedItem.Result.PluginID)) // Do not show context menu for history items { SelectedResults = ContextMenu; } @@ -1260,22 +1261,12 @@ namespace Flow.Launcher.ViewModel var selected = Results.SelectedItem?.Result; - if (selected != null) // SelectedItem returns null if selection is empty. + if (selected != null && // SelectedItem returns null if selection is empty. + !string.IsNullOrEmpty(selected.PluginID)) // SelectedItem must have a valid PluginID { - List results; - if (selected.PluginID == null) // SelectedItem from history in home page. - { - results = new() - { - ContextMenuTopMost(selected) - }; - } - else - { - results = PluginManager.GetContextMenusForPlugin(selected); - results.Add(ContextMenuTopMost(selected)); - results.Add(ContextMenuPluginInfo(selected)); - } + List results = PluginManager.GetContextMenusForPlugin(selected); + results.Add(ContextMenuTopMost(selected)); + results.Add(ContextMenuPluginInfo(selected)); if (!string.IsNullOrEmpty(query)) { From 3745c444556c2edd818d11fc0a779b1e5215612f Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 Jan 2026 20:11:36 +0800 Subject: [PATCH 51/77] Revert `Update the result when executing history item` --- Flow.Launcher/Storage/QueryHistory.cs | 23 ----------------------- Flow.Launcher/ViewModel/MainViewModel.cs | 6 ------ 2 files changed, 29 deletions(-) diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs index 803591662..6af2a5908 100644 --- a/Flow.Launcher/Storage/QueryHistory.cs +++ b/Flow.Launcher/Storage/QueryHistory.cs @@ -86,29 +86,6 @@ namespace Flow.Launcher.Storage } } - /// - /// Update a result into the last-opened history list (). - /// - public void Update(Result result) - { - if (string.IsNullOrEmpty(result.OriginQuery.TrimmedQuery)) return; - // History results triggered from homepage do not contain PluginID, - // these are intentionally not saved otherwise cause duplicates due to subtitle - // containing datetime string. - if (string.IsNullOrEmpty(result.PluginID)) return; - - // If the last item is the same as the current result, just update the timestamp and the icon path - if (LastOpenedHistoryItems.Count > 0 && - TryGetLastOpenedHistoryResult(result, out var existingHistoryItem)) - { - existingHistoryItem.ExecutedDateTime = DateTime.Now; - if (existingHistoryItem.IcoPath != result.IcoPath) - { - existingHistoryItem.IcoPath = result.IcoPath; - } - } - } - /// /// Attempts to find an existing in /// that is considered equal to the supplied . diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 3d5c99edc..63710b5fc 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1373,12 +1373,6 @@ namespace Flow.Launcher.ViewModel var reflectResult = await ResultHelper.PopulateResultsAsync(item); if (reflectResult != null) { - // Record the user selected record for result ranking - _userSelectedRecord.Add(reflectResult); - - // Update the history with the reflected result - _history.Update(reflectResult); - // Since some actions may need to hide the Flow window to execute // So let us populate the results of them return await reflectResult.ExecuteAsync(c); From 382a77d2c52ac6511a93b45ad0f0c1c608663d65 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 7 Jan 2026 13:40:13 +0800 Subject: [PATCH 52/77] Fix ShellPath directory for shell path selection Updated OpenShellPath to use Settings.ShellPath directory when prompting for a file path, ensuring the dialog opens in the relevant location instead of the editor path directory. --- .../ViewModels/SettingsViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs index 956c84db2..30c2c7f14 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs @@ -532,7 +532,7 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels [RelayCommand] private void OpenShellPath() { - var path = PromptUserSelectPath(ResultType.File, Settings.EditorPath != null ? Path.GetDirectoryName(Settings.EditorPath) : null); + var path = PromptUserSelectPath(ResultType.File, Settings.ShellPath != null ? Path.GetDirectoryName(Settings.ShellPath) : null); if (path is null) return; From 34257cad6077e0203bea23dee8a7ef225ca61518 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 10 Jan 2026 06:27:26 +0000 Subject: [PATCH 53/77] Initial plan From 649b142b46b3c8f49b3c92f2a2b71a993e7e1f0b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 10 Jan 2026 06:30:57 +0000 Subject: [PATCH 54/77] Add UseThousandsSeparator setting to Calculator plugin Co-authored-by: Jack251970 <53996452+Jack251970@users.noreply.github.com> --- Flow.Launcher.Test/Plugins/CalculatorTest.cs | 35 ++++++++++++++++++- .../Languages/en.xaml | 1 + .../Flow.Launcher.Plugin.Calculator/Main.cs | 2 +- .../Settings.cs | 2 ++ .../Views/CalculatorSettings.xaml | 11 ++++++ 5 files changed, 49 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Test/Plugins/CalculatorTest.cs b/Flow.Launcher.Test/Plugins/CalculatorTest.cs index b075813db..8178cb712 100644 --- a/Flow.Launcher.Test/Plugins/CalculatorTest.cs +++ b/Flow.Launcher.Test/Plugins/CalculatorTest.cs @@ -16,7 +16,8 @@ namespace Flow.Launcher.Test.Plugins { DecimalSeparator = DecimalSeparator.UseSystemLocale, MaxDecimalPlaces = 10, - ShowErrorMessage = false // Make sure we return the empty results when error occurs + ShowErrorMessage = false, // Make sure we return the empty results when error occurs + UseThousandsSeparator = true // Default value }; private readonly Engine _engine = new(new Configuration { @@ -41,6 +42,38 @@ namespace Flow.Launcher.Test.Plugins engineField.SetValue(null, _engine); } + [Test] + public void ThousandsSeparatorTest_Enabled() + { + _settings.UseThousandsSeparator = true; + _settings.DecimalSeparator = DecimalSeparator.Dot; + + var result = GetCalculationResult("1000+234"); + // When thousands separator is enabled, the result should contain a separator (comma in this case) + ClassicAssert.IsTrue(result.Contains(",") || result == "1234", + "Expected result to contain thousands separator or be without one if system doesn't use it"); + } + + [Test] + public void ThousandsSeparatorTest_Disabled() + { + _settings.UseThousandsSeparator = false; + _settings.DecimalSeparator = DecimalSeparator.Dot; + + var result = GetCalculationResult("1000+234"); + ClassicAssert.AreEqual("1234", result); + } + + [Test] + public void ThousandsSeparatorTest_LargeNumber() + { + _settings.UseThousandsSeparator = false; + _settings.DecimalSeparator = DecimalSeparator.Dot; + + var result = GetCalculationResult("1000000+234567"); + ClassicAssert.AreEqual("1234567", result); + } + // Basic operations [TestCase(@"1+1", "2")] [TestCase(@"2-1", "1")] diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml index b12972b1b..4b9fd92b1 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml @@ -14,6 +14,7 @@ Comma (,) Dot (.) Max. decimal places + Use thousands separator Copy failed, please try later Show error message when calculation fails \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index a20a1ad5d..1b9a38e18 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -363,7 +363,7 @@ namespace Flow.Launcher.Plugin.Calculator string integerPart = parts[0]; string fractionalPart = parts.Length > 1 ? parts[1] : string.Empty; - if (integerPart.Length > 3) + if (_settings.UseThousandsSeparator && integerPart.Length > 3) { integerPart = ThousandGroupRegex.Replace(integerPart, groupSeparator); } diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs index 1544dc41f..69cd17ed2 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs @@ -7,4 +7,6 @@ public class Settings public int MaxDecimalPlaces { get; set; } = 10; public bool ShowErrorMessage { get; set; } = false; + + public bool UseThousandsSeparator { get; set; } = true; } diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml index 9e7549b2d..82f8eec60 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml @@ -16,6 +16,7 @@ + @@ -66,6 +67,16 @@ Margin="{StaticResource SettingPanelItemTopBottomMargin}" HorizontalAlignment="Left" VerticalAlignment="Center" + Content="{DynamicResource flowlauncher_plugin_calculator_use_thousands_separator}" + IsChecked="{Binding Settings.UseThousandsSeparator, Mode=TwoWay}" /> + + From fb4da697a20aebb5c07ef88d765a7d0e509ee898 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 12 Jan 2026 11:37:06 +0800 Subject: [PATCH 55/77] Improve calculator thousands separator test --- Flow.Launcher.Test/Plugins/CalculatorTest.cs | 22 +++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Flow.Launcher.Test/Plugins/CalculatorTest.cs b/Flow.Launcher.Test/Plugins/CalculatorTest.cs index 8178cb712..563bfa615 100644 --- a/Flow.Launcher.Test/Plugins/CalculatorTest.cs +++ b/Flow.Launcher.Test/Plugins/CalculatorTest.cs @@ -46,20 +46,26 @@ namespace Flow.Launcher.Test.Plugins public void ThousandsSeparatorTest_Enabled() { _settings.UseThousandsSeparator = true; + _settings.DecimalSeparator = DecimalSeparator.Dot; - var result = GetCalculationResult("1000+234"); - // When thousands separator is enabled, the result should contain a separator (comma in this case) - ClassicAssert.IsTrue(result.Contains(",") || result == "1234", - "Expected result to contain thousands separator or be without one if system doesn't use it"); + // When thousands separator is enabled, the result should contain a separator + // Since decimal separator is dot, thousands separator should be comma + ClassicAssert.AreEqual("1,234", result); + + _settings.DecimalSeparator = DecimalSeparator.Comma; + var result2 = GetCalculationResult("1000+234"); + // When thousands separator is enabled, the result should contain a separator + // Since decimal separator is comma, thousands separator should be dot + ClassicAssert.AreEqual("1.234", result2); } [Test] public void ThousandsSeparatorTest_Disabled() { _settings.UseThousandsSeparator = false; - _settings.DecimalSeparator = DecimalSeparator.Dot; - + _settings.DecimalSeparator = DecimalSeparator.UseSystemLocale; + var result = GetCalculationResult("1000+234"); ClassicAssert.AreEqual("1234", result); } @@ -68,8 +74,8 @@ namespace Flow.Launcher.Test.Plugins public void ThousandsSeparatorTest_LargeNumber() { _settings.UseThousandsSeparator = false; - _settings.DecimalSeparator = DecimalSeparator.Dot; - + _settings.DecimalSeparator = DecimalSeparator.UseSystemLocale; + var result = GetCalculationResult("1000000+234567"); ClassicAssert.AreEqual("1234567", result); } From 013622a2a784d19d7009e7238db61f7ed47a92fa Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 12 Jan 2026 11:37:47 +0800 Subject: [PATCH 56/77] Restore to default in calculator tests --- Flow.Launcher.Test/Plugins/CalculatorTest.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher.Test/Plugins/CalculatorTest.cs b/Flow.Launcher.Test/Plugins/CalculatorTest.cs index 563bfa615..40c7ab29c 100644 --- a/Flow.Launcher.Test/Plugins/CalculatorTest.cs +++ b/Flow.Launcher.Test/Plugins/CalculatorTest.cs @@ -22,9 +22,9 @@ namespace Flow.Launcher.Test.Plugins private readonly Engine _engine = new(new Configuration { Scope = new Dictionary - { - { "e", Math.E }, // e is not contained in the default mages engine - } + { + { "e", Math.E }, // e is not contained in the default mages engine + } }); public CalculatorPluginTest() @@ -116,6 +116,9 @@ namespace Flow.Launcher.Test.Plugins [TestCase(@"invalid_expression", "")] public void CalculatorTest(string expression, string result) { + _settings.UseThousandsSeparator = false; + _settings.DecimalSeparator = DecimalSeparator.UseSystemLocale; + ClassicAssert.AreEqual(GetCalculationResult(expression), result); } From 7e7f85352b23e68e82da7e46683bea2f1f2dc7fe Mon Sep 17 00:00:00 2001 From: Jack Ye Date: Mon, 12 Jan 2026 14:13:01 +0800 Subject: [PATCH 57/77] Fix potential locale-dependent test failure Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Flow.Launcher.Test/Plugins/CalculatorTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Test/Plugins/CalculatorTest.cs b/Flow.Launcher.Test/Plugins/CalculatorTest.cs index 40c7ab29c..4e40d3645 100644 --- a/Flow.Launcher.Test/Plugins/CalculatorTest.cs +++ b/Flow.Launcher.Test/Plugins/CalculatorTest.cs @@ -117,7 +117,7 @@ namespace Flow.Launcher.Test.Plugins public void CalculatorTest(string expression, string result) { _settings.UseThousandsSeparator = false; - _settings.DecimalSeparator = DecimalSeparator.UseSystemLocale; + _settings.DecimalSeparator = DecimalSeparator.Dot; ClassicAssert.AreEqual(GetCalculationResult(expression), result); } From ed83da56690ce35ddacaed286082657a9b81d80f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 03:05:02 +0000 Subject: [PATCH 58/77] Bump Svg.Skia from 3.4.0 to 3.4.1 --- updated-dependencies: - dependency-name: Svg.Skia dependency-version: 3.4.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .../Flow.Launcher.Plugin.BrowserBookmark.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj index 1df65c2eb..8c41f200e 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj @@ -106,7 +106,7 @@ - + From e970bb4d4e6764ad669ba2bb11776bb0d2fb2d4a Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 13 Jan 2026 23:32:13 +1100 Subject: [PATCH 59/77] fix icon & glyph display based on selected history style results --- .../Storage/LastOpenedHistoryResult.cs | 47 +++++++++++++++---- Flow.Launcher/Storage/QueryHistory.cs | 3 -- Flow.Launcher/ViewModel/MainViewModel.cs | 26 +--------- 3 files changed, 39 insertions(+), 37 deletions(-) diff --git a/Flow.Launcher/Storage/LastOpenedHistoryResult.cs b/Flow.Launcher/Storage/LastOpenedHistoryResult.cs index b71282f14..7f571b768 100644 --- a/Flow.Launcher/Storage/LastOpenedHistoryResult.cs +++ b/Flow.Launcher/Storage/LastOpenedHistoryResult.cs @@ -1,4 +1,5 @@ using System; +using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin; namespace Flow.Launcher.Storage; @@ -56,24 +57,54 @@ public class LastOpenedHistoryResult : Result } /// - /// Selectively creates a deep copy of the required properties for . + /// Selectively creates a deep copy of the required properties for + /// based on the style of history- Last Opened or Query. /// This copy should be independent of original and full isolated. /// /// A new containing the same required data. - public LastOpenedHistoryResult DeepCopy() + public LastOpenedHistoryResult DeepCopyForHistoryStyle(bool isHistoryStyleLastOpened) { // queryValue and glyphValue are captured to ensure they are correctly referenced in the Action delegate. var queryValue = Query; var glyphValue = Glyph; + + var title = string.Empty; + var showBadge = false; + var badgeIcoPath = string.Empty; + var icoPath = string.Empty; + var glyph = null as GlyphInfo; + + if (isHistoryStyleLastOpened) + { + title = Title; + icoPath = IcoPath; + glyph = glyphValue != null + ? new GlyphInfo(glyphValue.FontFamily, glyphValue.Glyph) + : null; + showBadge = true; + badgeIcoPath = Constant.HistoryIcon; + } + else + { + title = Localize.executeQuery(Query); + icoPath = Constant.HistoryIcon; + glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C"); + showBadge = false; + } + return new LastOpenedHistoryResult { - Title = Title, - SubTitle = SubTitle, - PluginID = PluginID, + Title = title, + // Subtitle has datetime which can cause duplicates when saving. + SubTitle = Localize.lastExecuteTime(ExecutedDateTime), + // Empty PluginID so the source of last opened history results won't be updated, this copy is meant to be temporary. + PluginID = string.Empty, Query = Query, OriginQuery = new Query { TrimmedQuery = Query }, RecordKey = RecordKey, - IcoPath = IcoPath, + IcoPath = icoPath, + ShowBadge = showBadge, + BadgeIcoPath = badgeIcoPath, PluginDirectory = PluginDirectory, // Used for Query History style reopening Action = _ => @@ -84,9 +115,7 @@ public class LastOpenedHistoryResult : Result }, // Used for Last Opened History style reopening, currently need to be assigned at MainViewModel.cs AsyncAction = null, - Glyph = glyphValue != null - ? new GlyphInfo(glyphValue.FontFamily, glyphValue.Glyph) - : null, + Glyph = glyph, ExecutedDateTime = ExecutedDateTime // Note: Other properties are left as default — copy if needed. }; diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs index 6af2a5908..91dee90df 100644 --- a/Flow.Launcher/Storage/QueryHistory.cs +++ b/Flow.Launcher/Storage/QueryHistory.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; using Flow.Launcher.Core.Plugin; -using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin; namespace Flow.Launcher.Storage @@ -35,9 +34,7 @@ namespace Flow.Launcher.Storage LastOpenedHistoryItems.Add(new LastOpenedHistoryResult { Title = Localize.executeQuery(item.Query), - IcoPath = null, OriginQuery = new Query { TrimmedQuery = item.Query }, - Glyph = null, Query = item.Query, Action = _ => { diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 63710b5fc..504ede103 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; @@ -1340,30 +1339,7 @@ namespace Flow.Launcher.ViewModel foreach (var item in historyItems) { - var copiedItem = item.DeepCopy(); - - if (Settings.HistoryStyle == HistoryStyle.Query) - { - copiedItem.Title = Localize.executeQuery(copiedItem.Query); - copiedItem.IcoPath = Constant.HistoryIcon; - // TODO: Add Glyph here - // copiedItem.Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C"); - } - else - { - if (string.IsNullOrEmpty(copiedItem.IcoPath)) // Must manually set missing image icon here - { - copiedItem.IcoPath = Constant.MissingImgIcon; - } - copiedItem.ShowBadge = true; - copiedItem.BadgeIcoPath = Constant.HistoryIcon; - } - - // Subtitle has datetime which can cause duplicates when saving. - copiedItem.SubTitle = Localize.lastExecuteTime(copiedItem.ExecutedDateTime); - - // Empty PluginID so the source of last opened history results won't be updated, these results are meant to be temporary copy. - copiedItem.PluginID = string.Empty; + var copiedItem = item.DeepCopyForHistoryStyle(Settings.HistoryStyle == HistoryStyle.LastOpened); if (Settings.HistoryStyle == HistoryStyle.LastOpened) { From 98bb98c2650672f9d19d7ded72c29c1a180df46f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 22:45:07 +0000 Subject: [PATCH 60/77] Bump Microsoft.Data.Sqlite from 10.0.1 to 10.0.2 --- updated-dependencies: - dependency-name: Microsoft.Data.Sqlite dependency-version: 10.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .../Flow.Launcher.Plugin.BrowserBookmark.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj index 1df65c2eb..ed956cb18 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj @@ -105,7 +105,7 @@ - + From 91cd27298d341cc3277f5daf72e3de08cbdcd733 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 22:45:22 +0000 Subject: [PATCH 61/77] Bump Microsoft.SourceLink.GitHub from 8.0.0 to 10.0.102 --- updated-dependencies: - dependency-name: Microsoft.SourceLink.GitHub dependency-version: 10.0.102 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .../Flow.Launcher.Plugin.csproj | 2 +- Flow.Launcher.Plugin/packages.lock.json | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj index 649d59ad7..103a72113 100644 --- a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj +++ b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj @@ -72,7 +72,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all diff --git a/Flow.Launcher.Plugin/packages.lock.json b/Flow.Launcher.Plugin/packages.lock.json index 70f71f20d..3240b3981 100644 --- a/Flow.Launcher.Plugin/packages.lock.json +++ b/Flow.Launcher.Plugin/packages.lock.json @@ -16,12 +16,12 @@ }, "Microsoft.SourceLink.GitHub": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "requested": "[10.0.102, )", + "resolved": "10.0.102", + "contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==", "dependencies": { - "Microsoft.Build.Tasks.Git": "8.0.0", - "Microsoft.SourceLink.Common": "8.0.0" + "Microsoft.Build.Tasks.Git": "10.0.102", + "Microsoft.SourceLink.Common": "10.0.102" } }, "Microsoft.Windows.CsWin32": { @@ -46,13 +46,13 @@ }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + "resolved": "10.0.102", + "contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg==" }, "Microsoft.SourceLink.Common": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + "resolved": "10.0.102", + "contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A==" }, "Microsoft.Windows.SDK.Win32Docs": { "type": "Transitive", From befcc9b37c40e469120483ee296eaad2950e4626 Mon Sep 17 00:00:00 2001 From: VictoriousRaptor <10308169+VictoriousRaptor@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:59:47 +0800 Subject: [PATCH 62/77] Update Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml --- Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml index 4b9fd92b1..5f9a3ca5a 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml @@ -14,7 +14,7 @@ Comma (,) Dot (.) Max. decimal places - Use thousands separator + Show thousands separator in results Copy failed, please try later Show error message when calculation fails \ No newline at end of file From 90360022a4772b742ad4d86e1f629697dd981692 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 20 Jan 2026 21:26:04 +1100 Subject: [PATCH 63/77] update glyph when history result exists --- Flow.Launcher.Plugin/Result.cs | 4 ++-- Flow.Launcher/Storage/QueryHistory.cs | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index 78fa31974..1682c4d79 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -167,7 +167,7 @@ namespace Flow.Launcher.Plugin /// /// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons) /// - public GlyphInfo Glyph { get; init; } + public GlyphInfo Glyph { get; set; } /// /// An action to take in the form of a function call when the result has been selected. diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs index 91dee90df..093db0a58 100644 --- a/Flow.Launcher/Storage/QueryHistory.cs +++ b/Flow.Launcher/Storage/QueryHistory.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text.Json.Serialization; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Plugin; +using Svg; namespace Flow.Launcher.Storage { @@ -72,10 +73,13 @@ namespace Flow.Launcher.Storage TryGetLastOpenedHistoryResult(result, out var existingHistoryItem)) { existingHistoryItem.ExecutedDateTime = DateTime.Now; + if (existingHistoryItem.IcoPath != result.IcoPath) - { existingHistoryItem.IcoPath = result.IcoPath; - } + + if (existingHistoryItem.Glyph.Glyph != result.Glyph.Glyph + || existingHistoryItem.Glyph.FontFamily != result.Glyph.FontFamily) + existingHistoryItem.Glyph = result.Glyph; } else { From 5c241d7491f4d638dff327ea4c1bf3ff9a513db8 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 20 Jan 2026 21:47:38 +1100 Subject: [PATCH 64/77] fix to respect max history result shown setting --- Flow.Launcher/ViewModel/MainViewModel.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 504ede103..b51a8f8cf 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1316,19 +1316,13 @@ namespace Flow.Launcher.ViewModel } } - private List GetHistoryItems(IEnumerable historyItems, int? selectLast = null) + private List GetHistoryItems(IEnumerable historyItems, int? maxResult = null) { var results = new List(); // Order by executed time descending: Latest -> Oldest historyItems = historyItems.OrderByDescending(x => x.ExecutedDateTime); - // Select the last N items if specified - if (selectLast.HasValue) - { - historyItems = historyItems.Take(selectLast.Value); - } - if (Settings.HistoryStyle == HistoryStyle.LastOpened) { // Items saved to disk are differentiated by Query also, but LastOpened style only cares about unique results @@ -1337,6 +1331,10 @@ namespace Flow.Launcher.ViewModel .Select(g => g.First()); } + // Max history results to return for display + if (maxResult.HasValue) + historyItems = historyItems.Take(maxResult.Value); + foreach (var item in historyItems) { var copiedItem = item.DeepCopyForHistoryStyle(Settings.HistoryStyle == HistoryStyle.LastOpened); From 4ddb164bbdd18b33ac44c0eb1d9266af1a87f44d Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 20 Jan 2026 22:48:11 +1100 Subject: [PATCH 65/77] adjusted context menu call to exit early when history result selected --- Flow.Launcher/ViewModel/MainViewModel.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index b51a8f8cf..f6e289a31 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -429,16 +429,17 @@ namespace Flow.Launcher.ViewModel return; } + // Do not show context menu for history results, they will have no PluginID set. + if (string.IsNullOrEmpty(SelectedResults.SelectedItem?.Result?.PluginID)) + return; + // For query mode, we load context menu if (QueryResultsSelected()) { // When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing // i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing - if (SelectedResults.SelectedItem?.Result != null && - !string.IsNullOrEmpty(SelectedResults.SelectedItem.Result.PluginID)) // Do not show context menu for history items - { + if (SelectedResults.SelectedItem?.Result != null) SelectedResults = ContextMenu; - } } else { @@ -1260,8 +1261,7 @@ namespace Flow.Launcher.ViewModel var selected = Results.SelectedItem?.Result; - if (selected != null && // SelectedItem returns null if selection is empty. - !string.IsNullOrEmpty(selected.PluginID)) // SelectedItem must have a valid PluginID + if (selected != null) // SelectedItem returns null if selection is empty. { List results = PluginManager.GetContextMenusForPlugin(selected); results.Add(ContextMenuTopMost(selected)); From 657e6ae7d3d7fdb9a1fbba3305b91308e076bee4 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 20 Jan 2026 22:55:04 +1100 Subject: [PATCH 66/77] add null-safe comparison for glyph --- Flow.Launcher/Storage/QueryHistory.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs index 093db0a58..26dead008 100644 --- a/Flow.Launcher/Storage/QueryHistory.cs +++ b/Flow.Launcher/Storage/QueryHistory.cs @@ -77,8 +77,8 @@ namespace Flow.Launcher.Storage if (existingHistoryItem.IcoPath != result.IcoPath) existingHistoryItem.IcoPath = result.IcoPath; - if (existingHistoryItem.Glyph.Glyph != result.Glyph.Glyph - || existingHistoryItem.Glyph.FontFamily != result.Glyph.FontFamily) + if (existingHistoryItem.Glyph?.Glyph != result.Glyph?.Glyph + || existingHistoryItem.Glyph?.FontFamily != result.Glyph?.FontFamily) existingHistoryItem.Glyph = result.Glyph; } else From bf44637400f9551758754f3cd71edf8a8e55df5a Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 20 Jan 2026 23:18:00 +1100 Subject: [PATCH 67/77] revert context menu early exit when history result --- Flow.Launcher/ViewModel/MainViewModel.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index f6e289a31..f93aef5a6 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -429,17 +429,16 @@ namespace Flow.Launcher.ViewModel return; } - // Do not show context menu for history results, they will have no PluginID set. - if (string.IsNullOrEmpty(SelectedResults.SelectedItem?.Result?.PluginID)) - return; - // For query mode, we load context menu if (QueryResultsSelected()) { // When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing // i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing - if (SelectedResults.SelectedItem?.Result != null) + if (SelectedResults.SelectedItem?.Result != null && + !string.IsNullOrEmpty(SelectedResults.SelectedItem.Result.PluginID)) // Do not show context menu for history results + { SelectedResults = ContextMenu; + } } else { @@ -1261,7 +1260,8 @@ namespace Flow.Launcher.ViewModel var selected = Results.SelectedItem?.Result; - if (selected != null) // SelectedItem returns null if selection is empty. + if (selected != null && // SelectedItem returns null if selection is empty. + !string.IsNullOrEmpty(selected.PluginID)) // SelectedItem must have a valid PluginID, history results do not. { List results = PluginManager.GetContextMenusForPlugin(selected); results.Add(ContextMenuTopMost(selected)); From 8e127d033955013517ed57cb32777e1579146181 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 21 Jan 2026 18:13:27 +0800 Subject: [PATCH 68/77] Code cleanup --- Flow.Launcher/Storage/QueryHistory.cs | 1 - Flow.Launcher/ViewModel/MainViewModel.cs | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs index 26dead008..0f4d97677 100644 --- a/Flow.Launcher/Storage/QueryHistory.cs +++ b/Flow.Launcher/Storage/QueryHistory.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Text.Json.Serialization; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Plugin; -using Svg; namespace Flow.Launcher.Storage { diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index f93aef5a6..3b04d6f12 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1333,7 +1333,9 @@ namespace Flow.Launcher.ViewModel // Max history results to return for display if (maxResult.HasValue) + { historyItems = historyItems.Take(maxResult.Value); + } foreach (var item in historyItems) { From b703bcdd52c72103b7dbe794fe171dfd8a8c08f7 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 23 Jan 2026 15:51:24 +0800 Subject: [PATCH 69/77] Try to fix combability issue --- Flow.Launcher.Plugin/Result.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index 1682c4d79..137ec4892 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -164,10 +164,16 @@ namespace Flow.Launcher.Plugin [JsonIgnore] public IconDelegate BadgeIcon = null; + private GlyphInfo _glyph; + /// /// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons) /// - public GlyphInfo Glyph { get; set; } + public GlyphInfo Glyph + { + get => _glyph; + set => _glyph = value; + } /// /// An action to take in the form of a function call when the result has been selected. From 0f0ee9ffde013b3598b8ea83f304a9b5701ccc45 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 23 Jan 2026 16:05:03 +0800 Subject: [PATCH 70/77] Revert "Try to fix combability issue" This reverts commit b703bcdd52c72103b7dbe794fe171dfd8a8c08f7. --- Flow.Launcher.Plugin/Result.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index 137ec4892..1682c4d79 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -164,16 +164,10 @@ namespace Flow.Launcher.Plugin [JsonIgnore] public IconDelegate BadgeIcon = null; - private GlyphInfo _glyph; - /// /// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons) /// - public GlyphInfo Glyph - { - get => _glyph; - set => _glyph = value; - } + public GlyphInfo Glyph { get; set; } /// /// An action to take in the form of a function call when the result has been selected. From 935d41dc265972c433f599a5fbfdff0acd798542 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 23 Jan 2026 16:10:01 +0800 Subject: [PATCH 71/77] Ensure combability of glyph setter --- Flow.Launcher.Plugin/Result.cs | 19 +++++++++++++++++-- Flow.Launcher/Storage/QueryHistory.cs | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index 1682c4d79..e944081f9 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -164,10 +164,25 @@ namespace Flow.Launcher.Plugin [JsonIgnore] public IconDelegate BadgeIcon = null; + private GlyphInfo _glyph; + /// /// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons) /// - public GlyphInfo Glyph { get; set; } + public GlyphInfo Glyph + { + get => _glyph; + init => _glyph = value; + } + + /// + /// Set the Glyph Icon + /// + /// + public void SetGlyph(GlyphInfo glyph) + { + _glyph = glyph; + } /// /// An action to take in the form of a function call when the result has been selected. diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs index 0f4d97677..7bf948399 100644 --- a/Flow.Launcher/Storage/QueryHistory.cs +++ b/Flow.Launcher/Storage/QueryHistory.cs @@ -78,7 +78,7 @@ namespace Flow.Launcher.Storage if (existingHistoryItem.Glyph?.Glyph != result.Glyph?.Glyph || existingHistoryItem.Glyph?.FontFamily != result.Glyph?.FontFamily) - existingHistoryItem.Glyph = result.Glyph; + existingHistoryItem.SetGlyph(result.Glyph); } else { From e9dcd8b7ba36f51a0690de9d500e6f3f241ea289 Mon Sep 17 00:00:00 2001 From: Jack Ye Date: Fri, 23 Jan 2026 16:15:25 +0800 Subject: [PATCH 72/77] Fix code comments Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Flow.Launcher.Plugin/Result.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index e944081f9..e0c5d494e 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -69,7 +69,7 @@ namespace Flow.Launcher.Plugin /// /// Path or URI to the icon image for this result. - /// Updates appropriately when set. + /// Updates appropriately when set. /// /// /// Preferred usage: provide a path relative to the plugin directory (for example: "Images\icon.png"). From 248da93d05120dbe8b99db9afca35eff893f3d9b Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 26 Jan 2026 16:57:47 +1100 Subject: [PATCH 73/77] fix preview when toggle history mode via Ctrl + H --- Flow.Launcher.Plugin/Result.cs | 2 +- Flow.Launcher/ViewModel/MainViewModel.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index e0c5d494e..9404d1107 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -176,7 +176,7 @@ namespace Flow.Launcher.Plugin } /// - /// Set the Glyph Icon + /// Set the Glyph Icon after initialization /// /// public void SetGlyph(GlyphInfo glyph) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 3b04d6f12..ba170feb0 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -363,6 +363,8 @@ namespace Flow.Launcher.ViewModel else { SelectedResults = Results; + PreviewSelectedItem = Results.SelectedItem; + _ = UpdatePreviewAsync(); } } From 97d7eab1a7ef74791c300b4bae6c52321bb185a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 22:46:23 +0000 Subject: [PATCH 74/77] Bump Microsoft.Windows.CsWin32 from 0.3.205 to 0.3.269 --- updated-dependencies: - dependency-name: Microsoft.Windows.CsWin32 dependency-version: 0.3.269 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: Microsoft.Windows.CsWin32 dependency-version: 0.3.269 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: Microsoft.Windows.CsWin32 dependency-version: 0.3.269 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .../Flow.Launcher.Plugin.csproj | 2 +- Flow.Launcher.Plugin/packages.lock.json | 20 +++++++++---------- .../Flow.Launcher.Plugin.ProcessKiller.csproj | 2 +- .../Flow.Launcher.Plugin.Sys.csproj | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj index 103a72113..b5ba36f98 100644 --- a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj +++ b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj @@ -74,7 +74,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Flow.Launcher.Plugin/packages.lock.json b/Flow.Launcher.Plugin/packages.lock.json index 3240b3981..662192b4a 100644 --- a/Flow.Launcher.Plugin/packages.lock.json +++ b/Flow.Launcher.Plugin/packages.lock.json @@ -26,13 +26,13 @@ }, "Microsoft.Windows.CsWin32": { "type": "Direct", - "requested": "[0.3.205, )", - "resolved": "0.3.205", - "contentHash": "U5wGAnyKd7/I2YMd43nogm81VMtjiKzZ9dsLMVI4eAB7jtv5IEj0gprj0q/F3iRmAIaGv5omOf8iSYx2+nE6BQ==", + "requested": "[0.3.269, )", + "resolved": "0.3.269", + "contentHash": "O4GVJ0ymxcoFRGS07VcoEClj7A9PIciHIjWDrPymzonhYlOfM7V0ZqGBUK19cUH3BPca9MfSOH0KLK/9JzQ8+Q==", "dependencies": { "Microsoft.Windows.SDK.Win32Docs": "0.1.42-alpha", - "Microsoft.Windows.SDK.Win32Metadata": "61.0.15-preview", - "Microsoft.Windows.WDK.Win32Metadata": "0.12.8-experimental" + "Microsoft.Windows.SDK.Win32Metadata": "69.0.7-preview", + "Microsoft.Windows.WDK.Win32Metadata": "0.13.25-experimental" } }, "PropertyChanged.Fody": { @@ -61,15 +61,15 @@ }, "Microsoft.Windows.SDK.Win32Metadata": { "type": "Transitive", - "resolved": "61.0.15-preview", - "contentHash": "cysex3dazKtCPALCluC2XX3f5Aedy9H2pw5jb+TW5uas2rkem1Z7FRnbUrg2vKx0pk0Qz+4EJNr37HdYTEcvEQ==" + "resolved": "69.0.7-preview", + "contentHash": "RJoNjQJVCIDNLPbvYuaygCFknTyAxOUE45of1voj0jjOgJa9MB2m1/G8L8F3IYc+2EFG5aqa/9y8PEx7Tk2tLQ==" }, "Microsoft.Windows.WDK.Win32Metadata": { "type": "Transitive", - "resolved": "0.12.8-experimental", - "contentHash": "3n8R44/Z96Ly+ty4eYVJfESqbzvpw96lRLs3zOzyDmr1x1Kw7FNn5CyE416q+bZQV3e1HRuMUvyegMeRE/WedA==", + "resolved": "0.13.25-experimental", + "contentHash": "IM50tb/+UIwBr9FMr6ZKcZjCMW+Axo6NjGqKxgjUfyCY8dRnYUfrJEXxAaXoWtYP4X8EmASmC1Jtwh4XucseZg==", "dependencies": { - "Microsoft.Windows.SDK.Win32Metadata": "61.0.15-preview" + "Microsoft.Windows.SDK.Win32Metadata": "63.0.31-preview" } } } diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj index 39586771f..ec8a32b95 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj @@ -54,7 +54,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj b/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj index 4cf09baab..1ef6dc495 100644 --- a/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj +++ b/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj @@ -60,7 +60,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From d4f472a64ea30cf5567484af621f148df8f31d97 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 5 Feb 2026 20:28:37 +0800 Subject: [PATCH 75/77] Update AdjustTokenPrivileges PInvoke usage Refactored the AdjustTokenPrivileges call to use null for the previous state and an out variable for the return length, improving compatibility with the PInvoke signature and aligning with recommended usage. --- Plugins/Flow.Launcher.Plugin.Sys/Main.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs index 57b9749f7..e0da6cbe1 100644 --- a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs @@ -175,7 +175,7 @@ namespace Flow.Launcher.Plugin.Sys Privileges = new() { e0 = new LUID_AND_ATTRIBUTES { Luid = luid, Attributes = TOKEN_PRIVILEGES_ATTRIBUTES.SE_PRIVILEGE_ENABLED } } }; - if (!PInvoke.AdjustTokenPrivileges(tokenHandle, false, &privileges, 0, null, null)) + if (!PInvoke.AdjustTokenPrivileges(tokenHandle, false, &privileges, null, out var _)) { return false; } From 0717933459f3999c60c7eb910058340ef8dda1e4 Mon Sep 17 00:00:00 2001 From: Jack Ye Date: Sun, 8 Feb 2026 09:49:12 +0800 Subject: [PATCH 76/77] Fix progress query management and thread safety (#4135) --- Flow.Launcher/ViewModel/MainViewModel.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index ba170feb0..333ac3652 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; @@ -37,7 +38,7 @@ namespace Flow.Launcher.ViewModel private Query _lastQuery; private bool _previousIsHomeQuery; - private Query _progressQuery; // Used for QueryResultAsync + private readonly ConcurrentDictionary _progressQueryDict = new(); // Used for QueryResultAsync private Query _updateQuery; // Used for ResultsUpdated private string _queryTextBeforeLeaveResults; private string _ignoredQueryText; // Used to ignore query text change when switching between context menu and query results @@ -1415,6 +1416,9 @@ namespace Flow.Launcher.ViewModel return; } + // Create a Guid for this update session so that we can filter out in progress checking + var updateGuid = Guid.NewGuid(); + try { _updateSource?.Dispose(); @@ -1426,7 +1430,7 @@ namespace Flow.Launcher.ViewModel ProgressBarVisibility = Visibility.Hidden; - _progressQuery = query; + _progressQueryDict.TryAdd(updateGuid, query); _updateQuery = query; // Switch to ThreadPool thread @@ -1481,7 +1485,8 @@ namespace Flow.Launcher.ViewModel _ = Task.Delay(200, currentCancellationToken).ContinueWith(_ => { // start the progress bar if query takes more than 200 ms and this is the current running query and it didn't finish yet - if (_progressQuery != null && _progressQuery.OriginalQuery == query.OriginalQuery) + if (_progressQueryDict.TryGetValue(updateGuid, out var progressQuery) && + progressQuery.OriginalQuery == query.OriginalQuery) { ProgressBarVisibility = Visibility.Visible; } @@ -1537,7 +1542,7 @@ namespace Flow.Launcher.ViewModel // this should happen once after all queries are done so progress bar should continue // until the end of all querying - _progressQuery = null; + _progressQueryDict.Remove(updateGuid, out _); if (!currentCancellationToken.IsCancellationRequested) { @@ -1547,8 +1552,8 @@ namespace Flow.Launcher.ViewModel } finally { - // this make sures progress query is null when this query is canceled - _progressQuery = null; + // this ensures the query is removed from the progress tracking dictionary when this query is canceled or completes + _progressQueryDict.Remove(updateGuid, out _); } // Local function From 0f829d0b911d449b16ccaa11c0537e6920614d23 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 01:07:13 +0000 Subject: [PATCH 77/77] Bump SkiaSharp from 3.119.1 to 3.119.2 --- updated-dependencies: - dependency-name: SkiaSharp dependency-version: 3.119.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .../Flow.Launcher.Plugin.BrowserBookmark.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj index 3e78ac1a3..cb86f719e 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj @@ -107,7 +107,7 @@ - +