diff --git a/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs b/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs index b9b878a7b..273698b86 100644 --- a/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs @@ -20,7 +20,7 @@ namespace Flow.Launcher.Core.Plugin dependencyResolver = new AssemblyDependencyResolver(assemblyFilePath); assemblyName = new AssemblyName(Path.GetFileNameWithoutExtension(assemblyFilePath)); - referencedPluginPackageDependencyResolver = + referencedPluginPackageDependencyResolver = new AssemblyDependencyResolver(Path.Combine(Constant.ProgramDirectory, "Flow.Launcher.Plugin.dll")); } @@ -38,15 +38,15 @@ namespace Flow.Launcher.Core.Plugin // that use Newtonsoft.Json if (assemblyPath == null || ExistsInReferencedPluginPackage(assemblyName)) return null; - + return LoadFromAssemblyPath(assemblyPath); } - internal Type FromAssemblyGetTypeOfInterface(Assembly assembly, Type type) + internal Type FromAssemblyGetTypeOfInterface(Assembly assembly, params Type[] types) { var allTypes = assembly.ExportedTypes; - return allTypes.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Contains(type)); + return allTypes.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Intersect(types).Any()); } internal bool ExistsInReferencedPluginPackage(AssemblyName assemblyName) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 3b697a1ee..26167e945 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Logger; @@ -52,13 +53,14 @@ namespace Flow.Launcher.Core.Plugin } } - public static void ReloadData() + public static async Task ReloadData() { - foreach(var plugin in AllPlugins) + await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch { - var reloadablePlugin = plugin.Plugin as IReloadable; - reloadablePlugin?.ReloadData(); - } + IReloadable p => Task.Run(p.ReloadData), + IAsyncReloadable p => p.ReloadDataAsync(), + _ => Task.CompletedTask, + }).ToArray()); } static PluginManager() @@ -86,50 +88,62 @@ namespace Flow.Launcher.Core.Plugin /// Call initialize for all plugins /// /// return the list of failed to init plugins or null for none - public static void InitializePlugins(IPublicAPI api) + public static async Task InitializePlugins(IPublicAPI api) { API = api; var failedPlugins = new ConcurrentQueue(); - Parallel.ForEach(AllPlugins, pair => + + var InitTasks = AllPlugins.Select(pair => Task.Run(async delegate { try { - var milliseconds = Stopwatch.Debug($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>", () => + var milliseconds = pair.Plugin switch { - pair.Plugin.Init(new PluginInitContext - { - CurrentPluginMetadata = pair.Metadata, - API = API - }); - }); + IAsyncPlugin plugin + => await Stopwatch.DebugAsync($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>", + () => plugin.InitAsync(new PluginInitContext(pair.Metadata, API))), + IPlugin plugin + => Stopwatch.Debug($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>", + () => plugin.Init(new PluginInitContext(pair.Metadata, API))), + _ => throw new ArgumentException(), + }; pair.Metadata.InitTime += milliseconds; - Log.Info($"|PluginManager.InitializePlugins|Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>"); + Log.Info( + $"|PluginManager.InitializePlugins|Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>"); } catch (Exception e) { Log.Exception(nameof(PluginManager), $"Fail to Init plugin: {pair.Metadata.Name}", e); - pair.Metadata.Disabled = true; + pair.Metadata.Disabled = true; failedPlugins.Enqueue(pair); } - }); + })); + + await Task.WhenAll(InitTasks); _contextMenuPlugins = GetPluginsForInterface(); foreach (var plugin in AllPlugins) { - if (IsGlobalPlugin(plugin.Metadata)) - GlobalPlugins.Add(plugin); - - // Plugins may have multiple ActionKeywords, eg. WebSearch - plugin.Metadata.ActionKeywords - .Where(x => x != Query.GlobalPluginWildcardSign) - .ToList() - .ForEach(x => NonGlobalPlugins[x] = plugin); + foreach (var actionKeyword in plugin.Metadata.ActionKeywords) + { + switch (actionKeyword) + { + case Query.GlobalPluginWildcardSign: + GlobalPlugins.Add(plugin); + break; + default: + NonGlobalPlugins[actionKeyword] = plugin; + break; + } + } } if (failedPlugins.Any()) { var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name)); - API.ShowMsg($"Fail to Init Plugins", $"Plugins: {failed} - fail to load and would be disabled, please contact plugin creator for help", "", false); + API.ShowMsg($"Fail to Init Plugins", + $"Plugins: {failed} - fail to load and would be disabled, please contact plugin creator for help", + "", false); } } @@ -146,24 +160,46 @@ namespace Flow.Launcher.Core.Plugin } } - public static List QueryForPlugin(PluginPair pair, Query query) + public static async Task> QueryForPlugin(PluginPair pair, Query query, CancellationToken token) { var results = new List(); try { var metadata = pair.Metadata; - var milliseconds = Stopwatch.Debug($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}", () => + + long milliseconds = -1L; + + switch (pair.Plugin) { - results = pair.Plugin.Query(query) ?? new List(); - UpdatePluginMetadata(results, metadata, query); - }); + case IAsyncPlugin plugin: + milliseconds = await Stopwatch.DebugAsync($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}", + async () => results = await plugin.QueryAsync(query, token).ConfigureAwait(false)); + break; + case IPlugin plugin: + await Task.Run(() => milliseconds = Stopwatch.Debug($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}", + () => results = plugin.Query(query)), token).ConfigureAwait(false); + break; + default: + throw new ArgumentOutOfRangeException(); + } + token.ThrowIfCancellationRequested(); + UpdatePluginMetadata(results, metadata, query); + metadata.QueryCount += 1; - metadata.AvgQueryTime = metadata.QueryCount == 1 ? milliseconds : (metadata.AvgQueryTime + milliseconds) / 2; + metadata.AvgQueryTime = + metadata.QueryCount == 1 ? milliseconds : (metadata.AvgQueryTime + milliseconds) / 2; + token.ThrowIfCancellationRequested(); + } + catch (OperationCanceledException) + { + // null will be fine since the results will only be added into queue if the token hasn't been cancelled + return results = null; } catch (Exception e) { Log.Exception($"|PluginManager.QueryForPlugin|Exception for plugin <{pair.Metadata.Name}> when query <{query}>", e); } + return results; } @@ -182,11 +218,6 @@ namespace Flow.Launcher.Core.Plugin } } - private static bool IsGlobalPlugin(PluginMetadata metadata) - { - return metadata.ActionKeywords.Contains(Query.GlobalPluginWildcardSign); - } - /// /// get specified plugin, return null if not found /// @@ -222,16 +253,19 @@ namespace Flow.Launcher.Core.Plugin } catch (Exception e) { - Log.Exception($"|PluginManager.GetContextMenusForPlugin|Can't load context menus for plugin <{pluginPair.Metadata.Name}>", e); + Log.Exception( + $"|PluginManager.GetContextMenusForPlugin|Can't load context menus for plugin <{pluginPair.Metadata.Name}>", + e); } } + return results; } public static bool ActionKeywordRegistered(string actionKeyword) { return actionKeyword != Query.GlobalPluginWildcardSign - && NonGlobalPlugins.ContainsKey(actionKeyword); + && NonGlobalPlugins.ContainsKey(actionKeyword); } /// @@ -249,6 +283,7 @@ namespace Flow.Launcher.Core.Plugin { NonGlobalPlugins[newActionKeyword] = plugin; } + plugin.Metadata.ActionKeywords.Add(newActionKeyword); } @@ -262,16 +297,16 @@ namespace Flow.Launcher.Core.Plugin if (oldActionkeyword == Query.GlobalPluginWildcardSign && // Plugins may have multiple ActionKeywords that are global, eg. WebSearch plugin.Metadata.ActionKeywords - .Where(x => x == Query.GlobalPluginWildcardSign) - .ToList() - .Count == 1) + .Where(x => x == Query.GlobalPluginWildcardSign) + .ToList() + .Count == 1) { GlobalPlugins.Remove(plugin); } - + if (oldActionkeyword != Query.GlobalPluginWildcardSign) NonGlobalPlugins.Remove(oldActionkeyword); - + plugin.Metadata.ActionKeywords.Remove(oldActionkeyword); } diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index 224dbd85e..fcf178445 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -37,56 +37,59 @@ namespace Flow.Launcher.Core.Plugin foreach (var metadata in metadatas) { - var milliseconds = Stopwatch.Debug($"|PluginsLoader.DotNetPlugins|Constructor init cost for {metadata.Name}", () => - { - + var milliseconds = Stopwatch.Debug( + $"|PluginsLoader.DotNetPlugins|Constructor init cost for {metadata.Name}", () => + { #if DEBUG - var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath); - var assembly = assemblyLoader.LoadAssemblyAndDependencies(); - var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, typeof(IPlugin)); - var plugin = (IPlugin)Activator.CreateInstance(type); -#else - Assembly assembly = null; - IPlugin plugin = null; - - try - { var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath); - assembly = assemblyLoader.LoadAssemblyAndDependencies(); + var assembly = assemblyLoader.LoadAssemblyAndDependencies(); + var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, typeof(IPlugin), + typeof(IAsyncPlugin)); - var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, typeof(IPlugin)); + var plugin = Activator.CreateInstance(type); +#else + Assembly assembly = null; + object plugin = null; - plugin = (IPlugin)Activator.CreateInstance(type); - } - catch (Exception e) when (assembly == null) - { - Log.Exception($"|PluginsLoader.DotNetPlugins|Couldn't load assembly for the plugin: {metadata.Name}", e); - } - catch (InvalidOperationException e) - { - Log.Exception($"|PluginsLoader.DotNetPlugins|Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e); - } - catch (ReflectionTypeLoadException e) - { - Log.Exception($"|PluginsLoader.DotNetPlugins|The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e); - } - catch (Exception e) - { - Log.Exception($"|PluginsLoader.DotNetPlugins|The following plugin has errored and can not be loaded: <{metadata.Name}>", e); - } + try + { + var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath); + assembly = assemblyLoader.LoadAssemblyAndDependencies(); - if (plugin == null) - { - erroredPlugins.Add(metadata.Name); - return; - } + var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, typeof(IPlugin), + typeof(IAsyncPlugin)); + + plugin = Activator.CreateInstance(type); + } + catch (Exception e) when (assembly == null) + { + Log.Exception($"|PluginsLoader.DotNetPlugins|Couldn't load assembly for the plugin: {metadata.Name}", e); + } + catch (InvalidOperationException e) + { + Log.Exception($"|PluginsLoader.DotNetPlugins|Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e); + } + catch (ReflectionTypeLoadException e) + { + Log.Exception($"|PluginsLoader.DotNetPlugins|The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e); + } + catch (Exception e) + { + Log.Exception($"|PluginsLoader.DotNetPlugins|The following plugin has errored and can not be loaded: <{metadata.Name}>", e); + } + + if (plugin == null) + { + erroredPlugins.Add(metadata.Name); + return; + } #endif - plugins.Add(new PluginPair - { - Plugin = plugin, - Metadata = metadata + plugins.Add(new PluginPair + { + Plugin = plugin, + Metadata = metadata + }); }); - }); metadata.InitTime += milliseconds; } @@ -95,15 +98,15 @@ namespace Flow.Launcher.Core.Plugin var errorPluginString = String.Join(Environment.NewLine, erroredPlugins); var errorMessage = "The following " - + (erroredPlugins.Count > 1 ? "plugins have " : "plugin has ") - + "errored and cannot be loaded:"; + + (erroredPlugins.Count > 1 ? "plugins have " : "plugin has ") + + "errored and cannot be loaded:"; Task.Run(() => { MessageBox.Show($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" + - $"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" + - $"Please refer to the logs for more information","", - MessageBoxButtons.OK, MessageBoxIcon.Warning); + $"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" + + $"Please refer to the logs for more information", "", + MessageBoxButtons.OK, MessageBoxIcon.Warning); }); } @@ -179,6 +182,5 @@ namespace Flow.Launcher.Core.Plugin Metadata = metadata }); } - } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Infrastructure/Http/Http.cs b/Flow.Launcher.Infrastructure/Http/Http.cs index 78fa099c9..5af50bc6d 100644 --- a/Flow.Launcher.Infrastructure/Http/Http.cs +++ b/Flow.Launcher.Infrastructure/Http/Http.cs @@ -8,6 +8,7 @@ using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using System; using System.ComponentModel; +using System.Threading; namespace Flow.Launcher.Infrastructure.Http { @@ -75,11 +76,11 @@ namespace Flow.Launcher.Infrastructure.Http }; } - public static async Task DownloadAsync([NotNull] string url, [NotNull] string filePath) + public static async Task DownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default) { try { - using var response = await client.GetAsync(url); + using var response = await client.GetAsync(url, token); if (response.StatusCode == HttpStatusCode.OK) { await using var fileStream = new FileStream(filePath, FileMode.CreateNew); @@ -102,40 +103,32 @@ namespace Flow.Launcher.Infrastructure.Http /// When supposing the result larger than 83kb, try using GetStreamAsync to avoid reading as string /// /// - /// - public static Task GetAsync([NotNull] string url) + /// The Http result as string. Null if cancellation requested + public static Task GetAsync([NotNull] string url, CancellationToken token = default) { Log.Debug($"|Http.Get|Url <{url}>"); - return GetAsync(new Uri(url.Replace("#", "%23"))); + return GetAsync(new Uri(url.Replace("#", "%23")), token); } /// - /// Asynchrously get the result as string from url. - /// When supposing the result larger than 83kb, try using GetStreamAsync to avoid reading as string + /// /// /// - /// - public static async Task GetAsync([NotNull] Uri url) + /// + /// The Http result as string. Null if cancellation requested + public static async Task GetAsync([NotNull] Uri url, CancellationToken token = default) { Log.Debug($"|Http.Get|Url <{url}>"); - try + using var response = await client.GetAsync(url, token); + var content = await response.Content.ReadAsStringAsync(); + if (response.StatusCode == HttpStatusCode.OK) { - using var response = await client.GetAsync(url); - var content = await response.Content.ReadAsStringAsync(); - if (response.StatusCode == HttpStatusCode.OK) - { - return content; - } - else - { - throw new HttpRequestException( - $"Error code <{response.StatusCode}> with content <{content}> returned from <{url}>"); - } + return content; } - catch (HttpRequestException e) + else { - Log.Exception("Infrastructure.Http", "Http Request Error", e, "GetAsync"); - throw; + throw new HttpRequestException( + $"Error code <{response.StatusCode}> with content <{content}> returned from <{url}>"); } } @@ -144,19 +137,11 @@ namespace Flow.Launcher.Infrastructure.Http /// /// /// - public static async Task GetStreamAsync([NotNull] string url) + public static async Task GetStreamAsync([NotNull] string url, CancellationToken token = default) { - try - { - Log.Debug($"|Http.Get|Url <{url}>"); - var response = await client.GetAsync(url); - return await response.Content.ReadAsStreamAsync(); - } - catch (HttpRequestException e) - { - Log.Exception("Infrastructure.Http", "Http Request Error", e, "GetStreamAsync"); - throw; - } + Log.Debug($"|Http.Get|Url <{url}>"); + var response = await client.GetAsync(url, token); + return await response.Content.ReadAsStreamAsync(); } } } diff --git a/Flow.Launcher.Infrastructure/Image/ImageCache.cs b/Flow.Launcher.Infrastructure/Image/ImageCache.cs index b1c09024f..bb7ec6817 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageCache.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageCache.cs @@ -73,8 +73,7 @@ namespace Flow.Launcher.Infrastructure.Image public bool ContainsKey(string key) { - var contains = Data.ContainsKey(key) && Data[key] != null; - return contains; + return Data.ContainsKey(key) && Data[key].imageSource != null; } public int CacheSize() diff --git a/Flow.Launcher.Infrastructure/Logger/Log.cs b/Flow.Launcher.Infrastructure/Logger/Log.cs index 91eeb183d..94132b27f 100644 --- a/Flow.Launcher.Infrastructure/Logger/Log.cs +++ b/Flow.Launcher.Infrastructure/Logger/Log.cs @@ -50,14 +50,18 @@ namespace Flow.Launcher.Infrastructure.Logger return valid; } - + [MethodImpl(MethodImplOptions.Synchronized)] public static void Exception(string className, string message, System.Exception exception, [CallerMemberName] string methodName = "") { +#if DEBUG + throw exception; +#else var classNameWithMethod = CheckClassAndMessageAndReturnFullClassWithMethod(className, message, methodName); ExceptionInternal(classNameWithMethod, message, exception); +#endif } private static string CheckClassAndMessageAndReturnFullClassWithMethod(string className, string message, diff --git a/Flow.Launcher.Infrastructure/Stopwatch.cs b/Flow.Launcher.Infrastructure/Stopwatch.cs index d39d90e81..dd6edaff9 100644 --- a/Flow.Launcher.Infrastructure/Stopwatch.cs +++ b/Flow.Launcher.Infrastructure/Stopwatch.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Logger; namespace Flow.Launcher.Infrastructure @@ -22,7 +23,22 @@ namespace Flow.Launcher.Infrastructure Log.Debug(info); return milliseconds; } - + + /// + /// This stopwatch will appear only in Debug mode + /// + public static async Task DebugAsync(string message, Func action) + { + var stopWatch = new System.Diagnostics.Stopwatch(); + stopWatch.Start(); + await action(); + stopWatch.Stop(); + var milliseconds = stopWatch.ElapsedMilliseconds; + string info = $"{message} <{milliseconds}ms>"; + Log.Debug(info); + return milliseconds; + } + public static long Normal(string message, Action action) { var stopWatch = new System.Diagnostics.Stopwatch(); @@ -34,6 +50,20 @@ namespace Flow.Launcher.Infrastructure Log.Info(info); return milliseconds; } + + public static async Task NormalAsync(string message, Func action) + { + var stopWatch = new System.Diagnostics.Stopwatch(); + stopWatch.Start(); + await action(); + stopWatch.Stop(); + var milliseconds = stopWatch.ElapsedMilliseconds; + string info = $"{message} <{milliseconds}ms>"; + Log.Info(info); + return milliseconds; + } + + public static void StartCount(string name, Action action) { diff --git a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs index 268dc20b8..f0e4a79fc 100644 --- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs @@ -9,7 +9,7 @@ namespace Flow.Launcher.Infrastructure.Storage /// /// Serialize object using json format. /// - public class JsonStrorage + public class JsonStrorage where T : new() { private readonly JsonSerializerOptions _serializerSettings; private T _data; @@ -76,7 +76,7 @@ namespace Flow.Launcher.Infrastructure.Storage BackupOriginFile(); } - _data = JsonSerializer.Deserialize("{}", _serializerSettings); + _data = new T(); Save(); } diff --git a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj index b7b52ac35..698802ba3 100644 --- a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj +++ b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj @@ -14,10 +14,10 @@ - 1.3.1 - 1.3.1 - 1.3.1 - 1.3.1 + 1.4.0 + 1.4.0 + 1.4.0 + 1.4.0 Flow.Launcher.Plugin Flow-Launcher MIT @@ -64,4 +64,4 @@ - \ No newline at end of file + diff --git a/Flow.Launcher.Plugin/IAsyncPlugin.cs b/Flow.Launcher.Plugin/IAsyncPlugin.cs new file mode 100644 index 000000000..b0b41cc22 --- /dev/null +++ b/Flow.Launcher.Plugin/IAsyncPlugin.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Flow.Launcher.Plugin +{ + /// + /// Asynchronous Plugin Model for Flow Launcher + /// + public interface IAsyncPlugin + { + /// + /// Asynchronous Querying + /// + /// + /// If the Querying or Init method requires high IO transmission + /// or performing CPU intense jobs (performing better with cancellation), please use this IAsyncPlugin interface + /// + /// Query to search + /// Cancel when querying job is obsolete + /// + Task> QueryAsync(Query query, CancellationToken token); + + /// + /// Initialize plugin asynchrously (will still wait finish to continue) + /// + /// + /// + Task InitAsync(PluginInitContext context); + } +} diff --git a/Flow.Launcher.Plugin/IPlugin.cs b/Flow.Launcher.Plugin/IPlugin.cs index 8f7d279fa..203dc9af7 100644 --- a/Flow.Launcher.Plugin/IPlugin.cs +++ b/Flow.Launcher.Plugin/IPlugin.cs @@ -2,9 +2,30 @@ namespace Flow.Launcher.Plugin { + /// + /// Synchronous Plugin Model for Flow Launcher + /// + /// If the Querying or Init method requires high IO transmission + /// or performaing CPU intense jobs (performing better with cancellation), please try the IAsyncPlugin interface + /// + /// public interface IPlugin { + /// + /// Querying when user's search changes + /// + /// This method will be called within a Task.Run, + /// so please avoid synchrously wait for long. + /// + /// + /// Query to search + /// List Query(Query query); + + /// + /// Initialize plugin + /// + /// void Init(PluginInitContext context); } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Plugin/IPublicAPI.cs b/Flow.Launcher.Plugin/IPublicAPI.cs index ccc00d5e9..12e430e07 100644 --- a/Flow.Launcher.Plugin/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/IPublicAPI.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; namespace Flow.Launcher.Plugin { @@ -34,7 +35,7 @@ namespace Flow.Launcher.Plugin /// Plugin's in memory data with new content /// added by user. /// - void ReloadAllPluginData(); + Task ReloadAllPluginData(); /// /// Check for new Flow Launcher update diff --git a/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs new file mode 100644 index 000000000..fc4ac4715 --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; + +namespace Flow.Launcher.Plugin +{ + /// + /// This interface is to indicate and allow plugins to asyncronously reload their + /// in memory data cache or other mediums when user makes a new change + /// that is not immediately captured. For example, for BrowserBookmark and Program + /// plugin does not automatically detect when a user added a new bookmark or program, + /// so this interface's function is exposed to allow user manually do the reloading after + /// those new additions. + /// + /// The command that allows user to manual reload is exposed via Plugin.Sys, and + /// it will call the plugins that have implemented this interface. + /// + public interface IAsyncReloadable + { + Task ReloadDataAsync(); + } +} diff --git a/Flow.Launcher.Plugin/Interfaces/IReloadable.cs b/Flow.Launcher.Plugin/Interfaces/IReloadable.cs index 29b3c15c9..31611519c 100644 --- a/Flow.Launcher.Plugin/Interfaces/IReloadable.cs +++ b/Flow.Launcher.Plugin/Interfaces/IReloadable.cs @@ -1,7 +1,7 @@ namespace Flow.Launcher.Plugin { /// - /// This interface is to indicate and allow plugins to reload their + /// This interface is to indicate and allow plugins to synchronously reload their /// in memory data cache or other mediums when user makes a new change /// that is not immediately captured. For example, for BrowserBookmark and Program /// plugin does not automatically detect when a user added a new bookmark or program, @@ -10,6 +10,10 @@ /// /// The command that allows user to manual reload is exposed via Plugin.Sys, and /// it will call the plugins that have implemented this interface. + /// + /// + /// If requiring reloading data asynchronously, please use the IAsyncReloadable interface + /// /// public interface IReloadable { diff --git a/Flow.Launcher.Plugin/PluginInitContext.cs b/Flow.Launcher.Plugin/PluginInitContext.cs index 49366a5c6..04f20e984 100644 --- a/Flow.Launcher.Plugin/PluginInitContext.cs +++ b/Flow.Launcher.Plugin/PluginInitContext.cs @@ -4,6 +4,16 @@ namespace Flow.Launcher.Plugin { public class PluginInitContext { + public PluginInitContext() + { + } + + public PluginInitContext(PluginMetadata currentPluginMetadata, IPublicAPI api) + { + CurrentPluginMetadata = currentPluginMetadata; + API = api; + } + public PluginMetadata CurrentPluginMetadata { get; internal set; } /// diff --git a/Flow.Launcher.Plugin/PluginPair.cs b/Flow.Launcher.Plugin/PluginPair.cs index 910367ec6..e8954b7a0 100644 --- a/Flow.Launcher.Plugin/PluginPair.cs +++ b/Flow.Launcher.Plugin/PluginPair.cs @@ -2,7 +2,7 @@ { public class PluginPair { - public IPlugin Plugin { get; internal set; } + public object Plugin { get; internal set; } public PluginMetadata Metadata { get; internal set; } diff --git a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs index 27cd1a558..37222a1d0 100644 --- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs +++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs @@ -146,31 +146,23 @@ namespace Flow.Launcher.Plugin.SharedCommands /// This checks whether a given string is a directory path or network location string. /// It does not check if location actually exists. /// - public static bool IsLocationPathString(string querySearchString) + public static bool IsLocationPathString(this string querySearchString) { - if (string.IsNullOrEmpty(querySearchString)) + if (string.IsNullOrEmpty(querySearchString) || querySearchString.Length < 3) return false; // // shared folder location, and not \\\location\ - if (querySearchString.Length >= 3 - && querySearchString.StartsWith(@"\\") - && char.IsLetter(querySearchString[2])) + if (querySearchString.StartsWith(@"\\") + && querySearchString[2] != '\\') return true; // c:\ - if (querySearchString.Length == 3 - && char.IsLetter(querySearchString[0]) + if (char.IsLetter(querySearchString[0]) && querySearchString[1] == ':' && querySearchString[2] == '\\') - return true; - - // c:\\ - if (querySearchString.Length >= 4 - && char.IsLetter(querySearchString[0]) - && querySearchString[1] == ':' - && querySearchString[2] == '\\' - && char.IsLetter(querySearchString[3])) - return true; + { + return querySearchString.Length == 3 || querySearchString[3] != '\\'; + } return false; } diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs index c91144825..57747c8b2 100644 --- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs +++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs @@ -7,6 +7,8 @@ using Flow.Launcher.Plugin.SharedCommands; using NUnit.Framework; using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace Flow.Launcher.Test.Plugins { @@ -17,15 +19,15 @@ namespace Flow.Launcher.Test.Plugins [TestFixture] public class ExplorerTest { - private List MethodWindowsIndexSearchReturnsZeroResults(Query dummyQuery, string dummyString) + private async Task> MethodWindowsIndexSearchReturnsZeroResultsAsync(Query dummyQuery, string dummyString, CancellationToken dummyToken) { return new List(); } private List MethodDirectoryInfoClassSearchReturnsTwoResults(Query dummyQuery, string dummyString) { - return new List - { + return new List + { new Result { Title="Result 1" @@ -58,16 +60,16 @@ namespace Flow.Launcher.Test.Plugins $"Actual: {result}{Environment.NewLine}"); } - [TestCase("C:\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\'")] - [TestCase("C:\\SomeFolder\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\'")] + [TestCase("C:\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\' ORDER BY System.FileName")] + [TestCase("C:\\SomeFolder\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\' ORDER BY System.FileName")] public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_ThenQueryShouldUseExpectedString(string folderPath, string expectedString) { // Given var queryConstructor = new QueryConstructor(new Settings()); - + //When var queryString = queryConstructor.QueryForTopLevelDirectorySearch(folderPath); - + // Then Assert.IsTrue(queryString == expectedString, $"Expected string: {expectedString}{Environment.NewLine} " + @@ -77,7 +79,7 @@ namespace Flow.Launcher.Test.Plugins [TestCase("C:\\SomeFolder\\flow.launcher.sln", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " + "FROM SystemIndex WHERE (System.FileName LIKE 'flow.launcher.sln%' " + "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033))" + - " AND directory='file:C:\\SomeFolder'")] + " AND directory='file:C:\\SomeFolder' ORDER BY System.FileName")] public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryShouldUseExpectedString( string userSearchString, string expectedString) { @@ -112,13 +114,10 @@ namespace Flow.Launcher.Test.Plugins } [TestCase("scope='file:'")] - public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryWhereRestrictionsShouldUseScopeString(string expectedString) + public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryWhereRestrictionsShouldUseScopeString(string expectedString) { - // Given - var queryConstructor = new QueryConstructor(new Settings()); - //When - var resultString = queryConstructor.QueryWhereRestrictionsForAllFilesAndFoldersSearch(); + var resultString = QueryConstructor.QueryWhereRestrictionsForAllFilesAndFoldersSearch; // Then Assert.IsTrue(resultString == expectedString, @@ -128,9 +127,9 @@ namespace Flow.Launcher.Test.Plugins [TestCase("flow.launcher.sln", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" " + "FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " + - "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:'")] + "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY System.FileName")] public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShouldUseExpectedString( - string userSearchString, string expectedString) + string userSearchString, string expectedString) { // Given var queryConstructor = new QueryConstructor(new Settings()); @@ -145,18 +144,19 @@ namespace Flow.Launcher.Test.Plugins } [TestCase] - public void GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldContinueDirectoryInfoClassSearch() + public async Task GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldContinueDirectoryInfoClassSearch() { // Given var searchManager = new SearchManager(new Settings(), new PluginInitContext()); - + // When - var results = searchManager.TopLevelDirectorySearchBehaviour( - MethodWindowsIndexSearchReturnsZeroResults, - MethodDirectoryInfoClassSearchReturnsTwoResults, - false, + var results = await searchManager.TopLevelDirectorySearchBehaviourAsync( + MethodWindowsIndexSearchReturnsZeroResultsAsync, + MethodDirectoryInfoClassSearchReturnsTwoResults, + false, new Query(), - "string not used"); + "string not used", + default); // Then Assert.IsTrue(results.Count == 2, @@ -165,18 +165,19 @@ namespace Flow.Launcher.Test.Plugins } [TestCase] - public void GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldNotContinueDirectoryInfoClassSearch() + public async Task GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldNotContinueDirectoryInfoClassSearch() { // Given var searchManager = new SearchManager(new Settings(), new PluginInitContext()); // When - var results = searchManager.TopLevelDirectorySearchBehaviour( - MethodWindowsIndexSearchReturnsZeroResults, + var results = await searchManager.TopLevelDirectorySearchBehaviourAsync( + MethodWindowsIndexSearchReturnsZeroResultsAsync, MethodDirectoryInfoClassSearchReturnsTwoResults, true, new Query(), - "string not used"); + "string not used", + default); // Then Assert.IsTrue(results.Count == 0, @@ -201,7 +202,7 @@ namespace Flow.Launcher.Test.Plugins } [TestCase("some words", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " + - "FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:'")] + "FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY System.FileName")] public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseExpectedString( string userSearchString, string expectedString) { @@ -223,7 +224,7 @@ namespace Flow.Launcher.Test.Plugins var query = new Query { ActionKeyword = "doc:", Search = "search term" }; var searchManager = new SearchManager(new Settings(), new PluginInitContext()); - + // When var result = searchManager.IsFileContentSearch(query.ActionKeyword); @@ -239,6 +240,9 @@ namespace Flow.Launcher.Test.Plugins [TestCase(@"cc:\", false)] [TestCase(@"\\\SomeNetworkLocation\", false)] [TestCase("RandomFile", false)] + [TestCase(@"c:\>*", true)] + [TestCase(@"c:\>", true)] + [TestCase(@"c:\SomeLocation\SomeOtherLocation\>", true)] public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString(string querySearchString, bool expectedResult) { // When, Given @@ -250,7 +254,7 @@ namespace Flow.Launcher.Test.Plugins $"Actual check result is {result} {Environment.NewLine}"); } - + [TestCase(@"C:\SomeFolder\SomeApp", true, @"C:\SomeFolder\")] [TestCase(@"C:\SomeFolder\SomeApp\SomeFile", true, @"C:\SomeFolder\SomeApp\")] [TestCase(@"C:\NonExistentFolder\SomeApp", false, "")] @@ -291,10 +295,10 @@ namespace Flow.Launcher.Test.Plugins } [TestCase("c:\\SomeFolder\\>", "scope='file:c:\\SomeFolder'")] - [TestCase("c:\\SomeFolder\\>SomeName", "(System.FileName LIKE 'SomeName%' " + - "OR CONTAINS(System.FileName,'\"SomeName*\"',1033)) AND " + - "scope='file:c:\\SomeFolder'")] - public void GivenWindowsIndexSearch_WhenSearchPatternHotKeyIsSearchAll_ThenQueryWhereRestrictionsShouldUseScopeString(string path, string expectedString) + [TestCase("c:\\SomeFolder\\>SomeName", "(System.FileName LIKE 'SomeName%' " + + "OR CONTAINS(System.FileName,'\"SomeName*\"',1033)) AND " + + "scope='file:c:\\SomeFolder'")] + public void GivenWindowsIndexSearch_WhenSearchPatternHotKeyIsSearchAll_ThenQueryWhereRestrictionsShouldUseScopeString(string path, string expectedString) { // Given var queryConstructor = new QueryConstructor(new Settings()); @@ -308,7 +312,7 @@ namespace Flow.Launcher.Test.Plugins $"Actual string was: {resultString}{Environment.NewLine}"); } - [TestCase("c:\\somefolder\\>somefile","*somefile*")] + [TestCase("c:\\somefolder\\>somefile", "*somefile*")] [TestCase("c:\\somefolder\\somefile", "somefile*")] [TestCase("c:\\somefolder\\", "*")] public void GivenDirectoryInfoSearch_WhenSearchPatternHotKeyIsSearchAll_ThenSearchCriteriaShouldUseCriteriaString(string path, string expectedString) diff --git a/Flow.Launcher/App.xaml b/Flow.Launcher/App.xaml index f3347d7fb..18addac73 100644 --- a/Flow.Launcher/App.xaml +++ b/Flow.Launcher/App.xaml @@ -3,7 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ui="http://schemas.modernwpf.com/2019" ShutdownMode="OnMainWindowClose" - Startup="OnStartup"> + Startup="OnStartupAsync"> diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 59bdbc896..06bb16e3b 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -45,9 +45,9 @@ namespace Flow.Launcher } } - private void OnStartup(object sender, StartupEventArgs e) + private async void OnStartupAsync(object sender, StartupEventArgs e) { - Stopwatch.Normal("|App.OnStartup|Startup cost", () => + await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => { _portable.PreStartCleanUpAfterPortabilityUpdate(); @@ -68,9 +68,10 @@ namespace Flow.Launcher PluginManager.LoadPlugins(_settings.PluginSettings); _mainVM = new MainViewModel(_settings); - var window = new MainWindow(_settings, _mainVM); API = new PublicAPIInstance(_settingsVM, _mainVM, _alphabet); - PluginManager.InitializePlugins(API); + await PluginManager.InitializePlugins(API); + var window = new MainWindow(_settings, _mainVM); + Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}"); Current.MainWindow = window; diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index 90d4fff63..17673a62a 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -78,9 +78,9 @@ namespace Flow.Launcher ImageLoader.Save(); } - public void ReloadAllPluginData() + public Task ReloadAllPluginData() { - PluginManager.ReloadData(); + return PluginManager.ReloadData(); } public void ShowMsg(string title, string subTitle = "", string iconPath = "") @@ -92,7 +92,7 @@ namespace Flow.Launcher { Application.Current.Dispatcher.Invoke(() => { - var msg = useMainWindowAsOwner ? new Msg {Owner = Application.Current.MainWindow} : new Msg(); + var msg = useMainWindowAsOwner ? new Msg { Owner = Application.Current.MainWindow } : new Msg(); msg.Show(title, subTitle, iconPath); }); } diff --git a/Flow.Launcher/ResultListBox.xaml b/Flow.Launcher/ResultListBox.xaml index 072196605..2f9d06d81 100644 --- a/Flow.Launcher/ResultListBox.xaml +++ b/Flow.Launcher/ResultListBox.xaml @@ -9,7 +9,7 @@ d:DataContext="{d:DesignInstance vm:ResultsViewModel}" MaxHeight="{Binding MaxHeight}" SelectedIndex="{Binding SelectedIndex, Mode=TwoWay}" - SelectedItem="{Binding SelectedItem, Mode=OneWayToSource}" + SelectedItem="{Binding SelectedItem, Mode=TwoWay}" HorizontalContentAlignment="Stretch" ItemsSource="{Binding Results}" Margin="{Binding Margin}" Visibility="{Binding Visbility}" diff --git a/Flow.Launcher/Storage/TopMostRecord.cs b/Flow.Launcher/Storage/TopMostRecord.cs index 09e69f6d8..c92ef4956 100644 --- a/Flow.Launcher/Storage/TopMostRecord.cs +++ b/Flow.Launcher/Storage/TopMostRecord.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; +using System.Text.Json.Serialization; using Flow.Launcher.Plugin; namespace Flow.Launcher.Storage @@ -8,20 +9,24 @@ namespace Flow.Launcher.Storage // todo this class is not thread safe.... but used from multiple threads. public class TopMostRecord { - private Dictionary records = new Dictionary(); + /// + /// You should not directly access this field + /// + /// It is public due to System.Text.Json limitation in version 3.1 + /// + /// + /// TODO: Set it to private + public Dictionary records { get; set; } = new Dictionary(); internal bool IsTopMost(Result result) { - if (records.Count == 0) + if (records.Count == 0 || !records.ContainsKey(result.OriginQuery.RawQuery)) { return false; } - // since this dictionary should be very small (or empty) going over it should be pretty fast. - return records.Any(o => o.Value.Title == result.Title - && o.Value.SubTitle == result.SubTitle - && o.Value.PluginID == result.PluginID - && o.Key == result.OriginQuery.RawQuery); + // since this dictionary should be very small (or empty) going over it should be pretty fast. + return records[result.OriginQuery.RawQuery].Equals(result); } internal void Remove(Result result) @@ -53,5 +58,12 @@ namespace Flow.Launcher.Storage public string Title { get; set; } public string SubTitle { get; set; } public string PluginID { get; set; } + + public bool Equals(Result r) + { + return Title == r.Title + && SubTitle == r.SubTitle + && PluginID == r.PluginID; + } } } diff --git a/Flow.Launcher/Storage/UserSelectedRecord.cs b/Flow.Launcher/Storage/UserSelectedRecord.cs index c7ffe1a1d..bc7a2da73 100644 --- a/Flow.Launcher/Storage/UserSelectedRecord.cs +++ b/Flow.Launcher/Storage/UserSelectedRecord.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text.Json.Serialization; using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Plugin; @@ -6,14 +7,27 @@ namespace Flow.Launcher.Storage { public class UserSelectedRecord { - private Dictionary records = new Dictionary(); + /// + /// You should not directly access this field + /// + /// It is public due to System.Text.Json limitation in version 3.1 + /// + /// + /// TODO: Set it to private + [JsonPropertyName("records")] + public Dictionary records { get; set; } + + public UserSelectedRecord() + { + records = new Dictionary(); + } public void Add(Result result) { var key = result.ToString(); - if (records.TryGetValue(key, out int value)) + if (records.ContainsKey(key)) { - records[key] = value + 1; + records[key]++; } else { diff --git a/Flow.Launcher/Themes/BlurBlack Darker.xaml b/Flow.Launcher/Themes/BlurBlack Darker.xaml new file mode 100644 index 000000000..5c615d500 --- /dev/null +++ b/Flow.Launcher/Themes/BlurBlack Darker.xaml @@ -0,0 +1,63 @@ + + + + + + True + + + + + + + + + + + + + + + + + #356ef3 + + + + + + diff --git a/Flow.Launcher/Themes/BlurWhite.xaml b/Flow.Launcher/Themes/BlurWhite.xaml index 1c1f2f9ec..6a130bb39 100644 --- a/Flow.Launcher/Themes/BlurWhite.xaml +++ b/Flow.Launcher/Themes/BlurWhite.xaml @@ -17,7 +17,7 @@ @@ -26,7 +26,7 @@ - + diff --git a/Flow.Launcher/Themes/Gray.xaml b/Flow.Launcher/Themes/Gray.xaml index 16a1db274..1fbaa959a 100644 --- a/Flow.Launcher/Themes/Gray.xaml +++ b/Flow.Launcher/Themes/Gray.xaml @@ -4,21 +4,19 @@ @@ -31,15 +29,15 @@ - #00AAF6 + #787878 @@ -38,7 +38,7 @@ - #3875D7 + #909090 + + + + + + + + + + + #5e81ac + + + + diff --git a/Flow.Launcher/Themes/Nord.xaml b/Flow.Launcher/Themes/Nord.xaml new file mode 100644 index 000000000..2253b3410 --- /dev/null +++ b/Flow.Launcher/Themes/Nord.xaml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + #5e81ac + + + + diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index e83e28c33..8195c745a 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -19,9 +17,8 @@ using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Storage; -using System.Windows.Media; -using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Infrastructure.Logger; +using System.Threading.Tasks.Dataflow; namespace Flow.Launcher.ViewModel { @@ -48,6 +45,8 @@ namespace Flow.Launcher.ViewModel private bool _saved; private readonly Internationalization _translator = InternationalizationManager.Instance; + private BufferBlock _resultsUpdateQueue; + private Task _resultsViewUpdateTask; #endregion @@ -75,8 +74,11 @@ namespace Flow.Launcher.ViewModel _selectedResults = Results; InitializeKeyCommands(); + + RegisterViewUpdate(); RegisterResultsUpdatedEvent(); + SetHotkey(_settings.Hotkey, OnHotkey); SetCustomPluginHotkey(); SetOpenResultModifiers(); @@ -86,18 +88,54 @@ namespace Flow.Launcher.ViewModel { foreach (var pair in PluginManager.GetPluginsForInterface()) { - var plugin = (IResultUpdated)pair.Plugin; + var plugin = (IResultUpdated) pair.Plugin; plugin.ResultsUpdated += (s, e) => { - Task.Run(() => - { - PluginManager.UpdatePluginMetadata(e.Results, pair.Metadata, e.Query); - UpdateResultView(e.Results, pair.Metadata, e.Query); - }, _updateToken); + PluginManager.UpdatePluginMetadata(e.Results, pair.Metadata, e.Query); + if (e.Query.Search == _lastQuery.Search) + _resultsUpdateQueue.Post(new ResultsForUpdate(e.Results, pair.Metadata, e.Query, _updateToken)); }; } } + private void RegisterViewUpdate() + { + _resultsUpdateQueue = new BufferBlock(); + _resultsViewUpdateTask = + Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted); + + + async Task updateAction() + { + var queue = new Dictionary(); + while (await _resultsUpdateQueue.OutputAvailableAsync()) + { + queue.Clear(); + await Task.Delay(20); + while (_resultsUpdateQueue.TryReceive(out var item)) + { + if (!item.Token.IsCancellationRequested) + queue[item.ID] = item; + } + + UpdateResultView(queue.Values); + } + } + + ; + + void continueAction(Task t) + { +#if DEBUG + throw t.Exception; +#else + Log.Error($"Error happen in task dealing with viewupdate for results. {t.Exception}"); + _resultsViewUpdateTask = + Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted); +#endif + } + } + private void InitializeKeyCommands() { @@ -113,25 +151,13 @@ namespace Flow.Launcher.ViewModel } }); - SelectNextItemCommand = new RelayCommand(_ => - { - SelectedResults.SelectNextResult(); - }); + SelectNextItemCommand = new RelayCommand(_ => { SelectedResults.SelectNextResult(); }); - SelectPrevItemCommand = new RelayCommand(_ => - { - SelectedResults.SelectPrevResult(); - }); + SelectPrevItemCommand = new RelayCommand(_ => { SelectedResults.SelectPrevResult(); }); - SelectNextPageCommand = new RelayCommand(_ => - { - SelectedResults.SelectNextPage(); - }); + SelectNextPageCommand = new RelayCommand(_ => { SelectedResults.SelectNextPage(); }); - SelectPrevPageCommand = new RelayCommand(_ => - { - SelectedResults.SelectPrevPage(); - }); + SelectPrevPageCommand = new RelayCommand(_ => { SelectedResults.SelectPrevPage(); }); SelectFirstResultCommand = new RelayCommand(_ => SelectedResults.SelectFirstResult()); @@ -207,11 +233,13 @@ namespace Flow.Launcher.ViewModel public ResultsViewModel Results { get; private set; } public ResultsViewModel ContextMenu { get; private set; } public ResultsViewModel History { get; private set; } + private string _lastQueryText; private string _queryText; + public string QueryText { - get { return _queryText; } + get => _queryText; set { _queryText = value; @@ -229,10 +257,12 @@ namespace Flow.Launcher.ViewModel QueryTextCursorMovedToEnd = true; QueryText = queryText; } + public bool LastQuerySelected { get; set; } public bool QueryTextCursorMovedToEnd { get; set; } private ResultsViewModel _selectedResults; + private ResultsViewModel SelectedResults { get { return _selectedResults; } @@ -264,6 +294,7 @@ namespace Flow.Launcher.ViewModel QueryText = string.Empty; } } + _selectedResults.Visbility = Visibility.Visible; } } @@ -323,9 +354,20 @@ namespace Flow.Launcher.ViewModel { var filtered = results.Where ( - r => StringMatcher.FuzzySearch(query, r.Title).IsSearchPrecisionScoreMet() - || StringMatcher.FuzzySearch(query, r.SubTitle).IsSearchPrecisionScoreMet() - ).ToList(); + r => + { + var match = StringMatcher.FuzzySearch(query, r.Title); + if (!match.IsSearchPrecisionScoreMet()) + { + match = StringMatcher.FuzzySearch(query, r.SubTitle); + } + + if (!match.IsSearchPrecisionScoreMet()) return false; + + r.Score = match.Score; + return true; + + }).ToList(); ContextMenu.AddResults(filtered, id); } else @@ -351,7 +393,7 @@ namespace Flow.Launcher.ViewModel Title = string.Format(title, h.Query), SubTitle = string.Format(time, h.ExecutedDateTime), IcoPath = "Images\\history.png", - OriginQuery = new Query { RawQuery = h.Query }, + OriginQuery = new Query {RawQuery = h.Query}, Action = _ => { SelectedResults = Results; @@ -379,100 +421,128 @@ namespace Flow.Launcher.ViewModel private void QueryResults() { - if (!string.IsNullOrEmpty(QueryText)) + _updateSource?.Cancel(); + + if (string.IsNullOrWhiteSpace(QueryText)) { - _updateSource?.Cancel(); - var currentUpdateSource = new CancellationTokenSource(); - _updateSource = currentUpdateSource; - var currentCancellationToken = _updateSource.Token; - _updateToken = currentCancellationToken; + Results.Clear(); + Results.Visbility = Visibility.Collapsed; + return; + } - ProgressBarVisibility = Visibility.Hidden; - _isQueryRunning = true; - var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.NonGlobalPlugins); - if (query != null) + _updateSource?.Dispose(); + + var currentUpdateSource = new CancellationTokenSource(); + _updateSource = currentUpdateSource; + var currentCancellationToken = _updateSource.Token; + _updateToken = currentCancellationToken; + + ProgressBarVisibility = Visibility.Hidden; + _isQueryRunning = true; + + var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.NonGlobalPlugins); + + // handle the exclusiveness of plugin using action keyword + RemoveOldQueryResults(query); + + _lastQuery = query; + + var plugins = PluginManager.ValidPluginsForQuery(query); + + Task.Run(async () => { - // handle the exclusiveness of plugin using action keyword - RemoveOldQueryResults(query); + if (query.ActionKeyword == Plugin.Query.GlobalPluginWildcardSign) + { + // Wait 45 millisecond for query change in global query + // if query changes, return so that it won't be calculated + await Task.Delay(45, currentCancellationToken); + if (currentCancellationToken.IsCancellationRequested) + return; + } - _lastQuery = query; - Task.Delay(200, currentCancellationToken).ContinueWith(_ => - { // start the progress bar if query takes more than 200 ms and this is the current running query and it didn't finish yet - if (currentUpdateSource == _updateSource && _isQueryRunning) + _ = Task.Delay(200, currentCancellationToken).ContinueWith(_ => + { + // start the progress bar if query takes more than 200 ms and this is the current running query and it didn't finish yet + if (!currentCancellationToken.IsCancellationRequested && _isQueryRunning) { ProgressBarVisibility = Visibility.Visible; } }, currentCancellationToken); - var plugins = PluginManager.ValidPluginsForQuery(query); - Task.Run(() => + Task[] tasks = new Task[plugins.Count]; + try { - // so looping will stop once it was cancelled - var parallelOptions = new ParallelOptions { CancellationToken = currentCancellationToken }; - try + for (var i = 0; i < plugins.Count; i++) { - Parallel.ForEach(plugins, parallelOptions, plugin => + if (!plugins[i].Metadata.Disabled) { - if (!plugin.Metadata.Disabled) - { - try - { - var results = PluginManager.QueryForPlugin(plugin, query); - UpdateResultView(results, plugin.Metadata, query); - } - catch (Exception e) - { - Log.Exception("MainViewModel", $"Exception when querying the plugin {plugin.Metadata.Name}", e, "QueryResults"); - } - } - }); - } - catch (OperationCanceledException) - { - // nothing to do here + tasks[i] = QueryTask(plugins[i]); + } + else + { + tasks[i] = Task.CompletedTask; // Avoid Null + } } - - // this should happen once after all queries are done so progress bar should continue - // until the end of all querying - _isQueryRunning = false; - if (currentUpdateSource == _updateSource) - { // update to hidden if this is still the current query - ProgressBarVisibility = Visibility.Hidden; - } - }, currentCancellationToken).ContinueWith(t => + // Check the code, WhenAll will translate all type of IEnumerable or Collection to Array, so make an array at first + await Task.WhenAll(tasks); + } + catch (OperationCanceledException) { - Log.Exception("MainViewModel", "Error when querying plugins", t.Exception?.InnerException, "QueryResults"); - }, TaskContinuationOptions.OnlyOnFaulted); - } - } - else - { - Results.Clear(); - Results.Visbility = Visibility.Collapsed; - } + // nothing to do here + } + + if (currentCancellationToken.IsCancellationRequested) + return; + + // this should happen once after all queries are done so progress bar should continue + // until the end of all querying + _isQueryRunning = false; + if (!currentCancellationToken.IsCancellationRequested) + { + // update to hidden if this is still the current query + ProgressBarVisibility = Visibility.Hidden; + } + + // Local function + async Task QueryTask(PluginPair plugin) + { + // Since it is wrapped within a Task.Run, the synchronous context is null + // Task.Yield will force it to run in ThreadPool + await Task.Yield(); + + var results = await PluginManager.QueryForPlugin(plugin, query, currentCancellationToken); + if (!currentCancellationToken.IsCancellationRequested) + _resultsUpdateQueue.Post(new ResultsForUpdate(results, plugin.Metadata, query, + currentCancellationToken)); + } + }, currentCancellationToken) + .ContinueWith(t => Log.Exception("|MainViewModel|Plugins Query Exceptions", t.Exception), + TaskContinuationOptions.OnlyOnFaulted); } + private void RemoveOldQueryResults(Query query) { string lastKeyword = _lastQuery.ActionKeyword; + string keyword = query.ActionKeyword; if (string.IsNullOrEmpty(lastKeyword)) { if (!string.IsNullOrEmpty(keyword)) { - Results.RemoveResultsExcept(PluginManager.NonGlobalPlugins[keyword].Metadata); + Results.KeepResultsFor(PluginManager.NonGlobalPlugins[keyword].Metadata); } } else { if (string.IsNullOrEmpty(keyword)) { - Results.RemoveResultsFor(PluginManager.NonGlobalPlugins[lastKeyword].Metadata); + Results.KeepResultsExcept(PluginManager.NonGlobalPlugins[lastKeyword].Metadata); } else if (lastKeyword != keyword) { - Results.RemoveResultsExcept(PluginManager.NonGlobalPlugins[keyword].Metadata); + Results.KeepResultsFor(PluginManager.NonGlobalPlugins[keyword].Metadata); } } } @@ -510,6 +580,7 @@ namespace Flow.Launcher.ViewModel } }; } + return menu; } @@ -549,12 +620,12 @@ namespace Flow.Launcher.ViewModel return selected; } - private bool HistorySelected() { var selected = SelectedResults == History; return selected; } + #region Hotkey private void SetHotkey(string hotkeyStr, EventHandler action) @@ -573,7 +644,8 @@ namespace Flow.Launcher.ViewModel catch (Exception) { string errorMsg = - string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr); + string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), + hotkeyStr); MessageBox.Show(errorMsg); } } @@ -623,7 +695,6 @@ namespace Flow.Launcher.ViewModel { if (!ShouldIgnoreHotkeys()) { - if (_settings.LastQueryMode == LastQueryMode.Empty) { ChangeQueryText(string.Empty); @@ -677,30 +748,47 @@ namespace Flow.Launcher.ViewModel /// /// To avoid deadlock, this method should not called from main thread /// - public void UpdateResultView(List list, PluginMetadata metadata, Query originQuery) + public void UpdateResultView(IEnumerable resultsForUpdates) { - foreach (var result in list) + if (!resultsForUpdates.Any()) + return; + CancellationToken token; + + try { - if (_topMostRecord.IsTopMost(result)) + // Don't know why sometimes even resultsForUpdates is empty, the method won't return; + token = resultsForUpdates.Select(r => r.Token).Distinct().SingleOrDefault(); + } +#if DEBUG + catch + { + throw new ArgumentException("Unacceptable token"); + } +#else + catch + { + token = default; + } +#endif + + + foreach (var metaResults in resultsForUpdates) + { + foreach (var result in metaResults.Results) { - result.Score = int.MaxValue; - } - else - { - var priorityScore = metadata.Priority * 150; - result.Score += _userSelectedRecord.GetSelectedCount(result) * 5 + priorityScore; + if (_topMostRecord.IsTopMost(result)) + { + result.Score = int.MaxValue; + } + else + { + var priorityScore = metaResults.Metadata.Priority * 150; + result.Score += _userSelectedRecord.GetSelectedCount(result) * 5 + priorityScore; + } } } - if (originQuery.RawQuery == _lastQuery.RawQuery) - { - Results.AddResults(list, metadata.ID); - } - - if (Results.Visbility != Visibility.Visible && list.Count > 0) - { - Results.Visbility = Visibility.Visible; - } + Results.AddResults(resultsForUpdates, token); } #endregion diff --git a/Flow.Launcher/ViewModel/ResultViewModel.cs b/Flow.Launcher/ViewModel/ResultViewModel.cs index 4c65f2b9f..c91bbb107 100644 --- a/Flow.Launcher/ViewModel/ResultViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultViewModel.cs @@ -1,9 +1,7 @@ using System; -using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; -using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; @@ -106,14 +104,10 @@ namespace Flow.Launcher.ViewModel } if (ImageLoader.CacheContainImage(imagePath)) - { // will get here either when icoPath has value\icon delegate is null\when had exception in delegate return ImageLoader.Load(imagePath); - } - else - { - return await Task.Run(() => ImageLoader.Load(imagePath)); - } + + return await Task.Run(() => ImageLoader.Load(imagePath)); } public Result Result { get; } diff --git a/Flow.Launcher/ViewModel/ResultsForUpdate.cs b/Flow.Launcher/ViewModel/ResultsForUpdate.cs new file mode 100644 index 000000000..be48f53c1 --- /dev/null +++ b/Flow.Launcher/ViewModel/ResultsForUpdate.cs @@ -0,0 +1,35 @@ +using Flow.Launcher.Plugin; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; + +namespace Flow.Launcher.ViewModel +{ + public class ResultsForUpdate + { + public List Results { get; } + + public PluginMetadata Metadata { get; } + public string ID { get; } + + public Query Query { get; } + public CancellationToken Token { get; } + + public ResultsForUpdate(List results, string resultID, CancellationToken token) + { + Results = results; + ID = resultID; + Token = token; + } + + public ResultsForUpdate(List results, PluginMetadata metadata, Query query, CancellationToken token) + { + Results = results; + Metadata = metadata; + Query = query; + Token = token; + ID = metadata.ID; + } + } +} diff --git a/Flow.Launcher/ViewModel/ResultsViewModel.cs b/Flow.Launcher/ViewModel/ResultsViewModel.cs index d30854180..1b8dd602d 100644 --- a/Flow.Launcher/ViewModel/ResultsViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultsViewModel.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Collections.Specialized; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; @@ -17,7 +20,6 @@ namespace Flow.Launcher.ViewModel public ResultCollection Results { get; } - private readonly object _addResultsLock = new object(); private readonly object _collectionLock = new object(); private readonly Settings _settings; private int MaxResults => _settings?.MaxResultsToShow ?? 6; @@ -116,17 +118,20 @@ namespace Flow.Launcher.ViewModel public void Clear() { - Results.Clear(); + lock (_collectionLock) + Results.RemoveAll(); } - public void RemoveResultsExcept(PluginMetadata metadata) + public void KeepResultsFor(PluginMetadata metadata) { - Results.RemoveAll(r => r.Result.PluginID != metadata.ID); + lock (_collectionLock) + Results.Update(Results.Where(r => r.Result.PluginID == metadata.ID).ToList()); } - public void RemoveResultsFor(PluginMetadata metadata) + public void KeepResultsExcept(PluginMetadata metadata) { - Results.RemoveAll(r => r.Result.PluginID == metadata.ID); + lock (_collectionLock) + Results.Update(Results.Where(r => r.Result.PluginID != metadata.ID).ToList()); } /// @@ -134,70 +139,99 @@ namespace Flow.Launcher.ViewModel /// public void AddResults(List newRawResults, string resultId) { - lock (_addResultsLock) + lock (_collectionLock) { var newResults = NewResults(newRawResults, resultId); - // update UI in one run, so it can avoid UI flickering - Results.Update(newResults); - - if (Results.Count > 0) + // https://social.msdn.microsoft.com/Forums/vstudio/en-US/5ff71969-f183-4744-909d-50f7cd414954/binding-a-tabcontrols-selectedindex-not-working?forum=wpf + // fix selected index flow + var updateTask = Task.Run(() => { + // update UI in one run, so it can avoid UI flickering + + Results.Update(newResults); + if (Results.Any()) + SelectedItem = Results[0]; + }); + if (!updateTask.Wait(300)) + { + updateTask.Dispose(); + throw new TimeoutException("Update result use too much time."); + } + + } + + if (Visbility != Visibility.Visible && Results.Count > 0) + { + Margin = new Thickness { Top = 8 }; + SelectedIndex = 0; + Visbility = Visibility.Visible; + } + else + { + Margin = new Thickness { Top = 0 }; + Visbility = Visibility.Collapsed; + } + } + /// + /// To avoid deadlock, this method should not called from main thread + /// + public void AddResults(IEnumerable resultsForUpdates, CancellationToken token) + { + var newResults = NewResults(resultsForUpdates); + if (token.IsCancellationRequested) + return; + lock (_collectionLock) + { + // update UI in one run, so it can avoid UI flickering + + Results.Update(newResults, token); + if (Results.Any()) + SelectedItem = Results[0]; + } + + switch (Visbility) + { + case Visibility.Collapsed when Results.Count > 0: Margin = new Thickness { Top = 8 }; SelectedIndex = 0; - } - else - { + Visbility = Visibility.Visible; + break; + case Visibility.Visible when Results.Count == 0: Margin = new Thickness { Top = 0 }; - } + Visbility = Visibility.Collapsed; + break; } + } private List NewResults(List newRawResults, string resultId) { - var results = Results.ToList(); + if (newRawResults.Count == 0) + return Results.ToList(); + + var results = Results as IEnumerable; + var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings)).ToList(); - var oldResults = results.Where(r => r.Result.PluginID == resultId).ToList(); - // Find the same results in A (old results) and B (new newResults) - var sameResults = oldResults - .Where(t1 => newResults.Any(x => x.Result.Equals(t1.Result))) - .ToList(); + return results.Where(r => r.Result.PluginID != resultId) + .Concat(results.Intersect(newResults).Union(newResults)) + .OrderByDescending(r => r.Result.Score) + .ToList(); + } - // remove result of relative complement of B in A - foreach (var result in oldResults.Except(sameResults)) - { - results.Remove(result); - } + private List NewResults(IEnumerable resultsForUpdates) + { + if (!resultsForUpdates.Any()) + return Results.ToList(); - // update result with B's score and index position - foreach (var sameResult in sameResults) - { - int oldIndex = results.IndexOf(sameResult); - int oldScore = results[oldIndex].Result.Score; - var newResult = newResults[newResults.IndexOf(sameResult)]; - int newScore = newResult.Result.Score; - if (newScore != oldScore) - { - var oldResult = results[oldIndex]; + var results = Results as IEnumerable; - oldResult.Result.Score = newScore; - oldResult.Result.OriginQuery = newResult.Result.OriginQuery; - - results.RemoveAt(oldIndex); - int newIndex = InsertIndexOf(newScore, results); - results.Insert(newIndex, oldResult); - } - } - - // insert result in relative complement of A in B - foreach (var result in newResults.Except(sameResults)) - { - int newIndex = InsertIndexOf(result.Result.Score, results); - results.Insert(newIndex, result); - } - - return results; + return results.Where(r => r != null && !resultsForUpdates.Any(u => u.Metadata.ID == r.Result.PluginID)) + .Concat( + resultsForUpdates.SelectMany(u => u.Results, (u, r) => new ResultViewModel(r, _settings))) + .OrderByDescending(rv => rv.Result.Score) + .ToList(); } #endregion @@ -234,58 +268,71 @@ namespace Flow.Launcher.ViewModel public class ResultCollection : ObservableCollection { + private long editTime = 0; - public void RemoveAll(Predicate predicate) + private bool _suppressNotifying = false; + + private CancellationToken _token; + + protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { - CheckReentrancy(); - - for (int i = Count - 1; i >= 0; i--) + if (!_suppressNotifying) { - if (predicate(this[i])) - { - RemoveAt(i); - } + base.OnCollectionChanged(e); } } + public void BulkAddRange(IEnumerable resultViews) + { + // suppress notifying before adding all element + _suppressNotifying = true; + foreach (var item in resultViews) + { + Add(item); + } + _suppressNotifying = false; + // manually update event + // wpf use directx / double buffered already, so just reset all won't cause ui flickering + if (_token.IsCancellationRequested) + return; + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + public void AddRange(IEnumerable Items) + { + foreach (var item in Items) + { + if (_token.IsCancellationRequested) + return; + Add(item); + } + } + public void RemoveAll() + { + ClearItems(); + } + /// /// Update the results collection with new results, try to keep identical results /// /// - public void Update(List newItems) + public void Update(List newItems, CancellationToken token = default) { - int newCount = newItems.Count; - int oldCount = Items.Count; - int location = newCount > oldCount ? oldCount : newCount; + _token = token; + if (Count == 0 && newItems.Count == 0 || _token.IsCancellationRequested) + return; - for (int i = 0; i < location; i++) + if (editTime < 10 || newItems.Count < 30) { - ResultViewModel oldResult = this[i]; - ResultViewModel newResult = newItems[i]; - if (!oldResult.Equals(newResult)) - { // result is not the same update it in the current index - this[i] = newResult; - } - else if (oldResult.Result.Score != newResult.Result.Score) - { - this[i].Result.Score = newResult.Result.Score; - } - } - - - if (newCount >= oldCount) - { - for (int i = oldCount; i < newCount; i++) - { - Add(newItems[i]); - } + if (Count != 0) ClearItems(); + AddRange(newItems); + editTime++; + return; } else { - for (int i = oldCount - 1; i >= newCount; i--) - { - RemoveAt(i); - } + Clear(); + BulkAddRange(newItems); + editTime++; } } } diff --git a/Plugins/Flow.Launcher.Plugin.ControlPanel/ControlPanelList.cs b/Plugins/Flow.Launcher.Plugin.ControlPanel/ControlPanelList.cs index fdcffb0b3..70afda536 100644 --- a/Plugins/Flow.Launcher.Plugin.ControlPanel/ControlPanelList.cs +++ b/Plugins/Flow.Launcher.Plugin.ControlPanel/ControlPanelList.cs @@ -38,7 +38,7 @@ namespace Flow.Launcher.Plugin.ControlPanel int cxDesired, int cyDesired, uint fuLoad); [DllImport("user32.dll", CharSet = CharSet.Auto)] - extern static bool DestroyIcon(IntPtr handle); + static extern bool DestroyIcon(IntPtr handle); [DllImport("kernel32.dll")] static extern IntPtr FindResource(IntPtr hModule, IntPtr lpName, IntPtr lpType); diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs index 30a06e882..7b56df691 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs @@ -3,11 +3,13 @@ using Flow.Launcher.Plugin.Explorer.Search; using Flow.Launcher.Plugin.Explorer.ViewModels; using Flow.Launcher.Plugin.Explorer.Views; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using System.Windows.Controls; namespace Flow.Launcher.Plugin.Explorer { - public class Main : ISettingProvider, IPlugin, ISavable, IContextMenu, IPluginI18n + public class Main : ISettingProvider, IAsyncPlugin, ISavable, IContextMenu, IPluginI18n { internal PluginInitContext Context { get; set; } @@ -17,17 +19,21 @@ namespace Flow.Launcher.Plugin.Explorer private IContextMenu contextMenu; + private SearchManager searchManager; + public Control CreateSettingPanel() { return new ExplorerSettings(viewModel); } - public void Init(PluginInitContext context) + public async Task InitAsync(PluginInitContext context) { Context = context; viewModel = new SettingsViewModel(context); + await viewModel.LoadStorage(); Settings = viewModel.Settings; contextMenu = new ContextMenu(Context, Settings); + searchManager = new SearchManager(Settings, Context); } public List LoadContextMenus(Result selectedResult) @@ -35,9 +41,9 @@ namespace Flow.Launcher.Plugin.Explorer return contextMenu.LoadContextMenus(selectedResult); } - public List Query(Query query) + public async Task> QueryAsync(Query query, CancellationToken token) { - return new SearchManager(Settings, Context).Search(query); + return await searchManager.SearchAsync(query, token); } public void Save() diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs index 02de0eeae..88d7d6927 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs @@ -22,7 +22,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo if (search.LastIndexOf(Constants.AllFilesFolderSearchWildcard) > search.LastIndexOf(Constants.DirectorySeperator)) return DirectorySearch(SearchOption.AllDirectories, query, search, criteria); - + return DirectorySearch(SearchOption.TopDirectoryOnly, query, search, criteria); } @@ -57,9 +57,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo try { var directoryInfo = new System.IO.DirectoryInfo(path); - var fileSystemInfos = directoryInfo.GetFileSystemInfos(searchCriteria, searchOption); - foreach (var fileSystemInfo in fileSystemInfos) + foreach (var fileSystemInfo in directoryInfo.EnumerateFileSystemInfos(searchCriteria, searchOption)) { if ((fileSystemInfo.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) continue; diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs index 5b50b7fad..912c5f428 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs @@ -5,6 +5,8 @@ using Flow.Launcher.Plugin.SharedCommands; using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace Flow.Launcher.Plugin.Explorer.Search { @@ -28,20 +30,20 @@ namespace Flow.Launcher.Plugin.Explorer.Search this.settings = settings; } - internal List Search(Query query) + internal async Task> SearchAsync(Query query, CancellationToken token) { var results = new List(); var querySearch = query.Search; if (IsFileContentSearch(query.ActionKeyword)) - return WindowsIndexFileContentSearch(query, querySearch); + return await WindowsIndexFileContentSearchAsync(query, querySearch, token).ConfigureAwait(false); // This allows the user to type the assigned action keyword and only see the list of quick folder links if (settings.QuickFolderAccessLinks.Count > 0 && query.ActionKeyword == settings.SearchActionKeyword && string.IsNullOrEmpty(query.Search)) - return quickFolderAccess.FolderListAll(query, settings.QuickFolderAccessLinks, context); + return quickFolderAccess.FolderListAll(query, settings.QuickFolderAccessLinks, context); var quickFolderLinks = quickFolderAccess.FolderListMatched(query, settings.QuickFolderAccessLinks, context); @@ -54,11 +56,11 @@ namespace Flow.Launcher.Plugin.Explorer.Search return EnvironmentVariables.GetEnvironmentStringPathSuggestions(querySearch, query, context); // Query is a location path with a full environment variable, eg. %appdata%\somefolder\ - var isEnvironmentVariablePath = querySearch.Substring(1).Contains("%\\"); + var isEnvironmentVariablePath = querySearch[1..].Contains("%\\"); - if (!FilesFolders.IsLocationPathString(querySearch) && !isEnvironmentVariablePath) + if (!querySearch.IsLocationPathString() && !isEnvironmentVariablePath) { - results.AddRange(WindowsIndexFilesAndFoldersSearch(query, querySearch)); + results.AddRange(await WindowsIndexFilesAndFoldersSearchAsync(query, querySearch, token).ConfigureAwait(false)); return results; } @@ -68,33 +70,39 @@ namespace Flow.Launcher.Plugin.Explorer.Search if (isEnvironmentVariablePath) locationPath = EnvironmentVariables.TranslateEnvironmentVariablePath(locationPath); + // Check that actual location exists, otherwise directory search will throw directory not found exception if (!FilesFolders.LocationExists(FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath))) return results; var useIndexSearch = UseWindowsIndexForDirectorySearch(locationPath); - + results.Add(resultManager.CreateOpenCurrentFolderResult(locationPath, useIndexSearch)); - results.AddRange(TopLevelDirectorySearchBehaviour(WindowsIndexTopLevelFolderSearch, + if (token.IsCancellationRequested) + return null; + + results.AddRange(await TopLevelDirectorySearchBehaviourAsync(WindowsIndexTopLevelFolderSearchAsync, DirectoryInfoClassSearch, useIndexSearch, query, - locationPath)); + locationPath, + token).ConfigureAwait(false)); return results; } - private List WindowsIndexFileContentSearch(Query query, string querySearchString) + private async Task> WindowsIndexFileContentSearchAsync(Query query, string querySearchString, CancellationToken token) { var queryConstructor = new QueryConstructor(settings); if (string.IsNullOrEmpty(querySearchString)) return new List(); - return indexSearch.WindowsIndexSearch(querySearchString, + return await indexSearch.WindowsIndexSearchAsync(querySearchString, queryConstructor.CreateQueryHelper().ConnectionString, queryConstructor.QueryForFileContentSearch, - query); + query, + token).ConfigureAwait(false); } public bool IsFileContentSearch(string actionKeyword) @@ -109,37 +117,40 @@ namespace Flow.Launcher.Plugin.Explorer.Search return directoryInfoSearch.TopLevelDirectorySearch(query, querySearch); } - public List TopLevelDirectorySearchBehaviour( - Func> windowsIndexSearch, + public async Task> TopLevelDirectorySearchBehaviourAsync( + Func>> windowsIndexSearch, Func> directoryInfoClassSearch, bool useIndexSearch, Query query, - string querySearchString) + string querySearchString, + CancellationToken token) { if (!useIndexSearch) return directoryInfoClassSearch(query, querySearchString); - return windowsIndexSearch(query, querySearchString); + return await windowsIndexSearch(query, querySearchString, token); } - private List WindowsIndexFilesAndFoldersSearch(Query query, string querySearchString) + private async Task> WindowsIndexFilesAndFoldersSearchAsync(Query query, string querySearchString, CancellationToken token) { var queryConstructor = new QueryConstructor(settings); - return indexSearch.WindowsIndexSearch(querySearchString, + return await indexSearch.WindowsIndexSearchAsync(querySearchString, queryConstructor.CreateQueryHelper().ConnectionString, queryConstructor.QueryForAllFilesAndFolders, - query); + query, + token).ConfigureAwait(false); } - - private List WindowsIndexTopLevelFolderSearch(Query query, string path) + + private async Task> WindowsIndexTopLevelFolderSearchAsync(Query query, string path, CancellationToken token) { var queryConstructor = new QueryConstructor(settings); - return indexSearch.WindowsIndexSearch(path, + return await indexSearch.WindowsIndexSearchAsync(path, queryConstructor.CreateQueryHelper().ConnectionString, queryConstructor.QueryForTopLevelDirectorySearch, - query); + query, + token).ConfigureAwait(false); } private bool UseWindowsIndexForDirectorySearch(string locationPath) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs index 4f9325c77..8480e6328 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs @@ -1,82 +1,77 @@ -using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.Logger; using Microsoft.Search.Interop; using System; using System.Collections.Generic; using System.Data.OleDb; using System.Linq; using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex { internal class IndexSearch { - private readonly object _lock = new object(); - - private OleDbConnection conn; - - private OleDbCommand command; - - private OleDbDataReader dataReaderResults; - private readonly ResultManager resultManager; // Reserved keywords in oleDB - private readonly string reservedStringPattern = @"^[\/\\\$\%_]+$"; + private readonly string reservedStringPattern = @"^[`\@\#\^,\&\/\\\$\%_]+$"; internal IndexSearch(PluginInitContext context) { resultManager = new ResultManager(context); } - internal List ExecuteWindowsIndexSearch(string indexQueryString, string connectionString, Query query) + internal async Task> ExecuteWindowsIndexSearchAsync(string indexQueryString, string connectionString, Query query, CancellationToken token) { - var folderResults = new List(); - var fileResults = new List(); var results = new List(); + var fileResults = new List(); try { - using (conn = new OleDbConnection(connectionString)) + using var conn = new OleDbConnection(connectionString); + await conn.OpenAsync(token); + token.ThrowIfCancellationRequested(); + + using var command = new OleDbCommand(indexQueryString, conn); + // Results return as an OleDbDataReader. + using var dataReaderResults = await command.ExecuteReaderAsync(token) as OleDbDataReader; + token.ThrowIfCancellationRequested(); + + if (dataReaderResults.HasRows) { - conn.Open(); - - using (command = new OleDbCommand(indexQueryString, conn)) + while (await dataReaderResults.ReadAsync(token)) { - // Results return as an OleDbDataReader. - using (dataReaderResults = command.ExecuteReader()) + token.ThrowIfCancellationRequested(); + if (dataReaderResults.GetValue(0) != DBNull.Value && dataReaderResults.GetValue(1) != DBNull.Value) { - if (dataReaderResults.HasRows) - { - while (dataReaderResults.Read()) - { - if (dataReaderResults.GetValue(0) != DBNull.Value && dataReaderResults.GetValue(1) != DBNull.Value) - { - // # is URI syntax for the fragment component, need to be encoded so LocalPath returns complete path - var encodedFragmentPath = dataReaderResults - .GetString(1) - .Replace("#", "%23", StringComparison.OrdinalIgnoreCase); - - var path = new Uri(encodedFragmentPath).LocalPath; + // # is URI syntax for the fragment component, need to be encoded so LocalPath returns complete path + var encodedFragmentPath = dataReaderResults + .GetString(1) + .Replace("#", "%23", StringComparison.OrdinalIgnoreCase); - if (dataReaderResults.GetString(2) == "Directory") - { - folderResults.Add(resultManager.CreateFolderResult( - dataReaderResults.GetString(0), - path, - path, - query, true, true)); - } - else - { - fileResults.Add(resultManager.CreateFileResult(path, query, true, true)); - } - } - } + var path = new Uri(encodedFragmentPath).LocalPath; + + if (dataReaderResults.GetString(2) == "Directory") + { + results.Add(resultManager.CreateFolderResult( + dataReaderResults.GetString(0), + path, + path, + query, true, true)); + } + else + { + fileResults.Add(resultManager.CreateFileResult(path, query, true, true)); } } } } } + catch (OperationCanceledException) + { + return new List(); // The source code indicates that without adding members, it won't allocate an array + } catch (InvalidOperationException e) { // Internal error from ExecuteReader(): Connection closed. @@ -87,22 +82,24 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex LogException("General error from performing index search", e); } + results.AddRange(fileResults); + // Intial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection. - return results.Concat(folderResults.OrderBy(x => x.Title)).Concat(fileResults.OrderBy(x => x.Title)).ToList(); ; + return results; } - internal List WindowsIndexSearch(string searchString, string connectionString, Func constructQuery, Query query) + internal async Task> WindowsIndexSearchAsync(string searchString, string connectionString, + Func constructQuery, Query query, + CancellationToken token) { var regexMatch = Regex.Match(searchString, reservedStringPattern); if (regexMatch.Success) return new List(); - lock (_lock) - { - var constructedQuery = constructQuery(searchString); - return ExecuteWindowsIndexSearch(constructedQuery, connectionString, query); - } + var constructedQuery = constructQuery(searchString); + return await ExecuteWindowsIndexSearchAsync(constructedQuery, connectionString, query, token); + } internal bool PathIsIndexed(string path) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs index 5718fdb0a..20e85bbb5 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs @@ -42,7 +42,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex // Get the ISearchQueryHelper which will help us to translate AQS --> SQL necessary to query the indexer var queryHelper = catalogManager.GetQueryHelper(); - + return queryHelper; } @@ -81,11 +81,9 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex var previousLevelDirectory = path.Substring(0, indexOfSeparator); if (string.IsNullOrEmpty(itemName)) - return searchDepth + $"{previousLevelDirectory}'"; + return $"{searchDepth}{previousLevelDirectory}'"; - return $"(System.FileName LIKE '{itemName}%' " + - $"OR CONTAINS(System.FileName,'\"{itemName}*\"',1033)) AND " + - searchDepth + $"{previousLevelDirectory}'"; + return $"(System.FileName LIKE '{itemName}%' OR CONTAINS(System.FileName,'\"{itemName}*\"',1033)) AND {searchDepth}{previousLevelDirectory}'"; } /// @@ -96,9 +94,9 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex string query = "SELECT TOP " + settings.MaxResult + $" {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE "; if (path.LastIndexOf(Constants.AllFilesFolderSearchWildcard) > path.LastIndexOf(Constants.DirectorySeperator)) - return query + QueryWhereRestrictionsForTopLevelDirectoryAllFilesAndFoldersSearch(path); + return query + QueryWhereRestrictionsForTopLevelDirectoryAllFilesAndFoldersSearch(path) + QueryOrderByFileNameRestriction; - return query + QueryWhereRestrictionsForTopLevelDirectorySearch(path); + return query + QueryWhereRestrictionsForTopLevelDirectorySearch(path) + QueryOrderByFileNameRestriction; } /// @@ -107,16 +105,17 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex public string QueryForAllFilesAndFolders(string userSearchString) { // Generate SQL from constructed parameters, converting the userSearchString from AQS->WHERE clause - return CreateBaseQuery().GenerateSQLFromUserQuery(userSearchString) + " AND " + QueryWhereRestrictionsForAllFilesAndFoldersSearch(); + return CreateBaseQuery().GenerateSQLFromUserQuery(userSearchString) + " AND " + QueryWhereRestrictionsForAllFilesAndFoldersSearch + + QueryOrderByFileNameRestriction; } /// /// Set the required WHERE clause restriction to search for all files and folders. /// - public string QueryWhereRestrictionsForAllFilesAndFoldersSearch() - { - return $"scope='file:'"; - } + public const string QueryWhereRestrictionsForAllFilesAndFoldersSearch = "scope='file:'"; + + public const string QueryOrderByFileNameRestriction = " ORDER BY System.FileName"; + /// /// Search will be performed on all indexed file contents for the specified search keywords. @@ -125,7 +124,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex { string query = "SELECT TOP " + settings.MaxResult + $" {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE "; - return query + QueryWhereRestrictionsForFileContentSearch(userSearchString) + " AND " + QueryWhereRestrictionsForAllFilesAndFoldersSearch(); + return query + QueryWhereRestrictionsForFileContentSearch(userSearchString) + " AND " + QueryWhereRestrictionsForAllFilesAndFoldersSearch + + QueryOrderByFileNameRestriction; } /// diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs index e62ea93fc..e62637896 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs @@ -1,7 +1,6 @@ using Flow.Launcher.Plugin.Explorer.Search; using Flow.Launcher.Plugin.Explorer.Search.FolderLinks; using System.Collections.Generic; -using System.Text.Json.Serialization; namespace Flow.Launcher.Plugin.Explorer { diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs index 7fcd77f07..21bc49741 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs @@ -3,6 +3,7 @@ using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Plugin.Explorer.Search; using Flow.Launcher.Plugin.Explorer.Search.FolderLinks; using System.Diagnostics; +using System.Threading.Tasks; namespace Flow.Launcher.Plugin.Explorer.ViewModels { @@ -21,6 +22,11 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels Settings = storage.Load(); } + public Task LoadStorage() + { + return Task.Run(() => Settings = storage.Load()); + } + public void Save() { storage.Save(); diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json b/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json index aa44c4413..76fd36bb5 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json @@ -7,7 +7,7 @@ "Name": "Explorer", "Description": "Search and manage files and folders. Explorer utilises Windows Index Search", "Author": "Jeremy Wu", - "Version": "1.2.6", + "Version": "1.4.1", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.Explorer.dll", diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs index f10f022d7..66bfd2ab5 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs @@ -7,10 +7,11 @@ using System.Windows.Controls; using Flow.Launcher.Infrastructure; using System; using System.Threading.Tasks; +using System.Threading; namespace Flow.Launcher.Plugin.PluginsManager { - public class Main : ISettingProvider, IPlugin, ISavable, IContextMenu, IPluginI18n, IReloadable + public class Main : ISettingProvider, IAsyncPlugin, ISavable, IContextMenu, IPluginI18n, IAsyncReloadable { internal PluginInitContext Context { get; set; } @@ -29,14 +30,29 @@ namespace Flow.Launcher.Plugin.PluginsManager return new PluginsManagerSettings(viewModel); } - public void Init(PluginInitContext context) + public Task InitAsync(PluginInitContext context) { Context = context; viewModel = new SettingsViewModel(context); Settings = viewModel.Settings; contextMenu = new ContextMenu(Context); pluginManager = new PluginsManager(Context, Settings); - lastUpdateTime = DateTime.Now; + var updateManifestTask = pluginManager.UpdateManifest(); + _ = updateManifestTask.ContinueWith(t => + { + if (t.IsCompletedSuccessfully) + { + lastUpdateTime = DateTime.Now; + } + else + { + context.API.ShowMsg("Plugin Manifest Download Fail.", + "Please check if you can connect to github.com. " + + "This error means you may not be able to Install and Update Plugin.", pluginManager.icoPath, false); + } + }); + + return Task.CompletedTask; } public List LoadContextMenus(Result selectedResult) @@ -44,7 +60,7 @@ namespace Flow.Launcher.Plugin.PluginsManager return contextMenu.LoadContextMenus(selectedResult); } - public List Query(Query query) + public async Task> QueryAsync(Query query, CancellationToken token) { var search = query.Search.ToLower(); @@ -53,16 +69,13 @@ namespace Flow.Launcher.Plugin.PluginsManager if ((DateTime.Now - lastUpdateTime).TotalHours > 12) // 12 hours { - Task.Run(async () => - { - await pluginManager.UpdateManifest(); - lastUpdateTime = DateTime.Now; - }); + await pluginManager.UpdateManifest(); + lastUpdateTime = DateTime.Now; } return search switch { - var s when s.StartsWith(Settings.HotKeyInstall) => pluginManager.RequestInstallOrUpdate(s), + var s when s.StartsWith(Settings.HotKeyInstall) => await pluginManager.RequestInstallOrUpdate(s, token), var s when s.StartsWith(Settings.HotkeyUninstall) => pluginManager.RequestUninstall(s), var s when s.StartsWith(Settings.HotkeyUpdate) => pluginManager.RequestUpdate(s), _ => pluginManager.GetDefaultHotKeys().Where(hotkey => @@ -88,10 +101,10 @@ namespace Flow.Launcher.Plugin.PluginsManager return Context.API.GetTranslation("plugin_pluginsmanager_plugin_description"); } - public void ReloadData() + public async Task ReloadDataAsync() { - Task.Run(() => pluginManager.UpdateManifest()).Wait(); + await pluginManager.UpdateManifest(); lastUpdateTime = DateTime.Now; } } -} +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Models/PluginsManifest.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Models/PluginsManifest.cs index 814e0764d..145aadc98 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Models/PluginsManifest.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Models/PluginsManifest.cs @@ -9,12 +9,7 @@ namespace Flow.Launcher.Plugin.PluginsManager.Models { internal class PluginsManifest { - internal List UserPlugins { get; private set; } - - internal PluginsManifest() - { - Task.Run(async () => await DownloadManifest()).Wait(); - } + internal List UserPlugins { get; private set; } = new List(); internal async Task DownloadManifest() { diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs index 68df5bc1f..0f5e6d9e8 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using System.Windows; @@ -36,7 +37,7 @@ namespace Flow.Launcher.Plugin.PluginsManager } } - private readonly string icoPath = "Images\\pluginsmanager.png"; + internal readonly string icoPath = "Images\\pluginsmanager.png"; internal PluginsManager(PluginInitContext context, Settings settings) { @@ -64,27 +65,27 @@ namespace Flow.Launcher.Plugin.PluginsManager return false; } }, - new Result() + new Result() + { + Title = Settings.HotkeyUninstall, + IcoPath = icoPath, + Action = _ => { - Title = Settings.HotkeyUninstall, - IcoPath = icoPath, - Action = _ => - { - Context.API.ChangeQuery("pm uninstall "); - return false; - } - }, - new Result() - { - Title = Settings.HotkeyUpdate, - IcoPath = icoPath, - Action = _ => - { - Context.API.ChangeQuery("pm update "); - return false; - } + Context.API.ChangeQuery("pm uninstall "); + return false; } - }; + }, + new Result() + { + Title = Settings.HotkeyUpdate, + IcoPath = icoPath, + Action = _ => + { + Context.API.ChangeQuery("pm update "); + return false; + } + } + }; } internal async Task InstallOrUpdate(UserPlugin plugin) @@ -137,7 +138,8 @@ namespace Flow.Launcher.Plugin.PluginsManager catch (Exception e) { Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"), - string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"), plugin.Name)); + string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"), + plugin.Name)); Log.Exception("PluginsManager", "An error occured while downloading plugin", e, "InstallOrUpdate"); @@ -164,7 +166,8 @@ namespace Flow.Launcher.Plugin.PluginsManager from existingPlugin in Context.API.GetAllPlugins() join pluginFromManifest in pluginsManifest.UserPlugins on existingPlugin.Metadata.ID equals pluginFromManifest.ID - where existingPlugin.Metadata.Version.CompareTo(pluginFromManifest.Version) < 0 // if current version precedes manifest version + where existingPlugin.Metadata.Version.CompareTo(pluginFromManifest.Version) < + 0 // if current version precedes manifest version select new { @@ -214,22 +217,29 @@ namespace Flow.Launcher.Plugin.PluginsManager Task.Run(async delegate { - Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), - Context.API.GetTranslation("plugin_pluginsmanager_please_wait")); + Context.API.ShowMsg( + Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), + Context.API.GetTranslation("plugin_pluginsmanager_please_wait")); - await Http.DownloadAsync(x.PluginNewUserPlugin.UrlDownload, downloadToFilePath).ConfigureAwait(false); + await Http.DownloadAsync(x.PluginNewUserPlugin.UrlDownload, downloadToFilePath) + .ConfigureAwait(false); - Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), - Context.API.GetTranslation("plugin_pluginsmanager_download_success")); + Context.API.ShowMsg( + Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), + Context.API.GetTranslation("plugin_pluginsmanager_download_success")); Install(x.PluginNewUserPlugin, downloadToFilePath); Context.API.RestartApp(); }).ContinueWith(t => { - Log.Exception("PluginsManager", $"Update failed for {x.Name}", t.Exception.InnerException, "RequestUpdate"); - Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"), - string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"), x.Name)); + Log.Exception("PluginsManager", $"Update failed for {x.Name}", + t.Exception.InnerException, "RequestUpdate"); + Context.API.ShowMsg( + Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"), + string.Format( + Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"), + x.Name)); }, TaskContinuationOptions.OnlyOnFaulted); return true; @@ -264,8 +274,21 @@ namespace Flow.Launcher.Plugin.PluginsManager .ToList(); } - internal List RequestInstallOrUpdate(string searchName) + private Task _downloadManifestTask = Task.CompletedTask; + + internal async ValueTask> RequestInstallOrUpdate(string searchName, CancellationToken token) { + if (!pluginsManifest.UserPlugins.Any() && + _downloadManifestTask.Status != TaskStatus.Running) + { + _downloadManifestTask = pluginsManifest.DownloadManifest(); + } + + await _downloadManifestTask; + + if (token.IsCancellationRequested) + return null; + var searchNameWithoutKeyword = searchName.Replace(Settings.HotKeyInstall, string.Empty).Trim(); var results = @@ -406,4 +429,4 @@ namespace Flow.Launcher.Plugin.PluginsManager return new List(); } } -} +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json b/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json index 60ab7ab18..75d6038d4 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json @@ -6,7 +6,7 @@ "Name": "Plugins Manager", "Description": "Management of installing, uninstalling or updating Flow Launcher plugins", "Author": "Jeremy Wu", - "Version": "1.5.0", + "Version": "1.6.1", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.PluginsManager.dll", diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index 8f124f3a4..22f4aea59 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; using System.Windows.Controls; using Flow.Launcher.Infrastructure.Logger; @@ -12,9 +13,8 @@ using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; namespace Flow.Launcher.Plugin.Program { - public class Main : ISettingProvider, IPlugin, IPluginI18n, IContextMenu, ISavable, IReloadable + public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, ISavable, IAsyncReloadable { - private static readonly object IndexLock = new object(); internal static Win32[] _win32s { get; set; } internal static UWP.Application[] _uwps { get; set; } internal static Settings _settings { get; set; } @@ -30,33 +30,6 @@ namespace Flow.Launcher.Plugin.Program public Main() { _settingsStorage = new PluginJsonStorage(); - _settings = _settingsStorage.Load(); - - Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Preload programs cost", () => - { - _win32Storage = new BinaryStorage("Win32"); - _win32s = _win32Storage.TryLoad(new Win32[] { }); - _uwpStorage = new BinaryStorage("UWP"); - _uwps = _uwpStorage.TryLoad(new UWP.Application[] { }); - }); - 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}>"); - - var a = Task.Run(() => - { - if (IsStartupIndexProgramsRequired || !_win32s.Any()) - Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexWin32Programs); - }); - - var b = Task.Run(() => - { - if (IsStartupIndexProgramsRequired || !_uwps.Any()) - Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexUWPPrograms); - }); - - Task.WaitAll(a, b); - - _settings.LastIndexTime = DateTime.Today; } public void Save() @@ -66,28 +39,92 @@ namespace Flow.Launcher.Plugin.Program _uwpStorage.Save(_uwps); } - public List Query(Query query) + public async Task> QueryAsync(Query query, CancellationToken token) { + if (IsStartupIndexProgramsRequired) + _ = IndexPrograms(); + Win32[] win32; UWP.Application[] uwps; win32 = _win32s; uwps = _uwps; - var result = win32.Cast() - .Concat(uwps) - .AsParallel() - .Where(p => p.Enabled) - .Select(p => p.Result(query.Search, _context.API)) - .Where(r => r?.Score > 0) - .ToList(); + try + { + var result = await Task.Run(delegate + { + try + { + return win32.Cast() + .Concat(uwps) + .AsParallel() + .WithCancellation(token) + .Where(p => p.Enabled) + .Select(p => p.Result(query.Search, _context.API)) + .Where(r => r?.Score > 0) + .ToList(); + } + catch (OperationCanceledException) + { + return null; + } + }, token).ConfigureAwait(false); - return result; + token.ThrowIfCancellationRequested(); + + return result; + } + catch (OperationCanceledException) + { + return null; + } } - public void Init(PluginInitContext context) + public async Task InitAsync(PluginInitContext context) { _context = context; + + await Task.Run(() => + { + _settings = _settingsStorage.Load(); + + Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Preload programs cost", () => + { + _win32Storage = new BinaryStorage("Win32"); + _win32s = _win32Storage.TryLoad(new Win32[] { }); + _uwpStorage = new BinaryStorage("UWP"); + _uwps = _uwpStorage.TryLoad(new UWP.Application[] { }); + }); + 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}>"); + }); + + bool indexedWinApps = false; + bool indexedUWPApps = false; + + var a = Task.Run(() => + { + if (IsStartupIndexProgramsRequired || !_win32s.Any()) + { + Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexWin32Programs); + indexedWinApps = true; + } + }); + + var b = Task.Run(() => + { + if (IsStartupIndexProgramsRequired || !_uwps.Any()) + { + Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexUwpPrograms); + indexedUWPApps = true; + } + }); + + await Task.WhenAll(a, b); + + if (indexedWinApps && indexedUWPApps) + _settings.LastIndexTime = DateTime.Today; } public static void IndexWin32Programs() @@ -95,10 +132,9 @@ namespace Flow.Launcher.Plugin.Program var win32S = Win32.All(_settings); _win32s = win32S; - } - public static void IndexUWPPrograms() + public static void IndexUwpPrograms() { var windows10 = new Version(10, 0); var support = Environment.OSVersion.Version.Major >= windows10.Major; @@ -106,16 +142,15 @@ namespace Flow.Launcher.Plugin.Program var applications = support ? UWP.All() : new UWP.Application[] { }; _uwps = applications; - } - public static void IndexPrograms() + public static async Task IndexPrograms() { - var t1 = Task.Run(() => IndexWin32Programs()); + var t1 = Task.Run(IndexWin32Programs); - var t2 = Task.Run(() => IndexUWPPrograms()); + var t2 = Task.Run(IndexUwpPrograms); - Task.WaitAll(t1, t2); + await Task.WhenAll(t1, t2).ConfigureAwait(false); _settings.LastIndexTime = DateTime.Today; } @@ -145,19 +180,21 @@ namespace Flow.Launcher.Plugin.Program } menuOptions.Add( - new Result - { - Title = _context.API.GetTranslation("flowlauncher_plugin_program_disable_program"), - Action = c => - { - DisableProgram(program); - _context.API.ShowMsg(_context.API.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_success"), - _context.API.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_success_message")); - return false; - }, - IcoPath = "Images/disable.png" - } - ); + new Result + { + Title = _context.API.GetTranslation("flowlauncher_plugin_program_disable_program"), + Action = c => + { + DisableProgram(program); + _context.API.ShowMsg( + _context.API.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_success"), + _context.API.GetTranslation( + "flowlauncher_plugin_program_disable_dlgtitle_success_message")); + return false; + }, + IcoPath = "Images/disable.png" + } + ); return menuOptions; } @@ -168,21 +205,25 @@ namespace Flow.Launcher.Plugin.Program return; if (_uwps.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier)) - _uwps.Where(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier).FirstOrDefault().Enabled = false; + _uwps.Where(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier) + .FirstOrDefault() + .Enabled = false; if (_win32s.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier)) - _win32s.Where(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier).FirstOrDefault().Enabled = false; + _win32s.Where(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier) + .FirstOrDefault() + .Enabled = false; _settings.DisabledProgramSources - .Add( - new Settings.DisabledProgramSource - { - Name = programToDelete.Name, - Location = programToDelete.Location, - UniqueIdentifier = programToDelete.UniqueIdentifier, - Enabled = false - } - ); + .Add( + new Settings.DisabledProgramSource + { + Name = programToDelete.Name, + Location = programToDelete.Location, + UniqueIdentifier = programToDelete.UniqueIdentifier, + Enabled = false + } + ); } public static void StartProcess(Func runProcess, ProcessStartInfo info) @@ -200,9 +241,9 @@ namespace Flow.Launcher.Plugin.Program } } - public void ReloadData() + public async Task ReloadDataAsync() { - IndexPrograms(); + await IndexPrograms(); } } -} \ No newline at end of file +} diff --git a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs index e4e92b9bc..d66ca345e 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs @@ -51,7 +51,7 @@ namespace Flow.Launcher.Plugin.Program.Views private void ViewRefresh() { - if(programSourceView.Items.Count == 0 + if (programSourceView.Items.Count == 0 && btnProgramSourceStatus.Visibility == Visibility.Visible && btnEditProgramSource.Visibility == Visibility.Visible) { @@ -70,21 +70,19 @@ namespace Flow.Launcher.Plugin.Program.Views programSourceView.Items.Refresh(); } - private void ReIndexing() + private async void ReIndexing() { ViewRefresh(); - Task.Run(() => - { - Dispatcher.Invoke(() => { indexingPanel.Visibility = Visibility.Visible; }); - Main.IndexPrograms(); - Dispatcher.Invoke(() => { indexingPanel.Visibility = Visibility.Hidden; }); - }); + + indexingPanel.Visibility = Visibility.Visible; + await Main.IndexPrograms(); + indexingPanel.Visibility = Visibility.Hidden; } private void btnAddProgramSource_OnClick(object sender, RoutedEventArgs e) { var add = new AddProgramSource(context, _settings); - if(add.ShowDialog() ?? false) + if (add.ShowDialog() ?? false) { ReIndexing(); } @@ -165,14 +163,14 @@ namespace Flow.Launcher.Plugin.Program.Views UniqueIdentifier = directory }; - directoriesToAdd.Add(source); + directoriesToAdd.Add(source); } } if (directoriesToAdd.Count() > 0) { directoriesToAdd.ForEach(x => _settings.ProgramSources.Add(x)); - directoriesToAdd.ForEach(x => ProgramSettingDisplayList.Add(x)); + directoriesToAdd.ForEach(x => ProgramSettingDisplayList.Add(x)); ViewRefresh(); ReIndexing(); @@ -238,8 +236,8 @@ namespace Flow.Launcher.Plugin.Program.Views ProgramSettingDisplayList.SetProgramSourcesStatus(selectedItems, true); ProgramSettingDisplayList.RemoveDisabledFromSettings(); - } - + } + if (selectedItems.IsReindexRequired()) ReIndexing(); @@ -282,7 +280,7 @@ namespace Flow.Launcher.Plugin.Program.Views var sortBy = columnBinding?.Path.Path ?? headerClicked.Column.Header as string; Sort(sortBy, direction); - + _lastHeaderClicked = headerClicked; _lastDirection = direction; } diff --git a/Plugins/Flow.Launcher.Plugin.Program/plugin.json b/Plugins/Flow.Launcher.Plugin.Program/plugin.json index 6c2c18e47..1dedd9909 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Program/plugin.json @@ -4,7 +4,7 @@ "Name": "Program", "Description": "Search programs in Flow.Launcher", "Author": "qianlifeng", - "Version": "1.2.3", + "Version": "1.3.0", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.Program.dll", diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs index 5642b62ed..624fe05bc 100644 --- a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Threading.Tasks; using System.Windows; using System.Windows.Forms; using System.Windows.Interop; @@ -67,13 +68,15 @@ namespace Flow.Launcher.Plugin.Sys { c.TitleHighlightData = titleMatch.MatchData; } - else + else { c.SubTitleHighlightData = subTitleMatch.MatchData; } + results.Add(c); } } + return results; } @@ -94,13 +97,15 @@ namespace Flow.Launcher.Plugin.Sys IcoPath = "Images\\shutdown.png", Action = c => { - var reuslt = MessageBox.Show(context.API.GetTranslation("flowlauncher_plugin_sys_dlgtext_shutdown_computer"), - context.API.GetTranslation("flowlauncher_plugin_sys_shutdown_computer"), - MessageBoxButton.YesNo, MessageBoxImage.Warning); + var reuslt = MessageBox.Show( + context.API.GetTranslation("flowlauncher_plugin_sys_dlgtext_shutdown_computer"), + context.API.GetTranslation("flowlauncher_plugin_sys_shutdown_computer"), + MessageBoxButton.YesNo, MessageBoxImage.Warning); if (reuslt == MessageBoxResult.Yes) { Process.Start("shutdown", "/s /t 0"); } + return true; } }, @@ -111,13 +116,15 @@ namespace Flow.Launcher.Plugin.Sys IcoPath = "Images\\restart.png", Action = c => { - var result = MessageBox.Show(context.API.GetTranslation("flowlauncher_plugin_sys_dlgtext_restart_computer"), - context.API.GetTranslation("flowlauncher_plugin_sys_restart_computer"), - MessageBoxButton.YesNo, MessageBoxImage.Warning); + var result = MessageBox.Show( + context.API.GetTranslation("flowlauncher_plugin_sys_dlgtext_restart_computer"), + context.API.GetTranslation("flowlauncher_plugin_sys_restart_computer"), + MessageBoxButton.YesNo, MessageBoxImage.Warning); if (result == MessageBoxResult.Yes) { Process.Start("shutdown", "/r /t 0"); } + return true; } }, @@ -164,13 +171,14 @@ namespace Flow.Launcher.Plugin.Sys // FYI, couldn't find documentation for this but if the recycle bin is already empty, it will return -2147418113 (0x8000FFFF (E_UNEXPECTED)) // 0 for nothing var result = SHEmptyRecycleBin(new WindowInteropHelper(Application.Current.MainWindow).Handle, 0); - if (result != (uint) HRESULT.S_OK && result != (uint)0x8000FFFF) + if (result != (uint) HRESULT.S_OK && result != (uint) 0x8000FFFF) { MessageBox.Show($"Error emptying recycle bin, error code: {result}\n" + "please refer to https://msdn.microsoft.com/en-us/library/windows/desktop/aa378137", - "Error", - MessageBoxButton.OK, MessageBoxImage.Error); + "Error", + MessageBoxButton.OK, MessageBoxImage.Error); } + return true; } }, @@ -229,9 +237,13 @@ namespace Flow.Launcher.Plugin.Sys { // Hide the window first then show msg after done because sometimes the reload could take a while, so not to make user think it's frozen. Application.Current.MainWindow.Hide(); - context.API.ReloadAllPluginData(); - context.API.ShowMsg(context.API.GetTranslation("flowlauncher_plugin_sys_dlgtitle_success"), - context.API.GetTranslation("flowlauncher_plugin_sys_dlgtext_all_applicableplugins_reloaded")); + + context.API.ReloadAllPluginData().ContinueWith(_ => + context.API.ShowMsg( + context.API.GetTranslation("flowlauncher_plugin_sys_dlgtitle_success"), + context.API.GetTranslation( + "flowlauncher_plugin_sys_dlgtext_all_applicableplugins_reloaded"))); + return true; } }, diff --git a/Plugins/Flow.Launcher.Plugin.Sys/plugin.json b/Plugins/Flow.Launcher.Plugin.Sys/plugin.json index cf8ed6041..75d7073b8 100644 --- a/Plugins/Flow.Launcher.Plugin.Sys/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Sys/plugin.json @@ -4,7 +4,7 @@ "Name": "System Commands", "Description": "Provide System related commands. e.g. shutdown,lock,setting etc.", "Author": "qianlifeng", - "Version": "1.1.2", + "Version": "1.2.0", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.Sys.dll", diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs index 3c4d4c67d..f76e28112 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using System.Windows.Controls; @@ -13,14 +14,12 @@ using Flow.Launcher.Plugin.SharedCommands; namespace Flow.Launcher.Plugin.WebSearch { - public class Main : IPlugin, ISettingProvider, IPluginI18n, ISavable, IResultUpdated + public class Main : IAsyncPlugin, ISettingProvider, IPluginI18n, ISavable, IResultUpdated { private PluginInitContext _context; private readonly Settings _settings; private readonly SettingsViewModel _viewModel; - private CancellationTokenSource _updateSource; - private CancellationToken _updateToken; internal const string Images = "Images"; internal static string DefaultImagesDirectory; @@ -33,7 +32,7 @@ namespace Flow.Launcher.Plugin.WebSearch _viewModel.Save(); } - public List Query(Query query) + public async Task> QueryAsync(Query query, CancellationToken token) { if (FilesFolders.IsLocationPathString(query.Search)) return new List(); @@ -41,102 +40,94 @@ namespace Flow.Launcher.Plugin.WebSearch var searchSourceList = new List(); var results = new List(); - _updateSource?.Cancel(); - _updateSource = new CancellationTokenSource(); - _updateToken = _updateSource.Token; - - _settings.SearchSources.Where(o => (o.ActionKeyword == query.ActionKeyword || o.ActionKeyword == SearchSourceGlobalPluginWildCardSign) - && o.Enabled) - .ToList() - .ForEach(x => searchSourceList.Add(x)); - - if (searchSourceList.Any()) + foreach (SearchSource searchSource in _settings.SearchSources.Where(o => (o.ActionKeyword == query.ActionKeyword || + o.ActionKeyword == SearchSourceGlobalPluginWildCardSign) + && o.Enabled)) { - foreach (SearchSource searchSource in searchSourceList) + string keyword = string.Empty; + keyword = searchSource.ActionKeyword == SearchSourceGlobalPluginWildCardSign ? query.ToString() : query.Search; + var title = keyword; + string subtitle = _context.API.GetTranslation("flowlauncher_plugin_websearch_search") + " " + searchSource.Title; + + if (string.IsNullOrEmpty(keyword)) { - string keyword = string.Empty; - keyword = searchSource.ActionKeyword == SearchSourceGlobalPluginWildCardSign ? query.ToString() : query.Search; - var title = keyword; - string subtitle = _context.API.GetTranslation("flowlauncher_plugin_websearch_search") + " " + searchSource.Title; - - if (string.IsNullOrEmpty(keyword)) + var result = new Result { - var result = new Result - { - Title = subtitle, - SubTitle = string.Empty, - IcoPath = searchSource.IconPath - }; - results.Add(result); - } - else - { - var result = new Result - { - Title = title, - SubTitle = subtitle, - Score = 6, - IcoPath = searchSource.IconPath, - ActionKeywordAssigned = searchSource.ActionKeyword == SearchSourceGlobalPluginWildCardSign ? string.Empty : searchSource.ActionKeyword, - Action = c => - { - if (_settings.OpenInNewBrowser) - { - searchSource.Url.Replace("{q}", Uri.EscapeDataString(keyword)).NewBrowserWindow(_settings.BrowserPath); - } - else - { - searchSource.Url.Replace("{q}", Uri.EscapeDataString(keyword)).NewTabInBrowser(_settings.BrowserPath); - } - - return true; - } - }; - - results.Add(result); - ResultsUpdated?.Invoke(this, new ResultUpdatedEventArgs - { - Results = results, - Query = query - }); - - UpdateResultsFromSuggestion(results, keyword, subtitle, searchSource, query); - } + Title = subtitle, + SubTitle = string.Empty, + IcoPath = searchSource.IconPath + }; + results.Add(result); } + else + { + var result = new Result + { + Title = title, + SubTitle = subtitle, + Score = 6, + IcoPath = searchSource.IconPath, + ActionKeywordAssigned = searchSource.ActionKeyword == SearchSourceGlobalPluginWildCardSign ? string.Empty : searchSource.ActionKeyword, + Action = c => + { + if (_settings.OpenInNewBrowser) + { + searchSource.Url.Replace("{q}", Uri.EscapeDataString(keyword)).NewBrowserWindow(_settings.BrowserPath); + } + else + { + searchSource.Url.Replace("{q}", Uri.EscapeDataString(keyword)).NewTabInBrowser(_settings.BrowserPath); + } + + return true; + } + }; + + results.Add(result); + } + + ResultsUpdated?.Invoke(this, new ResultUpdatedEventArgs + { + Results = results, + Query = query + }); + + await UpdateResultsFromSuggestionAsync(results, keyword, subtitle, searchSource, query, token).ConfigureAwait(false); + + if (token.IsCancellationRequested) + return null; + } return results; } - private void UpdateResultsFromSuggestion(List results, string keyword, string subtitle, - SearchSource searchSource, Query query) + private async Task UpdateResultsFromSuggestionAsync(List results, string keyword, string subtitle, + SearchSource searchSource, Query query, CancellationToken token) { if (_settings.EnableSuggestion) { - const int waittime = 300; - var task = Task.Run(async () => - { - var suggestions = await Suggestions(keyword, subtitle, searchSource); - results.AddRange(suggestions); - }, _updateToken); + var suggestions = await SuggestionsAsync(keyword, subtitle, searchSource, token).ConfigureAwait(false); + if (token.IsCancellationRequested || !suggestions.Any()) + return; - if (!task.Wait(waittime)) - { - task.ContinueWith(_ => ResultsUpdated?.Invoke(this, new ResultUpdatedEventArgs - { - Results = results, - Query = query - }), _updateToken); - } + + results.AddRange(suggestions); + + token.ThrowIfCancellationRequested(); } } - private async Task> Suggestions(string keyword, string subtitle, SearchSource searchSource) + private async Task> SuggestionsAsync(string keyword, string subtitle, SearchSource searchSource, CancellationToken token) { var source = _settings.SelectedSuggestion; if (source != null) { - var suggestions = await source.Suggestions(keyword); + var suggestions = await source.Suggestions(keyword, token); + + if (token.IsCancellationRequested) + return null; + var resultsFromSuggestion = suggestions.Select(o => new Result { Title = o, @@ -169,19 +160,24 @@ namespace Flow.Launcher.Plugin.WebSearch _settings = _viewModel.Settings; } - public void Init(PluginInitContext context) + public Task InitAsync(PluginInitContext context) { - _context = context; - var pluginDirectory = _context.CurrentPluginMetadata.PluginDirectory; - var bundledImagesDirectory = Path.Combine(pluginDirectory, Images); - - // Default images directory is in the WebSearch's application folder - DefaultImagesDirectory = Path.Combine(pluginDirectory, Images); - Helper.ValidateDataDirectory(bundledImagesDirectory, DefaultImagesDirectory); + return Task.Run(Init); - // Custom images directory is in the WebSearch's data location folder - var name = Path.GetFileNameWithoutExtension(_context.CurrentPluginMetadata.ExecuteFileName); - CustomImagesDirectory = Path.Combine(DataLocation.PluginSettingsDirectory, name, "CustomIcons"); + void Init() + { + _context = context; + var pluginDirectory = _context.CurrentPluginMetadata.PluginDirectory; + var bundledImagesDirectory = Path.Combine(pluginDirectory, Images); + + // Default images directory is in the WebSearch's application folder + DefaultImagesDirectory = Path.Combine(pluginDirectory, Images); + Helper.ValidateDataDirectory(bundledImagesDirectory, DefaultImagesDirectory); + + // Custom images directory is in the WebSearch's data location folder + var name = Path.GetFileNameWithoutExtension(_context.CurrentPluginMetadata.ExecuteFileName); + CustomImagesDirectory = Path.Combine(DataLocation.PluginSettingsDirectory, name, "CustomIcons"); + }; } #region ISettingProvider Members diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Baidu.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Baidu.cs index 2e385510f..b7e2017f9 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Baidu.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Baidu.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Logger; using System.Net.Http; +using System.Threading; namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources { @@ -15,14 +16,18 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources { private readonly Regex _reg = new Regex("window.baidu.sug\\((.*)\\)"); - public override async Task> Suggestions(string query) + public override async Task> Suggestions(string query, CancellationToken token) { string result; try { const string api = "http://suggestion.baidu.com/su?json=1&wd="; - result = await Http.GetAsync(api + Uri.EscapeUriString(query)).ConfigureAwait(false); + result = await Http.GetAsync(api + Uri.EscapeUriString(query), token).ConfigureAwait(false); + } + catch (TaskCanceledException) + { + return null; } catch (HttpRequestException e) { diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs index 9c4746711..ffde2fda2 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs @@ -9,33 +9,37 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Text.Json; using System.Linq; +using System.Threading; namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources { class Bing : SuggestionSource { - public override async Task> Suggestions(string query) + public override async Task> Suggestions(string query, CancellationToken token) { - Stream resultStream; + JsonElement json; try { const string api = "https://api.bing.com/qsonhs.aspx?q="; - resultStream = await Http.GetStreamAsync(api + Uri.EscapeUriString(query)).ConfigureAwait(false); + + using var resultStream = await Http.GetStreamAsync(api + Uri.EscapeUriString(query), token).ConfigureAwait(false); + + if (resultStream.Length == 0) + return new List(); // this handles the cancellation + + json = (await JsonDocument.ParseAsync(resultStream, cancellationToken: token)).RootElement.GetProperty("AS"); + + } + catch (TaskCanceledException) + { + return null; } catch (HttpRequestException e) { Log.Exception("|Bing.Suggestions|Can't get suggestion from Bing", e); return new List(); } - - if (resultStream.Length == 0) return new List(); - - JsonElement json; - try - { - json = (await JsonDocument.ParseAsync(resultStream)).RootElement.GetProperty("AS"); - } catch (JsonException e) { Log.Exception("|Bing.Suggestions|can't parse suggestions", e); diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs index f23cb66ff..c33ebd7e1 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs @@ -6,6 +6,7 @@ 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; using System.IO; @@ -13,25 +14,31 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources { public class Google : SuggestionSource { - public override async Task> Suggestions(string query) + public override async Task> Suggestions(string query, CancellationToken token) { - Stream resultStream; + JsonDocument json; + try { const string api = "https://www.google.com/complete/search?output=chrome&q="; - resultStream = await Http.GetStreamAsync(api + Uri.EscapeUriString(query)).ConfigureAwait(false); + + using var resultStream = await Http.GetStreamAsync(api + Uri.EscapeUriString(query)).ConfigureAwait(false); + + if (resultStream.Length == 0) + return new List(); + + json = await JsonDocument.ParseAsync(resultStream); + + } + catch (TaskCanceledException) + { + return null; } catch (HttpRequestException e) { Log.Exception("|Google.Suggestions|Can't get suggestion from google", e); return new List(); } - if (resultStream.Length == 0) return new List(); - JsonDocument json; - try - { - json = await JsonDocument.ParseAsync(resultStream); - } catch (JsonException e) { Log.Exception("|Google.Suggestions|can't parse suggestions", e); @@ -41,6 +48,7 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources var results = json?.RootElement.EnumerateArray().ElementAt(1); return results?.EnumerateArray().Select(o => o.GetString()).ToList() ?? new List(); + } public override string ToString() diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/SuggestionSource.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/SuggestionSource.cs index d6d89415f..c58e61141 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/SuggestionSource.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/SuggestionSource.cs @@ -1,10 +1,11 @@ using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources { public abstract class SuggestionSource { - public abstract Task> Suggestions(string query); + public abstract Task> Suggestions(string query, CancellationToken token); } -} \ No newline at end of file +} diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json b/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json index c036fbf8b..2696ab622 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json @@ -25,7 +25,7 @@ "Name": "Web Searches", "Description": "Provide the web search ability", "Author": "qianlifeng", - "Version": "1.2.1", + "Version": "1.3.0", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.WebSearch.dll",