Skip to content
Merged
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
236 changes: 228 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,150 @@ 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 recordToRemove = value.FirstOrDefault(r => r.Equals(result));
if (recordToRemove != null)
{
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)
{
// 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
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);
}
}
}
}

/// <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