Merge pull request #4042 from 01Dri/feature/history_mode

Add "Last opened" history mode & Fix non-result history item save issue
This commit is contained in:
Jack Ye 2025-10-15 12:47:45 +08:00 committed by GitHub
commit 3d9ef2c63f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 262 additions and 57 deletions

View file

@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
<!-- Work around https://github.com/dotnet/runtime/issues/109682 -->
<!-- Workaround https://github.com/dotnet/runtime/issues/109682 -->
<CETCompat>false</CETCompat>
</PropertyGroup>
</Project>

View file

@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Localization.Attributes;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedModels;
@ -513,6 +514,21 @@ namespace Flow.Launcher.Infrastructure.UserSettings
[JsonConverter(typeof(JsonStringEnumConverter))]
public LastQueryMode LastQueryMode { get; set; } = LastQueryMode.Selected;
private HistoryStyle _historyStyle = HistoryStyle.Query;
[JsonConverter(typeof(JsonStringEnumConverter))]
public HistoryStyle HistoryStyle
{
get => _historyStyle;
set
{
if (_historyStyle != value)
{
_historyStyle = value;
OnPropertyChanged();
}
}
}
[JsonConverter(typeof(JsonStringEnumConverter))]
public AnimationSpeeds AnimationSpeed { get; set; } = AnimationSpeeds.Medium;
public int CustomAnimationLength { get; set; } = 360;
@ -695,4 +711,14 @@ namespace Flow.Launcher.Infrastructure.UserSettings
FullPathOpen,
Directory
}
[EnumLocalize]
public enum HistoryStyle
{
[EnumLocalizeKey(nameof(Localize.queryHistory))]
Query,
[EnumLocalizeKey(nameof(Localize.executedHistory))]
LastOpened
}
}

View file

@ -185,7 +185,7 @@
</Target>
<Target Name="RemoveDuplicateAnalyzers" BeforeTargets="CoreCompile">
<!-- Work around https://github.com/dotnet/wpf/issues/6792 -->
<!-- Workaround https://github.com/dotnet/wpf/issues/6792 -->
<ItemGroup>
<FilteredAnalyzer Include="@(Analyzer-&gt;Distinct())" />
<Analyzer Remove="@(Analyzer)" />

View file

@ -0,0 +1,45 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Plugin;
using Flow.Launcher.Storage;
namespace Flow.Launcher.Helper;
#nullable enable
public static class ResultHelper
{
public static async Task<Result?> PopulateResultsAsync(LastOpenedHistoryItem item)
{
return await PopulateResultsAsync(item.PluginID, item.Query, item.Title, item.SubTitle, item.RecordKey);
}
public static async Task<Result?> PopulateResultsAsync(string pluginId, string rawQuery, string title, string subTitle, string recordKey)
{
var plugin = PluginManager.GetPluginForId(pluginId);
if (plugin == null) return null;
var query = QueryBuilder.Build(rawQuery, PluginManager.NonGlobalPlugins);
if (query == null) return null;
try
{
var freshResults = await plugin.Plugin.QueryAsync(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))
{
return freshResults?.FirstOrDefault(r => r.Title == title && r.SubTitle == subTitle);
}
else
{
return freshResults?.FirstOrDefault(r => r.RecordKey == recordKey) ??
freshResults?.FirstOrDefault(r => r.Title == title && r.SubTitle == subTitle);
}
}
catch (System.Exception e)
{
App.API.LogException(nameof(ResultHelper), $"Failed to query results for {plugin.Metadata.Name}", e);
return null;
}
}
}

View file

@ -166,6 +166,10 @@
<system:String x:Key="homePageToolTip">Show home page results when query text is empty.</system:String>
<system:String x:Key="historyResultsForHomePage">Show History Results in Home Page</system:String>
<system:String x:Key="historyResultsCountForHomePage">Maximum History Results Shown in Home Page</system:String>
<system:String x:Key="historyStyle">History Style</system:String>
<system:String x:Key="historyStyleTooltip">Choose the type of history to show in the History and Home Page</system:String>
<system:String x:Key="queryHistory">Query history</system:String>
<system:String x:Key="executedHistory">Last opened history</system:String>
<system:String x:Key="homeToggleBoxToolTip">This can only be edited if plugin supports Home feature and Home Page is enabled.</system:String>
<system:String x:Key="showAtTopmost">Show Search Window at Foremost</system:String>
<system:String x:Key="showAtTopmostToolTip">Overrides other programs' 'Always on Top' setting and displays Flow in the foremost position.</system:String>

View file

@ -322,6 +322,7 @@ namespace Flow.Launcher
break;
case nameof(Settings.ShowHomePage):
case nameof(Settings.ShowHistoryResultsForHomePage):
case nameof(Settings.HistoryStyle):
if (_viewModel.QueryResultsSelected() && string.IsNullOrEmpty(_viewModel.QueryText))
{
_viewModel.QueryResults();
@ -859,7 +860,7 @@ namespace Flow.Launcher
public void UpdatePosition()
{
// Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910
// Initialize call twice to workaround multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
InitializeDialogJumpPosition();
@ -883,7 +884,7 @@ namespace Flow.Launcher
private void InitializePosition()
{
// Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910
// Initialize call twice to workaround multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910
InitializePositionInner();
InitializePositionInner();
return;

View file

@ -147,6 +147,8 @@ public partial class SettingsPaneGeneralViewModel : BaseModel
public List<LastQueryModeData> LastQueryModes { get; } =
DropdownDataGeneric<LastQueryMode>.GetValues<LastQueryModeData>("LastQuery");
public List<HistoryStyleLocalized> HistoryStyles { get; } = HistoryStyleLocalized.GetValues();
public bool EnableDialogJump
{
get => Settings.EnableDialogJump;
@ -213,6 +215,7 @@ public partial class SettingsPaneGeneralViewModel : BaseModel
DropdownDataGeneric<SearchWindowAligns>.UpdateLabels(SearchWindowAligns);
DropdownDataGeneric<SearchPrecisionScore>.UpdateLabels(SearchPrecisionScores);
DropdownDataGeneric<LastQueryMode>.UpdateLabels(LastQueryModes);
HistoryStyleLocalized.UpdateLabels(HistoryStyles);
DropdownDataGeneric<DoublePinyinSchemas>.UpdateLabels(DoublePinyinSchemas);
DropdownDataGeneric<DialogJumpWindowPositions>.UpdateLabels(DialogJumpWindowPositions);
DropdownDataGeneric<DialogJumpResultBehaviours>.UpdateLabels(DialogJumpResultBehaviours);

View file

@ -245,6 +245,22 @@
SelectedValuePath="Value" />
</ui:SettingsCard>
<ui:SettingsCard
Margin="0 14 0 0"
Description="{DynamicResource historyStyleTooltip}"
Header="{DynamicResource historyStyle}">
<ui:SettingsCard.HeaderIcon>
<ui:FontIcon Glyph="&#xE81C;" />
</ui:SettingsCard.HeaderIcon>
<ComboBox
MaxWidth="200"
DisplayMemberPath="Display"
ItemsSource="{Binding HistoryStyles}"
SelectedValue="{Binding Settings.HistoryStyle}"
SelectedValuePath="Value" />
</ui:SettingsCard>
<ui:SettingsCard
Margin="0 14 0 0"
Description="{DynamicResource autoRestartAfterChangingToolTip}"

View file

@ -1,7 +1,8 @@
using System;
using System;
namespace Flow.Launcher.Storage
{
[Obsolete("Use LastOpenedHistoryItem instead. This class will be removed in future versions.")]
public class HistoryItem
{
public string Query { get; set; }
@ -42,4 +43,4 @@ namespace Flow.Launcher.Storage
return string.Empty;
}
}
}
}

View file

@ -0,0 +1,31 @@
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 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

@ -2,33 +2,63 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Storage
{
public class History
{
[JsonInclude]
public List<HistoryItem> Items { get; private set; } = new List<HistoryItem>();
#pragma warning disable CS0618 // Type or member is obsolete
public List<HistoryItem> Items { get; private set; } = [];
#pragma warning restore CS0618 // Type or member is obsolete
private int _maxHistory = 300;
[JsonInclude]
public List<LastOpenedHistoryItem> LastOpenedHistoryItems { get; private set; } = [];
public void Add(string query)
private readonly int _maxHistory = 300;
public void PopulateHistoryFromLegacyHistory()
{
if (string.IsNullOrEmpty(query)) return;
if (Items.Count > _maxHistory)
if (Items.Count == 0) return;
// Migrate old history items to new LastOpenedHistoryItems
foreach (var item in Items)
{
Items.RemoveAt(0);
LastOpenedHistoryItems.Add(new LastOpenedHistoryItem
{
Query = item.Query,
ExecutedDateTime = item.ExecutedDateTime
});
}
Items.Clear();
}
public void Add(Result result)
{
if (string.IsNullOrEmpty(result.OriginQuery.RawQuery)) return;
if (string.IsNullOrEmpty(result.PluginID)) return;
// Maintain the max history limit
if (LastOpenedHistoryItems.Count > _maxHistory)
{
LastOpenedHistoryItems.RemoveAt(0);
}
if (Items.Count > 0 && Items.Last().Query == query)
// If the last item is the same as the current result, just update the timestamp
if (LastOpenedHistoryItems.Count > 0 &&
LastOpenedHistoryItems.Last().Equals(result))
{
Items.Last().ExecutedDateTime = DateTime.Now;
LastOpenedHistoryItems.Last().ExecutedDateTime = DateTime.Now;
}
else
{
Items.Add(new HistoryItem
LastOpenedHistoryItems.Add(new LastOpenedHistoryItem
{
Query = query,
Title = result.Title,
SubTitle = result.SubTitle,
PluginID = result.PluginID,
Query = result.OriginQuery.RawQuery,
RecordKey = result.RecordKey,
ExecutedDateTime = DateTime.Now
});
}

View file

@ -8,16 +8,17 @@ using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
@ -151,6 +152,7 @@ namespace Flow.Launcher.ViewModel
_userSelectedRecordStorage = new FlowLauncherJsonStorage<UserSelectedRecord>();
_topMostRecord = new FlowLauncherJsonStorageTopMostRecord();
_history = _historyItemsStorage.Load();
_history.PopulateHistoryFromLegacyHistory();
_userSelectedRecord = _userSelectedRecordStorage.Load();
ContextMenu = new ResultsViewModel(Settings, this)
@ -352,7 +354,7 @@ namespace Flow.Launcher.ViewModel
if (QueryResultsSelected())
{
SelectedResults = History;
History.SelectedIndex = _history.Items.Count - 1;
History.SelectedIndex = _history.LastOpenedHistoryItems.Count - 1;
}
else
{
@ -380,10 +382,11 @@ namespace Flow.Launcher.ViewModel
[RelayCommand]
public void ReverseHistory()
{
if (_history.Items.Count > 0)
var historyItems = _history.LastOpenedHistoryItems;
if (historyItems.Count > 0)
{
ChangeQueryText(_history.Items[^lastHistoryIndex].Query);
if (lastHistoryIndex < _history.Items.Count)
ChangeQueryText(historyItems[^lastHistoryIndex].Query);
if (lastHistoryIndex < historyItems.Count)
{
lastHistoryIndex++;
}
@ -393,9 +396,10 @@ namespace Flow.Launcher.ViewModel
[RelayCommand]
public void ForwardHistory()
{
if (_history.Items.Count > 0)
var historyItems = _history.LastOpenedHistoryItems;
if (historyItems.Count > 0)
{
ChangeQueryText(_history.Items[^lastHistoryIndex].Query);
ChangeQueryText(historyItems[^lastHistoryIndex].Query);
if (lastHistoryIndex > 1)
{
lastHistoryIndex--;
@ -487,6 +491,8 @@ namespace Flow.Launcher.ViewModel
[RelayCommand]
private async Task OpenResultAsync(string index)
{
// Must check query results selected before executing the action
var queryResultsSelected = QueryResultsSelected();
var results = SelectedResults;
if (index is not null)
{
@ -527,10 +533,12 @@ namespace Flow.Launcher.ViewModel
}
}
if (QueryResultsSelected())
// Record user selected result for result ranking
_userSelectedRecord.Add(result);
// Add item to history only if it is from results but not context menu or history
if (queryResultsSelected)
{
_userSelectedRecord.Add(result);
_history.Add(result.OriginQuery.RawQuery);
_history.Add(result);
lastHistoryIndex = 1;
}
}
@ -559,7 +567,7 @@ namespace Flow.Launcher.ViewModel
resultsCopy.Add(resultCopy);
}
}
return resultsCopy;
}
@ -606,10 +614,11 @@ namespace Flow.Launcher.ViewModel
[RelayCommand]
private void SelectPrevItem()
{
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.
&& _history.Items.Count > 0) // Have history items
&& historyItems.Count > 0) // Have history items
{
lastHistoryIndex = 1;
ReverseHistory();
@ -908,7 +917,7 @@ namespace Flow.Launcher.ViewModel
private string _placeholderText;
public string PlaceholderText
{
get => string.IsNullOrEmpty(_placeholderText) ? Localize.queryTextBoxPlaceholder(): _placeholderText;
get => string.IsNullOrEmpty(_placeholderText) ? Localize.queryTextBoxPlaceholder() : _placeholderText;
set
{
_placeholderText = value;
@ -1150,6 +1159,7 @@ namespace Flow.Launcher.ViewModel
HideInternalPreview();
_ = OpenExternalPreviewAsync(path);
}
break;
case true
when !CanExternalPreviewSelectedResult(out var _):
@ -1263,18 +1273,18 @@ namespace Flow.Launcher.ViewModel
var filtered = results.Select(x => x.Clone()).Where
(
r =>
{
var match = App.API.FuzzySearch(query, r.Title);
if (!match.IsSearchPrecisionScoreMet())
{
match = App.API.FuzzySearch(query, r.SubTitle);
}
{
var match = App.API.FuzzySearch(query, r.Title);
if (!match.IsSearchPrecisionScoreMet())
{
match = App.API.FuzzySearch(query, r.SubTitle);
}
if (!match.IsSearchPrecisionScoreMet()) return false;
if (!match.IsSearchPrecisionScoreMet()) return false;
r.Score = match.Score;
return true;
}).ToList();
r.Score = match.Score;
return true;
}).ToList();
ContextMenu.AddResults(filtered, id);
}
else
@ -1290,7 +1300,7 @@ namespace Flow.Launcher.ViewModel
var query = QueryText.ToLower().Trim();
History.Clear();
var results = GetHistoryItems(_history.Items);
var results = GetHistoryItems(_history.LastOpenedHistoryItems);
if (!string.IsNullOrEmpty(query))
{
@ -1307,26 +1317,64 @@ namespace Flow.Launcher.ViewModel
}
}
private static List<Result> GetHistoryItems(IEnumerable<HistoryItem> historyItems)
private List<Result> GetHistoryItems(IEnumerable<LastOpenedHistoryItem> historyItems)
{
var results = new List<Result>();
foreach (var h in historyItems)
if (Settings.HistoryStyle == HistoryStyle.Query)
{
var result = new Result
foreach (var h in historyItems)
{
Title = Localize.executeQuery(h.Query),
SubTitle = Localize.lastExecuteTime(h.ExecutedDateTime),
IcoPath = Constant.HistoryIcon,
OriginQuery = new Query { RawQuery = h.Query },
Action = _ =>
var result = new Result
{
App.API.BackToQueryResults();
App.API.ChangeQuery(h.Query);
return false;
},
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C")
};
results.Add(result);
Title = Localize.executeQuery(h.Query),
SubTitle = Localize.lastExecuteTime(h.ExecutedDateTime),
IcoPath = Constant.HistoryIcon,
OriginQuery = new Query { RawQuery = h.Query },
Action = _ =>
{
App.API.BackToQueryResults();
App.API.ChangeQuery(h.Query);
return false;
},
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C")
};
results.Add(result);
}
}
else
{
foreach (var h in historyItems)
{
var 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),
IcoPath = Constant.HistoryIcon,
OriginQuery = new Query { RawQuery = h.Query },
AsyncAction = async c =>
{
var reflectResult = await ResultHelper.PopulateResultsAsync(h);
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(h.Query);
return false;
},
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C")
};
results.Add(result);
}
}
return results;
}
@ -1558,7 +1606,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.Items.TakeLast(Settings.MaxHistoryResultsToShowForHomePage).Reverse();
var historyItems = _history.LastOpenedHistoryItems.TakeLast(Settings.MaxHistoryResultsToShowForHomePage).Reverse();
var results = GetHistoryItems(historyItems);