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);