Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
7883e7a
add result IcoPath & Glyph + handling show badge switching
jjw24 Oct 17, 2025
10d3ba2
update history result icon display logic & remove badge icon logic
jjw24 Oct 19, 2025
26ad473
change getting last history item to get the same result if exists
jjw24 Oct 19, 2025
92551c5
show distinct records when history style is last opened
jjw24 Oct 21, 2025
25aa5bf
show history result sorted descending by execution time
jjw24 Oct 21, 2025
0f26945
remove duplicate history item sort call
jjw24 Oct 21, 2025
7958b17
wip show result icon
jjw24 Nov 20, 2025
d78d313
update to use IcoAbsoluteLocalPath
jjw24 Dec 26, 2025
de0d022
rename last opened class; use inheritance; initialise at startup
jjw24 Dec 31, 2025
6b6a9a9
simplify GetHistoryItems
jjw24 Jan 1, 2026
8d7fa1d
revert QueryHistoryItems property
jjw24 Jan 1, 2026
810725c
add explicit JsonInclude for PluginID internal set
jjw24 Jan 1, 2026
2e8bb35
Merge branch 'dev' into last_history_show_result_icon
jjw24 Jan 1, 2026
20854ba
formatting
jjw24 Jan 1, 2026
7c670de
fix history result highlight and scroll when history mode activated
jjw24 Jan 1, 2026
f9df44a
rename to IcoPathAbsolute
jjw24 Jan 1, 2026
6d6003f
switch to using TrimmedQuery
jjw24 Jan 1, 2026
9c2f239
add method comments for LastOpenedHistoryResult
jjw24 Jan 1, 2026
024eeaf
changed LastOpenHistoryResult's Copy to DeepCopy to clarify intent
jjw24 Jan 2, 2026
101e5e6
fix Glyph optional null when copying
jjw24 Jan 4, 2026
4174b5e
fix title when query history style used
jjw24 Jan 4, 2026
94ebbab
update legacy item to show Query style title
jjw24 Jan 4, 2026
25037e3
update IcoPath if existing is different to the selected
jjw24 Jan 4, 2026
55673cd
add method comments
jjw24 Jan 4, 2026
ea7a2d2
mark PopulateHistoryFromLegacyHistory obsolete
jjw24 Jan 4, 2026
19fa107
minor fixes and adjustments for code quality
jjw24 Jan 4, 2026
216b6f5
Code cleanup
Jack251970 Jan 5, 2026
5f99235
Code cleanup
Jack251970 Jan 5, 2026
177e607
Add code comments
Jack251970 Jan 5, 2026
11645fd
ensure history results ordered by ExecutedDateTime
jjw24 Jan 5, 2026
58d910d
fix deep copy Glyph on LastOpenHistoryResult
jjw24 Jan 5, 2026
f22b644
Fix issue when querying history items in home page
Jack251970 Jan 5, 2026
e6a91a9
Ensure history items have valid icons in QueryHistory
Jack251970 Jan 5, 2026
cf4268d
Fix preview panel blank issue
Jack251970 Jan 5, 2026
5b2fb1f
Revert "Ensure history items have valid icons in QueryHistory"
Jack251970 Jan 5, 2026
e403c41
Change copied icon logic as origin
Jack251970 Jan 5, 2026
c610100
Use QueryManager function
Jack251970 Jan 6, 2026
e404d02
Update the result when executing history item
Jack251970 Jan 6, 2026
26e4529
Update the result when executing history item
Jack251970 Jan 6, 2026
583bf74
Update code comments
Jack251970 Jan 6, 2026
938a87c
Fix preview panel blank issue
Jack251970 Jan 6, 2026
9d9ab1f
Do not allow context menu for history items
Jack251970 Jan 6, 2026
3745c44
Revert `Update the result when executing history item`
Jack251970 Jan 6, 2026
e970bb4
fix icon & glyph display based on selected history style results
jjw24 Jan 13, 2026
9036002
update glyph when history result exists
jjw24 Jan 20, 2026
5c241d7
fix to respect max history result shown setting
jjw24 Jan 20, 2026
4ddb164
adjusted context menu call to exit early when history result selected
jjw24 Jan 20, 2026
657e6ae
add null-safe comparison for glyph
jjw24 Jan 20, 2026
bf44637
revert context menu early exit when history result
jjw24 Jan 20, 2026
8e127d0
Code cleanup
Jack251970 Jan 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 40 additions & 8 deletions Flow.Launcher.Plugin/Result.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Media;
using System.Text.Json.Serialization;

namespace Flow.Launcher.Plugin
{
/// <summary>
/// Describes a result of a <see cref="Query"/> executed by a plugin
/// Describes a result of a <see cref="Query"/> executed by a plugin.
/// This or its child classes is serializable.
/// </summary>
public class Result
{
Expand All @@ -21,6 +23,8 @@

private string _icoPath;

private string _icoPathAbsolute;

private string _copyText = string.Empty;

private string _badgeIcoPath;
Expand Down Expand Up @@ -64,15 +68,27 @@
public string AutoCompleteText { get; set; }

/// <summary>
/// The image to be displayed for the result.
/// Path or URI to the icon image for this result.
/// Updates <IcoPathAbsolute/> appropriately when set.
/// </summary>
/// <value>Can be a local file path or a URL.</value>
/// <remarks>GlyphInfo is prioritized if not null</remarks>
/// <remarks>
/// Preferred usage: provide a path relative to the plugin directory (for example: "Images\icon.png").
/// Because <see cref="IcoPath"/> is serialized, using relative paths keeps the icon reference portable
/// when Flow is moved.
///
/// Accepted formats:
/// - Relative file paths (resolved against <see cref="PluginDirectory"/> into <see cref="IcoPathAbsolute"/>)
/// - Absolute file paths (left as-is)
/// - HTTP/HTTPS URLs (left as-is)
/// - Data URIs (left as-is)
/// </remarks>
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)
Expand All @@ -81,15 +97,23 @@
&& !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
&& !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase))
{
_icoPath = Path.Combine(PluginDirectory, value);
_icoPathAbsolute = Path.Combine(PluginDirectory, value);
}
else
{
_icoPath = value;
_icoPathAbsolute = value;
}
}
}

/// <summary>
/// Absolute path or URI which is used to load and display the result icon for Flow.
/// This is populated by the <see cref="IcoPath"/> setter.
/// If a relative path was provided to <see cref="IcoPath"/>, this property will contain the resolved
/// absolute local path after combining with <see cref="PluginDirectory"/>.
/// </summary>
public string IcoPathAbsolute => _icoPathAbsolute;

/// <summary>
/// The image to be displayed for the badge of the result.
/// </summary>
Expand Down Expand Up @@ -131,17 +155,19 @@
/// <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>
/// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons)
/// </summary>
public GlyphInfo Glyph { get; init; }
public GlyphInfo Glyph { get; set; }

/// <summary>
/// An action to take in the form of a function call when the result has been selected.
Expand All @@ -151,6 +177,7 @@
/// 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>
Expand All @@ -161,6 +188,7 @@
/// 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>
Expand Down Expand Up @@ -203,11 +231,13 @@
/// <example>
/// As external information for ContextMenu
/// </example>
[JsonIgnore]
public object ContextData { get; set; }

/// <summary>
/// Plugin ID that generated this result
/// </summary>
[JsonInclude]
public string PluginID { get; internal set; }

/// <summary>
Expand All @@ -223,6 +253,7 @@
/// <summary>
/// Customized Preview Panel
/// </summary>
[JsonIgnore]
public Lazy<UserControl> PreviewPanel { get; set; }

/// <summary>
Expand All @@ -242,7 +273,7 @@
public PreviewInfo Preview { get; set; } = PreviewInfo.Default;

/// <summary>
/// Determines if the user selection count should be added to the score. This can be useful when set to false to allow the result sequence order to be the same everytime instead of changing based on selection.

Check warning on line 276 in Flow.Launcher.Plugin/Result.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`everytime` is not a recognized word. (unrecognized-spelling)
/// </summary>
public bool AddSelectedCount { get; set; } = true;

Expand Down Expand Up @@ -352,6 +383,7 @@
/// <summary>
/// Delegate to get the preview panel's image
/// </summary>
[JsonIgnore]
public IconDelegate PreviewDelegate { get; set; } = null;

/// <summary>
Expand Down
3 changes: 3 additions & 0 deletions Flow.Launcher/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,9 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () =>

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
// And home page is created without full plugin list
if (_settings.ShowHomePage && _mainVM.QueryResultsSelected() && string.IsNullOrEmpty(_mainVM.QueryText))
Expand Down
4 changes: 2 additions & 2 deletions Flow.Launcher/Helper/ResultHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
Grid.Column="1"
Margin="5 24 0 0">

<ikw:SimpleStackPanel

Check warning on line 55 in Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml

View workflow job for this annotation

GitHub Actions / Check Spelling

`ikw` is not a recognized word. (unrecognized-spelling)
HorizontalAlignment="Right"
VerticalAlignment="Center"
DockPanel.Dock="Right"
Expand Down Expand Up @@ -333,7 +333,7 @@
Margin="18 24 0 0"
HorizontalAlignment="Left"
RenderOptions.BitmapScalingMode="Fant"
Source="{Binding IcoPath, IsAsync=True}" />
Source="{Binding IcoPathAbsolute, IsAsync=True}" />
<Border
x:Name="LabelUpdate"
Height="12"
Expand Down
2 changes: 1 addition & 1 deletion Flow.Launcher/Storage/HistoryItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Flow.Launcher.Storage
{
[Obsolete("Use LastOpenedHistoryItem instead. This class will be removed in future versions.")]
[Obsolete("Use LastOpenedHistoryResult instead. This class will be removed in future versions.")]
public class HistoryItem
{
public string Query { get; set; }
Expand Down
31 changes: 0 additions & 31 deletions Flow.Launcher/Storage/LastOpenedHistoryItem.cs

This file was deleted.

146 changes: 146 additions & 0 deletions Flow.Launcher/Storage/LastOpenedHistoryResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using System;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin;

namespace Flow.Launcher.Storage;

/// <summary>
/// A serializable result used to record the last opened history for reopening results.
/// Inherits common result fields from <see cref="Result"/> and adds the original query and execution time.
/// </summary>
public class LastOpenedHistoryResult : Result
{
/// <summary>
/// The query string from Query.TrimmedQuery property, it is stored as a string instead of the entire Query class <see cref="Result"/>.
/// This is used so results can be reopened or re-run using the serialized query string.
/// </summary>
public string Query { get; set; } = string.Empty;

/// <summary>
/// The local date and time when this result was executed/opened.
/// </summary>
public DateTime ExecutedDateTime { get; set; }

/// <summary>
/// Initializes a new instance of <see cref="LastOpenedHistoryResult"/>.
/// </summary>
public LastOpenedHistoryResult()
{
}

/// <summary>
/// Creates a <see cref="LastOpenedHistoryResult"/> from an existing <see cref="Result"/>.
/// Copies required fields and sets up default reopening actions.
/// </summary>
/// <param name="result">The original result to create history from.</param>
public LastOpenedHistoryResult(Result result)
{
Title = result.Title;
SubTitle = result.SubTitle;
PluginID = result.PluginID;
Query = result.OriginQuery.TrimmedQuery;
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.TrimmedQuery);
return false;
};
// Used for Last Opened History style reopening, currently need to be assigned at MainViewModel.cs
AsyncAction = null;
}

/// <summary>
/// Selectively creates a deep copy of the required properties for <see cref="LastOpenedHistoryResult"/>
/// based on the style of history- Last Opened or Query.
/// This copy should be independent of original and full isolated.
/// </summary>
/// <returns>A new <see cref="LastOpenedHistoryResult"/> containing the same required data.</returns>
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 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,
ShowBadge = showBadge,
BadgeIcoPath = badgeIcoPath,
PluginDirectory = PluginDirectory,
// 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 = glyph,
ExecutedDateTime = ExecutedDateTime
// Note: Other properties are left as default — copy if needed.
};
}

/// <summary>
/// Determines whether the specified <see cref="Result"/> is equivalent to this history result.
/// Comparison uses <see cref="Result.RecordKey"/> when available; otherwise falls back to title/subtitle/plugin id and query.
/// </summary>
/// <param name="r">The result to compare to.</param>
/// <returns><c>true</c> if the results are considered equal; otherwise <c>false</c>.</returns>
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.TrimmedQuery;
}
else
{
return RecordKey == r.RecordKey
&& PluginID == r.PluginID
&& Query == r.OriginQuery.TrimmedQuery;
}
}
}
Loading
Loading