From de0d022268dd0e8ee682042444e357a5e83e2547 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 31 Dec 2025 12:14:07 +1100 Subject: [PATCH] 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);