diff --git a/Directory.Build.props b/Directory.Build.props index a5545af12..b8c1d13ea 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ true - + false \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index f70c4559b..6adefdb6a 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -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 + } } diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index 8c7670426..576bf6f2f 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -185,7 +185,7 @@ - + diff --git a/Flow.Launcher/Helper/ResultHelper.cs b/Flow.Launcher/Helper/ResultHelper.cs new file mode 100644 index 000000000..389b06b4f --- /dev/null +++ b/Flow.Launcher/Helper/ResultHelper.cs @@ -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 PopulateResultsAsync(LastOpenedHistoryItem item) + { + return await PopulateResultsAsync(item.PluginID, item.Query, item.Title, item.SubTitle, item.RecordKey); + } + + public static async Task 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; + } + } +} diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index a51782f40..2ced29353 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -166,6 +166,10 @@ Show home page results when query text is empty. Show History Results in Home Page Maximum History Results Shown in Home Page + History Style + Choose the type of history to show in the History and Home Page + Query history + Last opened history This can only be edited if plugin supports Home feature and Home Page is enabled. Show Search Window at Foremost Overrides other programs' 'Always on Top' setting and displays Flow in the foremost position. diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index b2ba33269..530ca8488 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -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; diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs index 6641ac689..aa78849ba 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs @@ -147,6 +147,8 @@ public partial class SettingsPaneGeneralViewModel : BaseModel public List LastQueryModes { get; } = DropdownDataGeneric.GetValues("LastQuery"); + public List HistoryStyles { get; } = HistoryStyleLocalized.GetValues(); + public bool EnableDialogJump { get => Settings.EnableDialogJump; @@ -213,6 +215,7 @@ public partial class SettingsPaneGeneralViewModel : BaseModel DropdownDataGeneric.UpdateLabels(SearchWindowAligns); DropdownDataGeneric.UpdateLabels(SearchPrecisionScores); DropdownDataGeneric.UpdateLabels(LastQueryModes); + HistoryStyleLocalized.UpdateLabels(HistoryStyles); DropdownDataGeneric.UpdateLabels(DoublePinyinSchemas); DropdownDataGeneric.UpdateLabels(DialogJumpWindowPositions); DropdownDataGeneric.UpdateLabels(DialogJumpResultBehaviours); diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index e4445b0f2..720cb440b 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -245,6 +245,22 @@ SelectedValuePath="Value" /> + + + + + + + + Items { get; private set; } = new List(); +#pragma warning disable CS0618 // Type or member is obsolete + public List Items { get; private set; } = []; +#pragma warning restore CS0618 // Type or member is obsolete - private int _maxHistory = 300; + [JsonInclude] + public List 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 }); } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index ec19e2e8b..706be8bb8 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -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(); _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 GetHistoryItems(IEnumerable historyItems) + private List GetHistoryItems(IEnumerable historyItems) { var results = new List(); - 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);