Skip to content

Support Multiple Topmost Records #3500

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: dev
Choose a base branch
from
16 changes: 16 additions & 0 deletions Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,22 @@ public JsonStorage(string filePath)
FilesFolders.ValidateDirectory(DirectoryPath);
}

public bool Exists()
{
return File.Exists(FilePath);
}

public void Delete()
{
foreach (var path in new[] { FilePath, BackupFilePath, TempFilePath })
{
if (File.Exists(path))
{
File.Delete(path);
}
}
}

public async Task<T> LoadAsync()
{
if (Data != null)
Expand Down
221 changes: 213 additions & 8 deletions Flow.Launcher/Storage/TopMostRecord.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,96 @@
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 TopMostRecord
public class FlowLauncherJsonStorageTopMostRecord
{
private readonly FlowLauncherJsonStorage<MultipleTopMostRecord> _topMostRecordStorage;
private readonly MultipleTopMostRecord _topMostRecord;

public FlowLauncherJsonStorageTopMostRecord()
{
// Get old data & new data
var topMostRecordStorage = new FlowLauncherJsonStorage<TopMostRecord>();
_topMostRecordStorage = new FlowLauncherJsonStorage<MultipleTopMostRecord>();

// Check if data exist
var oldDataExist = topMostRecordStorage.Exists();
var newDataExist = _topMostRecordStorage.Exists();

// 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 - Flow will delete the old data during next startup
}
_topMostRecord = _topMostRecordStorage.Load();
}
// 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 - Flow will delete the old data during next startup
}
Save();
}
// If both data do not exist, we just need to create a new data
else
{
_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);
}
}

/// <summary>
/// Old data structure to support only one top most record for the same query
/// </summary>
internal class TopMostRecord
{
[JsonInclude]
public ConcurrentDictionary<string, Record> records { get; private set; } = new ConcurrentDictionary<string, Record>();
public ConcurrentDictionary<string, Record> records { get; private set; } = new();

internal bool IsTopMost(Result result)
{
Expand Down Expand Up @@ -56,12 +138,135 @@ internal void AddOrUpdate(Result result)
}
}

public class Record
/// <summary>
/// New data structure to support multiple top most records for the same query
/// </summary>
internal class MultipleTopMostRecord
{
[JsonInclude]
[JsonConverter(typeof(ConcurrentDictionaryConcurrentBagConverter))]
public ConcurrentDictionary<string, ConcurrentBag<Record>> 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> { 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 bag = new ConcurrentQueue<Record>(value.Where(r => !r.Equals(result)));
records[result.OriginQuery.RawQuery] = new ConcurrentBag<Record>(bag);

// 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)
{
// 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>()
{
record
};
records.TryAdd(result.OriginQuery.RawQuery, value);
}
else
{
// add or update the record in the bag
var bag = new ConcurrentQueue<Record>(value.Where(r => !r.Equals(result))); // make sure we don't have duplicates
bag.Enqueue(record);
records[result.OriginQuery.RawQuery] = new ConcurrentBag<Record>(bag);
}
}
}

/// <summary>
/// Because ConcurrentBag does not support serialization, we need to convert it to a List
/// </summary>
internal class ConcurrentDictionaryConcurrentBagConverter : JsonConverter<ConcurrentDictionary<string, ConcurrentBag<Record>>>
{
public override ConcurrentDictionary<string, ConcurrentBag<Record>> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var dictionary = JsonSerializer.Deserialize<Dictionary<string, List<Record>>>(ref reader, options);
var concurrentDictionary = new ConcurrentDictionary<string, ConcurrentBag<Record>>();
foreach (var kvp in dictionary)
{
concurrentDictionary.TryAdd(kvp.Key, new ConcurrentBag<Record>(kvp.Value));
}
return concurrentDictionary;
}

public override void Write(Utf8JsonWriter writer, ConcurrentDictionary<string, ConcurrentBag<Record>> value, JsonSerializerOptions options)
{
var dict = new Dictionary<string, List<Record>>();
foreach (var kvp in value)
{
dict.Add(kvp.Key, kvp.Value.ToList());
}
JsonSerializer.Serialize(writer, dict, options);
}
}

internal 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)
{
Expand Down
8 changes: 3 additions & 5 deletions Flow.Launcher/ViewModel/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,10 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable

private readonly FlowLauncherJsonStorage<History> _historyItemsStorage;
private readonly FlowLauncherJsonStorage<UserSelectedRecord> _userSelectedRecordStorage;
private readonly FlowLauncherJsonStorage<TopMostRecord> _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;
Expand Down Expand Up @@ -134,10 +133,9 @@ public MainViewModel()

_historyItemsStorage = new FlowLauncherJsonStorage<History>();
_userSelectedRecordStorage = new FlowLauncherJsonStorage<UserSelectedRecord>();
_topMostRecordStorage = new FlowLauncherJsonStorage<TopMostRecord>();
_topMostRecord = new FlowLauncherJsonStorageTopMostRecord();
_history = _historyItemsStorage.Load();
_userSelectedRecord = _userSelectedRecordStorage.Load();
_topMostRecord = _topMostRecordStorage.Load();

ContextMenu = new ResultsViewModel(Settings)
{
Expand Down Expand Up @@ -1612,7 +1610,7 @@ public void Save()
{
_historyItemsStorage.Save();
_userSelectedRecordStorage.Save();
_topMostRecordStorage.Save();
_topMostRecord.Save();
}

/// <summary>
Expand Down
Loading