Merge branch 'dev' into 250331-NewPluginsPage

This commit is contained in:
DB P 2025-04-08 22:35:47 +09:00 committed by GitHub
commit c351a3870f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 621 additions and 258 deletions

View file

@ -12,7 +12,7 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
{
public class JsonRPCPublicAPI
{
private IPublicAPI _api;
private readonly IPublicAPI _api;
public JsonRPCPublicAPI(IPublicAPI api)
{
@ -104,7 +104,6 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
return _api.GetAllPlugins();
}
public MatchResult FuzzySearch(string query, string stringToCompare)
{
return _api.FuzzySearch(query, stringToCompare);
@ -156,6 +155,11 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
_api.LogWarn(className, message, methodName);
}
public void LogError(string className, string message, [CallerMemberName] string methodName = "")
{
_api.LogError(className, message, methodName);
}
public void OpenDirectory(string DirectoryPath, string FileNameOrFilePath = null)
{
_api.OpenDirectory(DirectoryPath, FileNameOrFilePath);
@ -185,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

@ -16,6 +16,7 @@ using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedModels;
using Microsoft.Win32;
namespace Flow.Launcher.Core.Resource
@ -81,11 +82,6 @@ namespace Flow.Launcher.Core.Resource
#region Theme Resources
public string GetCurrentTheme()
{
return _settings.Theme;
}
private void MakeSureThemeDirectoriesExist()
{
foreach (var dir in _themeDirectories.Where(dir => !Directory.Exists(dir)))
@ -127,7 +123,7 @@ namespace Flow.Launcher.Core.Resource
try
{
// Load a ResourceDictionary for the specified theme.
var themeName = GetCurrentTheme();
var themeName = _settings.Theme;
var dict = GetThemeResourceDictionary(themeName);
// Apply font settings to the theme resource.
@ -330,7 +326,7 @@ namespace Flow.Launcher.Core.Resource
private ResourceDictionary GetCurrentResourceDictionary()
{
return GetResourceDictionary(GetCurrentTheme());
return GetResourceDictionary(_settings.Theme);
}
private ThemeData GetThemeDataFromPath(string path)
@ -383,9 +379,20 @@ namespace Flow.Launcher.Core.Resource
#endregion
#region Load & Change
#region Get & Change Theme
public List<ThemeData> LoadAvailableThemes()
public ThemeData GetCurrentTheme()
{
var themes = GetAvailableThemes();
var matchingTheme = themes.FirstOrDefault(t => t.FileNameWithoutExtension == _settings.Theme);
if (matchingTheme == null)
{
Log.Warn($"No matching theme found for '{_settings.Theme}'. Falling back to the first available theme.");
}
return matchingTheme ?? themes.FirstOrDefault();
}
public List<ThemeData> GetAvailableThemes()
{
List<ThemeData> themes = new List<ThemeData>();
foreach (var themeDirectory in _themeDirectories)
@ -403,7 +410,7 @@ namespace Flow.Launcher.Core.Resource
public bool ChangeTheme(string theme = null)
{
if (string.IsNullOrEmpty(theme))
theme = GetCurrentTheme();
theme = _settings.Theme;
string path = GetThemePath(theme);
try
@ -426,7 +433,7 @@ namespace Flow.Launcher.Core.Resource
BlurEnabled = IsBlurTheme();
// Can only apply blur but here also apply drop shadow effect to avoid possible drop shadow effect issues
// Apply blur and drop shadow effect so that we do not need to call it again
_ = RefreshFrameAsync();
return true;
@ -591,7 +598,7 @@ namespace Flow.Launcher.Core.Resource
{
AutoDropShadow(useDropShadowEffect);
}
SetBlurForWindow(GetCurrentTheme(), backdropType);
SetBlurForWindow(_settings.Theme, backdropType);
if (!BlurEnabled)
{
@ -610,7 +617,7 @@ namespace Flow.Launcher.Core.Resource
// Get the actual backdrop type and drop shadow effect settings
var (backdropType, _) = GetActualValue();
SetBlurForWindow(GetCurrentTheme(), backdropType);
SetBlurForWindow(_settings.Theme, backdropType);
}, DispatcherPriority.Render);
}
@ -898,11 +905,5 @@ namespace Flow.Launcher.Core.Resource
}
#endregion
#region Classes
public record ThemeData(string FileNameWithoutExtension, string Name, bool? IsDark = null, bool? HasBlur = null);
#endregion
}
}

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);
@ -284,7 +285,7 @@ namespace Flow.Launcher.Infrastructure.Image
return ImageCache.TryGetValue(path, loadFullImage, out image);
}
public static async ValueTask<ImageSource> LoadAsync(string path, bool loadFullImage = false)
public static async ValueTask<ImageSource> LoadAsync(string path, bool loadFullImage = false, bool cacheImage = true)
{
var imageResult = await LoadInternalAsync(path, loadFullImage);
@ -300,16 +301,18 @@ namespace Flow.Launcher.Infrastructure.Image
// image already exists
img = ImageCache[key, loadFullImage] ?? img;
}
else
else if (cacheImage)
{
// new guid
// save guid key
GuidToKey[hash] = path;
}
}
// update cache
ImageCache[path, loadFullImage] = img;
if (cacheImage)
{
// update cache
ImageCache[path, loadFullImage] = img;
}
}
return img;

View file

@ -1,12 +1,12 @@
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using Flow.Launcher.Infrastructure.UserSettings;
using NLog;
using NLog.Config;
using NLog.Targets;
using Flow.Launcher.Infrastructure.UserSettings;
using NLog.Targets.Wrappers;
using System.Runtime.ExceptionServices;
namespace Flow.Launcher.Infrastructure.Logger
{

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

@ -1,6 +1,4 @@
using Flow.Launcher.Plugin.SharedModels;
using JetBrains.Annotations;
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
@ -8,6 +6,9 @@ using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using Flow.Launcher.Plugin.SharedModels;
using JetBrains.Annotations;
namespace Flow.Launcher.Plugin
{
@ -227,6 +228,11 @@ namespace Flow.Launcher.Plugin
/// </summary>
void LogWarn(string className, string message, [CallerMemberName] string methodName = "");
/// <summary>
/// Log error message. Preferred error logging method for plugins.
/// </summary>
void LogError(string className, string message, [CallerMemberName] string methodName = "");
/// <summary>
/// Log an Exception. Will throw if in debug mode so developer will be aware,
/// otherwise logs the eror message. This is the primary logging method used for Flow
@ -346,6 +352,72 @@ namespace Flow.Launcher.Plugin
public void StopLoadingBar();
/// <summary>
/// Get all available themes
/// </summary>
/// <returns></returns>
public List<ThemeData> GetAvailableThemes();
/// <summary>
/// Get the current theme
/// </summary>
/// <returns></returns>
public ThemeData GetCurrentTheme();
/// <summary>
/// Set the current theme
/// </summary>
/// <param name="theme"></param>
/// <returns>
/// True if the theme is set successfully, false otherwise.
/// </returns>
public bool SetCurrentTheme(ThemeData theme);
/// 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>
/// <param name="path">The path of the image.</param>
/// <param name="loadFullImage">
/// Load full image or not.
/// </param>
/// <param name="cacheImage">
/// Cache the image or not. Cached image will be stored in FL cache.
/// If the image is just used one time, it's better to set this to false.
/// </param>
/// <returns></returns>
ValueTask<ImageSource> LoadImageAsync(string path, bool loadFullImage = false, bool cacheImage = true);
/// Update the plugin manifest
/// </summary>
/// <param name="usePrimaryUrlOnly">

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

@ -0,0 +1,77 @@
using System;
namespace Flow.Launcher.Plugin.SharedModels;
/// <summary>
/// Theme data model
/// </summary>
public class ThemeData
{
/// <summary>
/// Theme file name without extension
/// </summary>
public string FileNameWithoutExtension { get; private init; }
/// <summary>
/// Theme name
/// </summary>
public string Name { get; private init; }
/// <summary>
/// Indicates whether the theme supports dark mode
/// </summary>
public bool? IsDark { get; private init; }
/// <summary>
/// Indicates whether the theme supports blur effects
/// </summary>
public bool? HasBlur { get; private init; }
/// <summary>
/// Theme data constructor
/// </summary>
public ThemeData(string fileNameWithoutExtension, string name, bool? isDark = null, bool? hasBlur = null)
{
FileNameWithoutExtension = fileNameWithoutExtension;
Name = name;
IsDark = isDark;
HasBlur = hasBlur;
}
/// <inheritdoc />
public static bool operator ==(ThemeData left, ThemeData right)
{
if (left is null && right is null)
return true;
if (left is null || right is null)
return false;
return left.Equals(right);
}
/// <inheritdoc />
public static bool operator !=(ThemeData left, ThemeData right)
{
return !(left == right);
}
/// <inheritdoc />
public override bool Equals(object obj)
{
if (obj is not ThemeData other)
return false;
return FileNameWithoutExtension == other.FileNameWithoutExtension &&
Name == other.Name;
}
/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(FileNameWithoutExtension, Name);
}
/// <inheritdoc />
public override string ToString()
{
return Name;
}
}

View file

@ -156,7 +156,7 @@ namespace Flow.Launcher
private async Task SetImageAsync(string imageName)
{
var imagePath = Path.Combine(Constant.ProgramDirectory, "Images", imageName);
var imageSource = await ImageLoader.LoadAsync(imagePath);
var imageSource = await App.API.LoadImageAsync(imagePath);
Img.Source = imageSource;
}

View file

@ -43,7 +43,7 @@ namespace Flow.Launcher
private async System.Threading.Tasks.Task LoadImageAsync()
{
imgClose.Source = await ImageLoader.LoadAsync(Path.Combine(Infrastructure.Constant.ProgramDirectory, "Images\\close.png"));
imgClose.Source = await App.API.LoadImageAsync(Path.Combine(Constant.ProgramDirectory, "Images\\close.png"));
}
void imgClose_MouseUp(object sender, MouseButtonEventArgs e)
@ -71,11 +71,11 @@ namespace Flow.Launcher
if (!File.Exists(iconPath))
{
imgIco.Source = await ImageLoader.LoadAsync(Path.Combine(Constant.ProgramDirectory, "Images\\app.png"));
imgIco.Source = await App.API.LoadImageAsync(Path.Combine(Constant.ProgramDirectory, "Images\\app.png"));
}
else
{
imgIco.Source = await ImageLoader.LoadAsync(iconPath);
imgIco.Source = await App.API.LoadImageAsync(iconPath);
}
Show();

View file

@ -10,10 +10,13 @@ using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using CommunityToolkit.Mvvm.DependencyInjection;
using Squirrel;
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;
@ -27,17 +30,19 @@ using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.ViewModel;
using JetBrains.Annotations;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Core.ExternalPlugins;
using Squirrel;
namespace Flow.Launcher
{
public class PublicAPIInstance : IPublicAPI
public class PublicAPIInstance : IPublicAPI, IRemovable
{
private readonly Settings _settings;
private readonly Internationalization _translater;
private readonly MainViewModel _mainVM;
private Theme _theme;
private Theme Theme => _theme ??= Ioc.Default.GetRequiredService<Theme>();
private readonly object _saveSettingsLock = new();
#region Constructor
@ -88,7 +93,11 @@ namespace Flow.Launcher
public bool IsMainWindowVisible() => _mainVM.MainWindowVisibilityStatus;
public event VisibilityChangedEventHandler VisibilityChanged { add => _mainVM.VisibilityChanged += value; remove => _mainVM.VisibilityChanged -= value; }
public event VisibilityChangedEventHandler VisibilityChanged
{
add => _mainVM.VisibilityChanged += value;
remove => _mainVM.VisibilityChanged -= value;
}
// Must use Ioc.Default.GetRequiredService<Updater>() to avoid circular dependency
public void CheckForNewUpdate() => _ = Ioc.Default.GetRequiredService<Updater>().UpdateAppAsync(false);
@ -175,13 +184,14 @@ namespace Flow.Launcher
public MatchResult FuzzySearch(string query, string stringToCompare) =>
StringMatcher.FuzzySearch(query, stringToCompare);
public Task<string> HttpGetStringAsync(string url, CancellationToken token = default) => Http.GetAsync(url, token);
public Task<string> HttpGetStringAsync(string url, CancellationToken token = default) =>
Http.GetAsync(url, token);
public Task<Stream> HttpGetStreamAsync(string url, CancellationToken token = default) =>
Http.GetStreamAsync(url, token);
public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action<double> reportProgress = null,
CancellationToken token = default) => Http.DownloadAsync(url, filePath, reportProgress, token);
CancellationToken token = default) =>Http.DownloadAsync(url, filePath, reportProgress, token);
public void AddActionKeyword(string pluginId, string newActionKeyword) =>
PluginManager.AddActionKeyword(pluginId, newActionKeyword);
@ -200,8 +210,11 @@ namespace Flow.Launcher
public void LogWarn(string className, string message, [CallerMemberName] string methodName = "") =>
Log.Warn(className, message, methodName);
public void LogException(string className, string message, Exception e,
[CallerMemberName] string methodName = "") => Log.Exception(className, message, e, methodName);
public void LogError(string className, string message, [CallerMemberName] string methodName = "") =>
Log.Error(className, message, methodName);
public void LogException(string className, string message, Exception e, [CallerMemberName] string methodName = "") =>
Log.Exception(className, message, e, methodName);
private readonly ConcurrentDictionary<Type, object> _pluginJsonStorages = new();
@ -214,20 +227,17 @@ namespace Flow.Launcher
var name = value.GetType().GetField("AssemblyName")?.GetValue(value)?.ToString();
if (name == assemblyName)
{
_pluginJsonStorages.Remove(key, out var pluginJsonStorage);
_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();
}
}
@ -249,14 +259,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();
@ -343,17 +345,75 @@ namespace Flow.Launcher
private readonly List<Func<int, int, SpecialKeyState, bool>> _globalKeyboardHandlers = new();
public void RegisterGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback) => _globalKeyboardHandlers.Add(callback);
public void RemoveGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback) => _globalKeyboardHandlers.Remove(callback);
public void RegisterGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback) =>
_globalKeyboardHandlers.Add(callback);
public void RemoveGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback) =>
_globalKeyboardHandlers.Remove(callback);
public void ReQuery(bool reselect = true) => _mainVM.ReQuery(reselect);
public void BackToQueryResults() => _mainVM.BackToQueryResults();
public MessageBoxResult ShowMsgBox(string messageBoxText, string caption = "", MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None, MessageBoxResult defaultResult = MessageBoxResult.OK) =>
public MessageBoxResult ShowMsgBox(string messageBoxText, string caption = "",
MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None,
MessageBoxResult defaultResult = MessageBoxResult.OK) =>
MessageBoxEx.Show(messageBoxText, caption, button, icon, defaultResult);
public Task ShowProgressBoxAsync(string caption, Func<Action<double>, Task> reportProgressAsync, Action cancelProgress = null) => ProgressBoxEx.ShowAsync(caption, reportProgressAsync, cancelProgress);
public Task ShowProgressBoxAsync(string caption, Func<Action<double>, Task> reportProgressAsync,
Action cancelProgress = null) => ProgressBoxEx.ShowAsync(caption, reportProgressAsync, cancelProgress);
public List<ThemeData> GetAvailableThemes() => Theme.GetAvailableThemes();
public ThemeData GetCurrentTheme() => Theme.GetCurrentTheme();
public bool SetCurrentTheme(ThemeData theme) =>
Theme.ChangeTheme(theme.FileNameWithoutExtension);
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);
public Task<bool> UpdatePluginManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default) =>
PluginsManifest.UpdateManifestAsync(usePrimaryUrlOnly, token);

View file

@ -12,6 +12,7 @@ using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.ViewModel;
using ModernWpf;
using ThemeManagerForColorSchemeSwitch = ModernWpf.ThemeManager;
@ -28,25 +29,23 @@ public partial class SettingsPaneThemeViewModel : BaseModel
public static string LinkHowToCreateTheme => @"https://www.flowlauncher.com/theme-builder/";
public static string LinkThemeGallery => "https://github.com/Flow-Launcher/Flow.Launcher/discussions/1438";
private List<Theme.ThemeData> _themes;
public List<Theme.ThemeData> Themes => _themes ??= _theme.LoadAvailableThemes();
private List<ThemeData> _themes;
public List<ThemeData> Themes => _themes ??= App.API.GetAvailableThemes();
private Theme.ThemeData _selectedTheme;
public Theme.ThemeData SelectedTheme
private ThemeData _selectedTheme;
public ThemeData SelectedTheme
{
get => _selectedTheme ??= Themes.Find(v => v.FileNameWithoutExtension == _theme.GetCurrentTheme());
get => _selectedTheme ??= Themes.Find(v => v == App.API.GetCurrentTheme());
set
{
_selectedTheme = value;
_theme.ChangeTheme(value.FileNameWithoutExtension);
App.API.SetCurrentTheme(value);
// Update UI state
OnPropertyChanged(nameof(BackdropType));
OnPropertyChanged(nameof(IsBackdropEnabled));
OnPropertyChanged(nameof(IsDropShadowEnabled));
OnPropertyChanged(nameof(DropShadowEffect));
_ = _theme.RefreshFrameAsync();
}
}

View file

@ -1168,7 +1168,7 @@ namespace Flow.Launcher.ViewModel
else if (plugins.Count == 1)
{
PluginIconPath = plugins.Single().Metadata.IcoPath;
PluginIconSource = await ImageLoader.LoadAsync(PluginIconPath);
PluginIconSource = await App.API.LoadImageAsync(PluginIconPath);
SearchIconVisibility = Visibility.Hidden;
}
else

View file

@ -49,7 +49,7 @@ namespace Flow.Launcher.ViewModel
private async Task LoadIconAsync()
{
Image = await ImageLoader.LoadAsync(PluginPair.Metadata.IcoPath);
Image = await App.API.LoadImageAsync(PluginPair.Metadata.IcoPath);
OnPropertyChanged(nameof(Image));
}

View file

@ -199,7 +199,7 @@ namespace Flow.Launcher.ViewModel
}
}
return await ImageLoader.LoadAsync(imagePath, loadFullImage).ConfigureAwait(false);
return await App.API.LoadImageAsync(imagePath, loadFullImage).ConfigureAwait(false);
}
private async Task LoadImageAsync()

View file

@ -7,7 +7,6 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Flow.Launcher.Infrastructure.Image;
using Flow.Launcher.Plugin.Explorer.Search;
namespace Flow.Launcher.Plugin.Explorer.Views;
@ -89,7 +88,7 @@ public partial class PreviewPanel : UserControl, INotifyPropertyChanged
private async Task LoadImageAsync()
{
PreviewImage = await ImageLoader.LoadAsync(FilePath, true).ConfigureAwait(false);
PreviewImage = await Main.Context.API.LoadImageAsync(FilePath, true).ConfigureAwait(false);
}
public event PropertyChangedEventHandler? PropertyChanged;

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();

View file

@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Plugin.SharedModels;
namespace Flow.Launcher.Plugin.Sys
{
@ -11,32 +10,6 @@ namespace Flow.Launcher.Plugin.Sys
private readonly PluginInitContext _context;
// Do not initialize it in the constructor, because it will cause null reference in
// var dicts = Application.Current.Resources.MergedDictionaries; line of Theme
private Theme theme = null;
private Theme Theme => theme ??= Ioc.Default.GetRequiredService<Theme>();
#region Theme Selection
// Theme select codes simplified from SettingsPaneThemeViewModel.cs
private Theme.ThemeData _selectedTheme;
public Theme.ThemeData SelectedTheme
{
get => _selectedTheme ??= Themes.Find(v => v.FileNameWithoutExtension == Theme.GetCurrentTheme());
set
{
_selectedTheme = value;
Theme.ChangeTheme(value.FileNameWithoutExtension);
_ = Theme.RefreshFrameAsync();
}
}
private List<Theme.ThemeData> Themes => Theme.LoadAvailableThemes();
#endregion
public ThemeSelector(PluginInitContext context)
{
_context = context;
@ -44,28 +17,30 @@ namespace Flow.Launcher.Plugin.Sys
public List<Result> Query(Query query)
{
var themes = _context.API.GetAvailableThemes();
var selectedTheme = _context.API.GetCurrentTheme();
var search = query.SecondToEndSearch;
if (string.IsNullOrWhiteSpace(search))
{
return Themes.Select(CreateThemeResult)
return themes.Select(x => CreateThemeResult(x, selectedTheme))
.OrderBy(x => x.Title)
.ToList();
}
return Themes.Select(theme => (theme, matchResult: _context.API.FuzzySearch(search, theme.Name)))
return themes.Select(theme => (theme, matchResult: _context.API.FuzzySearch(search, theme.Name)))
.Where(x => x.matchResult.IsSearchPrecisionScoreMet())
.Select(x => CreateThemeResult(x.theme, x.matchResult.Score, x.matchResult.MatchData))
.Select(x => CreateThemeResult(x.theme, selectedTheme, x.matchResult.Score, x.matchResult.MatchData))
.OrderBy(x => x.Title)
.ToList();
}
private Result CreateThemeResult(Theme.ThemeData theme) => CreateThemeResult(theme, 0, null);
private Result CreateThemeResult(ThemeData theme, ThemeData selectedTheme) => CreateThemeResult(theme, selectedTheme, 0, null);
private Result CreateThemeResult(Theme.ThemeData theme, int score, IList<int> highlightData)
private Result CreateThemeResult(ThemeData theme, ThemeData selectedTheme, int score, IList<int> highlightData)
{
string themeName = theme.Name;
string title;
if (theme == SelectedTheme)
if (theme == selectedTheme)
{
title = $"{theme.Name} ★";
// Set current theme to the top
@ -101,8 +76,10 @@ namespace Flow.Launcher.Plugin.Sys
Score = score,
Action = c =>
{
SelectedTheme = theme;
_context.API.ReQuery();
if (_context.API.SetCurrentTheme(theme))
{
_context.API.ReQuery();
}
return false;
}
};

View file

@ -1,5 +1,4 @@
using Flow.Launcher.Infrastructure.Image;
using System;
using System;
using System.IO;
using System.Threading.Tasks;
#pragma warning disable IDE0005
@ -41,8 +40,8 @@ namespace Flow.Launcher.Plugin.WebSearch
#if DEBUG
throw;
#else
Main._context.API.ShowMsgBox(string.Format("Copying the selected image file to {0} has failed, changes will now be reverted", destinationFileNameFullPath));
UpdateIconAttributes(selectedSearchSource, fullPathToOriginalImage);
Main._context.API.ShowMsgBox(string.Format("Copying the selected image file to {0} has failed, changes will now be reverted", destinationFileNameFullPath));
UpdateIconAttributes(selectedSearchSource, fullPathToOriginalImage);
#endif
}
}
@ -61,7 +60,7 @@ namespace Flow.Launcher.Plugin.WebSearch
internal async ValueTask<ImageSource> LoadPreviewIconAsync(string pathToPreviewIconImage)
{
return await ImageLoader.LoadAsync(pathToPreviewIconImage);
return await Main._context.API.LoadImageAsync(pathToPreviewIconImage);
}
}
}

View file

@ -4,8 +4,6 @@ using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Logger;
using System.Net.Http;
using System.Threading;
@ -22,11 +20,11 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
try
{
const string api = "http://suggestion.baidu.com/su?json=1&wd=";
result = await Http.GetAsync(api + Uri.EscapeDataString(query), token).ConfigureAwait(false);
result = await Main._context.API.HttpGetStringAsync(api + Uri.EscapeDataString(query), token).ConfigureAwait(false);
}
catch (Exception e) when (e is HttpRequestException or {InnerException: TimeoutException})
{
Log.Exception("|Baidu.Suggestions|Can't get suggestion from baidu", e);
Main._context.API.LogException(nameof(Baidu), "Can't get suggestion from Baidu", e);
return null;
}
@ -41,7 +39,7 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
}
catch (JsonException e)
{
Log.Exception("|Baidu.Suggestions|can't parse suggestions", e);
Main._context.API.LogException(nameof(Baidu), "Can't parse suggestions", e);
return new List<string>();
}

View file

@ -1,6 +1,4 @@
using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Logger;
using System;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
@ -10,16 +8,15 @@ using System.Threading;
namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
{
class Bing : SuggestionSource
public class Bing : SuggestionSource
{
public override async Task<List<string>> SuggestionsAsync(string query, CancellationToken token)
{
try
{
const string api = "https://api.bing.com/qsonhs.aspx?q=";
await using var resultStream = await Http.GetStreamAsync(api + Uri.EscapeDataString(query), token).ConfigureAwait(false);
await using var resultStream = await Main._context.API.HttpGetStreamAsync(api + Uri.EscapeDataString(query), token).ConfigureAwait(false);
using var json = (await JsonDocument.ParseAsync(resultStream, cancellationToken: token));
var root = json.RootElement.GetProperty("AS");
@ -33,18 +30,15 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
.EnumerateArray()
.Select(s => s.GetProperty("Txt").GetString()))
.ToList();
}
catch (Exception e) when (e is HttpRequestException or { InnerException: TimeoutException })
{
Log.Exception("|Baidu.Suggestions|Can't get suggestion from baidu", e);
Main._context.API.LogException(nameof(Bing), "Can't get suggestion from Bing", e);
return null;
}
catch (JsonException e)
{
Log.Exception("|Bing.Suggestions|can't parse suggestions", e);
Main._context.API.LogException(nameof(Bing), "Can't parse suggestions", e);
return new List<string>();
}
}

View file

@ -2,8 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Logger;
using System.Net.Http;
using System.Threading;
using System.Text.Json;
@ -25,7 +23,7 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
{
const string api = "https://duckduckgo.com/ac/?type=list&q=";
await using var resultStream = await Http.GetStreamAsync(api + Uri.EscapeDataString(query), token: token).ConfigureAwait(false);
await using var resultStream = await Main._context.API.HttpGetStreamAsync(api + Uri.EscapeDataString(query), token: token).ConfigureAwait(false);
using var json = await JsonDocument.ParseAsync(resultStream, cancellationToken: token);
@ -36,12 +34,12 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
}
catch (Exception e) when (e is HttpRequestException or {InnerException: TimeoutException})
{
Log.Exception("|DuckDuckGo.Suggestions|Can't get suggestion from DuckDuckGo", e);
Main._context.API.LogException(nameof(DuckDuckGo), "Can't get suggestion from DuckDuckGo", e);
return null;
}
catch (JsonException e)
{
Log.Exception("|DuckDuckGo.Suggestions|can't parse suggestions", e);
Main._context.API.LogException(nameof(DuckDuckGo), "Can't parse suggestions", e);
return new List<string>();
}
}

View file

@ -2,8 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Logger;
using System.Net.Http;
using System.Threading;
using System.Text.Json;
@ -18,7 +16,7 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
{
const string api = "https://www.google.com/complete/search?output=chrome&q=";
await using var resultStream = await Http.GetStreamAsync(api + Uri.EscapeDataString(query), token: token).ConfigureAwait(false);
await using var resultStream = await Main._context.API.HttpGetStreamAsync(api + Uri.EscapeDataString(query), token: token).ConfigureAwait(false);
using var json = await JsonDocument.ParseAsync(resultStream, cancellationToken: token);
@ -29,12 +27,12 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
}
catch (Exception e) when (e is HttpRequestException or {InnerException: TimeoutException})
{
Log.Exception("|Baidu.Suggestions|Can't get suggestion from baidu", e);
Main._context.API.LogException(nameof(Google), "Can't get suggestion from Google", e);
return null;
}
catch (JsonException e)
{
Log.Exception("|Google.Suggestions|can't parse suggestions", e);
Main._context.API.LogException(nameof(Google), "Can't parse suggestions", e);
return new List<string>();
}
}