rename last opened class; use inheritance; initialise at startup

This commit is contained in:
Jeremy 2025-12-31 12:14:07 +11:00
parent d78d313372
commit de0d022268
7 changed files with 293 additions and 111 deletions

View file

@ -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
/// <summary>
/// Delegate to load an icon for this result.
/// </summary>
[JsonIgnore]
public IconDelegate Icon = null;
/// <summary>
/// Delegate to load an icon for the badge of this result.
/// </summary>
[JsonIgnore]
public IconDelegate BadgeIcon = null;
/// <summary>
@ -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.
/// </remarks>
[JsonIgnore]
public Func<ActionContext, bool> Action { get; set; }
/// <summary>
@ -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.
/// </remarks>
[JsonIgnore]
public Func<ActionContext, ValueTask<bool>> AsyncAction { get; set; }
/// <summary>
@ -239,6 +244,7 @@ namespace Flow.Launcher.Plugin
/// <example>
/// As external information for ContextMenu
/// </example>
[JsonIgnore]
public object ContextData { get; set; }
/// <summary>
@ -259,6 +265,7 @@ namespace Flow.Launcher.Plugin
/// <summary>
/// Customized Preview Panel
/// </summary>
[JsonIgnore]
public Lazy<UserControl> PreviewPanel { get; set; }
/// <summary>
@ -388,6 +395,7 @@ namespace Flow.Launcher.Plugin
/// <summary>
/// Delegate to get the preview panel's image
/// </summary>
[JsonIgnore]
public IconDelegate PreviewDelegate { get; set; } = null;
/// <summary>

View file

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

View file

@ -11,7 +11,7 @@ namespace Flow.Launcher.Helper;
public static class ResultHelper
{
public static async Task<Result?> PopulateResultsAsync(LastOpenedHistoryItem item)
public static async Task<Result?> PopulateResultsAsync(LastOpenedHistoryResult item)
{
return await PopulateResultsAsync(item.PluginID, item.Query, item.Title, item.SubTitle, item.RecordKey);
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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<LastOpenedHistoryItem> LastOpenedHistoryItems { get; private set; } = [];
public List<LastOpenedHistoryResult> 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;
}
/// <summary>
/// Refresh stored PluginDirectory (and optionally normalize relative ico paths)
/// using current plugin metadata. Call this after plugins are loaded/initialized.
/// </summary>
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;
}
}
}
}

View file

@ -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<History> _historyItemsStorage;
private int QueryHistoryItemsInitialized = 0;
private FlowLauncherJsonStorage<History> _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<UserSelectedRecord> _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<History>();
_userSelectedRecordStorage = new FlowLauncherJsonStorage<UserSelectedRecord>();
_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<Result> GetHistoryItems(IEnumerable<LastOpenedHistoryItem> historyItems)
private List<Result> GetHistoryItems(IEnumerable<LastOpenedHistoryResult> historyItems)
{
var results = new List<Result>();
@ -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;
}
/// <summary>
/// TODO COMMENT- Requires the plugins to have initialized first because
/// it needs the plugin directory paths for initialization
/// </summary>
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);