Merge pull request #3410 from Jack251970/binary_storage_api

New API Functions Plugin Cache Storage & Use API Functions for Program Plugin & Remove Reflection with Interfaces
This commit is contained in:
Jeremy Wu 2025-04-08 22:06:13 +10:00 committed by GitHub
commit c159b4885e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 378 additions and 129 deletions

View file

@ -189,5 +189,10 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
{
_api.StopLoadingBar();
}
public void SavePluginCaches()
{
_api.SavePluginCaches();
}
}
}

View file

@ -13,6 +13,7 @@ using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using IRemovable = Flow.Launcher.Core.Storage.IRemovable;
using ISavable = Flow.Launcher.Plugin.ISavable;
namespace Flow.Launcher.Core.Plugin
@ -65,6 +66,7 @@ namespace Flow.Launcher.Core.Plugin
}
API.SavePluginSettings();
API.SavePluginCaches();
}
public static async ValueTask DisposePluginsAsync()
@ -575,11 +577,11 @@ namespace Flow.Launcher.Core.Plugin
if (removePluginSettings)
{
// For dotnet plugins, we need to remove their PluginJsonStorage instance
if (AllowedLanguage.IsDotNet(plugin.Language))
// For dotnet plugins, we need to remove their PluginJsonStorage and PluginBinaryStorage instances
if (AllowedLanguage.IsDotNet(plugin.Language) && API is IRemovable removable)
{
var method = API.GetType().GetMethod("RemovePluginSettings");
method?.Invoke(API, new object[] { plugin.AssemblyName });
removable.RemovePluginSettings(plugin.AssemblyName);
removable.RemovePluginCaches(plugin.PluginCacheDirectoryPath);
}
try

View file

@ -0,0 +1,19 @@
namespace Flow.Launcher.Core.Storage;
/// <summary>
/// Remove storage instances from <see cref="Launcher.Plugin.IPublicAPI"/> instance
/// </summary>
public interface IRemovable
{
/// <summary>
/// Remove all <see cref="Infrastructure.Storage.PluginJsonStorage{T}"/> instances of one plugin
/// </summary>
/// <param name="assemblyName"></param>
public void RemovePluginSettings(string assemblyName);
/// <summary>
/// Remove all <see cref="Infrastructure.Storage.PluginBinaryStorage{T}"/> instances of one plugin
/// </summary>
/// <param name="cacheDirectory"></param>
public void RemovePluginCaches(string cacheDirectory);
}

View file

@ -34,6 +34,7 @@ namespace Flow.Launcher.Infrastructure.Image
_hashGenerator = new ImageHashGenerator();
var usage = await LoadStorageToConcurrentDictionaryAsync();
_storage.ClearData();
ImageCache.Initialize(usage);

View file

@ -2,9 +2,12 @@
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using MemoryPack;
#nullable enable
namespace Flow.Launcher.Infrastructure.Storage
{
/// <summary>
@ -12,44 +15,55 @@ namespace Flow.Launcher.Infrastructure.Storage
/// Normally, it has better performance, but not readable
/// </summary>
/// <remarks>
/// It utilize MemoryPack, which means the object must be MemoryPackSerializable <see href="https://github.com/Cysharp/MemoryPack"/>
/// It utilizes MemoryPack, which means the object must be MemoryPackSerializable <see href="https://github.com/Cysharp/MemoryPack"/>
/// </remarks>
public class BinaryStorage<T>
public class BinaryStorage<T> : ISavable
{
protected T? Data;
public const string FileSuffix = ".cache";
// Let the derived class to set the file path
public BinaryStorage(string filename, string directoryPath = null)
{
directoryPath ??= DataLocation.CacheDirectory;
FilesFolders.ValidateDirectory(directoryPath);
protected string FilePath { get; init; } = null!;
FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}");
protected string DirectoryPath { get; init; } = null!;
// Let the derived class to set the file path
protected BinaryStorage()
{
}
public string FilePath { get; }
public BinaryStorage(string filename)
{
DirectoryPath = DataLocation.CacheDirectory;
FilesFolders.ValidateDirectory(DirectoryPath);
FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}");
}
public async ValueTask<T> TryLoadAsync(T defaultData)
{
if (Data != null) return Data;
if (File.Exists(FilePath))
{
if (new FileInfo(FilePath).Length == 0)
{
Log.Error($"|BinaryStorage.TryLoad|Zero length cache file <{FilePath}>");
await SaveAsync(defaultData);
return defaultData;
Data = defaultData;
await SaveAsync();
}
await using var stream = new FileStream(FilePath, FileMode.Open);
var d = await DeserializeAsync(stream, defaultData);
return d;
Data = await DeserializeAsync(stream, defaultData);
}
else
{
Log.Info("|BinaryStorage.TryLoad|Cache file not exist, load default data");
await SaveAsync(defaultData);
return defaultData;
Data = defaultData;
await SaveAsync();
}
return Data;
}
private static async ValueTask<T> DeserializeAsync(Stream stream, T defaultData)
@ -57,7 +71,7 @@ namespace Flow.Launcher.Infrastructure.Storage
try
{
var t = await MemoryPackSerializer.DeserializeAsync<T>(stream);
return t;
return t ?? defaultData;
}
catch (System.Exception)
{
@ -66,6 +80,27 @@ namespace Flow.Launcher.Infrastructure.Storage
}
}
public void Save()
{
var serialized = MemoryPackSerializer.Serialize(Data);
File.WriteAllBytes(FilePath, serialized);
}
public async ValueTask SaveAsync()
{
await SaveAsync(Data.NonNull());
}
// ImageCache need to convert data into concurrent dictionary for usage,
// so we would better to clear the data
public void ClearData()
{
Data = default;
}
// ImageCache storages data in its class,
// so we need to pass it to SaveAsync
public async ValueTask SaveAsync(T data)
{
await using var stream = new FileStream(FilePath, FileMode.Create);

View file

@ -1,18 +1,20 @@
#nullable enable
using System;
using System;
using System.Globalization;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
#nullable enable
namespace Flow.Launcher.Infrastructure.Storage
{
/// <summary>
/// Serialize object using json format.
/// </summary>
public class JsonStorage<T> where T : new()
public class JsonStorage<T> : ISavable where T : new()
{
protected T? Data;

View file

@ -0,0 +1,49 @@
using System.IO;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Infrastructure.Storage
{
public class PluginBinaryStorage<T> : BinaryStorage<T> where T : new()
{
private static readonly string ClassName = "PluginBinaryStorage";
// We should not initialize API in static constructor because it will create another API instance
private static IPublicAPI api = null;
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
public PluginBinaryStorage(string cacheName, string cacheDirectory)
{
DirectoryPath = cacheDirectory;
FilesFolders.ValidateDirectory(DirectoryPath);
FilePath = Path.Combine(DirectoryPath, $"{cacheName}{FileSuffix}");
}
public new void Save()
{
try
{
base.Save();
}
catch (System.Exception e)
{
API.LogException(ClassName, $"Failed to save plugin caches to path: {FilePath}", e);
}
}
public new async Task SaveAsync()
{
try
{
await base.SaveAsync();
}
catch (System.Exception e)
{
API.LogException(ClassName, $"Failed to save plugin caches to path: {FilePath}", e);
}
}
}
}

View file

@ -352,6 +352,38 @@ namespace Flow.Launcher.Plugin
public void StopLoadingBar();
/// <summary>
/// Save all Flow's plugins caches
/// </summary>
void SavePluginCaches();
/// <summary>
/// Load BinaryStorage for current plugin's cache. This is the method used to load cache from binary in Flow.
/// When the file is not exist, it will create a new instance for the specific type.
/// </summary>
/// <typeparam name="T">Type for deserialization</typeparam>
/// <param name="cacheName">Cache file name</param>
/// <param name="cacheDirectory">Cache directory from plugin metadata</param>
/// <param name="defaultData">Default data to return</param>
/// <returns></returns>
/// <remarks>
/// BinaryStorage utilizes MemoryPack, which means the object must be MemoryPackSerializable <see href="https://github.com/Cysharp/MemoryPack"/>
/// </remarks>
Task<T> LoadCacheBinaryStorageAsync<T>(string cacheName, string cacheDirectory, T defaultData) where T : new();
/// <summary>
/// Save BinaryStorage for current plugin's cache. This is the method used to save cache to binary in Flow.Launcher
/// This method will save the original instance loaded with LoadCacheBinaryStorageAsync.
/// This API call is for manually Save. Flow will automatically save all cache type that has called LoadCacheBinaryStorageAsync or SaveCacheBinaryStorageAsync previously.
/// </summary>
/// <typeparam name="T">Type for Serialization</typeparam>
/// <param name="cacheName">Cache file name</param>
/// <param name="cacheDirectory">Cache directory from plugin metadata</param>
/// <returns></returns>
/// <remarks>
/// BinaryStorage utilizes MemoryPack, which means the object must be MemoryPackSerializable <see href="https://github.com/Cysharp/MemoryPack"/>
/// </remarks>
Task SaveCacheBinaryStorageAsync<T>(string cacheName, string cacheDirectory) where T : new();
/// Load image from path. Support local, remote and data:image url.
/// If image path is missing, it will return a missing icon.
/// </summary>

View file

@ -1,18 +1,21 @@
namespace Flow.Launcher.Plugin
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Inherit this interface if additional data e.g. cache needs to be saved.
/// Inherit this interface if you need to save additional data which is not a setting or cache,
/// please implement this interface.
/// </summary>
/// <remarks>
/// For storing plugin settings, prefer <see cref="IPublicAPI.LoadSettingJsonStorage{T}"/>
/// or <see cref="IPublicAPI.SaveSettingJsonStorage{T}"/>.
/// Once called, your settings will be automatically saved by Flow.
/// or <see cref="IPublicAPI.SaveSettingJsonStorage{T}"/>.
/// For storing plugin caches, prefer <see cref="IPublicAPI.LoadCacheBinaryStorageAsync{T}"/>
/// or <see cref="IPublicAPI.SaveCacheBinaryStorageAsync{T}(string, string)"/>.
/// Once called, those settings and caches will be automatically saved by Flow.
/// </remarks>
public interface ISavable : IFeatures
{
/// <summary>
/// Save additional plugin data, such as cache.
/// Save additional plugin data.
/// </summary>
void Save();
}
}
}

View file

@ -16,6 +16,7 @@ using Flow.Launcher.Core;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Core.ExternalPlugins;
using Flow.Launcher.Core.Storage;
using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Http;
@ -33,7 +34,7 @@ using Squirrel;
namespace Flow.Launcher
{
public class PublicAPIInstance : IPublicAPI
public class PublicAPIInstance : IPublicAPI, IRemovable
{
private readonly Settings _settings;
private readonly Internationalization _translater;
@ -223,20 +224,17 @@ namespace Flow.Launcher
var name = value.GetType().GetField("AssemblyName")?.GetValue(value)?.ToString();
if (name == assemblyName)
{
_pluginJsonStorages.Remove(key, out var _);
_pluginJsonStorages.TryRemove(key, out var _);
}
}
}
/// <summary>
/// Save plugin settings.
/// </summary>
public void SavePluginSettings()
{
foreach (var value in _pluginJsonStorages.Values)
{
var method = value.GetType().GetMethod("Save");
method?.Invoke(value, null);
var savable = value as ISavable;
savable?.Save();
}
}
@ -258,14 +256,6 @@ namespace Flow.Launcher
((PluginJsonStorage<T>)_pluginJsonStorages[type]).Save();
}
public void SaveJsonStorage<T>(T settings) where T : new()
{
var type = typeof(T);
_pluginJsonStorages[type] = new PluginJsonStorage<T>(settings);
((PluginJsonStorage<T>)_pluginJsonStorages[type]).Save();
}
public void OpenDirectory(string DirectoryPath, string FileNameOrFilePath = null)
{
using var explorer = new Process();
@ -370,6 +360,48 @@ namespace Flow.Launcher
public Task ShowProgressBoxAsync(string caption, Func<Action<double>, Task> reportProgressAsync,
Action cancelProgress = null) => ProgressBoxEx.ShowAsync(caption, reportProgressAsync, cancelProgress);
private readonly ConcurrentDictionary<(string, string, Type), object> _pluginBinaryStorages = new();
public void RemovePluginCaches(string cacheDirectory)
{
foreach (var keyValuePair in _pluginBinaryStorages)
{
var key = keyValuePair.Key;
var currentCacheDirectory = key.Item2;
if (cacheDirectory == currentCacheDirectory)
{
_pluginBinaryStorages.TryRemove(key, out var _);
}
}
}
public void SavePluginCaches()
{
foreach (var value in _pluginBinaryStorages.Values)
{
var savable = value as ISavable;
savable?.Save();
}
}
public async Task<T> LoadCacheBinaryStorageAsync<T>(string cacheName, string cacheDirectory, T defaultData) where T : new()
{
var type = typeof(T);
if (!_pluginBinaryStorages.ContainsKey((cacheName, cacheDirectory, type)))
_pluginBinaryStorages[(cacheName, cacheDirectory, type)] = new PluginBinaryStorage<T>(cacheName, cacheDirectory);
return await ((PluginBinaryStorage<T>)_pluginBinaryStorages[(cacheName, cacheDirectory, type)]).TryLoadAsync(defaultData);
}
public async Task SaveCacheBinaryStorageAsync<T>(string cacheName, string cacheDirectory) where T : new()
{
var type = typeof(T);
if (!_pluginBinaryStorages.ContainsKey((cacheName, cacheDirectory, type)))
_pluginBinaryStorages[(cacheName, cacheDirectory, type)] = new PluginBinaryStorage<T>(cacheName, cacheDirectory);
await ((PluginBinaryStorage<T>)_pluginBinaryStorages[(cacheName, cacheDirectory, type)]).SaveAsync();
}
public ValueTask<ImageSource> LoadImageAsync(string path, bool loadFullImage = false, bool cacheImage = true) =>
ImageLoader.LoadAsync(path, loadFullImage, cacheImage);

View file

@ -7,7 +7,6 @@ using System.Threading;
using System.Threading.Tasks;
using System.Windows.Controls;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin.Program.Programs;
using Flow.Launcher.Plugin.Program.Views;
@ -19,19 +18,20 @@ using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
namespace Flow.Launcher.Plugin.Program
{
public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, ISavable, IAsyncReloadable,
IDisposable
public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, IAsyncReloadable, IDisposable
{
internal static Win32[] _win32s { get; set; }
internal static UWPApp[] _uwps { get; set; }
internal static Settings _settings { get; set; }
private const string Win32CacheName = "Win32";
private const string UwpCacheName = "UWP";
internal static List<Win32> _win32s { get; private set; }
internal static List<UWPApp> _uwps { get; private set; }
internal static Settings _settings { get; private set; }
internal static SemaphoreSlim _win32sLock = new(1, 1);
internal static SemaphoreSlim _uwpsLock = new(1, 1);
internal static PluginInitContext Context { get; private set; }
private static BinaryStorage<Win32[]> _win32Storage;
private static BinaryStorage<UWPApp[]> _uwpStorage;
private static readonly List<Result> emptyResults = new();
private static readonly MemoryCacheOptions cacheOptions = new() { SizeLimit = 1560 };
@ -77,22 +77,15 @@ namespace Flow.Launcher.Plugin.Program
private static readonly string WindowsAppPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "WindowsApps");
static Main()
{
}
public void Save()
{
_win32Storage.SaveAsync(_win32s);
_uwpStorage.SaveAsync(_uwps);
}
public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
{
var result = await cache.GetOrCreateAsync(query.Search, async entry =>
{
var resultList = await Task.Run(() =>
var resultList = await Task.Run(async () =>
{
await _win32sLock.WaitAsync(token);
await _uwpsLock.WaitAsync(token);
try
{
// Collect all UWP Windows app directories
@ -119,7 +112,11 @@ namespace Flow.Launcher.Plugin.Program
Log.Debug("|Flow.Launcher.Plugin.Program.Main|Query operation cancelled");
return emptyResults;
}
finally
{
_uwpsLock.Release();
_win32sLock.Release();
}
}, token);
resultList = resultList.Any() ? resultList : emptyResults;
@ -189,9 +186,12 @@ namespace Flow.Launcher.Plugin.Program
_settings = context.API.LoadSettingJsonStorage<Settings>();
var _win32sCount = 0;
var _uwpsCount = 0;
await Stopwatch.NormalAsync("|Flow.Launcher.Plugin.Program.Main|Preload programs cost", async () =>
{
FilesFolders.ValidateDirectory(Context.CurrentPluginMetadata.PluginCacheDirectoryPath);
var pluginCacheDirectory = Context.CurrentPluginMetadata.PluginCacheDirectoryPath;
FilesFolders.ValidateDirectory(pluginCacheDirectory);
static void MoveFile(string sourcePath, string destinationPath)
{
@ -236,22 +236,27 @@ namespace Flow.Launcher.Plugin.Program
}
// Move old cache files to the new cache directory
var oldWin32CacheFile = Path.Combine(DataLocation.CacheDirectory, $"Win32.cache");
var newWin32CacheFile = Path.Combine(Context.CurrentPluginMetadata.PluginCacheDirectoryPath, $"Win32.cache");
var oldWin32CacheFile = Path.Combine(DataLocation.CacheDirectory, $"{Win32CacheName}.cache");
var newWin32CacheFile = Path.Combine(pluginCacheDirectory, $"{Win32CacheName}.cache");
MoveFile(oldWin32CacheFile, newWin32CacheFile);
var oldUWPCacheFile = Path.Combine(DataLocation.CacheDirectory, $"UWP.cache");
var newUWPCacheFile = Path.Combine(Context.CurrentPluginMetadata.PluginCacheDirectoryPath, $"UWP.cache");
var oldUWPCacheFile = Path.Combine(DataLocation.CacheDirectory, $"{UwpCacheName}.cache");
var newUWPCacheFile = Path.Combine(pluginCacheDirectory, $"{UwpCacheName}.cache");
MoveFile(oldUWPCacheFile, newUWPCacheFile);
_win32Storage = new BinaryStorage<Win32[]>("Win32", Context.CurrentPluginMetadata.PluginCacheDirectoryPath);
_win32s = await _win32Storage.TryLoadAsync(Array.Empty<Win32>());
_uwpStorage = new BinaryStorage<UWPApp[]>("UWP", Context.CurrentPluginMetadata.PluginCacheDirectoryPath);
_uwps = await _uwpStorage.TryLoadAsync(Array.Empty<UWPApp>());
});
Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload win32 programs <{_win32s.Length}>");
Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload uwps <{_uwps.Length}>");
await _win32sLock.WaitAsync();
_win32s = await context.API.LoadCacheBinaryStorageAsync(Win32CacheName, pluginCacheDirectory, new List<Win32>());
_win32sCount = _win32s.Count;
_win32sLock.Release();
bool cacheEmpty = !_win32s.Any() || !_uwps.Any();
await _uwpsLock.WaitAsync();
_uwps = await context.API.LoadCacheBinaryStorageAsync(UwpCacheName, pluginCacheDirectory, new List<UWPApp>());
_uwpsCount = _uwps.Count;
_uwpsLock.Release();
});
Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload win32 programs <{_win32sCount}>");
Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload uwps <{_uwpsCount}>");
var cacheEmpty = _win32sCount == 0 || _uwpsCount == 0;
if (cacheEmpty || _settings.LastIndexTime.AddHours(30) < DateTime.Now)
{
@ -273,36 +278,69 @@ namespace Flow.Launcher.Plugin.Program
}
}
public static void IndexWin32Programs()
public static async Task IndexWin32ProgramsAsync()
{
var win32S = Win32.All(_settings);
_win32s = win32S;
ResetCache();
_win32Storage.SaveAsync(_win32s);
_settings.LastIndexTime = DateTime.Now;
await _win32sLock.WaitAsync();
try
{
var win32S = Win32.All(_settings);
_win32s.Clear();
foreach (var win32 in win32S)
{
_win32s.Add(win32);
}
ResetCache();
await Context.API.SaveCacheBinaryStorageAsync<List<Win32>>(Win32CacheName, Context.CurrentPluginMetadata.PluginCacheDirectoryPath);
_settings.LastIndexTime = DateTime.Now;
}
catch (Exception e)
{
Log.Exception("|Flow.Launcher.Plugin.Program.Main|Failed to index Win32 programs", e);
}
finally
{
_win32sLock.Release();
}
}
public static void IndexUwpPrograms()
public static async Task IndexUwpProgramsAsync()
{
var applications = UWPPackage.All(_settings);
_uwps = applications;
ResetCache();
_uwpStorage.SaveAsync(_uwps);
_settings.LastIndexTime = DateTime.Now;
await _uwpsLock.WaitAsync();
try
{
var uwps = UWPPackage.All(_settings);
_uwps.Clear();
foreach (var uwp in uwps)
{
_uwps.Add(uwp);
}
ResetCache();
await Context.API.SaveCacheBinaryStorageAsync<List<UWPApp>>(UwpCacheName, Context.CurrentPluginMetadata.PluginCacheDirectoryPath);
_settings.LastIndexTime = DateTime.Now;
}
catch (Exception e)
{
Log.Exception("|Flow.Launcher.Plugin.Program.Main|Failed to index Uwp programs", e);
}
finally
{
_uwpsLock.Release();
}
}
public static async Task IndexProgramsAsync()
{
var a = Task.Run(() =>
var win32Task = Task.Run(async () =>
{
Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexWin32Programs);
await Stopwatch.NormalAsync("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexWin32ProgramsAsync);
});
var b = Task.Run(() =>
var uwpTask = Task.Run(async () =>
{
Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|UWPProgram index cost", IndexUwpPrograms);
await Stopwatch.NormalAsync("|Flow.Launcher.Plugin.Program.Main|UWPProgram index cost", IndexUwpProgramsAsync);
});
await Task.WhenAll(a, b).ConfigureAwait(false);
await Task.WhenAll(win32Task, uwpTask).ConfigureAwait(false);
}
internal static void ResetCache()
@ -314,7 +352,7 @@ namespace Flow.Launcher.Plugin.Program
public Control CreateSettingPanel()
{
return new ProgramSetting(Context, _settings, _win32s, _uwps);
return new ProgramSetting(Context, _settings);
}
public string GetTranslatedPluginTitle()
@ -342,7 +380,7 @@ namespace Flow.Launcher.Plugin.Program
Title = Context.API.GetTranslation("flowlauncher_plugin_program_disable_program"),
Action = c =>
{
DisableProgram(program);
_ = DisableProgramAsync(program);
Context.API.ShowMsg(
Context.API.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_success"),
Context.API.GetTranslation(
@ -358,30 +396,43 @@ namespace Flow.Launcher.Plugin.Program
return menuOptions;
}
private static void DisableProgram(IProgram programToDelete)
private static async Task DisableProgramAsync(IProgram programToDelete)
{
if (_settings.DisabledProgramSources.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier))
return;
await _uwpsLock.WaitAsync();
if (_uwps.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier))
{
var program = _uwps.First(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier);
program.Enabled = false;
_settings.DisabledProgramSources.Add(new ProgramSource(program));
_ = Task.Run(() =>
{
IndexUwpPrograms();
});
_uwpsLock.Release();
// Reindex UWP programs
_ = Task.Run(IndexUwpProgramsAsync);
return;
}
else if (_win32s.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier))
else
{
_uwpsLock.Release();
}
await _win32sLock.WaitAsync();
if (_win32s.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier))
{
var program = _win32s.First(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier);
program.Enabled = false;
_settings.DisabledProgramSources.Add(new ProgramSource(program));
_ = Task.Run(() =>
{
IndexWin32Programs();
});
_win32sLock.Release();
// Reindex Win32 programs
_ = Task.Run(IndexWin32ProgramsAsync);
return;
}
else
{
_win32sLock.Release();
}
}

View file

@ -317,7 +317,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
await Task.Delay(3000).ConfigureAwait(false);
PackageChangeChannel.Reader.TryRead(out _);
await Task.Run(Main.IndexUwpPrograms);
await Task.Run(Main.IndexUwpProgramsAsync);
}
}
}

View file

@ -797,7 +797,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
}
await Task.Run(Main.IndexWin32Programs);
await Task.Run(Main.IndexWin32ProgramsAsync);
}
}

View file

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Flow.Launcher.Plugin.Program.Views.Models;
namespace Flow.Launcher.Plugin.Program.Views.Commands
@ -15,21 +16,24 @@ namespace Flow.Launcher.Plugin.Program.Views.Commands
.ToList();
}
internal static void DisplayAllPrograms()
internal static async Task DisplayAllProgramsAsync()
{
await Main._win32sLock.WaitAsync();
var win32 = Main._win32s
.Where(t1 => !ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier))
.Select(x => new ProgramSource(x));
ProgramSetting.ProgramSettingDisplayList.AddRange(win32);
Main._win32sLock.Release();
await Main._uwpsLock.WaitAsync();
var uwp = Main._uwps
.Where(t1 => !ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier))
.Select(x => new ProgramSource(x));
ProgramSetting.ProgramSettingDisplayList.AddRange(win32);
ProgramSetting.ProgramSettingDisplayList.AddRange(uwp);
Main._uwpsLock.Release();
}
internal static void SetProgramSourcesStatus(List<ProgramSource> selectedProgramSourcesToDisable, bool status)
internal static async Task SetProgramSourcesStatusAsync(List<ProgramSource> selectedProgramSourcesToDisable, bool status)
{
foreach(var program in ProgramSetting.ProgramSettingDisplayList)
{
@ -39,14 +43,17 @@ namespace Flow.Launcher.Plugin.Program.Views.Commands
}
}
foreach(var program in Main._win32s)
await Main._win32sLock.WaitAsync();
foreach (var program in Main._win32s)
{
if (selectedProgramSourcesToDisable.Any(x => x.UniqueIdentifier == program.UniqueIdentifier && program.Enabled != status))
{
program.Enabled = status;
}
}
Main._win32sLock.Release();
await Main._uwpsLock.WaitAsync();
foreach (var program in Main._uwps)
{
if (selectedProgramSourcesToDisable.Any(x => x.UniqueIdentifier == program.UniqueIdentifier && program.Enabled != status))
@ -54,6 +61,7 @@ namespace Flow.Launcher.Plugin.Program.Views.Commands
program.Enabled = status;
}
}
Main._uwpsLock.Release();
}
internal static void StoreDisabledInSettings()
@ -72,12 +80,22 @@ namespace Flow.Launcher.Plugin.Program.Views.Commands
Main._settings.DisabledProgramSources.RemoveAll(t1 => t1.Enabled);
}
internal static bool IsReindexRequired(this List<ProgramSource> selectedItems)
internal static async Task<bool> IsReindexRequiredAsync(this List<ProgramSource> selectedItems)
{
// Not in cache
if (selectedItems.Any(t1 => t1.Enabled && !Main._uwps.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier))
await Main._win32sLock.WaitAsync();
await Main._uwpsLock.WaitAsync();
try
{
if (selectedItems.Any(t1 => t1.Enabled && !Main._uwps.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier))
&& selectedItems.Any(t1 => t1.Enabled && !Main._win32s.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier)))
return true;
return true;
}
finally
{
Main._win32sLock.Release();
Main._uwpsLock.Release();
}
// ProgramSources holds list of user added directories,
// so when we enable/disable we need to reindex to show/not show the programs

View file

@ -18,8 +18,8 @@ namespace Flow.Launcher.Plugin.Program.Views
/// </summary>
public partial class ProgramSetting : UserControl
{
private PluginInitContext context;
private Settings _settings;
private readonly PluginInitContext context;
private readonly Settings _settings;
private GridViewColumnHeader _lastHeaderClicked;
private ListSortDirection _lastDirection;
@ -109,7 +109,7 @@ namespace Flow.Launcher.Plugin.Program.Views
public bool ShowUWPCheckbox => UWPPackage.SupportUWP();
public ProgramSetting(PluginInitContext context, Settings settings, Win32[] win32s, UWPApp[] uwps)
public ProgramSetting(PluginInitContext context, Settings settings)
{
this.context = context;
_settings = settings;
@ -183,7 +183,7 @@ namespace Flow.Launcher.Plugin.Program.Views
EditProgramSource(selectedProgramSource);
}
private void EditProgramSource(ProgramSource selectedProgramSource)
private async void EditProgramSource(ProgramSource selectedProgramSource)
{
if (selectedProgramSource == null)
{
@ -202,13 +202,13 @@ namespace Flow.Launcher.Plugin.Program.Views
{
if (selectedProgramSource.Enabled)
{
ProgramSettingDisplay.SetProgramSourcesStatus(new List<ProgramSource> { selectedProgramSource },
await ProgramSettingDisplay.SetProgramSourcesStatusAsync(new List<ProgramSource> { selectedProgramSource },
true); // sync status in win32, uwp and disabled
ProgramSettingDisplay.RemoveDisabledFromSettings();
}
else
{
ProgramSettingDisplay.SetProgramSourcesStatus(new List<ProgramSource> { selectedProgramSource },
await ProgramSettingDisplay.SetProgramSourcesStatusAsync(new List<ProgramSource> { selectedProgramSource },
false);
ProgramSettingDisplay.StoreDisabledInSettings();
}
@ -277,14 +277,14 @@ namespace Flow.Launcher.Plugin.Program.Views
}
}
private void btnLoadAllProgramSource_OnClick(object sender, RoutedEventArgs e)
private async void btnLoadAllProgramSource_OnClick(object sender, RoutedEventArgs e)
{
ProgramSettingDisplay.DisplayAllPrograms();
await ProgramSettingDisplay.DisplayAllProgramsAsync();
ViewRefresh();
}
private void btnProgramSourceStatus_OnClick(object sender, RoutedEventArgs e)
private async void btnProgramSourceStatus_OnClick(object sender, RoutedEventArgs e)
{
var selectedItems = programSourceView
.SelectedItems.Cast<ProgramSource>()
@ -311,18 +311,18 @@ namespace Flow.Launcher.Plugin.Program.Views
}
else if (HasMoreOrEqualEnabledItems(selectedItems))
{
ProgramSettingDisplay.SetProgramSourcesStatus(selectedItems, false);
await ProgramSettingDisplay.SetProgramSourcesStatusAsync(selectedItems, false);
ProgramSettingDisplay.StoreDisabledInSettings();
}
else
{
ProgramSettingDisplay.SetProgramSourcesStatus(selectedItems, true);
await ProgramSettingDisplay.SetProgramSourcesStatusAsync(selectedItems, true);
ProgramSettingDisplay.RemoveDisabledFromSettings();
}
if (selectedItems.IsReindexRequired())
if (await selectedItems.IsReindexRequiredAsync())
ReIndexing();
programSourceView.SelectedItems.Clear();