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",