From 980b792cb360515f4d4b17c49a955a156a50bc15 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 1 May 2025 11:06:52 +0800 Subject: [PATCH 01/12] Wrap operation in one class --- Flow.Launcher/Storage/TopMostRecord.cs | 34 ++++++++++++++++++++++++ Flow.Launcher/ViewModel/MainViewModel.cs | 8 +++--- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher/Storage/TopMostRecord.cs b/Flow.Launcher/Storage/TopMostRecord.cs index 7f35904a520..63f0a2fcebd 100644 --- a/Flow.Launcher/Storage/TopMostRecord.cs +++ b/Flow.Launcher/Storage/TopMostRecord.cs @@ -1,10 +1,44 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Text.Json.Serialization; +using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Plugin; namespace Flow.Launcher.Storage { + public class FlowLauncherJsonStorageTopMostRecord : ISavable + { + private readonly FlowLauncherJsonStorage _topMostRecordStorage; + + private readonly TopMostRecord _topMostRecord; + + public FlowLauncherJsonStorageTopMostRecord() + { + _topMostRecordStorage = new FlowLauncherJsonStorage(); + _topMostRecord = _topMostRecordStorage.Load(); + } + + public void Save() + { + _topMostRecordStorage.Save(); + } + + public bool IsTopMost(Result result) + { + return _topMostRecord.IsTopMost(result); + } + + public void Remove(Result result) + { + _topMostRecord.Remove(result); + } + + public void AddOrUpdate(Result result) + { + _topMostRecord.AddOrUpdate(result); + } + } + public class TopMostRecord { [JsonInclude] diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 00675149b41..54ad6c288cd 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -37,11 +37,10 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable private readonly FlowLauncherJsonStorage _historyItemsStorage; private readonly FlowLauncherJsonStorage _userSelectedRecordStorage; - private readonly FlowLauncherJsonStorage _topMostRecordStorage; + private readonly FlowLauncherJsonStorageTopMostRecord _topMostRecord; private readonly History _history; private int lastHistoryIndex = 1; private readonly UserSelectedRecord _userSelectedRecord; - private readonly TopMostRecord _topMostRecord; private CancellationTokenSource _updateSource; private CancellationToken _updateToken; @@ -134,10 +133,9 @@ public MainViewModel() _historyItemsStorage = new FlowLauncherJsonStorage(); _userSelectedRecordStorage = new FlowLauncherJsonStorage(); - _topMostRecordStorage = new FlowLauncherJsonStorage(); + _topMostRecord = new FlowLauncherJsonStorageTopMostRecord(); _history = _historyItemsStorage.Load(); _userSelectedRecord = _userSelectedRecordStorage.Load(); - _topMostRecord = _topMostRecordStorage.Load(); ContextMenu = new ResultsViewModel(Settings) { @@ -1612,7 +1610,7 @@ public void Save() { _historyItemsStorage.Save(); _userSelectedRecordStorage.Save(); - _topMostRecordStorage.Save(); + _topMostRecord.Save(); } /// From 0d619085753cb8e443044c6cd016b86b5fc5b1d3 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 1 May 2025 11:41:53 +0800 Subject: [PATCH 02/12] Support multiple topmost records --- .../Storage/JsonStorage.cs | 5 + Flow.Launcher/Storage/TopMostRecord.cs | 137 ++++++++++++++++-- 2 files changed, 132 insertions(+), 10 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs index 0b10382ee9b..20aacb3447e 100644 --- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs @@ -45,6 +45,11 @@ public JsonStorage(string filePath) FilesFolders.ValidateDirectory(DirectoryPath); } + public bool Exists() + { + return File.Exists(FilePath); + } + public async Task LoadAsync() { if (Data != null) diff --git a/Flow.Launcher/Storage/TopMostRecord.cs b/Flow.Launcher/Storage/TopMostRecord.cs index 63f0a2fcebd..25774a2c0e4 100644 --- a/Flow.Launcher/Storage/TopMostRecord.cs +++ b/Flow.Launcher/Storage/TopMostRecord.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Text.Json.Serialization; using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Plugin; @@ -8,14 +9,29 @@ namespace Flow.Launcher.Storage { public class FlowLauncherJsonStorageTopMostRecord : ISavable { - private readonly FlowLauncherJsonStorage _topMostRecordStorage; - - private readonly TopMostRecord _topMostRecord; + private readonly FlowLauncherJsonStorage _topMostRecordStorage; + private readonly MultipleTopMostRecord _topMostRecord; public FlowLauncherJsonStorageTopMostRecord() { - _topMostRecordStorage = new FlowLauncherJsonStorage(); - _topMostRecord = _topMostRecordStorage.Load(); + var topMostRecordStorage = new FlowLauncherJsonStorage(); + var exist = topMostRecordStorage.Exists(); + if (exist) + { + // Get old data + var topMostRecord = topMostRecordStorage.Load(); + + // Convert to new data + _topMostRecordStorage = new FlowLauncherJsonStorage(); + _topMostRecord = _topMostRecordStorage.Load(); + _topMostRecord.Add(topMostRecord); + } + else + { + // Get new data + _topMostRecordStorage = new FlowLauncherJsonStorage(); + _topMostRecord = _topMostRecordStorage.Load(); + } } public void Save() @@ -42,7 +58,7 @@ public void AddOrUpdate(Result result) public class TopMostRecord { [JsonInclude] - public ConcurrentDictionary records { get; private set; } = new ConcurrentDictionary(); + public ConcurrentDictionary records { get; private set; } = new(); internal bool IsTopMost(Result result) { @@ -90,12 +106,113 @@ internal void AddOrUpdate(Result result) } } + public class MultipleTopMostRecord + { + [JsonInclude] + public ConcurrentDictionary> records { get; private set; } = new(); + + internal void Add(TopMostRecord topMostRecord) + { + if (topMostRecord == null || topMostRecord.records.IsEmpty) + { + return; + } + + foreach (var record in topMostRecord.records) + { + records.AddOrUpdate(record.Key, new ConcurrentBag { record.Value }, (key, oldValue) => + { + oldValue.Add(record.Value); + return oldValue; + }); + } + } + + internal bool IsTopMost(Result result) + { + // origin query is null when user select the context menu item directly of one item from query list + // in this case, we do not need to check if the result is top most + if (records.IsEmpty || result.OriginQuery == null || + !records.TryGetValue(result.OriginQuery.RawQuery, out var value)) + { + return false; + } + + // since this dictionary should be very small (or empty) going over it should be pretty fast. + return value.Any(record => record.Equals(result)); + } + + internal void Remove(Result result) + { + // origin query is null when user select the context menu item directly of one item from query list + // in this case, we do not need to remove the record + if (result.OriginQuery == null || + !records.TryGetValue(result.OriginQuery.RawQuery, out var value)) + { + return; + } + + // remove the record from the bag + var recordToRemove = value.FirstOrDefault(r => r.Equals(result)); + if (recordToRemove != null) + { + value.TryTake(out recordToRemove); + } + } + + internal void AddOrUpdate(Result result) + { + // origin query is null when user select the context menu item directly of one item from query list + // in this case, we do not need to add or update the record + if (result.OriginQuery == null) + { + return; + } + + var record = new Record + { + PluginID = result.PluginID, + Title = result.Title, + SubTitle = result.SubTitle, + RecordKey = result.RecordKey + }; + if (!records.TryGetValue(result.OriginQuery.RawQuery, out var value)) + { + // create a new bag if it does not exist + value = new ConcurrentBag() + { + record + }; + records.TryAdd(result.OriginQuery.RawQuery, value); + } + else + { + // add or update the record in the bag + if (value.Any(r => r.Equals(result))) + { + // update the record + var recordToUpdate = value.FirstOrDefault(r => r.Equals(result)); + if (recordToUpdate != null) + { + value.TryTake(out recordToUpdate); + value.Add(record); + } + } + else + { + // add the record + value.Add(record); + } + } + } + } + public class Record { - public string Title { get; set; } - public string SubTitle { get; set; } - public string PluginID { get; set; } - public string RecordKey { get; set; } + public string Title { get; init; } + public string SubTitle { get; init; } + public string PluginID { get; init; } + public string RecordKey { get; init; } public bool Equals(Result r) { From af087fb85b50096b69fc8965436b109eecdc3950 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 1 May 2025 12:49:20 +0800 Subject: [PATCH 03/12] Support multiple topmost records storage --- .../Storage/JsonStorage.cs | 16 ++++ Flow.Launcher/Storage/TopMostRecord.cs | 78 ++++++++++++++++--- 2 files changed, 82 insertions(+), 12 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs index 20aacb3447e..db9ccc28a7e 100644 --- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs @@ -50,6 +50,22 @@ public bool Exists() return File.Exists(FilePath); } + public void Delete() + { + if (File.Exists(FilePath)) + { + File.Delete(FilePath); + } + if (File.Exists(BackupFilePath)) + { + File.Delete(BackupFilePath); + } + if (File.Exists(TempFilePath)) + { + File.Delete(TempFilePath); + } + } + public async Task LoadAsync() { if (Data != null) diff --git a/Flow.Launcher/Storage/TopMostRecord.cs b/Flow.Launcher/Storage/TopMostRecord.cs index 25774a2c0e4..f7459dc402b 100644 --- a/Flow.Launcher/Storage/TopMostRecord.cs +++ b/Flow.Launcher/Storage/TopMostRecord.cs @@ -1,35 +1,64 @@ -using System.Collections.Concurrent; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Text.Json.Serialization; using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Plugin; namespace Flow.Launcher.Storage { - public class FlowLauncherJsonStorageTopMostRecord : ISavable + public class FlowLauncherJsonStorageTopMostRecord { private readonly FlowLauncherJsonStorage _topMostRecordStorage; private readonly MultipleTopMostRecord _topMostRecord; public FlowLauncherJsonStorageTopMostRecord() { + // Get old data & new data var topMostRecordStorage = new FlowLauncherJsonStorage(); - var exist = topMostRecordStorage.Exists(); - if (exist) - { - // Get old data - var topMostRecord = topMostRecordStorage.Load(); + _topMostRecordStorage = new FlowLauncherJsonStorage(); + + // Check if data exist + var oldDataExist = topMostRecordStorage.Exists(); + var newDataExist = _topMostRecordStorage.Exists(); - // Convert to new data - _topMostRecordStorage = new FlowLauncherJsonStorage(); + // If new data exist, it means we have already migrated the old data + // So we can safely delete the old data and load the new data + if (newDataExist) + { + try + { + topMostRecordStorage.Delete(); + } + catch + { + // Ignored + } _topMostRecord = _topMostRecordStorage.Load(); - _topMostRecord.Add(topMostRecord); } + // If new data does not exist and old data exist, we need to migrate the old data to the new data + else if (oldDataExist) + { + // Migrate old data to new data + _topMostRecord = _topMostRecordStorage.Load(); + _topMostRecord.Add(topMostRecordStorage.Load()); + + // Delete old data and save the new data + try + { + topMostRecordStorage.Delete(); + } + catch + { + // Ignored + } + Save(); + } + // If both data do not exist, we just need to create a new data else { - // Get new data - _topMostRecordStorage = new FlowLauncherJsonStorage(); _topMostRecord = _topMostRecordStorage.Load(); } } @@ -109,6 +138,7 @@ internal void AddOrUpdate(Result result) public class MultipleTopMostRecord { [JsonInclude] + [JsonConverter(typeof(ConcurrentDictionaryConcurrentBagConverter))] public ConcurrentDictionary> records { get; private set; } = new(); internal void Add(TopMostRecord topMostRecord) @@ -207,6 +237,30 @@ internal void AddOrUpdate(Result result) } } + public class ConcurrentDictionaryConcurrentBagConverter : JsonConverter>> + { + public override ConcurrentDictionary> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var dictionary = JsonSerializer.Deserialize>>(ref reader, options); + var concurrentDictionary = new ConcurrentDictionary>(); + foreach (var kvp in dictionary) + { + concurrentDictionary.TryAdd(kvp.Key, new ConcurrentBag(kvp.Value)); + } + return concurrentDictionary; + } + + public override void Write(Utf8JsonWriter writer, ConcurrentDictionary> value, JsonSerializerOptions options) + { + var dict = new Dictionary>(); + foreach (var kvp in value) + { + dict.Add(kvp.Key, kvp.Value.ToList()); + } + JsonSerializer.Serialize(writer, dict, options); + } + } + public class Record { public string Title { get; init; } From 845c331cac40301da43d107ae547c2818faaff29 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 1 May 2025 13:16:16 +0800 Subject: [PATCH 04/12] Remove the bag from dictionary if the bag is empty --- Flow.Launcher/Storage/TopMostRecord.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Flow.Launcher/Storage/TopMostRecord.cs b/Flow.Launcher/Storage/TopMostRecord.cs index f7459dc402b..aaf7194e706 100644 --- a/Flow.Launcher/Storage/TopMostRecord.cs +++ b/Flow.Launcher/Storage/TopMostRecord.cs @@ -188,6 +188,12 @@ internal void Remove(Result result) { value.TryTake(out recordToRemove); } + + // if the bag is empty, remove the bag from the dictionary + if (value.IsEmpty) + { + records.TryRemove(result.OriginQuery.RawQuery, out _); + } } internal void AddOrUpdate(Result result) From 07f44f23771e4ac21f5b837fcee66a884513a282 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 1 May 2025 13:19:23 +0800 Subject: [PATCH 05/12] Add code comments --- Flow.Launcher/Storage/TopMostRecord.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Flow.Launcher/Storage/TopMostRecord.cs b/Flow.Launcher/Storage/TopMostRecord.cs index aaf7194e706..d604eabc180 100644 --- a/Flow.Launcher/Storage/TopMostRecord.cs +++ b/Flow.Launcher/Storage/TopMostRecord.cs @@ -243,6 +243,9 @@ internal void AddOrUpdate(Result result) } } + /// + /// Because ConcurrentBag does not support serialization, we need to convert it to a List + /// public class ConcurrentDictionaryConcurrentBagConverter : JsonConverter>> { public override ConcurrentDictionary> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) From 0673d07c2a14a6595d4c495c1ab7b3fde3395782 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 1 May 2025 13:23:22 +0800 Subject: [PATCH 06/12] Improve code comments & Make classes internal --- Flow.Launcher/Storage/TopMostRecord.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Flow.Launcher/Storage/TopMostRecord.cs b/Flow.Launcher/Storage/TopMostRecord.cs index d604eabc180..7c45bcf66f3 100644 --- a/Flow.Launcher/Storage/TopMostRecord.cs +++ b/Flow.Launcher/Storage/TopMostRecord.cs @@ -34,7 +34,7 @@ public FlowLauncherJsonStorageTopMostRecord() } catch { - // Ignored + // Ignored - Flow will delete the old data during next startup } _topMostRecord = _topMostRecordStorage.Load(); } @@ -52,7 +52,7 @@ public FlowLauncherJsonStorageTopMostRecord() } catch { - // Ignored + // Ignored - Flow will delete the old data during next startup } Save(); } @@ -84,7 +84,10 @@ public void AddOrUpdate(Result result) } } - public class TopMostRecord + /// + /// Old data structure to support only one top most record for the same query + /// + internal class TopMostRecord { [JsonInclude] public ConcurrentDictionary records { get; private set; } = new(); @@ -135,7 +138,10 @@ internal void AddOrUpdate(Result result) } } - public class MultipleTopMostRecord + /// + /// New data structure to support multiple top most records for the same query + /// + internal class MultipleTopMostRecord { [JsonInclude] [JsonConverter(typeof(ConcurrentDictionaryConcurrentBagConverter))] @@ -246,7 +252,7 @@ internal void AddOrUpdate(Result result) /// /// Because ConcurrentBag does not support serialization, we need to convert it to a List /// - public class ConcurrentDictionaryConcurrentBagConverter : JsonConverter>> + internal class ConcurrentDictionaryConcurrentBagConverter : JsonConverter>> { public override ConcurrentDictionary> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { @@ -270,7 +276,7 @@ public override void Write(Utf8JsonWriter writer, ConcurrentDictionary Date: Thu, 1 May 2025 13:33:24 +0800 Subject: [PATCH 07/12] Code quality --- .../Storage/JsonStorage.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs index db9ccc28a7e..c7eba05fd5c 100644 --- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs @@ -52,17 +52,12 @@ public bool Exists() public void Delete() { - if (File.Exists(FilePath)) - { - File.Delete(FilePath); - } - if (File.Exists(BackupFilePath)) + foreach (var path in new[] { FilePath, BackupFilePath, TempFilePath }) { - File.Delete(BackupFilePath); - } - if (File.Exists(TempFilePath)) - { - File.Delete(TempFilePath); + if (File.Exists(path)) + { + File.Delete(path); + } } } From c49b3b7cba86ffadaec56e748751041f5e5c47c1 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 1 May 2025 13:42:54 +0800 Subject: [PATCH 08/12] Fix TryTake may remove the wrong element --- Flow.Launcher/Storage/TopMostRecord.cs | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/Flow.Launcher/Storage/TopMostRecord.cs b/Flow.Launcher/Storage/TopMostRecord.cs index 7c45bcf66f3..0a9921f7338 100644 --- a/Flow.Launcher/Storage/TopMostRecord.cs +++ b/Flow.Launcher/Storage/TopMostRecord.cs @@ -189,11 +189,8 @@ internal void Remove(Result result) } // remove the record from the bag - var recordToRemove = value.FirstOrDefault(r => r.Equals(result)); - if (recordToRemove != null) - { - value.TryTake(out recordToRemove); - } + var bag = new ConcurrentQueue(value.Where(r => !r.Equals(result))); + records[result.OriginQuery.RawQuery] = new ConcurrentBag(bag); // if the bag is empty, remove the bag from the dictionary if (value.IsEmpty) @@ -230,21 +227,9 @@ internal void AddOrUpdate(Result result) else { // add or update the record in the bag - if (value.Any(r => r.Equals(result))) - { - // update the record - var recordToUpdate = value.FirstOrDefault(r => r.Equals(result)); - if (recordToUpdate != null) - { - value.TryTake(out recordToUpdate); - value.Add(record); - } - } - else - { - // add the record - value.Add(record); - } + var bag = new ConcurrentQueue(value.Where(r => !r.Equals(result))); // make sure we don't have duplicates + bag.Enqueue(record); + records[result.OriginQuery.RawQuery] = new ConcurrentBag(bag); } } } From 58b9f0c19cbff0fa202701dad68c875562829456 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 1 May 2025 13:47:35 +0800 Subject: [PATCH 09/12] Improve remove logic --- Flow.Launcher/Storage/TopMostRecord.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher/Storage/TopMostRecord.cs b/Flow.Launcher/Storage/TopMostRecord.cs index 0a9921f7338..47ebb4a9598 100644 --- a/Flow.Launcher/Storage/TopMostRecord.cs +++ b/Flow.Launcher/Storage/TopMostRecord.cs @@ -190,13 +190,16 @@ internal void Remove(Result result) // remove the record from the bag var bag = new ConcurrentQueue(value.Where(r => !r.Equals(result))); - records[result.OriginQuery.RawQuery] = new ConcurrentBag(bag); - - // if the bag is empty, remove the bag from the dictionary - if (value.IsEmpty) + if (bag.IsEmpty) { + // if the bag is empty, remove the bag from the dictionary records.TryRemove(result.OriginQuery.RawQuery, out _); } + else + { + // change the bag in the dictionary + records[result.OriginQuery.RawQuery] = new ConcurrentBag(bag); + } } internal void AddOrUpdate(Result result) From 7103c8d24afa9e65e7a03a9c6721aa2eac787cc2 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 2 May 2025 09:59:06 +0800 Subject: [PATCH 10/12] Mark old data as obsolete --- Flow.Launcher/Storage/TopMostRecord.cs | 31 +++++++++++--------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/Flow.Launcher/Storage/TopMostRecord.cs b/Flow.Launcher/Storage/TopMostRecord.cs index 47ebb4a9598..7ddca721b57 100644 --- a/Flow.Launcher/Storage/TopMostRecord.cs +++ b/Flow.Launcher/Storage/TopMostRecord.cs @@ -16,8 +16,10 @@ public class FlowLauncherJsonStorageTopMostRecord public FlowLauncherJsonStorageTopMostRecord() { +#pragma warning disable CS0618 // Type or member is obsolete // Get old data & new data var topMostRecordStorage = new FlowLauncherJsonStorage(); +#pragma warning restore CS0618 // Type or member is obsolete _topMostRecordStorage = new FlowLauncherJsonStorage(); // Check if data exist @@ -43,7 +45,16 @@ public FlowLauncherJsonStorageTopMostRecord() { // Migrate old data to new data _topMostRecord = _topMostRecordStorage.Load(); - _topMostRecord.Add(topMostRecordStorage.Load()); + var oldTopMostRecord = topMostRecordStorage.Load(); + if (oldTopMostRecord == null || oldTopMostRecord.records.IsEmpty) return; + foreach (var record in oldTopMostRecord.records) + { + _topMostRecord.records.AddOrUpdate(record.Key, new ConcurrentBag { record.Value }, (key, oldValue) => + { + oldValue.Add(record.Value); + return oldValue; + }); + } // Delete old data and save the new data try @@ -87,6 +98,7 @@ public void AddOrUpdate(Result result) /// /// Old data structure to support only one top most record for the same query /// + [Obsolete("Use MultipleTopMostRecord instead. This class will be removed in future versions.")] internal class TopMostRecord { [JsonInclude] @@ -147,23 +159,6 @@ internal class MultipleTopMostRecord [JsonConverter(typeof(ConcurrentDictionaryConcurrentBagConverter))] public ConcurrentDictionary> records { get; private set; } = new(); - internal void Add(TopMostRecord topMostRecord) - { - if (topMostRecord == null || topMostRecord.records.IsEmpty) - { - return; - } - - foreach (var record in topMostRecord.records) - { - records.AddOrUpdate(record.Key, new ConcurrentBag { record.Value }, (key, oldValue) => - { - oldValue.Add(record.Value); - return oldValue; - }); - } - } - internal bool IsTopMost(Result result) { // origin query is null when user select the context menu item directly of one item from query list From b25777b73457d872637ad594cad571d65fdea43c Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 2 May 2025 10:28:35 +0800 Subject: [PATCH 11/12] Use ConcurrentQueue to storage the data sequence & Put latter record topper --- Flow.Launcher/Storage/TopMostRecord.cs | 81 ++++++++++++++++-------- Flow.Launcher/ViewModel/MainViewModel.cs | 5 +- 2 files changed, 59 insertions(+), 27 deletions(-) diff --git a/Flow.Launcher/Storage/TopMostRecord.cs b/Flow.Launcher/Storage/TopMostRecord.cs index 7ddca721b57..d75e9cb7908 100644 --- a/Flow.Launcher/Storage/TopMostRecord.cs +++ b/Flow.Launcher/Storage/TopMostRecord.cs @@ -49,9 +49,11 @@ public FlowLauncherJsonStorageTopMostRecord() if (oldTopMostRecord == null || oldTopMostRecord.records.IsEmpty) return; foreach (var record in oldTopMostRecord.records) { - _topMostRecord.records.AddOrUpdate(record.Key, new ConcurrentBag { record.Value }, (key, oldValue) => + var newValue = new ConcurrentQueue(); + newValue.Enqueue(record.Value); + _topMostRecord.records.AddOrUpdate(record.Key, newValue, (key, oldValue) => { - oldValue.Add(record.Value); + oldValue.Enqueue(record.Value); return oldValue; }); } @@ -84,6 +86,11 @@ public bool IsTopMost(Result result) return _topMostRecord.IsTopMost(result); } + public int GetTopMostIndex(Result result) + { + return _topMostRecord.GetTopMostIndex(result); + } + public void Remove(Result result) { _topMostRecord.Remove(result); @@ -156,8 +163,8 @@ internal void AddOrUpdate(Result result) internal class MultipleTopMostRecord { [JsonInclude] - [JsonConverter(typeof(ConcurrentDictionaryConcurrentBagConverter))] - public ConcurrentDictionary> records { get; private set; } = new(); + [JsonConverter(typeof(ConcurrentDictionaryConcurrentQueueConverter))] + public ConcurrentDictionary> records { get; private set; } = new(); internal bool IsTopMost(Result result) { @@ -173,6 +180,32 @@ internal bool IsTopMost(Result result) return value.Any(record => record.Equals(result)); } + internal int GetTopMostIndex(Result result) + { + // origin query is null when user select the context menu item directly of one item from query list + // in this case, we do not need to check if the result is top most + if (records.IsEmpty || result.OriginQuery == null || + !records.TryGetValue(result.OriginQuery.RawQuery, out var value)) + { + return -1; + } + + // since this dictionary should be very small (or empty) going over it should be pretty fast. + // since the latter items should be more recent, we should return the smaller index for score to subtract + // which can make them more topmost + // A, B, C => 2, 1, 0 => (max - 2), (max - 1), (max - 0) + var index = 0; + foreach (var record in value) + { + if (record.Equals(result)) + { + return value.Count - 1 - index; + } + index++; + } + return -1; + } + internal void Remove(Result result) { // origin query is null when user select the context menu item directly of one item from query list @@ -183,17 +216,17 @@ internal void Remove(Result result) return; } - // remove the record from the bag - var bag = new ConcurrentQueue(value.Where(r => !r.Equals(result))); - if (bag.IsEmpty) + // remove the record from the queue + var queue = new ConcurrentQueue(value.Where(r => !r.Equals(result))); + if (queue.IsEmpty) { - // if the bag is empty, remove the bag from the dictionary + // if the queue is empty, remove the queue from the dictionary records.TryRemove(result.OriginQuery.RawQuery, out _); } else { - // change the bag in the dictionary - records[result.OriginQuery.RawQuery] = new ConcurrentBag(bag); + // change the queue in the dictionary + records[result.OriginQuery.RawQuery] = queue; } } @@ -215,40 +248,38 @@ internal void AddOrUpdate(Result result) }; if (!records.TryGetValue(result.OriginQuery.RawQuery, out var value)) { - // create a new bag if it does not exist - value = new ConcurrentBag() - { - record - }; + // create a new queue if it does not exist + value = new ConcurrentQueue(); + value.Enqueue(record); records.TryAdd(result.OriginQuery.RawQuery, value); } else { - // add or update the record in the bag - var bag = new ConcurrentQueue(value.Where(r => !r.Equals(result))); // make sure we don't have duplicates - bag.Enqueue(record); - records[result.OriginQuery.RawQuery] = new ConcurrentBag(bag); + // add or update the record in the queue + var queue = new ConcurrentQueue(value.Where(r => !r.Equals(result))); // make sure we don't have duplicates + queue.Enqueue(record); + records[result.OriginQuery.RawQuery] = queue; } } } /// - /// Because ConcurrentBag does not support serialization, we need to convert it to a List + /// Because ConcurrentQueue does not support serialization, we need to convert it to a List /// - internal class ConcurrentDictionaryConcurrentBagConverter : JsonConverter>> + internal class ConcurrentDictionaryConcurrentQueueConverter : JsonConverter>> { - public override ConcurrentDictionary> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ConcurrentDictionary> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var dictionary = JsonSerializer.Deserialize>>(ref reader, options); - var concurrentDictionary = new ConcurrentDictionary>(); + var concurrentDictionary = new ConcurrentDictionary>(); foreach (var kvp in dictionary) { - concurrentDictionary.TryAdd(kvp.Key, new ConcurrentBag(kvp.Value)); + concurrentDictionary.TryAdd(kvp.Key, new ConcurrentQueue(kvp.Value)); } return concurrentDictionary; } - public override void Write(Utf8JsonWriter writer, ConcurrentDictionary> value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ConcurrentDictionary> value, JsonSerializerOptions options) { var dict = new Dictionary>(); foreach (var kvp in value) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 54ad6c288cd..adb04279e56 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1643,9 +1643,10 @@ public void UpdateResultView(ICollection resultsForUpdates) { foreach (var result in metaResults.Results) { - if (_topMostRecord.IsTopMost(result)) + var deviationIndex = _topMostRecord.GetTopMostIndex(result); + if (deviationIndex != -1) { - result.Score = Result.MaxScore; + result.Score = Result.MaxScore - deviationIndex; } else { From 1cf264a3a9c5d519482f5585e7fb54ad23ce9dfe Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Fri, 2 May 2025 10:32:24 +0800 Subject: [PATCH 12/12] Add code comments Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index adb04279e56..96e039ebefd 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1646,6 +1646,8 @@ public void UpdateResultView(ICollection resultsForUpdates) var deviationIndex = _topMostRecord.GetTopMostIndex(result); if (deviationIndex != -1) { + // Adjust the score based on the result's position in the top-most list. + // A lower deviationIndex (closer to the top) results in a higher score. result.Score = Result.MaxScore - deviationIndex; } else