diff --git a/Directory.Build.props b/Directory.Build.props index a5545af1248..b8c1d13ea91 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 f70c4559b38..6adefdb6a21 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -7,6 +7,7 @@ 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 @@ public bool ShowAtTopmost [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 @@ public enum DialogJumpFileResultBehaviours 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 8c7670426bb..576bf6f2f13 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 00000000000..389b06b4f72 --- /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 a51782f4040..2ced2935326 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 b2ba33269af..530ca8488bd 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -322,6 +322,7 @@ private void OnLoaded(object sender, RoutedEventArgs e) 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 @@ private void InitializeContextMenu() 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 @@ private async Task PositionResetAsync() 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 6641ac6895c..aa78849bad9 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs @@ -147,6 +147,8 @@ public bool PortableMode public List LastQueryModes { get; } = DropdownDataGeneric.GetValues("LastQuery"); + public List HistoryStyles { get; } = HistoryStyleLocalized.GetValues(); + public bool EnableDialogJump { get => Settings.EnableDialogJump; @@ -213,6 +215,7 @@ private void UpdateEnumDropdownLocalizations() 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 e4445b0f20d..720cb440b9b 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; } = []; + + private readonly int _maxHistory = 300; + + public void PopulateHistoryFromLegacyHistory() + { + if (Items.Count == 0) return; + // Migrate old history items to new LastOpenedHistoryItems + foreach (var item in Items) + { + LastOpenedHistoryItems.Add(new LastOpenedHistoryItem + { + Query = item.Query, + ExecutedDateTime = item.ExecutedDateTime + }); + } + Items.Clear(); + } - public void Add(string query) + public void Add(Result result) { - if (string.IsNullOrEmpty(query)) return; - if (Items.Count > _maxHistory) + if (string.IsNullOrEmpty(result.OriginQuery.RawQuery)) return; + if (string.IsNullOrEmpty(result.PluginID)) return; + + // Maintain the max history limit + if (LastOpenedHistoryItems.Count > _maxHistory) { - Items.RemoveAt(0); + 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 ec19e2e8b67..706be8bb8e6 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -8,16 +8,17 @@ 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 @@ public MainViewModel() _userSelectedRecordStorage = new FlowLauncherJsonStorage(); _topMostRecord = new FlowLauncherJsonStorageTopMostRecord(); _history = _historyItemsStorage.Load(); + _history.PopulateHistoryFromLegacyHistory(); _userSelectedRecord = _userSelectedRecordStorage.Load(); ContextMenu = new ResultsViewModel(Settings, this) @@ -352,7 +354,7 @@ private void LoadHistory() if (QueryResultsSelected()) { SelectedResults = History; - History.SelectedIndex = _history.Items.Count - 1; + History.SelectedIndex = _history.LastOpenedHistoryItems.Count - 1; } else { @@ -380,10 +382,11 @@ public void ReQuery(bool reselect) [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 @@ public void ReverseHistory() [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 @@ private void AutocompleteQuery() [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 @@ private async Task OpenResultAsync(string index) } } - 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 @@ private static IReadOnlyList DeepCloneResults(IReadOnlyList resu resultsCopy.Add(resultCopy); } } - + return resultsCopy; } @@ -606,10 +614,11 @@ private void SelectNextPage() [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 @@ public UserControl CustomizedPreviewControl 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 @@ when CanExternalPreviewSelectedResult(out var path): HideInternalPreview(); _ = OpenExternalPreviewAsync(path); } + break; case true when !CanExternalPreviewSelectedResult(out var _): @@ -1263,18 +1273,18 @@ private void QueryContextMenu() 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); - } - - if (!match.IsSearchPrecisionScoreMet()) return false; - - r.Score = match.Score; - return true; - }).ToList(); + { + var match = App.API.FuzzySearch(query, r.Title); + if (!match.IsSearchPrecisionScoreMet()) + { + match = App.API.FuzzySearch(query, r.SubTitle); + } + + if (!match.IsSearchPrecisionScoreMet()) return false; + + r.Score = match.Score; + return true; + }).ToList(); ContextMenu.AddResults(filtered, id); } else @@ -1290,7 +1300,7 @@ private void QueryHistory() 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 @@ private void QueryHistory() } } - 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 @@ await PluginManager.QueryHomeForPluginAsync(plugin, query, token) : 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);