Skip to content

Commit c1ba81e

Browse files
authored
Merge pull request #3500 from Flow-Launcher/multiple_topmost
Support Multiple Topmost Records
2 parents 7023f83 + c64f3df commit c1ba81e

File tree

3 files changed

+266
-15
lines changed

3 files changed

+266
-15
lines changed

Flow.Launcher.Infrastructure/Storage/JsonStorage.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,22 @@ public JsonStorage(string filePath)
4545
FilesFolders.ValidateDirectory(DirectoryPath);
4646
}
4747

48+
public bool Exists()
49+
{
50+
return File.Exists(FilePath);
51+
}
52+
53+
public void Delete()
54+
{
55+
foreach (var path in new[] { FilePath, BackupFilePath, TempFilePath })
56+
{
57+
if (File.Exists(path))
58+
{
59+
File.Delete(path);
60+
}
61+
}
62+
}
63+
4864
public async Task<T> LoadAsync()
4965
{
5066
if (Data != null)

Flow.Launcher/Storage/TopMostRecord.cs

Lines changed: 242 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,115 @@
1-
using System.Collections.Concurrent;
1+
using System;
2+
using System.Collections.Concurrent;
23
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Text.Json;
36
using System.Text.Json.Serialization;
7+
using Flow.Launcher.Infrastructure.Storage;
48
using Flow.Launcher.Plugin;
59

610
namespace Flow.Launcher.Storage
711
{
8-
public class TopMostRecord
12+
public class FlowLauncherJsonStorageTopMostRecord
13+
{
14+
private readonly FlowLauncherJsonStorage<MultipleTopMostRecord> _topMostRecordStorage;
15+
private readonly MultipleTopMostRecord _topMostRecord;
16+
17+
public FlowLauncherJsonStorageTopMostRecord()
18+
{
19+
#pragma warning disable CS0618 // Type or member is obsolete
20+
// Get old data & new data
21+
var topMostRecordStorage = new FlowLauncherJsonStorage<TopMostRecord>();
22+
#pragma warning restore CS0618 // Type or member is obsolete
23+
_topMostRecordStorage = new FlowLauncherJsonStorage<MultipleTopMostRecord>();
24+
25+
// Check if data exist
26+
var oldDataExist = topMostRecordStorage.Exists();
27+
var newDataExist = _topMostRecordStorage.Exists();
28+
29+
// If new data exist, it means we have already migrated the old data
30+
// So we can safely delete the old data and load the new data
31+
if (newDataExist)
32+
{
33+
try
34+
{
35+
topMostRecordStorage.Delete();
36+
}
37+
catch
38+
{
39+
// Ignored - Flow will delete the old data during next startup
40+
}
41+
_topMostRecord = _topMostRecordStorage.Load();
42+
}
43+
// If new data does not exist and old data exist, we need to migrate the old data to the new data
44+
else if (oldDataExist)
45+
{
46+
// Migrate old data to new data
47+
_topMostRecord = _topMostRecordStorage.Load();
48+
var oldTopMostRecord = topMostRecordStorage.Load();
49+
if (oldTopMostRecord == null || oldTopMostRecord.records.IsEmpty) return;
50+
foreach (var record in oldTopMostRecord.records)
51+
{
52+
var newValue = new ConcurrentQueue<Record>();
53+
newValue.Enqueue(record.Value);
54+
_topMostRecord.records.AddOrUpdate(record.Key, newValue, (key, oldValue) =>
55+
{
56+
oldValue.Enqueue(record.Value);
57+
return oldValue;
58+
});
59+
}
60+
61+
// Delete old data and save the new data
62+
try
63+
{
64+
topMostRecordStorage.Delete();
65+
}
66+
catch
67+
{
68+
// Ignored - Flow will delete the old data during next startup
69+
}
70+
Save();
71+
}
72+
// If both data do not exist, we just need to create a new data
73+
else
74+
{
75+
_topMostRecord = _topMostRecordStorage.Load();
76+
}
77+
}
78+
79+
public void Save()
80+
{
81+
_topMostRecordStorage.Save();
82+
}
83+
84+
public bool IsTopMost(Result result)
85+
{
86+
return _topMostRecord.IsTopMost(result);
87+
}
88+
89+
public int GetTopMostIndex(Result result)
90+
{
91+
return _topMostRecord.GetTopMostIndex(result);
92+
}
93+
94+
public void Remove(Result result)
95+
{
96+
_topMostRecord.Remove(result);
97+
}
98+
99+
public void AddOrUpdate(Result result)
100+
{
101+
_topMostRecord.AddOrUpdate(result);
102+
}
103+
}
104+
105+
/// <summary>
106+
/// Old data structure to support only one top most record for the same query
107+
/// </summary>
108+
[Obsolete("Use MultipleTopMostRecord instead. This class will be removed in future versions.")]
109+
internal class TopMostRecord
9110
{
10111
[JsonInclude]
11-
public ConcurrentDictionary<string, Record> records { get; private set; } = new ConcurrentDictionary<string, Record>();
112+
public ConcurrentDictionary<string, Record> records { get; private set; } = new();
12113

13114
internal bool IsTopMost(Result result)
14115
{
@@ -39,12 +140,145 @@ internal void AddOrUpdate(Result result)
39140
}
40141
}
41142

42-
public class Record
143+
/// <summary>
144+
/// New data structure to support multiple top most records for the same query
145+
/// </summary>
146+
internal class MultipleTopMostRecord
147+
{
148+
[JsonInclude]
149+
[JsonConverter(typeof(ConcurrentDictionaryConcurrentQueueConverter))]
150+
public ConcurrentDictionary<string, ConcurrentQueue<Record>> records { get; private set; } = new();
151+
152+
internal bool IsTopMost(Result result)
153+
{
154+
// origin query is null when user select the context menu item directly of one item from query list
155+
// in this case, we do not need to check if the result is top most
156+
if (records.IsEmpty || result.OriginQuery == null ||
157+
!records.TryGetValue(result.OriginQuery.RawQuery, out var value))
158+
{
159+
return false;
160+
}
161+
162+
// since this dictionary should be very small (or empty) going over it should be pretty fast.
163+
return value.Any(record => record.Equals(result));
164+
}
165+
166+
internal int GetTopMostIndex(Result result)
167+
{
168+
// origin query is null when user select the context menu item directly of one item from query list
169+
// in this case, we do not need to check if the result is top most
170+
if (records.IsEmpty || result.OriginQuery == null ||
171+
!records.TryGetValue(result.OriginQuery.RawQuery, out var value))
172+
{
173+
return -1;
174+
}
175+
176+
// since this dictionary should be very small (or empty) going over it should be pretty fast.
177+
// since the latter items should be more recent, we should return the smaller index for score to subtract
178+
// which can make them more topmost
179+
// A, B, C => 2, 1, 0 => (max - 2), (max - 1), (max - 0)
180+
var index = 0;
181+
foreach (var record in value)
182+
{
183+
if (record.Equals(result))
184+
{
185+
return value.Count - 1 - index;
186+
}
187+
index++;
188+
}
189+
return -1;
190+
}
191+
192+
internal void Remove(Result result)
193+
{
194+
// origin query is null when user select the context menu item directly of one item from query list
195+
// in this case, we do not need to remove the record
196+
if (result.OriginQuery == null ||
197+
!records.TryGetValue(result.OriginQuery.RawQuery, out var value))
198+
{
199+
return;
200+
}
201+
202+
// remove the record from the queue
203+
var queue = new ConcurrentQueue<Record>(value.Where(r => !r.Equals(result)));
204+
if (queue.IsEmpty)
205+
{
206+
// if the queue is empty, remove the queue from the dictionary
207+
records.TryRemove(result.OriginQuery.RawQuery, out _);
208+
}
209+
else
210+
{
211+
// change the queue in the dictionary
212+
records[result.OriginQuery.RawQuery] = queue;
213+
}
214+
}
215+
216+
internal void AddOrUpdate(Result result)
217+
{
218+
// origin query is null when user select the context menu item directly of one item from query list
219+
// in this case, we do not need to add or update the record
220+
if (result.OriginQuery == null)
221+
{
222+
return;
223+
}
224+
225+
var record = new Record
226+
{
227+
PluginID = result.PluginID,
228+
Title = result.Title,
229+
SubTitle = result.SubTitle,
230+
RecordKey = result.RecordKey
231+
};
232+
if (!records.TryGetValue(result.OriginQuery.RawQuery, out var value))
233+
{
234+
// create a new queue if it does not exist
235+
value = new ConcurrentQueue<Record>();
236+
value.Enqueue(record);
237+
records.TryAdd(result.OriginQuery.RawQuery, value);
238+
}
239+
else
240+
{
241+
// add or update the record in the queue
242+
var queue = new ConcurrentQueue<Record>(value.Where(r => !r.Equals(result))); // make sure we don't have duplicates
243+
queue.Enqueue(record);
244+
records[result.OriginQuery.RawQuery] = queue;
245+
}
246+
}
247+
}
248+
249+
/// <summary>
250+
/// Because ConcurrentQueue does not support serialization, we need to convert it to a List
251+
/// </summary>
252+
internal class ConcurrentDictionaryConcurrentQueueConverter : JsonConverter<ConcurrentDictionary<string, ConcurrentQueue<Record>>>
253+
{
254+
public override ConcurrentDictionary<string, ConcurrentQueue<Record>> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
255+
{
256+
var dictionary = JsonSerializer.Deserialize<Dictionary<string, List<Record>>>(ref reader, options);
257+
var concurrentDictionary = new ConcurrentDictionary<string, ConcurrentQueue<Record>>();
258+
foreach (var kvp in dictionary)
259+
{
260+
concurrentDictionary.TryAdd(kvp.Key, new ConcurrentQueue<Record>(kvp.Value));
261+
}
262+
return concurrentDictionary;
263+
}
264+
265+
public override void Write(Utf8JsonWriter writer, ConcurrentDictionary<string, ConcurrentQueue<Record>> value, JsonSerializerOptions options)
266+
{
267+
var dict = new Dictionary<string, List<Record>>();
268+
foreach (var kvp in value)
269+
{
270+
dict.Add(kvp.Key, kvp.Value.ToList());
271+
}
272+
JsonSerializer.Serialize(writer, dict, options);
273+
}
274+
}
275+
276+
internal class Record
43277
{
44-
public string Title { get; set; }
45-
public string SubTitle { get; set; }
46-
public string PluginID { get; set; }
47-
public string RecordKey { get; set; }
278+
public string Title { get; init; }
279+
public string SubTitle { get; init; }
280+
public string PluginID { get; init; }
281+
public string RecordKey { get; init; }
48282

49283
public bool Equals(Result r)
50284
{

Flow.Launcher/ViewModel/MainViewModel.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,10 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable
3939

4040
private readonly FlowLauncherJsonStorage<History> _historyItemsStorage;
4141
private readonly FlowLauncherJsonStorage<UserSelectedRecord> _userSelectedRecordStorage;
42-
private readonly FlowLauncherJsonStorage<TopMostRecord> _topMostRecordStorage;
42+
private readonly FlowLauncherJsonStorageTopMostRecord _topMostRecord;
4343
private readonly History _history;
4444
private int lastHistoryIndex = 1;
4545
private readonly UserSelectedRecord _userSelectedRecord;
46-
private readonly TopMostRecord _topMostRecord;
4746

4847
private CancellationTokenSource _updateSource; // Used to cancel old query flows
4948
private CancellationToken _updateToken; // Used to avoid ObjectDisposedException of _updateSource.Token
@@ -143,10 +142,9 @@ public MainViewModel()
143142

144143
_historyItemsStorage = new FlowLauncherJsonStorage<History>();
145144
_userSelectedRecordStorage = new FlowLauncherJsonStorage<UserSelectedRecord>();
146-
_topMostRecordStorage = new FlowLauncherJsonStorage<TopMostRecord>();
145+
_topMostRecord = new FlowLauncherJsonStorageTopMostRecord();
147146
_history = _historyItemsStorage.Load();
148147
_userSelectedRecord = _userSelectedRecordStorage.Load();
149-
_topMostRecord = _topMostRecordStorage.Load();
150148

151149
ContextMenu = new ResultsViewModel(Settings, this)
152150
{
@@ -1823,7 +1821,7 @@ public void Save()
18231821
{
18241822
_historyItemsStorage.Save();
18251823
_userSelectedRecordStorage.Save();
1826-
_topMostRecordStorage.Save();
1824+
_topMostRecord.Save();
18271825
}
18281826

18291827
/// <summary>
@@ -1856,9 +1854,12 @@ public void UpdateResultView(ICollection<ResultsForUpdate> resultsForUpdates)
18561854
{
18571855
foreach (var result in metaResults.Results)
18581856
{
1859-
if (_topMostRecord.IsTopMost(result))
1857+
var deviationIndex = _topMostRecord.GetTopMostIndex(result);
1858+
if (deviationIndex != -1)
18601859
{
1861-
result.Score = Result.MaxScore;
1860+
// Adjust the score based on the result's position in the top-most list.
1861+
// A lower deviationIndex (closer to the top) results in a higher score.
1862+
result.Score = Result.MaxScore - deviationIndex;
18621863
}
18631864
else
18641865
{

0 commit comments

Comments
 (0)