diff --git a/Flow.Launcher/Images/mainsearch.png b/Doc/mainsearch.png
similarity index 100%
rename from Flow.Launcher/Images/mainsearch.png
rename to Doc/mainsearch.png
diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs
index 31bf0428664..3d4522498d9 100644
--- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs
+++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs
@@ -3,10 +3,10 @@
using System.Diagnostics;
using System.IO;
using System.Reflection;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
-using Newtonsoft.Json;
using Flow.Launcher.Infrastructure.Exception;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin;
@@ -65,7 +65,7 @@ private List DeserializedResult(string output)
{
List results = new List();
- JsonRPCQueryResponseModel queryResponseModel = JsonConvert.DeserializeObject(output);
+ JsonRPCQueryResponseModel queryResponseModel = JsonSerializer.Deserialize(output);
if (queryResponseModel.Result == null) return null;
foreach (JsonRPCResult result in queryResponseModel.Result)
@@ -84,7 +84,7 @@ private List DeserializedResult(string output)
else
{
string actionReponse = ExecuteCallback(result1.JsonRPCAction);
- JsonRPCRequestModel jsonRpcRequestModel = JsonConvert.DeserializeObject(actionReponse);
+ JsonRPCRequestModel jsonRpcRequestModel = JsonSerializer.Deserialize(actionReponse);
if (jsonRpcRequestModel != null
&& !String.IsNullOrEmpty(jsonRpcRequestModel.Method)
&& jsonRpcRequestModel.Method.StartsWith("Flow.Launcher."))
diff --git a/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs b/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs
index b9b878a7bda..273698b8676 100644
--- a/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs
+++ b/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs
@@ -20,7 +20,7 @@ internal PluginAssemblyLoader(string assemblyFilePath)
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 @@ protected override Assembly Load(AssemblyName assemblyName)
// 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/PluginConfig.cs b/Flow.Launcher.Core/Plugin/PluginConfig.cs
index b946fa44d21..46f79c60cad 100644
--- a/Flow.Launcher.Core/Plugin/PluginConfig.cs
+++ b/Flow.Launcher.Core/Plugin/PluginConfig.cs
@@ -2,10 +2,10 @@
using System.Collections.Generic;
using System.Linq;
using System.IO;
-using Newtonsoft.Json;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin;
+using System.Text.Json;
namespace Flow.Launcher.Core.Plugin
{
@@ -61,7 +61,7 @@ private static PluginMetadata GetPluginMetadata(string pluginDirectory)
PluginMetadata metadata;
try
{
- metadata = JsonConvert.DeserializeObject(File.ReadAllText(configPath));
+ metadata = JsonSerializer.Deserialize(File.ReadAllText(configPath));
metadata.PluginDirectory = pluginDirectory;
// for plugins which doesn't has ActionKeywords key
metadata.ActionKeywords = metadata.ActionKeywords ?? new List { metadata.ActionKeyword };
diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs
index 3b697a1ee6c..26167e945ac 100644
--- a/Flow.Launcher.Core/Plugin/PluginManager.cs
+++ b/Flow.Launcher.Core/Plugin/PluginManager.cs
@@ -3,6 +3,7 @@
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 @@ public static void Save()
}
}
- 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 @@ public static void LoadPlugins(PluginsSettings settings)
/// 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 @@ public static List ValidPluginsForQuery(Query query)
}
}
- 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 @@ public static void UpdatePluginMetadata(List results, PluginMetadata met
}
}
- private static bool IsGlobalPlugin(PluginMetadata metadata)
- {
- return metadata.ActionKeywords.Contains(Query.GlobalPluginWildcardSign);
- }
-
///
/// get specified plugin, return null if not found
///
@@ -222,16 +253,19 @@ public static List GetContextMenusForPlugin(Result result)
}
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 @@ public static void AddActionKeyword(string id, string newActionKeyword)
{
NonGlobalPlugins[newActionKeyword] = plugin;
}
+
plugin.Metadata.ActionKeywords.Add(newActionKeyword);
}
@@ -262,16 +297,16 @@ public static void RemoveActionKeyword(string id, string oldActionkeyword)
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 224dbd85e92..fcf17844598 100644
--- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs
+++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs
@@ -37,56 +37,59 @@ public static IEnumerable DotNetPlugins(List source)
foreach (var metadata in metadatas)
{
- 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 milliseconds = Stopwatch.Debug(
+ $"|PluginsLoader.DotNetPlugins|Constructor init cost for {metadata.Name}", () =>
{
+#if DEBUG
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
- assembly = assemblyLoader.LoadAssemblyAndDependencies();
-
- var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, typeof(IPlugin));
-
- 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);
- }
+ var assembly = assemblyLoader.LoadAssemblyAndDependencies();
+ var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, typeof(IPlugin),
+ typeof(IAsyncPlugin));
- if (plugin == null)
- {
- erroredPlugins.Add(metadata.Name);
- return;
- }
+ var plugin = Activator.CreateInstance(type);
+#else
+ Assembly assembly = null;
+ object plugin = null;
+
+ try
+ {
+ var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
+ assembly = assemblyLoader.LoadAssemblyAndDependencies();
+
+ 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 @@ public static IEnumerable DotNetPlugins(List source)
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 @@ public static IEnumerable ExecutablePlugins(IEnumerable
-
diff --git a/Flow.Launcher.Infrastructure/Helper.cs b/Flow.Launcher.Infrastructure/Helper.cs
index fa7e18533ed..faa4c93b513 100644
--- a/Flow.Launcher.Infrastructure/Helper.cs
+++ b/Flow.Launcher.Infrastructure/Helper.cs
@@ -1,12 +1,18 @@
using System;
using System.IO;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Converters;
+using System.Runtime.CompilerServices;
+using System.Text.Json;
+using System.Text.Json.Serialization;
namespace Flow.Launcher.Infrastructure
{
public static class Helper
{
+ static Helper()
+ {
+ jsonFormattedSerializerOptions.Converters.Add(new JsonStringEnumConverter());
+ }
+
///
/// http://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy
///
@@ -65,13 +71,18 @@ public static void ValidateDirectory(string path)
}
}
+ private static readonly JsonSerializerOptions jsonFormattedSerializerOptions = new JsonSerializerOptions
+ {
+ WriteIndented = true
+ };
+
public static string Formatted(this T t)
{
- var formatted = JsonConvert.SerializeObject(
- t,
- Formatting.Indented,
- new StringEnumConverter()
- );
+ var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions
+ {
+ WriteIndented = true
+ });
+
return formatted;
}
}
diff --git a/Flow.Launcher.Infrastructure/Http/Http.cs b/Flow.Launcher.Infrastructure/Http/Http.cs
index 8e2832690e4..de2e823590b 100644
--- a/Flow.Launcher.Infrastructure/Http/Http.cs
+++ b/Flow.Launcher.Infrastructure/Http/Http.cs
@@ -8,6 +8,7 @@
using Flow.Launcher.Infrastructure.UserSettings;
using System;
using System.ComponentModel;
+using System.Threading;
namespace Flow.Launcher.Infrastructure.Http
{
@@ -15,13 +16,7 @@ public static class Http
{
private const string UserAgent = @"Mozilla/5.0 (Trident/7.0; rv:11.0) like Gecko";
- private static HttpClient client;
-
- private static SocketsHttpHandler socketsHttpHandler = new SocketsHttpHandler()
- {
- UseProxy = true,
- Proxy = WebProxy
- };
+ private static HttpClient client = new HttpClient();
static Http()
{
@@ -31,8 +26,8 @@ static Http()
| SecurityProtocolType.Tls11
| SecurityProtocolType.Tls12;
- client = new HttpClient(socketsHttpHandler, false);
client.DefaultRequestHeaders.Add("User-Agent", UserAgent);
+ HttpClient.DefaultProxy = WebProxy;
}
private static HttpProxy proxy;
@@ -44,6 +39,7 @@ public static HttpProxy Proxy
{
proxy = value;
proxy.PropertyChanged += UpdateProxy;
+ UpdateProxy(ProxyProperty.Enabled);
}
}
@@ -75,36 +71,50 @@ public static void UpdateProxy(ProxyProperty property)
};
}
- public static async Task Download([NotNull] string url, [NotNull] string filePath)
+ public static async Task DownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default)
{
- using var response = await client.GetAsync(url);
- if (response.StatusCode == HttpStatusCode.OK)
+ try
{
- await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
- await response.Content.CopyToAsync(fileStream);
+ using var response = await client.GetAsync(url, token);
+ if (response.StatusCode == HttpStatusCode.OK)
+ {
+ await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
+ await response.Content.CopyToAsync(fileStream);
+ }
+ else
+ {
+ throw new HttpRequestException($"Error code <{response.StatusCode}> returned from <{url}>");
+ }
}
- else
+ catch (HttpRequestException e)
{
- throw new HttpRequestException($"Error code <{response.StatusCode}> returned from <{url}>");
+ Log.Exception("Infrastructure.Http", "Http Request Error", e, "DownloadAsync");
+ throw;
}
}
///
/// Asynchrously get the result as string from url.
- /// When supposing the result is long and large, try using GetStreamAsync to avoid reading as string
+ /// 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);
}
- 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}>");
- using var response = await client.GetAsync(url);
+ using var response = await client.GetAsync(url, token);
var content = await response.Content.ReadAsStringAsync();
if (response.StatusCode == HttpStatusCode.OK)
{
@@ -122,10 +132,10 @@ public static async Task GetAsync([NotNull] Uri url)
///
///
///
- public static async Task GetStreamAsync([NotNull] string url)
+ public static async Task GetStreamAsync([NotNull] string url, CancellationToken token = default)
{
Log.Debug($"|Http.Get|Url <{url}>");
- var response = await client.GetAsync(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 b1c09024f25..bb7ec681781 100644
--- a/Flow.Launcher.Infrastructure/Image/ImageCache.cs
+++ b/Flow.Launcher.Infrastructure/Image/ImageCache.cs
@@ -73,8 +73,7 @@ public ImageSource this[string path]
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 289ec5d6829..94132b27f18 100644
--- a/Flow.Launcher.Infrastructure/Logger/Log.cs
+++ b/Flow.Launcher.Infrastructure/Logger/Log.cs
@@ -50,14 +50,18 @@ private static bool FormatValid(string message)
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,
@@ -128,7 +132,7 @@ private static void LogInternal(string message, LogLevel level)
public static void Exception(string message, System.Exception e)
{
#if DEBUG
- throw e;
+ throw e;
#else
if (FormatValid(message))
{
diff --git a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs
index 80fd1282035..6c2a94e8205 100644
--- a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs
+++ b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Concurrent;
+using System.Collections.Generic;
using System.Linq;
using System.Text;
using JetBrains.Annotations;
@@ -8,14 +9,109 @@
namespace Flow.Launcher.Infrastructure
{
+ public class TranslationMapping
+ {
+ private bool constructed;
+
+ private List originalIndexs = new List();
+ private List translatedIndexs = new List();
+ private int translaedLength = 0;
+
+ public string key { get; private set; }
+
+ public void setKey(string key)
+ {
+ this.key = key;
+ }
+
+ public void AddNewIndex(int originalIndex, int translatedIndex, int length)
+ {
+ if (constructed)
+ throw new InvalidOperationException("Mapping shouldn't be changed after constructed");
+
+ originalIndexs.Add(originalIndex);
+ translatedIndexs.Add(translatedIndex);
+ translatedIndexs.Add(translatedIndex + length);
+ translaedLength += length - 1;
+ }
+
+ public int MapToOriginalIndex(int translatedIndex)
+ {
+ if (translatedIndex > translatedIndexs.Last())
+ return translatedIndex - translaedLength - 1;
+
+ int lowerBound = 0;
+ int upperBound = originalIndexs.Count - 1;
+
+ int count = 0;
+
+ // Corner case handle
+ if (translatedIndex < translatedIndexs[0])
+ return translatedIndex;
+ if (translatedIndex > translatedIndexs.Last())
+ {
+ int indexDef = 0;
+ for (int k = 0; k < originalIndexs.Count; k++)
+ {
+ indexDef += translatedIndexs[k * 2 + 1] - translatedIndexs[k * 2];
+ }
+
+ return translatedIndex - indexDef - 1;
+ }
+
+ // Binary Search with Range
+ for (int i = originalIndexs.Count / 2;; count++)
+ {
+ if (translatedIndex < translatedIndexs[i * 2])
+ {
+ // move to lower middle
+ upperBound = i;
+ i = (i + lowerBound) / 2;
+ }
+ else if (translatedIndex > translatedIndexs[i * 2 + 1] - 1)
+ {
+ lowerBound = i;
+ // move to upper middle
+ // due to floor of integer division, move one up on corner case
+ i = (i + upperBound + 1) / 2;
+ }
+ else
+ return originalIndexs[i];
+
+ if (upperBound - lowerBound <= 1 &&
+ translatedIndex > translatedIndexs[lowerBound * 2 + 1] &&
+ translatedIndex < translatedIndexs[upperBound * 2])
+ {
+ int indexDef = 0;
+
+ for (int j = 0; j < upperBound; j++)
+ {
+ indexDef += translatedIndexs[j * 2 + 1] - translatedIndexs[j * 2];
+ }
+
+ return translatedIndex - indexDef - 1;
+ }
+ }
+ }
+
+ public void endConstruct()
+ {
+ if (constructed)
+ throw new InvalidOperationException("Mapping has already been constructed");
+ constructed = true;
+ }
+ }
+
public interface IAlphabet
{
- string Translate(string stringToTranslate);
+ public (string translation, TranslationMapping map) Translate(string stringToTranslate);
}
public class PinyinAlphabet : IAlphabet
{
- private ConcurrentDictionary _pinyinCache = new ConcurrentDictionary();
+ private ConcurrentDictionary _pinyinCache =
+ new ConcurrentDictionary();
+
private Settings _settings;
public void Initialize([NotNull] Settings settings)
@@ -23,7 +119,7 @@ public void Initialize([NotNull] Settings settings)
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
}
- public string Translate(string content)
+ public (string translation, TranslationMapping map) Translate(string content)
{
if (_settings.ShouldUsePinyin)
{
@@ -34,14 +130,7 @@ public string Translate(string content)
var resultList = WordsHelper.GetPinyinList(content);
StringBuilder resultBuilder = new StringBuilder();
-
- for (int i = 0; i < resultList.Length; i++)
- {
- if (content[i] >= 0x3400 && content[i] <= 0x9FD5)
- resultBuilder.Append(resultList[i].First());
- }
-
- resultBuilder.Append(' ');
+ TranslationMapping map = new TranslationMapping();
bool pre = false;
@@ -49,6 +138,7 @@ public string Translate(string content)
{
if (content[i] >= 0x3400 && content[i] <= 0x9FD5)
{
+ map.AddNewIndex(i, resultBuilder.Length, resultList[i].Length + 1);
resultBuilder.Append(' ');
resultBuilder.Append(resultList[i]);
pre = true;
@@ -60,15 +150,21 @@ public string Translate(string content)
pre = false;
resultBuilder.Append(' ');
}
+
resultBuilder.Append(resultList[i]);
}
}
- return _pinyinCache[content] = resultBuilder.ToString();
+ map.endConstruct();
+
+ var key = resultBuilder.ToString();
+ map.setKey(key);
+
+ return _pinyinCache[content] = (key, map);
}
else
{
- return content;
+ return (content, null);
}
}
else
@@ -78,7 +174,7 @@ public string Translate(string content)
}
else
{
- return content;
+ return (content, null);
}
}
}
diff --git a/Flow.Launcher.Infrastructure/Stopwatch.cs b/Flow.Launcher.Infrastructure/Stopwatch.cs
index d39d90e81b8..dd6edaff93b 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 @@ public static long Debug(string message, Action action)
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 @@ public static long Normal(string message, Action action)
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 784c111106d..f0e4a79fcf6 100644
--- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
+++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
@@ -1,7 +1,7 @@
using System;
using System.Globalization;
using System.IO;
-using Newtonsoft.Json;
+using System.Text.Json;
using Flow.Launcher.Infrastructure.Logger;
namespace Flow.Launcher.Infrastructure.Storage
@@ -9,9 +9,9 @@ namespace Flow.Launcher.Infrastructure.Storage
///
/// Serialize object using json format.
///
- public class JsonStrorage
+ public class JsonStrorage where T : new()
{
- private readonly JsonSerializerSettings _serializerSettings;
+ private readonly JsonSerializerOptions _serializerSettings;
private T _data;
// need a new directory name
public const string DirectoryName = "Settings";
@@ -24,10 +24,9 @@ internal JsonStrorage()
{
// use property initialization instead of DefaultValueAttribute
// easier and flexible for default value of object
- _serializerSettings = new JsonSerializerSettings
+ _serializerSettings = new JsonSerializerOptions
{
- ObjectCreationHandling = ObjectCreationHandling.Replace,
- NullValueHandling = NullValueHandling.Ignore
+ IgnoreNullValues = false
};
}
@@ -56,7 +55,7 @@ private void Deserialize(string searlized)
{
try
{
- _data = JsonConvert.DeserializeObject(searlized, _serializerSettings);
+ _data = JsonSerializer.Deserialize(searlized, _serializerSettings);
}
catch (JsonException e)
{
@@ -77,7 +76,7 @@ private void LoadDefault()
BackupOriginFile();
}
- _data = JsonConvert.DeserializeObject("{}", _serializerSettings);
+ _data = new T();
Save();
}
@@ -94,7 +93,8 @@ private void BackupOriginFile()
public void Save()
{
- string serialized = JsonConvert.SerializeObject(_data, Formatting.Indented);
+ string serialized = JsonSerializer.Serialize(_data, new JsonSerializerOptions() { WriteIndented = true });
+
File.WriteAllText(FilePath, serialized);
}
}
diff --git a/Flow.Launcher.Infrastructure/StringMatcher.cs b/Flow.Launcher.Infrastructure/StringMatcher.cs
index 2a4270fb4b2..3ffa9f7b11c 100644
--- a/Flow.Launcher.Infrastructure/StringMatcher.cs
+++ b/Flow.Launcher.Infrastructure/StringMatcher.cs
@@ -1,8 +1,7 @@
+using Flow.Launcher.Plugin.SharedModels;
using System;
using System.Collections.Generic;
-using System.ComponentModel;
using System.Linq;
-using static Flow.Launcher.Infrastructure.StringMatcher;
namespace Flow.Launcher.Infrastructure
{
@@ -32,7 +31,20 @@ public MatchResult FuzzyMatch(string query, string stringToCompare)
}
///
- /// Current method:
+ /// Current method has two parts, Acronym Match and Fuzzy Search:
+ ///
+ /// Acronym Match:
+ /// Charater listed below will be considered as acronym
+ /// 1. Character on index 0
+ /// 2. Character appears after a space
+ /// 3. Character that is UpperCase
+ /// 4. Character that is number
+ ///
+ /// Acronym Match will succeed when all query characters match with acronyms in stringToCompare.
+ /// If any of the characters in the query isn't matched with stringToCompare, Acronym Match will fail.
+ /// Score will be calculated based the percentage of all query characters matched with total acronyms in stringToCompare.
+ ///
+ /// Fuzzy Search:
/// Character matching + substring matching;
/// 1. Query search string is split into substrings, separator is whitespace.
/// 2. Check each query substring's characters against full compare string,
@@ -44,20 +56,21 @@ public MatchResult FuzzyMatch(string query, string stringToCompare)
///
public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption opt)
{
- if (string.IsNullOrEmpty(stringToCompare) || string.IsNullOrEmpty(query)) return new MatchResult (false, UserSettingSearchPrecision);
-
+ if (string.IsNullOrEmpty(stringToCompare) || string.IsNullOrEmpty(query))
+ return new MatchResult(false, UserSettingSearchPrecision);
+
query = query.Trim();
+ TranslationMapping translationMapping;
+ (stringToCompare, translationMapping) = _alphabet?.Translate(stringToCompare) ?? (stringToCompare, null);
- if (_alphabet != null)
- {
- query = _alphabet.Translate(query);
- stringToCompare = _alphabet.Translate(stringToCompare);
- }
+ var currentAcronymQueryIndex = 0;
+ var acronymMatchData = new List();
+ int acronymsTotalCount = 0;
+ int acronymsMatched = 0;
var fullStringToCompareWithoutCase = opt.IgnoreCase ? stringToCompare.ToLower() : stringToCompare;
-
var queryWithoutCase = opt.IgnoreCase ? query.ToLower() : query;
-
+
var querySubstrings = queryWithoutCase.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
int currentQuerySubstringIndex = 0;
var currentQuerySubstring = querySubstrings[currentQuerySubstringIndex];
@@ -75,17 +88,44 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption
for (var compareStringIndex = 0; compareStringIndex < fullStringToCompareWithoutCase.Length; compareStringIndex++)
{
+ // If acronyms matching successfully finished, this gets the remaining not matched acronyms for score calculation
+ if (currentAcronymQueryIndex >= query.Length && acronymsMatched == query.Length)
+ {
+ if (IsAcronymCount(stringToCompare, compareStringIndex))
+ acronymsTotalCount++;
+ continue;
+ }
+
+ if (currentAcronymQueryIndex >= query.Length ||
+ currentAcronymQueryIndex >= query.Length && allQuerySubstringsMatched)
+ break;
// To maintain a list of indices which correspond to spaces in the string to compare
// To populate the list only for the first query substring
- if (fullStringToCompareWithoutCase[compareStringIndex].Equals(' ') && currentQuerySubstringIndex == 0)
- {
+ if (fullStringToCompareWithoutCase[compareStringIndex] == ' ' && currentQuerySubstringIndex == 0)
spaceIndices.Add(compareStringIndex);
+
+ // Acronym Match
+ if (IsAcronym(stringToCompare, compareStringIndex))
+ {
+ if (fullStringToCompareWithoutCase[compareStringIndex] ==
+ queryWithoutCase[currentAcronymQueryIndex])
+ {
+ acronymMatchData.Add(compareStringIndex);
+ acronymsMatched++;
+
+ currentAcronymQueryIndex++;
+ }
}
- if (fullStringToCompareWithoutCase[compareStringIndex] != currentQuerySubstring[currentQuerySubstringCharacterIndex])
+ if (IsAcronymCount(stringToCompare, compareStringIndex))
+ acronymsTotalCount++;
+
+ if (allQuerySubstringsMatched || fullStringToCompareWithoutCase[compareStringIndex] !=
+ currentQuerySubstring[currentQuerySubstringCharacterIndex])
{
matchFoundInPreviousLoop = false;
+
continue;
}
@@ -107,14 +147,16 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption
// in order to do so we need to verify all previous chars are part of the pattern
var startIndexToVerify = compareStringIndex - currentQuerySubstringCharacterIndex;
- if (AllPreviousCharsMatched(startIndexToVerify, currentQuerySubstringCharacterIndex, fullStringToCompareWithoutCase, currentQuerySubstring))
+ if (AllPreviousCharsMatched(startIndexToVerify, currentQuerySubstringCharacterIndex,
+ fullStringToCompareWithoutCase, currentQuerySubstring))
{
matchFoundInPreviousLoop = true;
// if it's the beginning character of the first query substring that is matched then we need to update start index
firstMatchIndex = currentQuerySubstringIndex == 0 ? startIndexToVerify : firstMatchIndex;
- indexList = GetUpdatedIndexList(startIndexToVerify, currentQuerySubstringCharacterIndex, firstMatchIndexInWord, indexList);
+ indexList = GetUpdatedIndexList(startIndexToVerify, currentQuerySubstringCharacterIndex,
+ firstMatchIndexInWord, indexList);
}
}
@@ -127,49 +169,96 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption
if (currentQuerySubstringCharacterIndex == currentQuerySubstring.Length)
{
// if any of the substrings was not matched then consider as all are not matched
- allSubstringsContainedInCompareString = matchFoundInPreviousLoop && allSubstringsContainedInCompareString;
+ allSubstringsContainedInCompareString =
+ matchFoundInPreviousLoop && allSubstringsContainedInCompareString;
currentQuerySubstringIndex++;
- allQuerySubstringsMatched = AllQuerySubstringsMatched(currentQuerySubstringIndex, querySubstrings.Length);
+ allQuerySubstringsMatched =
+ AllQuerySubstringsMatched(currentQuerySubstringIndex, querySubstrings.Length);
+
if (allQuerySubstringsMatched)
- break;
+ continue;
// otherwise move to the next query substring
currentQuerySubstring = querySubstrings[currentQuerySubstringIndex];
currentQuerySubstringCharacterIndex = 0;
}
}
-
+
+ // return acronym match if all query char matched
+ if (acronymsMatched > 0 && acronymsMatched == query.Length)
+ {
+ int acronymScore = acronymsMatched * 100 / acronymsTotalCount;
+
+ if (acronymScore >= (int)UserSettingSearchPrecision)
+ {
+ acronymMatchData = acronymMatchData.Select(x => translationMapping?.MapToOriginalIndex(x) ?? x).Distinct().ToList();
+ return new MatchResult(true, UserSettingSearchPrecision, acronymMatchData, acronymScore);
+ }
+ }
+
// proceed to calculate score if every char or substring without whitespaces matched
if (allQuerySubstringsMatched)
{
var nearestSpaceIndex = CalculateClosestSpaceIndex(spaceIndices, firstMatchIndex);
- var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1, lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString);
+ var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1,
+ lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString);
- return new MatchResult(true, UserSettingSearchPrecision, indexList, score);
+ var resultList = indexList.Select(x => translationMapping?.MapToOriginalIndex(x) ?? x).Distinct().ToList();
+ return new MatchResult(true, UserSettingSearchPrecision, resultList, score);
}
return new MatchResult(false, UserSettingSearchPrecision);
}
+ private bool IsAcronym(string stringToCompare, int compareStringIndex)
+ {
+ if (IsAcronymChar(stringToCompare, compareStringIndex) || IsAcronymNumber(stringToCompare, compareStringIndex))
+ return true;
+
+ return false;
+ }
+
+ // When counting acronyms, treat a set of numbers as one acronym ie. Visual 2019 as 2 acronyms instead of 5
+ private bool IsAcronymCount(string stringToCompare, int compareStringIndex)
+ {
+ if (IsAcronymChar(stringToCompare, compareStringIndex))
+ return true;
+
+ if (IsAcronymNumber(stringToCompare, compareStringIndex))
+ return compareStringIndex == 0 || char.IsWhiteSpace(stringToCompare[compareStringIndex - 1]);
+
+ return false;
+ }
+
+ private bool IsAcronymChar(string stringToCompare, int compareStringIndex)
+ => char.IsUpper(stringToCompare[compareStringIndex]) ||
+ compareStringIndex == 0 || // 0 index means char is the start of the compare string, which is an acronym
+ char.IsWhiteSpace(stringToCompare[compareStringIndex - 1]);
+
+ private bool IsAcronymNumber(string stringToCompare, int compareStringIndex)
+ => stringToCompare[compareStringIndex] >= 0 && stringToCompare[compareStringIndex] <= 9;
+
// To get the index of the closest space which preceeds the first matching index
private int CalculateClosestSpaceIndex(List spaceIndices, int firstMatchIndex)
{
- if (spaceIndices.Count == 0)
- {
- return -1;
- }
- else
+ var closestSpaceIndex = -1;
+
+ // spaceIndices should be ordered asc
+ foreach (var index in spaceIndices)
{
- int? ind = spaceIndices.OrderBy(item => (firstMatchIndex - item)).Where(item => firstMatchIndex > item).FirstOrDefault();
- int closestSpaceIndex = ind ?? -1;
- return closestSpaceIndex;
+ if (index < firstMatchIndex)
+ closestSpaceIndex = index;
+ else
+ break;
}
+
+ return closestSpaceIndex;
}
private static bool AllPreviousCharsMatched(int startIndexToVerify, int currentQuerySubstringCharacterIndex,
- string fullStringToCompareWithoutCase, string currentQuerySubstring)
+ string fullStringToCompareWithoutCase, string currentQuerySubstring)
{
var allMatch = true;
for (int indexToCheck = 0; indexToCheck < currentQuerySubstringCharacterIndex; indexToCheck++)
@@ -183,8 +272,9 @@ private static bool AllPreviousCharsMatched(int startIndexToVerify, int currentQ
return allMatch;
}
-
- private static List GetUpdatedIndexList(int startIndexToVerify, int currentQuerySubstringCharacterIndex, int firstMatchIndexInWord, List indexList)
+
+ private static List GetUpdatedIndexList(int startIndexToVerify, int currentQuerySubstringCharacterIndex,
+ int firstMatchIndexInWord, List indexList)
{
var updatedList = new List();
@@ -202,10 +292,12 @@ private static List GetUpdatedIndexList(int startIndexToVerify, int current
private static bool AllQuerySubstringsMatched(int currentQuerySubstringIndex, int querySubstringsLength)
{
+ // Acronym won't utilize the substring to match
return currentQuerySubstringIndex >= querySubstringsLength;
}
- private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, int matchLen, bool allSubstringsContainedInCompareString)
+ private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, int matchLen,
+ bool allSubstringsContainedInCompareString)
{
// A match found near the beginning of a string is scored more than a match found near the end
// A match is scored more if the characters in the patterns are closer to each other,
@@ -239,74 +331,6 @@ private static int CalculateSearchScore(string query, string stringToCompare, in
return score;
}
-
- public enum SearchPrecisionScore
- {
- Regular = 50,
- Low = 20,
- None = 0
- }
- }
-
- public class MatchResult
- {
- public MatchResult(bool success, SearchPrecisionScore searchPrecision)
- {
- Success = success;
- SearchPrecision = searchPrecision;
- }
-
- public MatchResult(bool success, SearchPrecisionScore searchPrecision, List matchData, int rawScore)
- {
- Success = success;
- SearchPrecision = searchPrecision;
- MatchData = matchData;
- RawScore = rawScore;
- }
-
- public bool Success { get; set; }
-
- ///
- /// The final score of the match result with search precision filters applied.
- ///
- public int Score { get; private set; }
-
- ///
- /// The raw calculated search score without any search precision filtering applied.
- ///
- private int _rawScore;
-
- public int RawScore
- {
- get { return _rawScore; }
- set
- {
- _rawScore = value;
- Score = ScoreAfterSearchPrecisionFilter(_rawScore);
- }
- }
-
- ///
- /// Matched data to highlight.
- ///
- public List MatchData { get; set; }
-
- public SearchPrecisionScore SearchPrecision { get; set; }
-
- public bool IsSearchPrecisionScoreMet()
- {
- return IsSearchPrecisionScoreMet(_rawScore);
- }
-
- private bool IsSearchPrecisionScoreMet(int rawScore)
- {
- return rawScore >= (int)SearchPrecision;
- }
-
- private int ScoreAfterSearchPrecisionFilter(int rawScore)
- {
- return IsSearchPrecisionScoreMet(rawScore) ? rawScore : 0;
- }
}
public class MatchOption
diff --git a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs
index ccd9beb868a..29bc11480a2 100644
--- a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs
+++ b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs
@@ -31,6 +31,7 @@ public void UpdatePluginSettings(List metadatas)
metadata.ActionKeyword = settings.ActionKeywords[0];
}
metadata.Disabled = settings.Disabled;
+ metadata.Priority = settings.Priority;
}
else
{
@@ -40,7 +41,8 @@ public void UpdatePluginSettings(List metadatas)
Name = metadata.Name,
Version = metadata.Version,
ActionKeywords = metadata.ActionKeywords,
- Disabled = metadata.Disabled
+ Disabled = metadata.Disabled,
+ Priority = metadata.Priority
};
}
}
@@ -52,6 +54,7 @@ public class Plugin
public string Name { get; set; }
public string Version { get; set; }
public List ActionKeywords { get; set; } // a reference of the action keywords from plugin manager
+ public int Priority { get; set; }
///
/// Used only to save the state of the plugin in settings
diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
index 832b6fbfaf2..76a370978bd 100644
--- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
+++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
@@ -1,9 +1,9 @@
using System;
using System.Collections.ObjectModel;
using System.Drawing;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Converters;
+using System.Text.Json.Serialization;
using Flow.Launcher.Plugin;
+using Flow.Launcher.Plugin.SharedModels;
namespace Flow.Launcher.Infrastructure.UserSettings
{
@@ -16,7 +16,8 @@ public class Settings : BaseModel
public bool ShowOpenResultHotkey { get; set; } = true;
public string Language
{
- get => language; set {
+ get => language; set
+ {
language = value;
OnPropertyChanged();
}
@@ -38,7 +39,7 @@ public string Language
///
public bool ShouldUsePinyin { get; set; } = false;
- internal StringMatcher.SearchPrecisionScore QuerySearchPrecision { get; private set; } = StringMatcher.SearchPrecisionScore.Regular;
+ internal SearchPrecisionScore QuerySearchPrecision { get; private set; } = SearchPrecisionScore.Regular;
[JsonIgnore]
public string QuerySearchPrecisionString
@@ -48,8 +49,8 @@ public string QuerySearchPrecisionString
{
try
{
- var precisionScore = (StringMatcher.SearchPrecisionScore)Enum
- .Parse(typeof(StringMatcher.SearchPrecisionScore), value);
+ var precisionScore = (SearchPrecisionScore)Enum
+ .Parse(typeof(SearchPrecisionScore), value);
QuerySearchPrecision = precisionScore;
StringMatcher.Instance.UserSettingSearchPrecision = precisionScore;
@@ -58,8 +59,8 @@ public string QuerySearchPrecisionString
{
Logger.Log.Exception(nameof(Settings), "Failed to load QuerySearchPrecisionString value from Settings file", e);
- QuerySearchPrecision = StringMatcher.SearchPrecisionScore.Regular;
- StringMatcher.Instance.UserSettingSearchPrecision = StringMatcher.SearchPrecisionScore.Regular;
+ QuerySearchPrecision = SearchPrecisionScore.Regular;
+ StringMatcher.Instance.UserSettingSearchPrecision = SearchPrecisionScore.Regular;
throw;
}
@@ -73,9 +74,7 @@ public string QuerySearchPrecisionString
public int MaxResultsToShow { get; set; } = 5;
public int ActivateTimes { get; set; }
- // Order defaults to 0 or -1, so 1 will let this property appear last
- [JsonProperty(Order = 1)]
- public PluginsSettings PluginSettings { get; set; } = new PluginsSettings();
+
public ObservableCollection CustomPluginHotkeys { get; set; } = new ObservableCollection();
public bool DontPromptUpdateMsg { get; set; }
@@ -100,8 +99,12 @@ public bool HideNotifyIcon
public HttpProxy Proxy { get; set; } = new HttpProxy();
- [JsonConverter(typeof(StringEnumConverter))]
+ [JsonConverter(typeof(JsonStringEnumConverter))]
public LastQueryMode LastQueryMode { get; set; } = LastQueryMode.Selected;
+
+
+ // This needs to be loaded last by staying at the bottom
+ public PluginsSettings PluginSettings { get; set; } = new PluginsSettings();
}
public enum LastQueryMode
diff --git a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj
index 70013c2740f..0eefe5c4fbd 100644
--- a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj
+++ b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj
@@ -1,4 +1,4 @@
-
+
netcoreapp3.1
@@ -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
@@ -62,7 +62,6 @@
-
-
\ 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 00000000000..b0b41cc2244
--- /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 8f7d279fa4a..203dc9af736 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 ccc00d5e938..dd73eb0e523 100644
--- a/Flow.Launcher.Plugin/IPublicAPI.cs
+++ b/Flow.Launcher.Plugin/IPublicAPI.cs
@@ -1,5 +1,10 @@
-using System;
+using Flow.Launcher.Plugin.SharedModels;
+using JetBrains.Annotations;
+using System;
using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
namespace Flow.Launcher.Plugin
{
@@ -34,7 +39,7 @@ public interface IPublicAPI
/// Plugin's in memory data with new content
/// added by user.
///
- void ReloadAllPluginData();
+ Task ReloadAllPluginData();
///
/// Check for new Flow Launcher update
@@ -82,5 +87,18 @@ public interface IPublicAPI
/// if you want to hook something like Ctrl+R, you should use this event
///
event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent;
+
+ MatchResult FuzzySearch(string query, string stringToCompare);
+
+ Task HttpGetStringAsync(string url, CancellationToken token = default);
+
+ Task HttpGetStreamAsync(string url, CancellationToken token = default);
+
+ Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath);
+
+ void AddActionKeyword(string pluginId, string newActionKeyword);
+
+ void RemoveActionKeyword(string pluginId, string oldActionKeyword);
+
}
}
diff --git a/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs
new file mode 100644
index 00000000000..fc4ac471550
--- /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 29b3c15c9b1..31611519cd1 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 49366a5c618..04f20e9846c 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/PluginMetadata.cs b/Flow.Launcher.Plugin/PluginMetadata.cs
index d81b442e250..e8f5cf74432 100644
--- a/Flow.Launcher.Plugin/PluginMetadata.cs
+++ b/Flow.Launcher.Plugin/PluginMetadata.cs
@@ -1,11 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
-using Newtonsoft.Json;
+using System.Text.Json.Serialization;
namespace Flow.Launcher.Plugin
{
- [JsonObject(MemberSerialization.OptOut)]
public class PluginMetadata : BaseModel
{
private string _pluginDirectory;
@@ -37,12 +36,15 @@ internal set
public List ActionKeywords { get; set; }
public string IcoPath { get; set;}
-
+
public override string ToString()
{
return Name;
}
+ [JsonIgnore]
+ public int Priority { get; set; }
+
///
/// Init time include both plugin load time and init time
///
diff --git a/Flow.Launcher.Plugin/PluginPair.cs b/Flow.Launcher.Plugin/PluginPair.cs
index 910367ec64e..e8954b7a0ef 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 27cd1a5584e..be33bd86c98 100644
--- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs
+++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs
@@ -121,7 +121,7 @@ public static bool FileExists(this string filePath)
public static void OpenPath(string fileOrFolderPath)
{
- var psi = new ProcessStartInfo { FileName = FileExplorerProgramName, UseShellExecute = true, Arguments = fileOrFolderPath };
+ var psi = new ProcessStartInfo { FileName = FileExplorerProgramName, UseShellExecute = true, Arguments = '"' + fileOrFolderPath + '"' };
try
{
if (LocationExists(fileOrFolderPath) || FileExists(fileOrFolderPath))
@@ -146,31 +146,23 @@ public static void OpenContainingFolder(string path)
/// 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.Plugin/SharedModels/MatchResult.cs b/Flow.Launcher.Plugin/SharedModels/MatchResult.cs
new file mode 100644
index 00000000000..5144eb61d66
--- /dev/null
+++ b/Flow.Launcher.Plugin/SharedModels/MatchResult.cs
@@ -0,0 +1,72 @@
+using System.Collections.Generic;
+
+namespace Flow.Launcher.Plugin.SharedModels
+{
+ public class MatchResult
+ {
+ public MatchResult(bool success, SearchPrecisionScore searchPrecision)
+ {
+ Success = success;
+ SearchPrecision = searchPrecision;
+ }
+
+ public MatchResult(bool success, SearchPrecisionScore searchPrecision, List matchData, int rawScore)
+ {
+ Success = success;
+ SearchPrecision = searchPrecision;
+ MatchData = matchData;
+ RawScore = rawScore;
+ }
+
+ public bool Success { get; set; }
+
+ ///
+ /// The final score of the match result with search precision filters applied.
+ ///
+ public int Score { get; private set; }
+
+ ///
+ /// The raw calculated search score without any search precision filtering applied.
+ ///
+ private int _rawScore;
+
+ public int RawScore
+ {
+ get { return _rawScore; }
+ set
+ {
+ _rawScore = value;
+ Score = ScoreAfterSearchPrecisionFilter(_rawScore);
+ }
+ }
+
+ ///
+ /// Matched data to highlight.
+ ///
+ public List MatchData { get; set; }
+
+ public SearchPrecisionScore SearchPrecision { get; set; }
+
+ public bool IsSearchPrecisionScoreMet()
+ {
+ return IsSearchPrecisionScoreMet(_rawScore);
+ }
+
+ private bool IsSearchPrecisionScoreMet(int rawScore)
+ {
+ return rawScore >= (int)SearchPrecision;
+ }
+
+ private int ScoreAfterSearchPrecisionFilter(int rawScore)
+ {
+ return IsSearchPrecisionScoreMet(rawScore) ? rawScore : 0;
+ }
+ }
+
+ public enum SearchPrecisionScore
+ {
+ Regular = 50,
+ Low = 20,
+ None = 0
+ }
+}
diff --git a/Flow.Launcher.Test/FuzzyMatcherTest.cs b/Flow.Launcher.Test/FuzzyMatcherTest.cs
index 468b944573e..bbddcbd2ad4 100644
--- a/Flow.Launcher.Test/FuzzyMatcherTest.cs
+++ b/Flow.Launcher.Test/FuzzyMatcherTest.cs
@@ -5,6 +5,7 @@
using NUnit.Framework;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin;
+using Flow.Launcher.Plugin.SharedModels;
namespace Flow.Launcher.Test
{
@@ -37,8 +38,8 @@ public List GetPrecisionScores()
{
var listToReturn = new List();
- Enum.GetValues(typeof(StringMatcher.SearchPrecisionScore))
- .Cast()
+ Enum.GetValues(typeof(SearchPrecisionScore))
+ .Cast()
.ToList()
.ForEach(x => listToReturn.Add((int)x));
@@ -92,7 +93,8 @@ public void WhenNotAllCharactersFoundInSearchString_ThenShouldReturnZeroScore(st
[TestCase("cand")]
[TestCase("cpywa")]
[TestCase("ccs")]
- public void GivenQueryString_WhenAppliedPrecisionFiltering_ThenShouldReturnGreaterThanPrecisionScoreResults(string searchTerm)
+ public void GivenQueryString_WhenAppliedPrecisionFiltering_ThenShouldReturnGreaterThanPrecisionScoreResults(
+ string searchTerm)
{
var results = new List();
var matcher = new StringMatcher();
@@ -108,9 +110,9 @@ public void GivenQueryString_WhenAppliedPrecisionFiltering_ThenShouldReturnGreat
foreach (var precisionScore in GetPrecisionScores())
{
var filteredResult = results.Where(result => result.Score >= precisionScore)
- .Select(result => result)
- .OrderByDescending(x => x.Score)
- .ToList();
+ .Select(result => result)
+ .OrderByDescending(x => x.Score)
+ .ToList();
Debug.WriteLine("");
Debug.WriteLine("###############################################");
@@ -119,6 +121,7 @@ public void GivenQueryString_WhenAppliedPrecisionFiltering_ThenShouldReturnGreat
{
Debug.WriteLine("SCORE: " + item.Score.ToString() + ", FoundString: " + item.Title);
}
+
Debug.WriteLine("###############################################");
Debug.WriteLine("");
@@ -128,37 +131,47 @@ public void GivenQueryString_WhenAppliedPrecisionFiltering_ThenShouldReturnGreat
[TestCase(Chrome, Chrome, 157)]
[TestCase(Chrome, LastIsChrome, 147)]
- [TestCase(Chrome, HelpCureHopeRaiseOnMindEntityChrome, 25)]
+ [TestCase("chro", HelpCureHopeRaiseOnMindEntityChrome, 50)]
+ [TestCase("chr", HelpCureHopeRaiseOnMindEntityChrome, 30)]
[TestCase(Chrome, UninstallOrChangeProgramsOnYourComputer, 21)]
[TestCase(Chrome, CandyCrushSagaFromKing, 0)]
[TestCase("sql", MicrosoftSqlServerManagementStudio, 110)]
- [TestCase("sql manag", MicrosoftSqlServerManagementStudio, 121)]//double spacing intended
+ [TestCase("sql manag", MicrosoftSqlServerManagementStudio, 121)] //double spacing intended
public void WhenGivenQueryString_ThenShouldReturn_TheDesiredScoring(
string queryString, string compareString, int expectedScore)
{
// When, Given
- var matcher = new StringMatcher();
+ var matcher = new StringMatcher {UserSettingSearchPrecision = SearchPrecisionScore.Regular};
var rawScore = matcher.FuzzyMatch(queryString, compareString).RawScore;
// Should
- Assert.AreEqual(expectedScore, rawScore,
+ Assert.AreEqual(expectedScore, rawScore,
$"Expected score for compare string '{compareString}': {expectedScore}, Actual: {rawScore}");
}
- [TestCase("goo", "Google Chrome", StringMatcher.SearchPrecisionScore.Regular, true)]
- [TestCase("chr", "Google Chrome", StringMatcher.SearchPrecisionScore.Low, true)]
- [TestCase("chr", "Chrome", StringMatcher.SearchPrecisionScore.Regular, true)]
- [TestCase("chr", "Help cure hope raise on mind entity Chrome", StringMatcher.SearchPrecisionScore.Regular, false)]
- [TestCase("chr", "Help cure hope raise on mind entity Chrome", StringMatcher.SearchPrecisionScore.Low, true)]
- [TestCase("chr", "Candy Crush Saga from King", StringMatcher.SearchPrecisionScore.Regular, false)]
- [TestCase("chr", "Candy Crush Saga from King", StringMatcher.SearchPrecisionScore.None, true)]
- [TestCase("ccs", "Candy Crush Saga from King", StringMatcher.SearchPrecisionScore.Low, true)]
- [TestCase("cand", "Candy Crush Saga from King",StringMatcher.SearchPrecisionScore.Regular, true)]
- [TestCase("cand", "Help cure hope raise on mind entity Chrome", StringMatcher.SearchPrecisionScore.Regular, false)]
+ [TestCase("goo", "Google Chrome", SearchPrecisionScore.Regular, true)]
+ [TestCase("chr", "Google Chrome", SearchPrecisionScore.Low, true)]
+ [TestCase("chr", "Chrome", SearchPrecisionScore.Regular, true)]
+ [TestCase("chr", "Help cure hope raise on mind entity Chrome", SearchPrecisionScore.Regular, false)]
+ [TestCase("chr", "Help cure hope raise on mind entity Chrome", SearchPrecisionScore.Low, true)]
+ [TestCase("chr", "Candy Crush Saga from King", SearchPrecisionScore.Regular, false)]
+ [TestCase("chr", "Candy Crush Saga from King", SearchPrecisionScore.None, true)]
+ [TestCase("ccs", "Candy Crush Saga from King", SearchPrecisionScore.Low, true)]
+ [TestCase("cand", "Candy Crush Saga from King", SearchPrecisionScore.Regular, true)]
+ [TestCase("cand", "Help cure hope raise on mind entity Chrome", SearchPrecisionScore.Regular, false)]
+ [TestCase("vsc", VisualStudioCode, SearchPrecisionScore.Regular, true)]
+ [TestCase("vs", VisualStudioCode, SearchPrecisionScore.Regular, true)]
+ [TestCase("vc", VisualStudioCode, SearchPrecisionScore.Regular, true)]
+ [TestCase("vts", VisualStudioCode, SearchPrecisionScore.Regular, false)]
+ [TestCase("vcs", VisualStudioCode, SearchPrecisionScore.Regular, false)]
+ [TestCase("wt", "Windows Terminal From Microsoft Store", SearchPrecisionScore.Regular, false)]
+ [TestCase("vsp", "Visual Studio 2019 Preview", SearchPrecisionScore.Regular, true)]
+ [TestCase("vsp", "2019 Visual Studio Preview", SearchPrecisionScore.Regular, true)]
+ [TestCase("2019p", "Visual Studio 2019 Preview", SearchPrecisionScore.Regular, true)]
public void WhenGivenDesiredPrecision_ThenShouldReturn_AllResultsGreaterOrEqual(
string queryString,
string compareString,
- StringMatcher.SearchPrecisionScore expectedPrecisionScore,
+ SearchPrecisionScore expectedPrecisionScore,
bool expectedPrecisionResult)
{
// When
@@ -170,48 +183,50 @@ public void WhenGivenDesiredPrecision_ThenShouldReturn_AllResultsGreaterOrEqual(
Debug.WriteLine("");
Debug.WriteLine("###############################################");
Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}");
- Debug.WriteLine($"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int)expectedPrecisionScore})");
+ Debug.WriteLine(
+ $"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})");
Debug.WriteLine("###############################################");
Debug.WriteLine("");
// Should
Assert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(),
- $"Query:{queryString}{Environment.NewLine} " +
- $"Compare:{compareString}{Environment.NewLine}" +
+ $"Query: {queryString}{Environment.NewLine} " +
+ $"Compare: {compareString}{Environment.NewLine}" +
$"Raw Score: {matchResult.RawScore}{Environment.NewLine}" +
$"Precision Score: {(int)expectedPrecisionScore}");
}
- [TestCase("exce", "OverLeaf-Latex: An online LaTeX editor", StringMatcher.SearchPrecisionScore.Regular, false)]
- [TestCase("term", "Windows Terminal (Preview)", StringMatcher.SearchPrecisionScore.Regular, true)]
- [TestCase("sql s managa", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)]
- [TestCase("sql' s manag", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)]
- [TestCase("sql s manag", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)]
- [TestCase("sql manag", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)]
- [TestCase("sql", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)]
- [TestCase("sql serv", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)]
- [TestCase("servez", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)]
- [TestCase("sql servz", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)]
- [TestCase("sql serv man", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)]
- [TestCase("sql studio", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)]
- [TestCase("mic", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)]
- [TestCase("chr", "Shutdown", StringMatcher.SearchPrecisionScore.Regular, false)]
- [TestCase("mssms", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)]
- [TestCase("chr", "Change settings for text-to-speech and for speech recognition (if installed).", StringMatcher.SearchPrecisionScore.Regular, false)]
- [TestCase("ch r", "Change settings for text-to-speech and for speech recognition (if installed).", StringMatcher.SearchPrecisionScore.Regular, true)]
- [TestCase("a test", "This is a test", StringMatcher.SearchPrecisionScore.Regular, true)]
- [TestCase("test", "This is a test", StringMatcher.SearchPrecisionScore.Regular, true)]
- [TestCase("cod", VisualStudioCode, StringMatcher.SearchPrecisionScore.Regular, true)]
- [TestCase("code", VisualStudioCode, StringMatcher.SearchPrecisionScore.Regular, true)]
- [TestCase("codes", "Visual Studio Codes", StringMatcher.SearchPrecisionScore.Regular, true)]
+ [TestCase("exce", "OverLeaf-Latex: An online LaTeX editor", SearchPrecisionScore.Regular, false)]
+ [TestCase("term", "Windows Terminal (Preview)", SearchPrecisionScore.Regular, true)]
+ [TestCase("sql s managa", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, false)]
+ [TestCase("sql' s manag", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, false)]
+ [TestCase("sql s manag", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)]
+ [TestCase("sql manag", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)]
+ [TestCase("sql", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)]
+ [TestCase("sql serv", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)]
+ [TestCase("servez", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, false)]
+ [TestCase("sql servz", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, false)]
+ [TestCase("sql serv man", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)]
+ [TestCase("sql studio", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)]
+ [TestCase("mic", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)]
+ [TestCase("mssms", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)]
+ [TestCase("msms", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)]
+ [TestCase("chr", "Shutdown", SearchPrecisionScore.Regular, false)]
+ [TestCase("chr", "Change settings for text-to-speech and for speech recognition (if installed).", SearchPrecisionScore.Regular, false)]
+ [TestCase("ch r", "Change settings for text-to-speech and for speech recognition (if installed).", SearchPrecisionScore.Regular, true)]
+ [TestCase("a test", "This is a test", SearchPrecisionScore.Regular, true)]
+ [TestCase("test", "This is a test", SearchPrecisionScore.Regular, true)]
+ [TestCase("cod", VisualStudioCode, SearchPrecisionScore.Regular, true)]
+ [TestCase("code", VisualStudioCode, SearchPrecisionScore.Regular, true)]
+ [TestCase("codes", "Visual Studio Codes", SearchPrecisionScore.Regular, true)]
public void WhenGivenQuery_ShouldReturnResults_ContainingAllQuerySubstrings(
string queryString,
string compareString,
- StringMatcher.SearchPrecisionScore expectedPrecisionScore,
+ SearchPrecisionScore expectedPrecisionScore,
bool expectedPrecisionResult)
{
// When
- var matcher = new StringMatcher { UserSettingSearchPrecision = expectedPrecisionScore };
+ var matcher = new StringMatcher {UserSettingSearchPrecision = expectedPrecisionScore};
// Given
var matchResult = matcher.FuzzyMatch(queryString, compareString);
@@ -219,7 +234,8 @@ public void WhenGivenQuery_ShouldReturnResults_ContainingAllQuerySubstrings(
Debug.WriteLine("");
Debug.WriteLine("###############################################");
Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}");
- Debug.WriteLine($"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int)expectedPrecisionScore})");
+ Debug.WriteLine(
+ $"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})");
Debug.WriteLine("###############################################");
Debug.WriteLine("");
@@ -238,7 +254,7 @@ public void WhenGivenAQuery_Scoring_ShouldGiveMoreWeightToStartOfNewWord(
string queryString, string compareString1, string compareString2)
{
// When
- var matcher = new StringMatcher { UserSettingSearchPrecision = StringMatcher.SearchPrecisionScore.Regular };
+ var matcher = new StringMatcher {UserSettingSearchPrecision = SearchPrecisionScore.Regular};
// Given
var compareString1Result = matcher.FuzzyMatch(queryString, compareString1);
@@ -247,8 +263,10 @@ public void WhenGivenAQuery_Scoring_ShouldGiveMoreWeightToStartOfNewWord(
Debug.WriteLine("");
Debug.WriteLine("###############################################");
Debug.WriteLine($"QueryString: \"{queryString}\"{Environment.NewLine}");
- Debug.WriteLine($"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}");
- Debug.WriteLine($"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}");
+ Debug.WriteLine(
+ $"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}");
+ Debug.WriteLine(
+ $"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}");
Debug.WriteLine("###############################################");
Debug.WriteLine("");
@@ -256,13 +274,13 @@ public void WhenGivenAQuery_Scoring_ShouldGiveMoreWeightToStartOfNewWord(
Assert.True(compareString1Result.Score > compareString2Result.Score,
$"Query: \"{queryString}\"{Environment.NewLine} " +
$"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" +
- $"Should be greater than{ Environment.NewLine}" +
+ $"Should be greater than{Environment.NewLine}" +
$"CompareString2: \"{compareString2}\", Score: {compareString1Result.Score}{Environment.NewLine}");
}
[TestCase("vim", "Vim", "ignoreDescription", "ignore.exe", "Vim Diff", "ignoreDescription", "ignore.exe")]
public void WhenMultipleResults_ExactMatchingResult_ShouldHaveGreatestScore(
- string queryString, string firstName, string firstDescription, string firstExecutableName,
+ string queryString, string firstName, string firstDescription, string firstExecutableName,
string secondName, string secondDescription, string secondExecutableName)
{
// Act
@@ -275,15 +293,39 @@ public void WhenMultipleResults_ExactMatchingResult_ShouldHaveGreatestScore(
var secondDescriptionMatch = matcher.FuzzyMatch(queryString, secondDescription).RawScore;
var secondExecutableNameMatch = matcher.FuzzyMatch(queryString, secondExecutableName).RawScore;
- var firstScore = new[] { firstNameMatch, firstDescriptionMatch, firstExecutableNameMatch }.Max();
- var secondScore = new[] { secondNameMatch, secondDescriptionMatch, secondExecutableNameMatch }.Max();
+ var firstScore = new[] {firstNameMatch, firstDescriptionMatch, firstExecutableNameMatch}.Max();
+ var secondScore = new[] {secondNameMatch, secondDescriptionMatch, secondExecutableNameMatch}.Max();
// Assert
Assert.IsTrue(firstScore > secondScore,
$"Query: \"{queryString}\"{Environment.NewLine} " +
$"Name of first: \"{firstName}\", Final Score: {firstScore}{Environment.NewLine}" +
- $"Should be greater than{ Environment.NewLine}" +
+ $"Should be greater than{Environment.NewLine}" +
$"Name of second: \"{secondName}\", Final Score: {secondScore}{Environment.NewLine}");
}
+
+ [TestCase("vsc", "Visual Studio Code", 100)]
+ [TestCase("jbr", "JetBrain Rider", 100)]
+ [TestCase("jr", "JetBrain Rider", 66)]
+ [TestCase("vs", "Visual Studio", 100)]
+ [TestCase("vs", "Visual Studio Preview", 66)]
+ [TestCase("vsp", "Visual Studio Preview", 100)]
+ [TestCase("pc", "postman canary", 100)]
+ [TestCase("psc", "Postman super canary", 100)]
+ [TestCase("psc", "Postman super Canary", 100)]
+ [TestCase("vsp", "Visual Studio", 0)]
+ [TestCase("vps", "Visual Studio", 0)]
+ [TestCase(Chrome, HelpCureHopeRaiseOnMindEntityChrome, 75)]
+ public void WhenGivenAnAcronymQuery_ShouldReturnAcronymScore(string queryString, string compareString,
+ int desiredScore)
+ {
+ var matcher = new StringMatcher();
+ var score = matcher.FuzzyMatch(queryString, compareString).Score;
+ Assert.IsTrue(score == desiredScore,
+ $@"Query: ""{queryString}""
+ CompareString: ""{compareString}""
+ Score: {score}
+ Desired Score: {desiredScore}");
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs
index c9114482599..3d0a9a64f73 100644
--- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs
+++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs
@@ -7,6 +7,8 @@
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)
+ private List MethodDirectoryInfoClassSearchReturnsTwoResults(Query dummyQuery, string dummyString, CancellationToken token)
{
- return new List
- {
+ return new List
+ {
new Result
{
Title="Result 1"
@@ -58,16 +60,16 @@ public void GivenWindowsIndexSearch_WhenProvidedFolderPath_ThenQueryWhereRestric
$"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 @@ public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_Then
[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 @@ public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificIte
}
[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 @@ public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryWhereR
[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 @@ public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShould
}
[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 @@ public void GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMe
}
[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 @@ public void GivenWindowsIndexSearch_WhenQueryWhereRestrictionsIsForFileContentSe
}
[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 @@ public void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileConte
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 @@ public void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileConte
[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 @@ public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString(
$"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 @@ public void WhenGivenAPath_ThenShouldReturnThePreviousDirectoryPathIfIncompleteO
}
[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,16 +312,14 @@ public void GivenWindowsIndexSearch_WhenSearchPatternHotKeyIsSearchAll_ThenQuery
$"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)
{
- // Given
- var criteriaConstructor = new DirectoryInfoSearch(new PluginInitContext());
//When
- var resultString = criteriaConstructor.ConstructSearchCriteria(path);
+ var resultString = DirectoryInfoSearch.ConstructSearchCriteria(path);
// Then
Assert.IsTrue(resultString == expectedString,
diff --git a/Flow.Launcher/App.xaml b/Flow.Launcher/App.xaml
index f3347d7fbf6..18addac7398 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 59bdbc8960f..7c4c6a36716 100644
--- a/Flow.Launcher/App.xaml.cs
+++ b/Flow.Launcher/App.xaml.cs
@@ -45,9 +45,9 @@ public static void Main()
}
}
- 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();
@@ -61,6 +61,8 @@ private void OnStartup(object sender, StartupEventArgs e)
_settingsVM = new SettingWindowViewModel(_updater, _portable);
_settings = _settingsVM.Settings;
+ Http.Proxy = _settings.Proxy;
+
_alphabet.Initialize(_settings);
_stringMatcher = new StringMatcher(_alphabet);
StringMatcher.Instance = _stringMatcher;
@@ -68,9 +70,10 @@ private void OnStartup(object sender, StartupEventArgs e)
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;
@@ -84,8 +87,6 @@ private void OnStartup(object sender, StartupEventArgs e)
ThemeManager.Instance.Settings = _settings;
ThemeManager.Instance.ChangeTheme(_settings.Theme);
- Http.Proxy = _settings.Proxy;
-
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
RegisterExitEvents();
diff --git a/Flow.Launcher/CustomQueryHotkeySetting.xaml b/Flow.Launcher/CustomQueryHotkeySetting.xaml
index 5f4cdff19e4..a97f9073316 100644
--- a/Flow.Launcher/CustomQueryHotkeySetting.xaml
+++ b/Flow.Launcher/CustomQueryHotkeySetting.xaml
@@ -5,7 +5,7 @@
Icon="Images\app.png"
ResizeMode="NoResize"
WindowStartupLocation="CenterScreen"
- Title="Custom Plugin Hotkey" Height="200" Width="674.766">
+ Title="{DynamicResource customeQueryHotkeyTitle}" Height="200" Width="674.766">
diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj
index 8548ba39e5f..289a502d0f7 100644
--- a/Flow.Launcher/Flow.Launcher.csproj
+++ b/Flow.Launcher/Flow.Launcher.csproj
@@ -60,6 +60,12 @@
Designer
PreserveNewest
+
+ PreserveNewest
+
+
+ PreserveNewest
+
@@ -78,7 +84,8 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
+
@@ -87,116 +94,7 @@
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Flow.Launcher/Images/mainsearch.svg b/Flow.Launcher/Images/mainsearch.svg
new file mode 100644
index 00000000000..5d28abdb3cc
--- /dev/null
+++ b/Flow.Launcher/Images/mainsearch.svg
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml
index adb49b65dee..b6bf76b7fc4 100644
--- a/Flow.Launcher/Languages/en.xaml
+++ b/Flow.Launcher/Languages/en.xaml
@@ -17,6 +17,7 @@
Flow Launcher Settings
General
+ Portable Mode
Start Flow Launcher on system startup
Hide Flow Launcher when focus is lost
Do not show new version notifications
@@ -39,10 +40,13 @@
Plugin
Find more plugins
+ Enable
Disable
Action keyword:
Current action keyword:
New action keyword:
+ Current Priority:
+ New Priority:
Plugin Directory
Author
Init time:
@@ -72,7 +76,7 @@
Are you sure you want to delete {0} plugin hotkey?
Query window shadow effect
Shadow effect has a substantial usage of GPU.
- Not recommended if you computer performance is limited.
+ Not recommended if your computer performance is limited.
HTTP Proxy
@@ -104,6 +108,10 @@
Release Notes
+
+ Greater the number, the higher the result will be ranked. Try setting it as 5. If you want the results to be lower than any other plugin's, provide a negative number
+ Please provide an valid integer for Priority!
+
Old Action Keyword
New Action Keyword
@@ -116,6 +124,7 @@
Use * if you don't want to specify an action keyword
+ Custom Plugin Hotkey
Preview
Hotkey is unavailable, please select a new hotkey
Invalid plugin hotkey
@@ -140,11 +149,23 @@
Failed to send report
Flow Launcher got an error
+
+ Please wait...
+
+ Checking for new update
+ You already have the latest Flow Launcher version
+ Update found
+ Updating...
+ Flow Launcher was not able to move your user profile data to the new update version.
+ Please manually move your profile data folder from {0} to {1}
+ New Update
New Flow Launcher release {0} is now available
An error occurred while trying to install software updates
Update
Cancel
+ Update Failed
+ Check your connection and try updating proxy settings to github-cloud.s3.amazonaws.com.
This upgrade will restart Flow Launcher
Following files will be updated
Update files
diff --git a/Flow.Launcher/Languages/sk.xaml b/Flow.Launcher/Languages/sk.xaml
index bf001d507c5..64230a93aba 100644
--- a/Flow.Launcher/Languages/sk.xaml
+++ b/Flow.Launcher/Languages/sk.xaml
@@ -17,6 +17,7 @@
Nastavenia Flow Launchera
Všeobecné
+ Prenosný režim
Spustiť Flow Launcher po štarte systému
Schovať Flow Launcher po strate fokusu
Nezobrazovať upozornenia na novú verziu
@@ -39,14 +40,17 @@
Plugin
Nájsť ďalšie pluginy
+ Povoliť
Zakázať
Skratka akcie
Aktuálna akcia skratky:
Nová akcia skratky:
+ Aktuálna priorita:
+ Nová priorita:
Priečinok s pluginmi
Autor
- Príprava: {0}ms
- Čas dopytu: {0}ms
+ Príprava:
+ Čas dopytu:
Motív
@@ -104,6 +108,10 @@
Poznámky k vydaniu
+
+ Vyššie číslo znamená, že výsledok bude vyššie. Skúste nastaviť napr. 5. Ak chcete, aby boli výsledky nižšie ako ktorékoľvek iné doplnky, zadajte záporné číslo
+ Prosím, zadajte platné číslo pre prioritu!
+
Stará skratka akcie
Nová skratka akcie
@@ -116,6 +124,7 @@
Použite * ak nechcete určiť skratku pre akciu
+ Vlastná klávesová skratka pre plugin
Náhľad
Klávesová skratka je nedostupná, prosím, zadajte novú
Neplatná klávesová skratka pluginu
@@ -140,11 +149,23 @@
Odoslanie hlásenia zlyhalo
Flow Launcher zaznamenal chybu
+
+ Čakajte, prosím…
+
- Je dostupná nová verzia Flow Launcher {0}
+ Kontrolujú sa akutalizácie
+ Už máte najnovšiu verizu Flow Launchera
+ Bola nájdená aktualizácia
+ Aktualizuje sa…
+ Flow Launcher nedokázal presunúť používateľské údaje do aktualizovanej verzie.
+ Prosím, presuňte profilový priečinok „data“ z {0} do {1}
+ Nová aktualizácia
+ Je dostupná nová verzia Flow Launchera {0}
Počas inštalácie aktualizácií došlo k chybe
Aktualizovať
Zrušiť
+ Aktualizácia zlyhala
+ Skontrolujte pripojenie a skúste aktualizovať nastavenia servera proxy na github-cloud.s3.amazonaws.com.
Tento upgrade reštartuje Flow Launcher
Nasledujúce súbory budú aktualizované
Aktualizovať súbory
diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml
index 07bb9633903..4cc0b4428b1 100644
--- a/Flow.Launcher/MainWindow.xaml
+++ b/Flow.Launcher/MainWindow.xaml
@@ -6,6 +6,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:converters="clr-namespace:Flow.Launcher.Converters"
+ xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
mc:Ignorable="d"
Title="Flow Launcher"
Topmost="True"
@@ -92,10 +93,11 @@
-
+
diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs
index 3812b4e1f0a..04a1063f857 100644
--- a/Flow.Launcher/MainWindow.xaml.cs
+++ b/Flow.Launcher/MainWindow.xaml.cs
@@ -26,6 +26,7 @@ public partial class MainWindow
#region Private Fields
private readonly Storyboard _progressBarStoryboard = new Storyboard();
+ private bool isProgressBarStoryboardPaused;
private Settings _settings;
private NotifyIcon _notifyIcon;
private MainViewModel _viewModel;
@@ -52,7 +53,7 @@ private void OnClosing(object sender, CancelEventArgs e)
private void OnInitialized(object sender, EventArgs e)
{
-
+
}
private void OnLoaded(object sender, RoutedEventArgs _)
@@ -73,7 +74,7 @@ private void OnLoaded(object sender, RoutedEventArgs _)
{
if (e.PropertyName == nameof(MainViewModel.MainWindowVisibility))
{
- if (Visibility == Visibility.Visible)
+ if (_viewModel.MainWindowVisibility == Visibility.Visible)
{
Activate();
QueryTextBox.Focus();
@@ -84,7 +85,34 @@ private void OnLoaded(object sender, RoutedEventArgs _)
QueryTextBox.SelectAll();
_viewModel.LastQuerySelected = true;
}
+
+ if (_viewModel.ProgressBarVisibility == Visibility.Visible && isProgressBarStoryboardPaused)
+ {
+ _progressBarStoryboard.Resume();
+ isProgressBarStoryboardPaused = false;
+ }
}
+ else if (!isProgressBarStoryboardPaused)
+ {
+ _progressBarStoryboard.Pause();
+ isProgressBarStoryboardPaused = true;
+ }
+ }
+ else if (e.PropertyName == nameof(MainViewModel.ProgressBarVisibility))
+ {
+ Dispatcher.Invoke(() =>
+ {
+ if (_viewModel.ProgressBarVisibility == Visibility.Hidden && !isProgressBarStoryboardPaused)
+ {
+ _progressBarStoryboard.Pause();
+ isProgressBarStoryboardPaused = true;
+ }
+ else if (_viewModel.MainWindowVisibility == Visibility.Visible && isProgressBarStoryboardPaused)
+ {
+ _progressBarStoryboard.Resume();
+ isProgressBarStoryboardPaused = false;
+ }
+ }, System.Windows.Threading.DispatcherPriority.Render);
}
};
_settings.PropertyChanged += (o, e) =>
@@ -170,6 +198,7 @@ private void InitProgressbarAnimation()
_progressBarStoryboard.RepeatBehavior = RepeatBehavior.Forever;
ProgressBar.BeginStoryboard(_progressBarStoryboard);
_viewModel.ProgressBarVisibility = Visibility.Hidden;
+ isProgressBarStoryboardPaused = true;
}
private void OnMouseDown(object sender, MouseButtonEventArgs e)
diff --git a/Flow.Launcher/PriorityChangeWindow.xaml b/Flow.Launcher/PriorityChangeWindow.xaml
new file mode 100644
index 00000000000..68b5a49b7d1
--- /dev/null
+++ b/Flow.Launcher/PriorityChangeWindow.xaml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher/PriorityChangeWindow.xaml.cs b/Flow.Launcher/PriorityChangeWindow.xaml.cs
new file mode 100644
index 00000000000..0adb1f08037
--- /dev/null
+++ b/Flow.Launcher/PriorityChangeWindow.xaml.cs
@@ -0,0 +1,69 @@
+using Flow.Launcher.Core.Plugin;
+using Flow.Launcher.Core.Resource;
+using Flow.Launcher.Infrastructure.UserSettings;
+using Flow.Launcher.Plugin;
+using Flow.Launcher.ViewModel;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Shapes;
+
+namespace Flow.Launcher
+{
+ ///
+ /// Interaction Logic of PriorityChangeWindow.xaml
+ ///
+ public partial class PriorityChangeWindow : Window
+ {
+ private readonly PluginPair plugin;
+ private Settings settings;
+ private readonly Internationalization translater = InternationalizationManager.Instance;
+ private readonly PluginViewModel pluginViewModel;
+
+ public PriorityChangeWindow(string pluginId, Settings settings, PluginViewModel pluginViewModel)
+ {
+ InitializeComponent();
+ plugin = PluginManager.GetPluginForId(pluginId);
+ this.settings = settings;
+ this.pluginViewModel = pluginViewModel;
+ if (plugin == null)
+ {
+ MessageBox.Show(translater.GetTranslation("cannotFindSpecifiedPlugin"));
+ Close();
+ }
+ }
+
+ private void BtnCancel_OnClick(object sender, RoutedEventArgs e)
+ {
+ Close();
+ }
+
+ private void btnDone_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (int.TryParse(tbAction.Text.Trim(), out var newPriority))
+ {
+ pluginViewModel.ChangePriority(newPriority);
+ Close();
+ }
+ else
+ {
+ string msg = translater.GetTranslation("invalidPriority");
+ MessageBox.Show(msg);
+ }
+
+ }
+
+ private void PriorityChangeWindow_Loaded(object sender, RoutedEventArgs e)
+ {
+ OldPriority.Text = pluginViewModel.Priority.ToString();
+ tbAction.Focus();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs
index 90d4fff63e8..427fd9fc639 100644
--- a/Flow.Launcher/PublicAPIInstance.cs
+++ b/Flow.Launcher/PublicAPIInstance.cs
@@ -5,7 +5,6 @@
using System.Threading.Tasks;
using System.Windows;
using Squirrel;
-using Flow.Launcher.Core;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Helper;
@@ -14,6 +13,11 @@
using Flow.Launcher.Infrastructure.Image;
using Flow.Launcher.Plugin;
using Flow.Launcher.ViewModel;
+using Flow.Launcher.Plugin.SharedModels;
+using System.Threading;
+using System.IO;
+using Flow.Launcher.Infrastructure.Http;
+using JetBrains.Annotations;
namespace Flow.Launcher
{
@@ -78,9 +82,9 @@ public void SaveAppAllSettings()
ImageLoader.Save();
}
- public void ReloadAllPluginData()
+ public Task ReloadAllPluginData()
{
- PluginManager.ReloadData();
+ return PluginManager.ReloadData();
}
public void ShowMsg(string title, string subTitle = "", string iconPath = "")
@@ -92,7 +96,7 @@ public void ShowMsg(string title, string subTitle, string iconPath, bool useMain
{
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);
});
}
@@ -127,6 +131,32 @@ public List GetAllPlugins()
public event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent;
+ public MatchResult FuzzySearch(string query, string stringToCompare) => StringMatcher.FuzzySearch(query, stringToCompare);
+
+ public Task HttpGetStringAsync(string url, CancellationToken token = default)
+ {
+ return Http.GetAsync(url);
+ }
+
+ public Task HttpGetStreamAsync(string url, CancellationToken token = default)
+ {
+ return Http.GetStreamAsync(url);
+ }
+
+ public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath)
+ {
+ return Http.DownloadAsync(url, filePath);
+ }
+
+ public void AddActionKeyword(string pluginId, string newActionKeyword)
+ {
+ PluginManager.AddActionKeyword(pluginId, newActionKeyword);
+ }
+
+ public void RemoveActionKeyword(string pluginId, string oldActionKeyword)
+ {
+ PluginManager.RemoveActionKeyword(pluginId, oldActionKeyword);
+ }
#endregion
#region Private Methods
@@ -139,6 +169,7 @@ private bool KListener_hookedKeyboardCallback(KeyEvent keyevent, int vkcode, Spe
}
return true;
}
+
#endregion
}
}
diff --git a/Flow.Launcher/ResultListBox.xaml b/Flow.Launcher/ResultListBox.xaml
index 072196605a6..2f9d06d814e 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/SettingWindow.xaml b/Flow.Launcher/SettingWindow.xaml
index e47f0e7791f..4c7eac114dd 100644
--- a/Flow.Launcher/SettingWindow.xaml
+++ b/Flow.Launcher/SettingWindow.xaml
@@ -8,6 +8,7 @@
xmlns:userSettings="clr-namespace:Flow.Launcher.Infrastructure.UserSettings;assembly=Flow.Launcher.Infrastructure"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:ui="http://schemas.modernwpf.com/2019"
+ xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
x:Class="Flow.Launcher.SettingWindow"
mc:Ignorable="d"
Icon="Images\app.png"
@@ -36,7 +37,7 @@
-
+
@@ -165,17 +166,21 @@
-
-
+
+
+ Margin="5 0 0 0"/>
-
+
diff --git a/Flow.Launcher/SettingWindow.xaml.cs b/Flow.Launcher/SettingWindow.xaml.cs
index e5583da338b..a922b4d67b4 100644
--- a/Flow.Launcher/SettingWindow.xaml.cs
+++ b/Flow.Launcher/SettingWindow.xaml.cs
@@ -206,7 +206,16 @@ private void OnPluginToggled(object sender, RoutedEventArgs e)
{
var id = viewModel.SelectedPlugin.PluginPair.Metadata.ID;
// used to sync the current status from the plugin manager into the setting to keep consistency after save
- settings.PluginSettings.Plugins[id].Disabled = viewModel.SelectedPlugin.PluginPair.Metadata.Disabled;
+ settings.PluginSettings.Plugins[id].Disabled = viewModel.SelectedPlugin.PluginPair.Metadata.Disabled;
+ }
+
+ private void OnPluginPriorityClick(object sender, MouseButtonEventArgs e)
+ {
+ if (e.ChangedButton == MouseButton.Left)
+ {
+ PriorityChangeWindow priorityChangeWindow = new PriorityChangeWindow(viewModel.SelectedPlugin.PluginPair.Metadata.ID, settings, viewModel.SelectedPlugin);
+ priorityChangeWindow.ShowDialog();
+ }
}
private void OnPluginActionKeywordsClick(object sender, MouseButtonEventArgs e)
@@ -281,5 +290,6 @@ private void OpenPluginFolder(object sender, RoutedEventArgs e)
{
FilesFolders.OpenPath(Path.Combine(DataLocation.DataDirectory(), Constant.Themes));
}
+
}
-}
+}
\ No newline at end of file
diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs
index de3bcaa2248..2b21036059d 100644
--- a/Flow.Launcher/Storage/QueryHistory.cs
+++ b/Flow.Launcher/Storage/QueryHistory.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using Newtonsoft.Json;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Storage
diff --git a/Flow.Launcher/Storage/TopMostRecord.cs b/Flow.Launcher/Storage/TopMostRecord.cs
index c110bdf92a5..c92ef49562a 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 Newtonsoft.Json;
+using System.Text.Json;
+using System.Text.Json.Serialization;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Storage
@@ -8,21 +9,24 @@ namespace Flow.Launcher.Storage
// todo this class is not thread safe.... but used from multiple threads.
public class TopMostRecord
{
- [JsonProperty]
- 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)
@@ -54,5 +58,12 @@ public class Record
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 1fda04e9b1e..bc7a2da73ce 100644
--- a/Flow.Launcher/Storage/UserSelectedRecord.cs
+++ b/Flow.Launcher/Storage/UserSelectedRecord.cs
@@ -1,5 +1,5 @@
using System.Collections.Generic;
-using Newtonsoft.Json;
+using System.Text.Json.Serialization;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Plugin;
@@ -7,15 +7,27 @@ namespace Flow.Launcher.Storage
{
public class UserSelectedRecord
{
- [JsonProperty]
- 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 00000000000..5c615d50095
--- /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 1c1f2f9ec29..6a130bb39ec 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 16a1db274aa..1fbaa959aa7 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 00000000000..2253b341015
--- /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 7a3aa9f2f7f..05dbb3a8b8b 100644
--- a/Flow.Launcher/ViewModel/MainViewModel.cs
+++ b/Flow.Launcher/ViewModel/MainViewModel.cs
@@ -1,10 +1,9 @@
using System;
using System.Collections.Generic;
-using System.ComponentModel;
-using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using System.Threading.Tasks.Dataflow;
using System.Windows;
using System.Windows.Input;
using NHotkey;
@@ -19,8 +18,7 @@
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;
namespace Flow.Launcher.ViewModel
{
@@ -48,6 +46,9 @@ public class MainViewModel : BaseModel, ISavable
private readonly Internationalization _translator = InternationalizationManager.Instance;
+ private BufferBlock _resultsUpdateQueue;
+ private Task _resultsViewUpdateTask;
+
#endregion
#region Constructor
@@ -74,6 +75,7 @@ public MainViewModel(Settings settings)
_selectedResults = Results;
InitializeKeyCommands();
+ RegisterViewUpdate();
RegisterResultsUpdatedEvent();
SetHotkey(_settings.Hotkey, OnHotkey);
@@ -81,6 +83,44 @@ public MainViewModel(Settings settings)
SetOpenResultModifiers();
}
+ 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 RegisterResultsUpdatedEvent()
{
foreach (var pair in PluginManager.GetPluginsForInterface())
@@ -88,11 +128,11 @@ private void RegisterResultsUpdatedEvent()
var plugin = (IResultUpdated)pair.Plugin;
plugin.ResultsUpdated += (s, e) =>
{
- Task.Run(() =>
+ if (e.Query.RawQuery == QueryText) // TODO: allow cancellation
{
PluginManager.UpdatePluginMetadata(e.Results, pair.Metadata, e.Query);
- UpdateResultView(e.Results, pair.Metadata, e.Query);
- }, _updateToken);
+ _resultsUpdateQueue.Post(new ResultsForUpdate(e.Results, pair.Metadata, e.Query, _updateToken));
+ }
};
}
}
@@ -112,25 +152,13 @@ private void InitializeKeyCommands()
}
});
- 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());
@@ -208,9 +236,10 @@ private void InitializeKeyCommands()
public ResultsViewModel History { get; private set; }
private string _queryText;
+
public string QueryText
{
- get { return _queryText; }
+ get => _queryText;
set
{
_queryText = value;
@@ -228,10 +257,12 @@ public void ChangeQueryText(string queryText)
QueryTextCursorMovedToEnd = true;
QueryText = queryText;
}
+
public bool LastQuerySelected { get; set; }
public bool QueryTextCursorMovedToEnd { get; set; }
private ResultsViewModel _selectedResults;
+
private ResultsViewModel SelectedResults
{
get { return _selectedResults; }
@@ -263,6 +294,7 @@ private ResultsViewModel SelectedResults
QueryText = string.Empty;
}
}
+
_selectedResults.Visbility = Visibility.Visible;
}
}
@@ -284,7 +316,7 @@ private ResultsViewModel SelectedResults
public string OpenResultCommandModifiers { get; private set; }
- public ImageSource Image => ImageLoader.Load(Constant.QueryTextBoxIconImagePath);
+ public string Image => Constant.QueryTextBoxIconImagePath;
#endregion
@@ -322,9 +354,20 @@ private void QueryContextMenu()
{
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
@@ -378,90 +421,128 @@ private void QueryHistory()
private void QueryResults()
{
- if (!string.IsNullOrEmpty(QueryText))
- {
- _updateSource?.Cancel();
- 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);
- if (query != null)
+ _updateSource?.Cancel();
+
+ if (string.IsNullOrWhiteSpace(QueryText))
+ {
+ Results.Clear();
+ Results.Visbility = Visibility.Collapsed;
+ return;
+ }
+
+ _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)
- {
- var results = PluginManager.QueryForPlugin(plugin, query);
- UpdateResultView(results, plugin.Metadata, query);
- }
- });
- }
- catch (OperationCanceledException)
- {
- // nothing to do here
- }
-
-
- // 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;
+ tasks[i] = QueryTask(plugins[i]);
+ }
+ else
+ {
+ tasks[i] = Task.CompletedTask; // Avoid Null
+ }
}
- }, currentCancellationToken);
- }
- }
- else
- {
- Results.Clear();
- Results.Visbility = Visibility.Collapsed;
- }
+
+ // 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)
+ {
+ // 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);
}
}
}
@@ -499,6 +580,7 @@ private Result ContextMenuTopMost(Result result)
}
};
}
+
return menu;
}
@@ -538,12 +620,12 @@ private bool ContextMenuSelected()
return selected;
}
-
private bool HistorySelected()
{
var selected = SelectedResults == History;
return selected;
}
+
#region Hotkey
private void SetHotkey(string hotkeyStr, EventHandler action)
@@ -562,7 +644,8 @@ private void SetHotkey(HotkeyModel hotkey, EventHandler action)
catch (Exception)
{
string errorMsg =
- string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr);
+ string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"),
+ hotkeyStr);
MessageBox.Show(errorMsg);
}
}
@@ -612,7 +695,6 @@ private void OnHotkey(object sender, HotkeyEventArgs e)
{
if (!ShouldIgnoreHotkeys())
{
-
if (_settings.LastQueryMode == LastQueryMode.Empty)
{
ChangeQueryText(string.Empty);
@@ -666,29 +748,47 @@ public void Save()
///
/// 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))
- {
- result.Score = int.MaxValue;
- }
- else
- {
- result.Score += _userSelectedRecord.GetSelectedCount(result) * 5;
- }
+ // Don't know why sometimes even resultsForUpdates is empty, the method won't return;
+ token = resultsForUpdates.Select(r => r.Token).Distinct().SingleOrDefault();
}
-
- if (originQuery.RawQuery == _lastQuery.RawQuery)
+#if DEBUG
+ catch
+ {
+ throw new ArgumentException("Unacceptable token");
+ }
+#else
+ catch
{
- Results.AddResults(list, metadata.ID);
+ token = default;
}
+#endif
- if (Results.Visbility != Visibility.Visible && list.Count > 0)
+
+ foreach (var metaResults in resultsForUpdates)
{
- Results.Visbility = Visibility.Visible;
+ foreach (var result in metaResults.Results)
+ {
+ if (_topMostRecord.IsTopMost(result))
+ {
+ result.Score = int.MaxValue;
+ }
+ else
+ {
+ var priorityScore = metaResults.Metadata.Priority * 150;
+ result.Score += _userSelectedRecord.GetSelectedCount(result) * 5 + priorityScore;
+ }
+ }
}
+
+ Results.AddResults(resultsForUpdates, token);
}
#endregion
diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs
index eb7e0054d10..7c8814b4188 100644
--- a/Flow.Launcher/ViewModel/PluginViewModel.cs
+++ b/Flow.Launcher/ViewModel/PluginViewModel.cs
@@ -26,6 +26,7 @@ public bool PluginState
public string InitilizaTime => PluginPair.Metadata.InitTime.ToString() + "ms";
public string QueryTime => PluginPair.Metadata.AvgQueryTime + "ms";
public string ActionKeywordsText => string.Join(Query.ActionKeywordSeperater, PluginPair.Metadata.ActionKeywords);
+ public int Priority => PluginPair.Metadata.Priority;
public void ChangeActionKeyword(string newActionKeyword, string oldActionKeyword)
{
@@ -34,6 +35,12 @@ public void ChangeActionKeyword(string newActionKeyword, string oldActionKeyword
OnPropertyChanged(nameof(ActionKeywordsText));
}
+ public void ChangePriority(int newPriority)
+ {
+ PluginPair.Metadata.Priority = newPriority;
+ OnPropertyChanged(nameof(Priority));
+ }
+
public bool IsActionKeywordRegistered(string newActionKeyword) => PluginManager.ActionKeywordRegistered(newActionKeyword);
}
}
diff --git a/Flow.Launcher/ViewModel/ResultViewModel.cs b/Flow.Launcher/ViewModel/ResultViewModel.cs
index 00a0e1ae562..c91bbb1074f 100644
--- a/Flow.Launcher/ViewModel/ResultViewModel.cs
+++ b/Flow.Launcher/ViewModel/ResultViewModel.cs
@@ -2,7 +2,6 @@
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;
@@ -12,9 +11,9 @@ namespace Flow.Launcher.ViewModel
{
public class ResultViewModel : BaseModel
{
- public class LazyAsync : Lazy>
+ public class LazyAsync : Lazy>
{
- private T defaultValue;
+ private readonly T defaultValue;
private readonly Action _updateCallback;
public new T Value
@@ -23,21 +22,27 @@ public class LazyAsync : Lazy>
{
if (!IsValueCreated)
{
- base.Value.ContinueWith(_ =>
- {
- _updateCallback();
- });
+ _ = Exercute(); // manually use callback strategy
return defaultValue;
}
-
- if (!base.Value.IsCompleted || base.Value.IsFaulted)
+
+ if (!base.Value.IsCompletedSuccessfully)
return defaultValue;
return base.Value.Result;
+
+ // If none of the variables captured by the local function are captured by other lambdas,
+ // the compiler can avoid heap allocations.
+ async ValueTask Exercute()
+ {
+ await base.Value.ConfigureAwait(false);
+ _updateCallback();
+ }
+
}
}
- public LazyAsync(Func> factory, T defaultValue, Action updateCallback) : base(factory)
+ public LazyAsync(Func> factory, T defaultValue, Action updateCallback) : base(factory)
{
if (defaultValue != null)
{
@@ -55,13 +60,13 @@ public ResultViewModel(Result result, Settings settings)
Result = result;
Image = new LazyAsync(
- SetImage,
+ SetImage,
ImageLoader.DefaultImage,
() =>
{
OnPropertyChanged(nameof(Image));
});
- }
+ }
Settings = settings;
}
@@ -82,7 +87,7 @@ public ResultViewModel(Result result, Settings settings)
public LazyAsync Image { get; set; }
- private async Task SetImage()
+ private async ValueTask SetImage()
{
var imagePath = Result.IcoPath;
if (string.IsNullOrEmpty(imagePath) && Result.Icon != null)
@@ -94,19 +99,15 @@ private async Task SetImage()
catch (Exception e)
{
Log.Exception($"|ResultViewModel.Image|IcoPath is empty and exception when calling Icon() for result <{Result.Title}> of plugin <{Result.PluginDirectory}>", e);
- imagePath = Constant.MissingImgIcon;
+ return ImageLoader.DefaultImage;
}
}
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 00000000000..be48f53c16b
--- /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 d3085418062..feab3a7513d 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 @@ public class ResultsViewModel : BaseModel
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 @@ public void SelectFirstResult()
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,73 @@ public void RemoveResultsFor(PluginMetadata metadata)
///
public void AddResults(List newRawResults, string resultId)
{
- lock (_addResultsLock)
- {
- var newResults = NewResults(newRawResults, resultId);
+ var newResults = NewResults(newRawResults, resultId);
+
+ UpdateResults(newResults);
+ }
+ ///
+ /// 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;
+
+ UpdateResults(newResults, token);
+ }
+
+ private void UpdateResults(List newResults, CancellationToken token = default)
+ {
+ lock (_collectionLock)
+ {
// update UI in one run, so it can avoid UI flickering
- Results.Update(newResults);
+ Results.Update(newResults, token);
+ if (Results.Any())
+ SelectedItem = Results[0];
+ }
- if (Results.Count > 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();
- var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings)).ToList();
- var oldResults = results.Where(r => r.Result.PluginID == resultId).ToList();
+ if (newRawResults.Count == 0)
+ return Results.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();
+ var results = Results as IEnumerable;
- // remove result of relative complement of B in A
- foreach (var result in oldResults.Except(sameResults))
- {
- results.Remove(result);
- }
+ var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings));
- // 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];
-
- oldResult.Result.Score = newScore;
- oldResult.Result.OriginQuery = newResult.Result.OriginQuery;
+ return results.Where(r => r.Result.PluginID != resultId)
+ .Concat(newResults)
+ .OrderByDescending(r => r.Result.Score)
+ .ToList();
+ }
- results.RemoveAt(oldIndex);
- int newIndex = InsertIndexOf(newScore, results);
- results.Insert(newIndex, oldResult);
- }
- }
+ private List NewResults(IEnumerable resultsForUpdates)
+ {
+ if (!resultsForUpdates.Any())
+ return Results.ToList();
- // 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);
- }
+ var results = Results as IEnumerable;
- 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
@@ -232,60 +240,78 @@ private static void FormattedTextPropertyChanged(DependencyObject d, DependencyP
}
#endregion
- public class ResultCollection : ObservableCollection
+ public class ResultCollection : List, INotifyCollectionChanged
{
+ private long editTime = 0;
+
+ private CancellationToken _token;
+
+ public event NotifyCollectionChangedEventHandler CollectionChanged;
+
- public void RemoveAll(Predicate predicate)
+ protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
- CheckReentrancy();
+ CollectionChanged?.Invoke(this, e);
+ }
+
+ public void BulkAddAll(List resultViews)
+ {
+ AddRange(resultViews);
+
+ // can return because the list will be cleared next time updated, which include a reset event
+ if (_token.IsCancellationRequested)
+ return;
- for (int i = Count - 1; i >= 0; i--)
+ // manually update event
+ // wpf use directx / double buffered already, so just reset all won't cause ui flickering
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+ }
+ private void AddAll(List Items)
+ {
+ for (int i = 0; i < Items.Count; i++)
{
- if (predicate(this[i]))
- {
- RemoveAt(i);
- }
+ var item = Items[i];
+ if (_token.IsCancellationRequested)
+ return;
+ Add(item);
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, i));
}
}
+ public void RemoveAll(int Capacity = 512)
+ {
+ Clear();
+ if (this.Capacity > 8000 && Capacity < this.Capacity)
+ this.Capacity = Capacity;
+
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+ }
///
/// 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) RemoveAll(newItems.Count);
+ AddAll(newItems);
+ editTime++;
+ return;
}
else
{
- for (int i = oldCount - 1; i >= newCount; i--)
+ Clear();
+ BulkAddAll(newItems);
+ if (Capacity > 8000 && newItems.Count < 3000)
{
- RemoveAt(i);
+ Capacity = newItems.Count;
}
+ editTime++;
}
}
}
diff --git a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs
index c122f8037d1..98685dc1b72 100644
--- a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs
+++ b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs
@@ -17,6 +17,7 @@
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
+using Flow.Launcher.Plugin.SharedModels;
namespace Flow.Launcher.ViewModel
{
@@ -88,6 +89,7 @@ public void Save()
var id = vm.PluginPair.Metadata.ID;
Settings.PluginSettings.Plugins[id].Disabled = vm.PluginPair.Metadata.Disabled;
+ Settings.PluginSettings.Plugins[id].Priority = vm.Priority;
}
PluginManager.Save();
@@ -152,7 +154,7 @@ public List QuerySearchPrecisionStrings
{
var precisionStrings = new List();
- var enumList = Enum.GetValues(typeof(StringMatcher.SearchPrecisionScore)).Cast().ToList();
+ var enumList = Enum.GetValues(typeof(SearchPrecisionScore)).Cast().ToList();
enumList.ForEach(x => precisionStrings.Add(x.ToString()));
@@ -438,7 +440,7 @@ public FamilyTypeface SelectedResultFontFaces
}
}
- public ImageSource ThemeImage => ImageLoader.Load(Constant.QueryTextBoxIconImagePath);
+ public string ThemeImage => Constant.QueryTextBoxIconImagePath;
#endregion
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/Bookmarks.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/Bookmarks.cs
index c7013aa677b..60c4a0ee660 100644
--- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/Bookmarks.cs
+++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/Bookmarks.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using Flow.Launcher.Infrastructure;
+using Flow.Launcher.Plugin.SharedModels;
namespace Flow.Launcher.Plugin.BrowserBookmark.Commands
{
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
index 85b745a6b83..d2a8736a638 100644
--- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
+++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
@@ -40,14 +40,6 @@
-
- Always
-
-
- Designer
- MSBuild:Compile
- PreserveNewest
-
Always
@@ -60,13 +52,16 @@
-
+
-
+
MSBuild:Compile
Designer
PreserveNewest
+
+ PreserveNewest
+
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json
index de4f3849bfb..b0c3d2e29b0 100644
--- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json
@@ -4,7 +4,7 @@
"Name": "Browser Bookmarks",
"Description": "Search your browser bookmarks",
"Author": "qianlifeng, Ioannis G.",
- "Version": "1.3.1",
+ "Version": "1.3.2",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.BrowserBookmark.dll",
diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj b/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj
index 9e1fefdb30d..1090926fc8e 100644
--- a/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj
+++ b/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj
@@ -11,6 +11,7 @@
true
false
false
+ en
@@ -43,57 +44,14 @@
-
-
-
- PreserveNewest
-
-
-
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
-
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
-
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
-
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
-
+
-
+
MSBuild:Compile
Designer
PreserveNewest
-
-
-
-
- MSBuild:Compile
- Designer
+
PreserveNewest
diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs
index 949911229e6..5b23ceacc30 100644
--- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs
@@ -91,7 +91,7 @@ public List Query(Query query)
};
}
}
- catch
+ catch (Exception)
{
// ignored
}
diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json b/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json
index 709757d1a09..7d9ca58d58f 100644
--- a/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json
@@ -4,7 +4,7 @@
"Name": "Calculator",
"Description": "Provide mathematical calculations.(Try 5*3-2 in Flow Launcher)",
"Author": "cxfksword",
- "Version": "1.1.3",
+ "Version": "1.1.4",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Caculator.dll",
diff --git a/Plugins/Flow.Launcher.Plugin.ControlPanel/ControlPanelList.cs b/Plugins/Flow.Launcher.Plugin.ControlPanel/ControlPanelList.cs
index fdcffb0b3c1..70afda53673 100644
--- a/Plugins/Flow.Launcher.Plugin.ControlPanel/ControlPanelList.cs
+++ b/Plugins/Flow.Launcher.Plugin.ControlPanel/ControlPanelList.cs
@@ -38,7 +38,7 @@ static extern IntPtr LoadImage(IntPtr hinst, IntPtr lpszName, uint uType,
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.ControlPanel/Flow.Launcher.Plugin.ControlPanel.csproj b/Plugins/Flow.Launcher.Plugin.ControlPanel/Flow.Launcher.Plugin.ControlPanel.csproj
index 69973763435..06969a1354e 100644
--- a/Plugins/Flow.Launcher.Plugin.ControlPanel/Flow.Launcher.Plugin.ControlPanel.csproj
+++ b/Plugins/Flow.Launcher.Plugin.ControlPanel/Flow.Launcher.Plugin.ControlPanel.csproj
@@ -45,53 +45,10 @@
-
- PreserveNewest
-
-
-
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
-
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
-
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
-
-
-
- MSBuild:Compile
- Designer
+
PreserveNewest
-
-
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
-
-
-
+
MSBuild:Compile
Designer
PreserveNewest
diff --git a/Plugins/Flow.Launcher.Plugin.ControlPanel/plugin.json b/Plugins/Flow.Launcher.Plugin.ControlPanel/plugin.json
index 4f552a0143b..23f35e9ac1c 100644
--- a/Plugins/Flow.Launcher.Plugin.ControlPanel/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.ControlPanel/plugin.json
@@ -4,7 +4,7 @@
"Name": "Control Panel",
"Description": "Search within the Control Panel.",
"Author": "CoenraadS",
- "Version": "1.1.1",
+ "Version": "1.1.2",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.ControlPanel.dll",
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs
index c9a0b730373..21eb844b449 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs
@@ -7,12 +7,13 @@
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Plugin.Explorer.Search;
-using Flow.Launcher.Plugin.Explorer.Search.FolderLinks;
+using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using System.Linq;
using MessageBox = System.Windows.Forms.MessageBox;
using MessageBoxIcon = System.Windows.Forms.MessageBoxIcon;
using MessageBoxButton = System.Windows.Forms.MessageBoxButtons;
using DialogResult = System.Windows.Forms.DialogResult;
+using Flow.Launcher.Plugin.Explorer.ViewModels;
namespace Flow.Launcher.Plugin.Explorer
{
@@ -22,10 +23,13 @@ internal class ContextMenu : IContextMenu
private Settings Settings { get; set; }
- public ContextMenu(PluginInitContext context, Settings settings)
+ private SettingsViewModel ViewModel { get; set; }
+
+ public ContextMenu(PluginInitContext context, Settings settings, SettingsViewModel vm)
{
Context = context;
Settings = settings;
+ ViewModel = vm;
}
public List LoadContextMenus(Result selectedResult)
@@ -50,6 +54,58 @@ public List LoadContextMenus(Result selectedResult)
var icoPath = (record.Type == ResultType.File) ? Constants.FileImagePath : Constants.FolderImagePath;
var fileOrFolder = (record.Type == ResultType.File) ? "file" : "folder";
+
+ if (!Settings.QuickAccessLinks.Any(x => x.Path == record.FullPath))
+ {
+ contextMenus.Add(new Result
+ {
+ Title = Context.API.GetTranslation("plugin_explorer_add_to_quickaccess_title"),
+ SubTitle = string.Format(Context.API.GetTranslation("plugin_explorer_add_to_quickaccess_subtitle"), fileOrFolder),
+ Action = (context) =>
+ {
+ Settings.QuickAccessLinks.Add(new AccessLink { Path = record.FullPath, Type = record.Type });
+
+ Context.API.ShowMsg(Context.API.GetTranslation("plugin_explorer_addfilefoldersuccess"),
+ string.Format(
+ Context.API.GetTranslation("plugin_explorer_addfilefoldersuccess_detail"),
+ fileOrFolder),
+ Constants.ExplorerIconImageFullPath);
+
+ ViewModel.Save();
+
+ return true;
+ },
+ SubTitleToolTip = Context.API.GetTranslation("plugin_explorer_contextmenu_titletooltip"),
+ TitleToolTip = Context.API.GetTranslation("plugin_explorer_contextmenu_titletooltip"),
+ IcoPath = Constants.QuickAccessImagePath
+ });
+ }
+ else
+ {
+ contextMenus.Add(new Result
+ {
+ Title = Context.API.GetTranslation("plugin_explorer_remove_from_quickaccess_title"),
+ SubTitle = string.Format(Context.API.GetTranslation("plugin_explorer_remove_from_quickaccess_subtitle"), fileOrFolder),
+ Action = (context) =>
+ {
+ Settings.QuickAccessLinks.Remove(Settings.QuickAccessLinks.FirstOrDefault(x => x.Path == record.FullPath));
+
+ Context.API.ShowMsg(Context.API.GetTranslation("plugin_explorer_removefilefoldersuccess"),
+ string.Format(
+ Context.API.GetTranslation("plugin_explorer_removefilefoldersuccess_detail"),
+ fileOrFolder),
+ Constants.ExplorerIconImageFullPath);
+
+ ViewModel.Save();
+
+ return true;
+ },
+ SubTitleToolTip = Context.API.GetTranslation("plugin_explorer_contextmenu_remove_titletooltip"),
+ TitleToolTip = Context.API.GetTranslation("plugin_explorer_contextmenu_remove_titletooltip"),
+ IcoPath = Constants.RemoveQuickAccessImagePath
+ });
+ }
+
contextMenus.Add(new Result
{
Title = Context.API.GetTranslation("plugin_explorer_copypath"),
@@ -228,7 +284,7 @@ private Result CreateAddToIndexSearchExclusionListResult(SearchResult record)
Action = _ =>
{
if(!Settings.IndexSearchExcludedSubdirectoryPaths.Any(x => x.Path == record.FullPath))
- Settings.IndexSearchExcludedSubdirectoryPaths.Add(new FolderLink { Path = record.FullPath });
+ Settings.IndexSearchExcludedSubdirectoryPaths.Add(new AccessLink { Path = record.FullPath });
Task.Run(() =>
{
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj
index a1a08843a50..9f0b46d9385 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj
@@ -7,6 +7,7 @@
true
true
false
+ en
@@ -26,73 +27,10 @@
-
- PreserveNewest
-
-
-
- PreserveNewest
-
-
-
- PreserveNewest
-
-
-
- PreserveNewest
-
-
-
- Always
-
-
-
- PreserveNewest
-
-
-
- PreserveNewest
-
-
-
- PreserveNewest
-
-
-
- PreserveNewest
-
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
-
- MSBuild:Compile
- Designer
+
PreserveNewest
-
-
+
MSBuild:Compile
Designer
PreserveNewest
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/quickaccess.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/quickaccess.png
new file mode 100644
index 00000000000..470a6782fe7
Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/quickaccess.png differ
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/removequickaccess.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/removequickaccess.png
new file mode 100644
index 00000000000..fbfb0b960ee
Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/removequickaccess.png differ
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml
index 2fb16e0e1a4..9ba0da3f6f5 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml
@@ -16,7 +16,7 @@
Edit
Add
Customise Action Keywords
- Quick Folder Access Paths
+ Quick Access Links
Index Search Excluded Paths
Indexing Options
Search Activation:
@@ -42,5 +42,15 @@
Open Windows Indexing Options
Manage indexed files and folders
Failed to open Windows Indexing Options
+ Add to Quick Access
+ Add the current {0} to Quick Access
+ Successfully Added
+ Successfully added to Quick Access
+ Successfully Removed
+ Successfully removed from Quick Access
+ Add to Quick Access so it can be opened with Explorer's Search Activation action keyword
+ Remove from Quick Access
+ Remove from Quick Access
+ Remove the current {0} from Quick Access
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs
index 30a06e882f3..ae7bf57d219 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs
@@ -1,13 +1,17 @@
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Plugin.Explorer.Search;
+using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using Flow.Launcher.Plugin.Explorer.ViewModels;
using Flow.Launcher.Plugin.Explorer.Views;
using System.Collections.Generic;
+using System.Linq;
+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 +21,30 @@ public class Main : ISettingProvider, IPlugin, ISavable, IContextMenu, IPluginI1
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);
+
+ // as at v1.7.0 this is to maintain backwards compatibility, need to be removed afterwards.
+ if (Settings.QuickFolderAccessLinks.Any())
+ {
+ Settings.QuickAccessLinks = Settings.QuickFolderAccessLinks;
+ Settings.QuickFolderAccessLinks = new List();
+ }
+
+ contextMenu = new ContextMenu(Context, Settings, viewModel);
+ searchManager = new SearchManager(Settings, Context);
+ ResultManager.Init(Context);
}
public List LoadContextMenus(Result selectedResult)
@@ -35,9 +52,9 @@ public List LoadContextMenus(Result selectedResult)
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/Constants.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs
index 38939e244a5..78c7c98a5ec 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs
@@ -15,6 +15,8 @@ internal static class Constants
internal const string ExplorerIconImagePath = "Images\\explorer.png";
internal const string DifferentUserIconImagePath = "Images\\user.png";
internal const string IndexingOptionsIconImagePath = "Images\\windowsindexingoptions.png";
+ internal const string QuickAccessImagePath = "Images\\quickaccess.png";
+ internal const string RemoveQuickAccessImagePath = "Images\\removequickaccess.png";
internal const string ToolTipOpenDirectory = "Ctrl + Enter to open the directory";
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs
index 02de0eeaedd..acd960ef149 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs
@@ -4,29 +4,27 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Threading;
namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
{
- public class DirectoryInfoSearch
+ public static class DirectoryInfoSearch
{
- private readonly ResultManager resultManager;
-
- public DirectoryInfoSearch(PluginInitContext context)
- {
- resultManager = new ResultManager(context);
- }
-
- internal List TopLevelDirectorySearch(Query query, string search)
+ internal static List TopLevelDirectorySearch(Query query, string search, CancellationToken token)
{
var criteria = ConstructSearchCriteria(search);
- if (search.LastIndexOf(Constants.AllFilesFolderSearchWildcard) > search.LastIndexOf(Constants.DirectorySeperator))
- return DirectorySearch(SearchOption.AllDirectories, query, search, criteria);
-
- return DirectorySearch(SearchOption.TopDirectoryOnly, query, search, criteria);
+ if (search.LastIndexOf(Constants.AllFilesFolderSearchWildcard) >
+ search.LastIndexOf(Constants.DirectorySeperator))
+ return DirectorySearch(new EnumerationOptions
+ {
+ RecurseSubdirectories = true
+ }, query, search, criteria, token);
+
+ return DirectorySearch(new EnumerationOptions(), query, search, criteria, token); // null will be passed as default
}
- public string ConstructSearchCriteria(string search)
+ public static string ConstructSearchCriteria(string search)
{
string incompleteName = "";
@@ -45,7 +43,8 @@ public string ConstructSearchCriteria(string search)
return incompleteName;
}
- private List DirectorySearch(SearchOption searchOption, Query query, string search, string searchCriteria)
+ private static List DirectorySearch(EnumerationOptions enumerationOption, Query query, string search,
+ string searchCriteria, CancellationToken token)
{
var results = new List();
@@ -57,40 +56,39 @@ private List DirectorySearch(SearchOption searchOption, Query query, str
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, enumerationOption))
{
- if ((fileSystemInfo.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) continue;
-
if (fileSystemInfo is System.IO.DirectoryInfo)
{
- folderList.Add(resultManager.CreateFolderResult(fileSystemInfo.Name, fileSystemInfo.FullName, fileSystemInfo.FullName, query, true, false));
+ folderList.Add(ResultManager.CreateFolderResult(fileSystemInfo.Name, fileSystemInfo.FullName,
+ fileSystemInfo.FullName, query, true, false));
}
else
{
- fileList.Add(resultManager.CreateFileResult(fileSystemInfo.FullName, query, true, false));
+ fileList.Add(ResultManager.CreateFileResult(fileSystemInfo.FullName, query, true, false));
}
+
+ token.ThrowIfCancellationRequested();
}
}
catch (Exception e)
{
- if (e is UnauthorizedAccessException || e is ArgumentException)
- {
- results.Add(new Result { Title = e.Message, Score = 501 });
+ if (!(e is ArgumentException))
+ throw e;
+
+ results.Add(new Result {Title = e.Message, Score = 501});
- return results;
- }
+ return results;
#if DEBUG // Please investigate and handle error from DirectoryInfo search
- throw e;
#else
Log.Exception($"|Flow.Launcher.Plugin.Explorer.DirectoryInfoSearch|Error from performing DirectoryInfoSearch", e);
-#endif
+#endif
}
- // Intial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection.
+ // Initial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection.
return results.Concat(folderList.OrderBy(x => x.Title)).Concat(fileList.OrderBy(x => x.Title)).ToList();
}
}
-}
+}
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs
index 6a870f1496f..1e9815cb97b 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs
@@ -76,7 +76,7 @@ internal static List GetEnvironmentStringPathSuggestions(string querySea
{
var expandedPath = environmentVariables[search];
- results.Add(new ResultManager(context).CreateFolderResult($"%{search}%", expandedPath, expandedPath, query));
+ results.Add(ResultManager.CreateFolderResult($"%{search}%", expandedPath, expandedPath, query));
return results;
}
@@ -95,7 +95,7 @@ internal static List GetEnvironmentStringPathSuggestions(string querySea
{
if (p.Key.StartsWith(search, StringComparison.InvariantCultureIgnoreCase))
{
- results.Add(new ResultManager(context).CreateFolderResult($"%{p.Key}%", p.Value, p.Value, query));
+ results.Add(ResultManager.CreateFolderResult($"%{p.Key}%", p.Value, p.Value, query));
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/QuickFolderAccess.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/QuickFolderAccess.cs
deleted file mode 100644
index 8bd19956eab..00000000000
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/QuickFolderAccess.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace Flow.Launcher.Plugin.Explorer.Search.FolderLinks
-{
- public class QuickFolderAccess
- {
- internal List FolderListMatched(Query query, List folderLinks, PluginInitContext context)
- {
- if (string.IsNullOrEmpty(query.Search))
- return new List();
-
- string search = query.Search.ToLower();
-
- var queriedFolderLinks = folderLinks.Where(x => x.Nickname.StartsWith(search, StringComparison.OrdinalIgnoreCase));
-
- return queriedFolderLinks.Select(item =>
- new ResultManager(context)
- .CreateFolderResult(item.Nickname, item.Path, item.Path, query))
- .ToList();
- }
-
- internal List FolderListAll(Query query, List folderLinks, PluginInitContext context)
- => folderLinks
- .Select(item =>
- new ResultManager(context).CreateFolderResult(item.Nickname, item.Path, item.Path, query))
- .ToList();
- }
-}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/FolderLink.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/AccessLink.cs
similarity index 71%
rename from Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/FolderLink.cs
rename to Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/AccessLink.cs
index 379b5848fa3..f623cc2ca17 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/FolderLink.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/AccessLink.cs
@@ -1,15 +1,16 @@
-using Newtonsoft.Json;
-using System;
+using System;
using System.Linq;
+using System.Text.Json.Serialization;
-namespace Flow.Launcher.Plugin.Explorer.Search.FolderLinks
+namespace Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks
{
- [JsonObject(MemberSerialization.OptIn)]
- public class FolderLink
+ public class AccessLink
{
- [JsonProperty]
public string Path { get; set; }
+ public ResultType Type { get; set; } = ResultType.Folder;
+
+ [JsonIgnore]
public string Nickname
{
get
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/QuickAccess.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/QuickAccess.cs
new file mode 100644
index 00000000000..d71e9ab49a8
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/QuickAccess.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks
+{
+ internal static class QuickAccess
+ {
+ internal static List AccessLinkListMatched(Query query, List accessLinks)
+ {
+ if (string.IsNullOrEmpty(query.Search))
+ return new List();
+
+ string search = query.Search.ToLower();
+
+ var queriedAccessLinks =
+ accessLinks
+ .Where(x => x.Nickname.StartsWith(search, StringComparison.OrdinalIgnoreCase))
+ .OrderBy(x => x.Type)
+ .ThenBy(x => x.Nickname);
+
+ return queriedAccessLinks.Select(l => l.Type switch
+ {
+ ResultType.Folder => ResultManager.CreateFolderResult(l.Nickname, l.Path, l.Path, query),
+ ResultType.File => ResultManager.CreateFileResult(l.Path, query),
+ _ => throw new ArgumentOutOfRangeException()
+
+ }).ToList();
+ }
+
+ internal static List AccessLinkListAll(Query query, List accessLinks)
+ => accessLinks
+ .OrderBy(x => x.Type)
+ .ThenBy(x => x.Nickname)
+ .Select(l => l.Type switch
+ {
+ ResultType.Folder => ResultManager.CreateFolderResult(l.Nickname, l.Path, l.Path, query),
+ ResultType.File => ResultManager.CreateFileResult(l.Path, query),
+ _ => throw new ArgumentOutOfRangeException()
+
+ }).ToList();
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs
index 6a336c59a89..600b573cfe5 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs
@@ -4,19 +4,21 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
+using System.Runtime.CompilerServices;
using System.Windows;
namespace Flow.Launcher.Plugin.Explorer.Search
{
- public class ResultManager
+ public static class ResultManager
{
- private readonly PluginInitContext context;
+ private static PluginInitContext Context;
- public ResultManager(PluginInitContext context)
+ public static void Init(PluginInitContext context)
{
- this.context = context;
+ Context = context;
}
- internal Result CreateFolderResult(string title, string subtitle, string path, Query query, bool showIndexState = false, bool windowsIndexed = false)
+
+ internal static Result CreateFolderResult(string title, string subtitle, string path, Query query, bool showIndexState = false, bool windowsIndexed = false)
{
return new Result
{
@@ -41,7 +43,7 @@ internal Result CreateFolderResult(string title, string subtitle, string path, Q
}
string changeTo = path.EndsWith(Constants.DirectorySeperator) ? path : path + Constants.DirectorySeperator;
- context.API.ChangeQuery(string.IsNullOrEmpty(query.ActionKeyword) ?
+ Context.API.ChangeQuery(string.IsNullOrEmpty(query.ActionKeyword) ?
changeTo :
query.ActionKeyword + " " + changeTo);
return false;
@@ -52,7 +54,7 @@ internal Result CreateFolderResult(string title, string subtitle, string path, Q
};
}
- internal Result CreateOpenCurrentFolderResult(string path, bool windowsIndexed = false)
+ internal static Result CreateOpenCurrentFolderResult(string path, bool windowsIndexed = false)
{
var retrievedDirectoryPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path);
@@ -94,7 +96,7 @@ internal Result CreateOpenCurrentFolderResult(string path, bool windowsIndexed =
};
}
- internal Result CreateFileResult(string filePath, Query query, bool showIndexState = false, bool windowsIndexed = false)
+ internal static Result CreateFileResult(string filePath, Query query, bool showIndexState = false, bool windowsIndexed = false)
{
var result = new Result
{
@@ -140,7 +142,7 @@ internal class SearchResult
public bool ShowIndexState { get; set; }
}
- internal enum ResultType
+ public enum ResultType
{
Volume,
Folder,
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs
index 5b50b7fada6..2af09bf2cbb 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs
@@ -1,10 +1,12 @@
using Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo;
-using Flow.Launcher.Plugin.Explorer.Search.FolderLinks;
+using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using Flow.Launcher.Plugin.Explorer.Search.WindowsIndex;
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
{
@@ -12,41 +14,31 @@ public class SearchManager
{
private readonly PluginInitContext context;
- private readonly IndexSearch indexSearch;
-
- private readonly QuickFolderAccess quickFolderAccess = new QuickFolderAccess();
-
- private readonly ResultManager resultManager;
-
private readonly Settings settings;
public SearchManager(Settings settings, PluginInitContext context)
{
this.context = context;
- indexSearch = new IndexSearch(context);
- resultManager = new ResultManager(context);
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);
+ if (string.IsNullOrEmpty(query.Search))
+ return QuickAccess.AccessLinkListAll(query, settings.QuickAccessLinks);
- var quickFolderLinks = quickFolderAccess.FolderListMatched(query, settings.QuickFolderAccessLinks, context);
+ var quickaccessLinks = QuickAccess.AccessLinkListMatched(query, settings.QuickAccessLinks);
- if (quickFolderLinks.Count > 0)
- results.AddRange(quickFolderLinks);
+ if (quickaccessLinks.Count > 0)
+ results.AddRange(quickaccessLinks);
var isEnvironmentVariable = EnvironmentVariables.IsEnvironmentVariableSearch(querySearch);
@@ -54,11 +46,11 @@ internal List Search(Query query)
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 +60,42 @@ internal List Search(Query query)
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,
- DirectoryInfoClassSearch,
- useIndexSearch,
- query,
- locationPath));
+ results.Add(ResultManager.CreateOpenCurrentFolderResult(locationPath, useIndexSearch));
+
+ token.ThrowIfCancellationRequested();
+
+ var directoryResult = await TopLevelDirectorySearchBehaviourAsync(WindowsIndexTopLevelFolderSearchAsync,
+ DirectoryInfoClassSearch,
+ useIndexSearch,
+ query,
+ locationPath,
+ token).ConfigureAwait(false);
+
+ token.ThrowIfCancellationRequested();
+
+ results.AddRange(directoryResult);
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)
@@ -102,44 +103,45 @@ public bool IsFileContentSearch(string actionKeyword)
return actionKeyword == settings.FileContentSearchActionKeyword;
}
- private List DirectoryInfoClassSearch(Query query, string querySearch)
+ private List DirectoryInfoClassSearch(Query query, string querySearch, CancellationToken token)
{
- var directoryInfoSearch = new DirectoryInfoSearch(context);
-
- return directoryInfoSearch.TopLevelDirectorySearch(query, querySearch);
+ return DirectoryInfoSearch.TopLevelDirectorySearch(query, querySearch, token);
}
- public List TopLevelDirectorySearchBehaviour(
- Func> windowsIndexSearch,
- Func> directoryInfoClassSearch,
+ 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 directoryInfoClassSearch(query, querySearchString, token);
- 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)
@@ -154,7 +156,7 @@ private bool UseWindowsIndexForDirectorySearch(string locationPath)
.StartsWith(x.Path, StringComparison.OrdinalIgnoreCase)))
return false;
- return indexSearch.PathIsIndexed(pathToDirectory);
+ return IndexSearch.PathIsIndexed(pathToDirectory);
}
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs
index 4f9325c7754..b1e1d762286 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs
@@ -1,82 +1,71 @@
-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
+ internal static 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 = @"^[\/\\\$\%_]+$";
-
- internal IndexSearch(PluginInitContext context)
- {
- resultManager = new ResultManager(context);
- }
+ private const string reservedStringPattern = @"^[`\@\#\^,\&\/\\\$\%_]+$";
- internal List ExecuteWindowsIndexSearch(string indexQueryString, string connectionString, Query query)
+ internal async static 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))
- {
- conn.Open();
+ using var conn = new OleDbConnection(connectionString);
+ await conn.OpenAsync(token);
+ token.ThrowIfCancellationRequested();
- using (command = new OleDbCommand(indexQueryString, conn))
+ 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)
+ {
+ 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)
+ // # 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;
+
+ if (dataReaderResults.GetString(2) == "Directory")
{
- 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;
-
- 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));
- }
- }
- }
+ 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,32 +76,34 @@ internal List ExecuteWindowsIndexSearch(string indexQueryString, string
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 static 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)
+ internal static bool PathIsIndexed(string path)
{
var csm = new CSearchManager();
var indexManager = csm.GetCatalog("SystemIndex").GetCrawlScopeManager();
return indexManager.IncludedInCrawlScope(path) > 0;
}
- private void LogException(string message, Exception e)
+ private static void LogException(string message, Exception e)
{
#if DEBUG // Please investigate and handle error from index search
throw e;
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs
index 5718fdb0a9c..20e85bbb598 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 @@ internal CSearchQueryHelper CreateQueryHelper()
// 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 @@ private string QueryWhereRestrictionsFromLocationPath(string path, string search
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 @@ public string QueryForTopLevelDirectorySearch(string path)
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 @@ public string QueryForTopLevelDirectorySearch(string path)
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 @@ public string QueryForFileContentSearch(string userSearchString)
{
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 5b12870c822..a8eac986d2e 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs
@@ -1,28 +1,24 @@
using Flow.Launcher.Plugin.Explorer.Search;
-using Flow.Launcher.Plugin.Explorer.Search.FolderLinks;
-using Newtonsoft.Json;
+using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using System.Collections.Generic;
namespace Flow.Launcher.Plugin.Explorer
{
public class Settings
{
- [JsonProperty]
public int MaxResult { get; set; } = 100;
- [JsonProperty]
- public List QuickFolderAccessLinks { get; set; } = new List();
+ public List QuickAccessLinks { get; set; } = new List();
+
+ // as at v1.7.0 this is to maintain backwards compatibility, need to be removed afterwards.
+ public List QuickFolderAccessLinks { get; set; } = new List();
- [JsonProperty]
public bool UseWindowsIndexForDirectorySearch { get; set; } = true;
- [JsonProperty]
- public List IndexSearchExcludedSubdirectoryPaths { get; set; } = new List();
+ public List IndexSearchExcludedSubdirectoryPaths { get; set; } = new List();
- [JsonProperty]
public string SearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign;
- [JsonProperty]
public string FileContentSearchActionKeyword { get; set; } = Constants.DefaultContentSearchActionKeyword;
}
}
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs
index 7fcd77f0775..791c06b66f4 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs
@@ -1,8 +1,9 @@
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Plugin.Explorer.Search;
-using Flow.Launcher.Plugin.Explorer.Search.FolderLinks;
+using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using System.Diagnostics;
+using System.Threading.Tasks;
namespace Flow.Launcher.Plugin.Explorer.ViewModels
{
@@ -21,14 +22,19 @@ public SettingsViewModel(PluginInitContext context)
Settings = storage.Load();
}
+ public Task LoadStorage()
+ {
+ return Task.Run(() => Settings = storage.Load());
+ }
+
public void Save()
{
storage.Save();
}
- internal void RemoveFolderLinkFromQuickFolders(FolderLink selectedRow) => Settings.QuickFolderAccessLinks.Remove(selectedRow);
+ internal void RemoveLinkFromQuickAccess(AccessLink selectedRow) => Settings.QuickAccessLinks.Remove(selectedRow);
- internal void RemoveFolderLinkFromExcludedIndexPaths(FolderLink selectedRow) => Settings.IndexSearchExcludedSubdirectoryPaths.Remove(selectedRow);
+ internal void RemoveAccessLinkFromExcludedIndexPaths(AccessLink selectedRow) => Settings.IndexSearchExcludedSubdirectoryPaths.Remove(selectedRow);
internal void OpenWindowsIndexingOptions()
{
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml
index 9d6f4976e9a..13d46394c38 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml
@@ -7,7 +7,7 @@
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
-
+
@@ -40,22 +40,22 @@
-
+ x:Name="lbxAccessLinks" AllowDrop="True"
+ Drop="lbxAccessLinks_Drop"
+ DragEnter="lbxAccessLinks_DragEnter"
+ ItemTemplate="{StaticResource ListViewTemplateAccessLinks}"/>
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml.cs
index 3b67b408d39..5d2980c55fb 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml.cs
@@ -1,4 +1,4 @@
-using Flow.Launcher.Plugin.Explorer.Search.FolderLinks;
+using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using Flow.Launcher.Plugin.Explorer.ViewModels;
using System;
using System.Collections.Generic;
@@ -29,7 +29,7 @@ public ExplorerSettings(SettingsViewModel viewModel)
this.viewModel = viewModel;
- lbxFolderLinks.ItemsSource = this.viewModel.Settings.QuickFolderAccessLinks;
+ lbxAccessLinks.ItemsSource = this.viewModel.Settings.QuickAccessLinks;
lbxExcludedPaths.ItemsSource = this.viewModel.Settings.IndexSearchExcludedSubdirectoryPaths;
@@ -54,7 +54,7 @@ public ExplorerSettings(SettingsViewModel viewModel)
public void RefreshView()
{
- lbxFolderLinks.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending));
+ lbxAccessLinks.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending));
lbxExcludedPaths.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending));
@@ -62,7 +62,7 @@ public void RefreshView()
btnEdit.Visibility = Visibility.Hidden;
btnAdd.Visibility = Visibility.Hidden;
- if (expFolderLinks.IsExpanded || expExcludedPaths.IsExpanded || expActionKeywords.IsExpanded)
+ if (expAccessLinks.IsExpanded || expExcludedPaths.IsExpanded || expActionKeywords.IsExpanded)
{
if (!expActionKeywords.IsExpanded)
btnAdd.Visibility = Visibility.Visible;
@@ -71,7 +71,7 @@ public void RefreshView()
&& btnEdit.Visibility == Visibility.Hidden)
btnEdit.Visibility = Visibility.Visible;
- if ((lbxFolderLinks.Items.Count == 0 && lbxExcludedPaths.Items.Count == 0)
+ if ((lbxAccessLinks.Items.Count == 0 && lbxExcludedPaths.Items.Count == 0)
&& btnDelete.Visibility == Visibility.Visible
&& btnEdit.Visibility == Visibility.Visible)
{
@@ -79,8 +79,8 @@ public void RefreshView()
btnEdit.Visibility = Visibility.Hidden;
}
- if (expFolderLinks.IsExpanded
- && lbxFolderLinks.Items.Count > 0
+ if (expAccessLinks.IsExpanded
+ && lbxAccessLinks.Items.Count > 0
&& btnDelete.Visibility == Visibility.Hidden
&& btnEdit.Visibility == Visibility.Hidden)
{
@@ -98,7 +98,7 @@ public void RefreshView()
}
}
- lbxFolderLinks.Items.Refresh();
+ lbxAccessLinks.Items.Refresh();
lbxExcludedPaths.Items.Refresh();
@@ -113,8 +113,8 @@ private void expActionKeywords_Click(object sender, RoutedEventArgs e)
if (expExcludedPaths.IsExpanded)
expExcludedPaths.IsExpanded = false;
- if (expFolderLinks.IsExpanded)
- expFolderLinks.IsExpanded = false;
+ if (expAccessLinks.IsExpanded)
+ expAccessLinks.IsExpanded = false;
RefreshView();
}
@@ -125,10 +125,10 @@ private void expActionKeywords_Collapsed(object sender, RoutedEventArgs e)
expActionKeywords.Height = Double.NaN;
}
- private void expFolderLinks_Click(object sender, RoutedEventArgs e)
+ private void expAccessLinks_Click(object sender, RoutedEventArgs e)
{
- if (expFolderLinks.IsExpanded)
- expFolderLinks.Height = 215;
+ if (expAccessLinks.IsExpanded)
+ expAccessLinks.Height = 215;
if (expExcludedPaths.IsExpanded)
expExcludedPaths.IsExpanded = false;
@@ -139,19 +139,19 @@ private void expFolderLinks_Click(object sender, RoutedEventArgs e)
RefreshView();
}
- private void expFolderLinks_Collapsed(object sender, RoutedEventArgs e)
+ private void expAccessLinks_Collapsed(object sender, RoutedEventArgs e)
{
- if (!expFolderLinks.IsExpanded)
- expFolderLinks.Height = Double.NaN;
+ if (!expAccessLinks.IsExpanded)
+ expAccessLinks.Height = Double.NaN;
}
private void expExcludedPaths_Click(object sender, RoutedEventArgs e)
{
if (expExcludedPaths.IsExpanded)
- expFolderLinks.Height = Double.NaN;
+ expAccessLinks.Height = Double.NaN;
- if (expFolderLinks.IsExpanded)
- expFolderLinks.IsExpanded = false;
+ if (expAccessLinks.IsExpanded)
+ expAccessLinks.IsExpanded = false;
if (expActionKeywords.IsExpanded)
expActionKeywords.IsExpanded = false;
@@ -161,7 +161,7 @@ private void expExcludedPaths_Click(object sender, RoutedEventArgs e)
private void btnDelete_Click(object sender, RoutedEventArgs e)
{
- var selectedRow = lbxFolderLinks.SelectedItem as FolderLink?? lbxExcludedPaths.SelectedItem as FolderLink;
+ var selectedRow = lbxAccessLinks.SelectedItem as AccessLink?? lbxExcludedPaths.SelectedItem as AccessLink;
if (selectedRow != null)
{
@@ -169,11 +169,11 @@ private void btnDelete_Click(object sender, RoutedEventArgs e)
if (MessageBox.Show(msg, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
- if (expFolderLinks.IsExpanded)
- viewModel.RemoveFolderLinkFromQuickFolders(selectedRow);
+ if (expAccessLinks.IsExpanded)
+ viewModel.RemoveLinkFromQuickAccess(selectedRow);
if (expExcludedPaths.IsExpanded)
- viewModel.RemoveFolderLinkFromExcludedIndexPaths(selectedRow);
+ viewModel.RemoveAccessLinkFromExcludedIndexPaths(selectedRow);
RefreshView();
}
@@ -199,7 +199,7 @@ private void btnEdit_Click(object sender, RoutedEventArgs e)
}
else
{
- var selectedRow = lbxFolderLinks.SelectedItem as FolderLink ?? lbxExcludedPaths.SelectedItem as FolderLink;
+ var selectedRow = lbxAccessLinks.SelectedItem as AccessLink ?? lbxExcludedPaths.SelectedItem as AccessLink;
if (selectedRow != null)
{
@@ -207,9 +207,9 @@ private void btnEdit_Click(object sender, RoutedEventArgs e)
folderBrowserDialog.SelectedPath = selectedRow.Path;
if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
{
- if (expFolderLinks.IsExpanded)
+ if (expAccessLinks.IsExpanded)
{
- var link = viewModel.Settings.QuickFolderAccessLinks.First(x => x.Path == selectedRow.Path);
+ var link = viewModel.Settings.QuickAccessLinks.First(x => x.Path == selectedRow.Path);
link.Path = folderBrowserDialog.SelectedPath;
}
@@ -235,36 +235,36 @@ private void btnAdd_Click(object sender, RoutedEventArgs e)
var folderBrowserDialog = new FolderBrowserDialog();
if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
{
- var newFolderLink = new FolderLink
+ var newAccessLink = new AccessLink
{
Path = folderBrowserDialog.SelectedPath
};
- AddFolderLink(newFolderLink);
+ AddAccessLink(newAccessLink);
}
RefreshView();
}
- private void lbxFolders_Drop(object sender, DragEventArgs e)
+ private void lbxAccessLinks_Drop(object sender, DragEventArgs e)
{
string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
if (files != null && files.Count() > 0)
{
- if (expFolderLinks.IsExpanded && viewModel.Settings.QuickFolderAccessLinks == null)
- viewModel.Settings.QuickFolderAccessLinks = new List();
+ if (expAccessLinks.IsExpanded && viewModel.Settings.QuickAccessLinks == null)
+ viewModel.Settings.QuickAccessLinks = new List();
foreach (string s in files)
{
if (Directory.Exists(s))
{
- var newFolderLink = new FolderLink
+ var newFolderLink = new AccessLink
{
Path = s
};
- AddFolderLink(newFolderLink);
+ AddAccessLink(newFolderLink);
}
RefreshView();
@@ -272,28 +272,28 @@ private void lbxFolders_Drop(object sender, DragEventArgs e)
}
}
- private void AddFolderLink(FolderLink newFolderLink)
+ private void AddAccessLink(AccessLink newAccessLink)
{
- if (expFolderLinks.IsExpanded
- && !viewModel.Settings.QuickFolderAccessLinks.Any(x => x.Path == newFolderLink.Path))
+ if (expAccessLinks.IsExpanded
+ && !viewModel.Settings.QuickAccessLinks.Any(x => x.Path == newAccessLink.Path))
{
- if (viewModel.Settings.QuickFolderAccessLinks == null)
- viewModel.Settings.QuickFolderAccessLinks = new List();
+ if (viewModel.Settings.QuickAccessLinks == null)
+ viewModel.Settings.QuickAccessLinks = new List();
- viewModel.Settings.QuickFolderAccessLinks.Add(newFolderLink);
+ viewModel.Settings.QuickAccessLinks.Add(newAccessLink);
}
if (expExcludedPaths.IsExpanded
- && !viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.Any(x => x.Path == newFolderLink.Path))
+ && !viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.Any(x => x.Path == newAccessLink.Path))
{
if (viewModel.Settings.IndexSearchExcludedSubdirectoryPaths == null)
- viewModel.Settings.IndexSearchExcludedSubdirectoryPaths = new List();
+ viewModel.Settings.IndexSearchExcludedSubdirectoryPaths = new List();
- viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.Add(newFolderLink);
+ viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.Add(newAccessLink);
}
}
- private void lbxFolders_DragEnter(object sender, DragEventArgs e)
+ private void lbxAccessLinks_DragEnter(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json b/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json
index 2c57ac668e8..9aa54fb8392 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.5",
+ "Version": "1.7.0",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Explorer.dll",
diff --git a/Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj b/Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj
index e6bfa7aa396..cc280b9a9cf 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj
+++ b/Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj
@@ -10,6 +10,7 @@
true
false
false
+ en
@@ -46,55 +47,12 @@
-
- PreserveNewest
-
-
-
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
-
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
-
-
-
+
MSBuild:Compile
Designer
PreserveNewest
-
-
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
-
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
-
-
-
- MSBuild:Compile
- Designer
+
PreserveNewest
diff --git a/Plugins/Flow.Launcher.Plugin.PluginIndicator/plugin.json b/Plugins/Flow.Launcher.Plugin.PluginIndicator/plugin.json
index 80900a445d2..7f73263a86b 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginIndicator/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.PluginIndicator/plugin.json
@@ -4,7 +4,7 @@
"Name": "Plugin Indicator",
"Description": "Provide plugin actionword suggestion",
"Author": "qianlifeng",
- "Version": "1.1.1",
+ "Version": "1.1.2",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.PluginIndicator.dll",
diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj b/Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj
index cc1a931ce04..cb2507a2b65 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj
+++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj
@@ -1,5 +1,4 @@
-
Library
netcoreapp3.1
@@ -27,15 +26,12 @@
PreserveNewest
-
+
-
+
PreserveNewest
-
-
-
-
+
PreserveNewest
diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml
index 8d24c145c45..3017f39c3bf 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml
+++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml
@@ -1,4 +1,4 @@
-
@@ -11,6 +11,8 @@
Plugin Install
Plugin Uninstall
Install failed: unable to find the plugin.json metadata file from the new plugin
+ Error installing plugin
+ Error occured while trying to install {0}
No update available
All plugins are up to date
{0} by {1} {2}{3}Would you like to update this plugin? After the update Flow will automatically restart.
diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/sk.xaml b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/sk.xaml
new file mode 100644
index 00000000000..211f2b4308e
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/sk.xaml
@@ -0,0 +1,39 @@
+
+
+
+ Sťahovanie pluginu
+ Čakajte, prosím…
+ Úspešne stiahnuté
+ {0} od {1} {2}{3}Chcete odinštalovať tento plugin? Po odinštalovaní sa Flow automaticky reštartuje.
+ {0} by {1} {2}{3}Chcete nainštalovať tento plugin? Po odinštalovaní sa Flow automaticky reštartuje.
+ Inštalovať plugin
+ Odinštalovať plugin
+ Inštalácia zlyhala: nepodarilo sa nájsť metadáta súboru plugin.json nového pluginu
+ Chyba inštalácie pluginu
+ Nastala chyba počas inštaláciu pluginu {0}
+ Nie je k dispozícii žiadna aktualizácia
+ Všetky pluginy sú aktuálne
+ {0} od {1} {2}{3}Chcete aktualizovať tento plugin? Po odinštalovaní sa Flow automaticky reštartuje.
+ Aktualizácia pluginu
+ Tento plugin má dostupnú aktualizáciu, chcete ju zobraziť?
+ Tento plugin je už nainštalovaný
+
+
+
+
+ Správca pluginov
+ Správa inštalácie, odinštalácie alebo aktualizácie pluginov programu Flow Launcher
+
+
+ Prejsť na webovú stránku
+ Prejsť na webovú stránku pluginu
+ Zobraziť zdrojový kód
+ Zobraziť zdrojový kód pluginu
+ Navrhnúť vylepšenie alebo nahlásiť chybu
+ Navrhnúť vylepšenie alebo nahlásiť chybu vývojárovi pluginu
+ Prejsť na repozitár pluginov spúšťača Flow
+ Prejsť na repozitár pluginov spúšťača Flow a zobraziť príspevky komunity
+
+
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs
index d700b9dfd27..66bfd2ab515 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs
@@ -7,10 +7,11 @@
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
+ public class Main : ISettingProvider, IAsyncPlugin, ISavable, IContextMenu, IPluginI18n, IAsyncReloadable
{
internal PluginInitContext Context { get; set; }
@@ -29,14 +30,29 @@ public Control CreateSettingPanel()
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 @@ public List LoadContextMenus(Result selectedResult)
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 @@ public List Query(Query query)
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 =>
@@ -87,5 +100,11 @@ public string GetTranslatedPluginDescription()
{
return Context.API.GetTranslation("plugin_pluginsmanager_plugin_description");
}
+
+ public async Task ReloadDataAsync()
+ {
+ 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 814e0764df7..145aadc986a 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 ac15618ca76..0f5e6d9e8b9 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs
+++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs
@@ -7,6 +7,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using System.Windows;
@@ -36,7 +37,7 @@ private bool ShouldHideWindow
}
}
- private readonly string icoPath = "Images\\pluginsmanager.png";
+ internal readonly string icoPath = "Images\\pluginsmanager.png";
internal PluginsManager(PluginInitContext context, Settings settings)
{
@@ -64,27 +65,27 @@ internal List GetDefaultHotKeys()
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()
+ Context.API.ChangeQuery("pm uninstall ");
+ return false;
+ }
+ },
+ new Result()
+ {
+ Title = Settings.HotkeyUpdate,
+ IcoPath = icoPath,
+ Action = _ =>
{
- Title = Settings.HotkeyUpdate,
- IcoPath = icoPath,
- Action = _ =>
- {
- Context.API.ChangeQuery("pm update ");
- return false;
- }
+ Context.API.ChangeQuery("pm update ");
+ return false;
}
- };
+ }
+ };
}
internal async Task InstallOrUpdate(UserPlugin plugin)
@@ -127,20 +128,24 @@ internal async Task InstallOrUpdate(UserPlugin plugin)
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"),
Context.API.GetTranslation("plugin_pluginsmanager_please_wait"));
- await Http.Download(plugin.UrlDownload, filePath).ConfigureAwait(false);
+ await Http.DownloadAsync(plugin.UrlDownload, filePath).ConfigureAwait(false);
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"),
Context.API.GetTranslation("plugin_pluginsmanager_download_success"));
+
+ Install(plugin, filePath);
}
catch (Exception e)
{
- 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_install_error_title"),
+ string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"),
+ plugin.Name));
- Log.Exception("PluginsManager", "An error occured while downloading plugin", e, "PluginDownload");
+ Log.Exception("PluginsManager", "An error occured while downloading plugin", e, "InstallOrUpdate");
+
+ return;
}
- Install(plugin, filePath);
Context.API.RestartApp();
}
@@ -161,7 +166,8 @@ internal List RequestUpdate(string search)
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
{
@@ -211,11 +217,30 @@ on existingPlugin.Metadata.ID equals pluginFromManifest.ID
Task.Run(async delegate
{
- await Http.Download(x.PluginNewUserPlugin.UrlDownload, downloadToFilePath).ConfigureAwait(false);
+ 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);
+
+ 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));
+ }, TaskContinuationOptions.OnlyOnFaulted);
return true;
}
@@ -249,8 +274,21 @@ internal List Search(IEnumerable results, string searchName)
.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 =
@@ -391,4 +429,4 @@ private List AutoCompleteReturnAllResults(string search, string hotkey,
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 d94af71a129..75d6038d431 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.3.1",
+ "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.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj
index cf9c9629402..a643ebf868f 100644
--- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj
+++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj
@@ -36,14 +36,14 @@
-
- PreserveNewest
-
-
+
Designer
MSBuild:Compile
PreserveNewest
+
+ PreserveNewest
+
Always
diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json b/Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json
index d769397a8fd..2bb40c64463 100644
--- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json
@@ -4,7 +4,7 @@
"Name":"Process Killer",
"Description":"kill running processes from Flow",
"Author":"Flow-Launcher",
- "Version":"1.2.1",
+ "Version":"1.2.2",
"Language":"csharp",
"Website":"https://github.com/Flow-Launcher/Flow.Launcher.Plugin.ProcessKiller",
"IcoPath":"Images\\app.png",
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj b/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj
index 3802297c70a..12e11385597 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj
+++ b/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj
@@ -53,52 +53,12 @@
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- Always
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
-
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
+
MSBuild:Compile
Designer
PreserveNewest
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
- MSBuild:Compile
- Designer
+
PreserveNewest
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs
index 8f124f3a40b..22f4aea592f 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs
@@ -2,6 +2,7 @@
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 @@
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 @@ public class Main : ISettingProvider, IPlugin, IPluginI18n, IContextMenu, ISavab
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 @@ public void Save()
_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();
-
- return result;
+ 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);
+
+ 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 @@ public static void IndexWin32Programs()
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 @@ public static void IndexUWPPrograms()
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 @@ public List LoadContextMenus(Result selectedResult)
}
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 @@ private void DisableProgram(IProgram programToDelete)
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 @@ public static void StartProcess(Func runProcess, Proc
}
}
- public void ReloadData()
+ public async Task ReloadDataAsync()
{
- IndexPrograms();
+ await IndexPrograms();
}
}
-}
\ No newline at end of file
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs
index 3ea78156d77..5db26aa70e6 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs
@@ -18,6 +18,7 @@
using Flow.Launcher.Plugin.Program.Logger;
using IStream = AppxPackaing.IStream;
using Rect = System.Windows.Rect;
+using Flow.Launcher.Plugin.SharedModels;
namespace Flow.Launcher.Plugin.Program.Programs
{
@@ -206,12 +207,11 @@ private static IEnumerable CurrentUserPackages()
}
catch (Exception e)
{
- ProgramLogger.LogException("UWP" ,"CurrentUserPackages", $"id","An unexpected error occured and "
+ ProgramLogger.LogException("UWP", "CurrentUserPackages", $"id", "An unexpected error occured and "
+ $"unable to verify if package is valid", e);
return false;
}
-
-
+
return valid;
});
return ps;
@@ -263,21 +263,40 @@ public class Application : IProgram
public string LogoPath { get; set; }
public UWP Package { get; set; }
- public Application(){}
+ public Application() { }
public Result Result(string query, IPublicAPI api)
{
- var title = (Name, Description) switch
- {
- (var n, null) => n,
- (var n, var d) when d.StartsWith(n) => d,
- (var n, var d) when n.StartsWith(d) => n,
- (var n, var d) when !string.IsNullOrEmpty(d) => $"{n}: {d}",
- _ => Name
- };
+ string title;
+ MatchResult matchResult;
- var matchResult = StringMatcher.FuzzySearch(query, title);
+ // We suppose Name won't be null
+ if (Description == null || Name.StartsWith(Description))
+ {
+ title = Name;
+ matchResult = StringMatcher.FuzzySearch(query, title);
+ }
+ else if (Description.StartsWith(Name))
+ {
+ title = Description;
+ matchResult = StringMatcher.FuzzySearch(query, Description);
+ }
+ else
+ {
+ title = $"{Name}: {Description}";
+ var nameMatch = StringMatcher.FuzzySearch(query, Name);
+ var desciptionMatch = StringMatcher.FuzzySearch(query, Description);
+ if (desciptionMatch.Score > nameMatch.Score)
+ {
+ for (int i = 0; i < desciptionMatch.MatchData.Count; i++)
+ {
+ desciptionMatch.MatchData[i] += Name.Length + 2; // 2 is ": "
+ }
+ matchResult = desciptionMatch;
+ }
+ else matchResult = nameMatch;
+ }
if (!matchResult.Success)
return null;
@@ -311,7 +330,7 @@ public List ContextMenus(IPublicAPI api)
Action = _ =>
{
- Main.StartProcess(Process.Start,
+ Main.StartProcess(Process.Start,
new ProcessStartInfo(
!string.IsNullOrEmpty(Main._settings.CustomizedExplorer)
? Main._settings.CustomizedExplorer
@@ -403,14 +422,14 @@ internal string ResourceFromPri(string packageFullName, string packageName, stri
public string FormattedPriReferenceValue(string packageName, string rawPriReferenceValue)
{
const string prefix = "ms-resource:";
-
+
if (string.IsNullOrWhiteSpace(rawPriReferenceValue) || !rawPriReferenceValue.StartsWith(prefix))
return rawPriReferenceValue;
string key = rawPriReferenceValue.Substring(prefix.Length);
if (key.StartsWith("//"))
return $"{prefix}{key}";
-
+
if (!key.StartsWith("/"))
{
key = $"/{key}";
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs
index 77278330a47..fd994aeb347 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs
@@ -12,6 +12,7 @@
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.Program.Logger;
using Flow.Launcher.Plugin.SharedCommands;
+using Flow.Launcher.Plugin.SharedModels;
namespace Flow.Launcher.Plugin.Program.Programs
{
@@ -36,19 +37,38 @@ public class Win32 : IProgram
public Result Result(string query, IPublicAPI api)
{
- var title = (Name, Description) switch
+ string title;
+ MatchResult matchResult;
+
+ // We suppose Name won't be null
+ if (Description == null || Name.StartsWith(Description))
{
- (var n, null) => n,
- (var n, var d) when d.StartsWith(n) => d,
- (var n, var d) when n.StartsWith(d) => n,
- (var n, var d) when !string.IsNullOrEmpty(d) => $"{n}: {d}",
- _ => Name
- };
+ title = Name;
+ matchResult = StringMatcher.FuzzySearch(query, title);
+ }
+ else if (Description.StartsWith(Name))
+ {
+ title = Description;
+ matchResult = StringMatcher.FuzzySearch(query, Description);
+ }
+ else
+ {
+ title = $"{Name}: {Description}";
+ var nameMatch = StringMatcher.FuzzySearch(query, Name);
+ var desciptionMatch = StringMatcher.FuzzySearch(query, Description);
+ if (desciptionMatch.Score > nameMatch.Score)
+ {
+ for (int i = 0; i < desciptionMatch.MatchData.Count; i++)
+ {
+ desciptionMatch.MatchData[i] += Name.Length + 2; // 2 is ": "
+ }
+ matchResult = desciptionMatch;
+ }
+ else matchResult = nameMatch;
+ }
- var matchResult = StringMatcher.FuzzySearch(query, title);
+ if (!matchResult.Success) return null;
- if (!matchResult.Success)
- return null;
var result = new Result
{
@@ -58,7 +78,7 @@ public Result Result(string query, IPublicAPI api)
Score = matchResult.Score,
TitleHighlightData = matchResult.MatchData,
ContextData = this,
- Action = e =>
+ Action = _ =>
{
var info = new ProcessStartInfo
{
@@ -268,10 +288,10 @@ private static IEnumerable ProgramPaths(string directory, string[] suffi
try
{
var paths = Directory.EnumerateFiles(directory, "*", new EnumerationOptions
- {
- IgnoreInaccessible = true,
- RecurseSubdirectories = true
- })
+ {
+ IgnoreInaccessible = true,
+ RecurseSubdirectories = true
+ })
.Where(x => suffixes.Contains(Extension(x)));
return paths;
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs
index e4e92b9bc7b..d66ca345e86 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 @@ private void Setting_Loaded(object sender, RoutedEventArgs e)
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 @@ private void ViewRefresh()
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 @@ private void programSourceView_Drop(object sender, DragEventArgs e)
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 @@ private void btnProgramSourceStatus_OnClick(object sender, RoutedEventArgs e)
ProgramSettingDisplayList.SetProgramSourcesStatus(selectedItems, true);
ProgramSettingDisplayList.RemoveDisabledFromSettings();
- }
-
+ }
+
if (selectedItems.IsReindexRequired())
ReIndexing();
@@ -282,7 +280,7 @@ private void GridViewColumnHeaderClickedHandler(object sender, RoutedEventArgs e
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 7d7a42e03ca..f713a33ece5 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.2",
+ "Version": "1.4.0",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Program.dll",
diff --git a/Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj b/Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj
index 178d95010f7..d3042722b9f 100644
--- a/Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj
+++ b/Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj
@@ -33,32 +33,6 @@
4
false
-
-
-
- Always
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
@@ -72,26 +46,18 @@
-
- PreserveNewest
-
-
-
-
-
+
MSBuild:Compile
Designer
PreserveNewest
-
- MSBuild:Compile
- Designer
+
PreserveNewest
-
- MSBuild:Compile
- Designer
-
+
+ MSBuild:Compile
+ Designer
+
diff --git a/Plugins/Flow.Launcher.Plugin.Shell/plugin.json b/Plugins/Flow.Launcher.Plugin.Shell/plugin.json
index 63e74d678cb..4ad572cf608 100644
--- a/Plugins/Flow.Launcher.Plugin.Shell/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.Shell/plugin.json
@@ -4,7 +4,7 @@
"Name": "Shell",
"Description": "Provide executing commands from Flow Launcher. Commands should start with >",
"Author": "qianlifeng",
- "Version": "1.1.1",
+ "Version": "1.1.2",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Shell.dll",
diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj b/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj
index bdab40457de..c25e759d39d 100644
--- a/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj
+++ b/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj
@@ -40,52 +40,18 @@
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
+
MSBuild:Compile
Designer
PreserveNewest
-
- MSBuild:Compile
- Designer
+
PreserveNewest
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
- MSBuild:Compile
- Designer
-
+
+ MSBuild:Compile
+ Designer
+
@@ -93,39 +59,4 @@
PreserveNewest
-
-
-
- PreserveNewest
-
-
-
-
-
- PreserveNewest
-
-
-
-
-
- PreserveNewest
-
-
-
-
-
- PreserveNewest
-
-
-
-
-
- PreserveNewest
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs
index 5642b62ed49..0aa37cdf583 100644
--- a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs
@@ -67,13 +67,15 @@ public List Query(Query query)
{
c.TitleHighlightData = titleMatch.MatchData;
}
- else
+ else
{
c.SubTitleHighlightData = subTitleMatch.MatchData;
}
+
results.Add(c);
}
}
+
return results;
}
@@ -94,13 +96,15 @@ private List Commands()
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 +115,15 @@ private List Commands()
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 +170,14 @@ private List Commands()
// 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 +236,13 @@ private List Commands()
{
// 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 8d4b9a238ba..75d7073b8bd 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.1",
+ "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.Url/Flow.Launcher.Plugin.Url.csproj b/Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj
index 7d802d81555..671a8b1c2c0 100644
--- a/Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj
+++ b/Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj
@@ -44,49 +44,14 @@
-
-
-
- PreserveNewest
-
-
-
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
-
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
-
-
-
- MSBuild:Compile
- Designer
- PreserveNewest
-
-
-
+
-
+
MSBuild:Compile
Designer
PreserveNewest
-
-
-
-
- MSBuild:Compile
- Designer
+
PreserveNewest
diff --git a/Plugins/Flow.Launcher.Plugin.Url/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Url/Languages/en.xaml
index 452be00ee54..eff1ac26354 100644
--- a/Plugins/Flow.Launcher.Plugin.Url/Languages/en.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Url/Languages/en.xaml
@@ -2,6 +2,10 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
+ Open search in:
+ New Window
+ New Tab
+
Open url:{0}
Can't open url:{0}
diff --git a/Plugins/Flow.Launcher.Plugin.Url/Languages/sk.xaml b/Plugins/Flow.Launcher.Plugin.Url/Languages/sk.xaml
index 69640735e71..97568be5a7d 100644
--- a/Plugins/Flow.Launcher.Plugin.Url/Languages/sk.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Url/Languages/sk.xaml
@@ -2,6 +2,10 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
+ Otvoriť vyhľadávanie v:
+ Nové okno
+ Nová karta
+
Otvoriť url:{0}
Adresa URL sa nedá otvoriť: {0}
diff --git a/Plugins/Flow.Launcher.Plugin.Url/SettingsControl.xaml b/Plugins/Flow.Launcher.Plugin.Url/SettingsControl.xaml
index f54aea878b5..9219a000930 100644
--- a/Plugins/Flow.Launcher.Plugin.Url/SettingsControl.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Url/SettingsControl.xaml
@@ -10,11 +10,11 @@
-
+
+ Content="{DynamicResource flowlauncher_plugin_new_window}" Click="OnNewBrowserWindowClick" />
+ Content="{DynamicResource flowlauncher_plugin_new_tab}" Click="OnNewTabClick" />
-
-
+
-
+
-
+
+ Click="OnChooseClick" FontSize="13" Content="{DynamicResource flowlauncher_plugin_websearch_choose}" Width="80"/>
> 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)
{
@@ -35,25 +39,20 @@ public override async Task> Suggestions(string query)
Match match = _reg.Match(result);
if (match.Success)
{
- JContainer json;
+ JsonDocument json;
try
{
- json = JsonConvert.DeserializeObject(match.Groups[1].Value) as JContainer;
+ json = JsonDocument.Parse(match.Groups[1].Value);
}
- catch (JsonSerializationException e)
+ catch(JsonException e)
{
Log.Exception("|Baidu.Suggestions|can't parse suggestions", e);
return new List();
}
- if (json != null)
- {
- var results = json["s"] as JArray;
- if (results != null)
- {
- return results.OfType().Select(o => o.Value).OfType().ToList();
- }
- }
+ var results = json?.RootElement.GetProperty("s");
+
+ return results?.EnumerateArray().Select(o => o.GetString()).ToList() ?? new List();
}
return new List();
diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs
index 9c4746711e5..ffde2fda292 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.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 5b9538091b9..c33ebd7e126 100644
--- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs
+++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs
@@ -3,49 +3,52 @@
using System.Linq;
using System.Net;
using System.Threading.Tasks;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Logger;
using System.Net.Http;
+using System.Threading;
+using System.Text.Json;
+using System.IO;
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)
{
- string result;
+ JsonDocument json;
+
try
{
const string api = "https://www.google.com/complete/search?output=chrome&q=";
- result = await Http.GetAsync(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 (string.IsNullOrEmpty(result)) return new List();
- JContainer json;
- try
- {
- json = JsonConvert.DeserializeObject(result) as JContainer;
- }
- catch (JsonSerializationException e)
+ catch (JsonException e)
{
Log.Exception("|Google.Suggestions|can't parse suggestions", e);
return new List();
}
- if (json != null)
- {
- var results = json[1] as JContainer;
- if (results != null)
- {
- return results.OfType().Select(o => o.Value).OfType().ToList();
- }
- }
- return new List();
+
+ 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 d6d89415f88..c58e61141ef 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 99fd2210aac..6e998674964 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.0",
+ "Version": "1.3.2",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.WebSearch.dll",
diff --git a/README.md b/README.md
index 02f48875883..ac861129804 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-
+
[](https://ci.appveyor.com/project/JohnTheGr8/flow-launcher/branch/dev)
[](https://github.com/Flow-Launcher/Flow.Launcher/releases)
[](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest)
@@ -26,6 +26,8 @@ Flow Launcher. Dedicated to make your workflow flow more seamlessly. Aimed at be
- Support of wide range of plugins.
- Fully portable.
+[
**SOFTPEDIA EDITOR'S PICK**](https://www.softpedia.com/get/System/Launchers-Shutdown-Tools/Flow-Launcher.shtml)
+
## Running Flow Launcher
| [Windows 7 and up](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest) |
diff --git a/Scripts/post_build.ps1 b/Scripts/post_build.ps1
index 59036842af4..b08fac8f67f 100644
--- a/Scripts/post_build.ps1
+++ b/Scripts/post_build.ps1
@@ -1,13 +1,13 @@
param(
[string]$config = "Release",
- [string]$solution,
- [string]$targetpath
+ [string]$solution = (Join-Path $PSScriptRoot ".." -Resolve)
)
Write-Host "Config: $config"
function Build-Version {
if ([string]::IsNullOrEmpty($env:flowVersion)) {
- $v = (Get-Command ${TargetPath}).FileVersionInfo.FileVersion
+ $targetPath = Join-Path $solution "Output/Release/Flow.Launcher.dll" -Resolve
+ $v = (Get-Command ${targetPath}).FileVersionInfo.FileVersion
} else {
$v = $env:flowVersion
}
@@ -31,21 +31,18 @@ function Build-Path {
return $p
}
-function Copy-Resources ($path, $config) {
- $project = "$path\Flow.Launcher"
- $output = "$path\Output"
- $target = "$output\$config"
- Copy-Item -Recurse -Force $project\Images\* $target\Images\
+function Copy-Resources ($path) {
# making version static as multiple versions can exist in the nuget folder and in the case a breaking change is introduced.
- Copy-Item -Force $env:USERPROFILE\.nuget\packages\squirrel.windows\1.5.2\tools\Squirrel.exe $output\Update.exe
+ Copy-Item -Force $env:USERPROFILE\.nuget\packages\squirrel.windows\1.5.2\tools\Squirrel.exe $path\Output\Update.exe
}
function Delete-Unused ($path, $config) {
$target = "$path\Output\$config"
$included = Get-ChildItem $target -Filter "*.dll"
foreach ($i in $included){
- Remove-Item -Path $target\Plugins -Include $i -Recurse
- Write-Host "Deleting duplicated $i"
+ $deleteList = Get-ChildItem $target\Plugins -Include $i -Recurse | Where { $_.VersionInfo.FileVersion -eq $i.VersionInfo.FileVersion -And $_.Name -eq "$i" }
+ $deleteList | ForEach-Object{ Write-Host Deleting duplicated $_.Name with version $_.VersionInfo.FileVersion at location $_.Directory.FullName }
+ $deleteList | Remove-Item
}
Remove-Item -Path $target -Include "*.xml" -Recurse
}
@@ -54,17 +51,6 @@ function Validate-Directory ($output) {
New-Item $output -ItemType Directory -Force
}
-function Zip-Release ($path, $version, $output) {
- Write-Host "Begin zip release"
-
- $content = "$path\Output\Release\*"
- $zipFile = "$output\Flow-Launcher-v$version.zip"
-
- Compress-Archive -Force -Path $content -DestinationPath $zipFile
-
- Write-Host "End zip release"
-}
-
function Pack-Squirrel-Installer ($path, $version, $output) {
# msbuild based installer generation is not working in appveyor, not sure why
Write-Host "Begin pack squirrel installer"
@@ -74,6 +60,8 @@ function Pack-Squirrel-Installer ($path, $version, $output) {
Write-Host "Packing: $spec"
Write-Host "Input path: $input"
+ # making version static as multiple versions can exist in the nuget folder and in the case a breaking change is introduced.
+ New-Alias Nuget $env:USERPROFILE\.nuget\packages\NuGet.CommandLine\5.4.0\tools\NuGet.exe -Force
# TODO: can we use dotnet pack here?
nuget pack $spec -Version $version -BasePath $input -OutputDirectory $output -Properties Configuration=Release
@@ -99,40 +87,30 @@ function Pack-Squirrel-Installer ($path, $version, $output) {
Write-Host "End pack squirrel installer"
}
-function IsDotNetCoreAppSelfContainedPublishEvent{
- return Test-Path $solution\Output\Release\coreclr.dll
-}
+function Publish-Self-Contained ($p) {
-function FixPublishLastWriteDateTimeError ($solutionPath) {
- #Fix error from publishing self contained app, when nuget tries to pack core dll references throws the error 'The DateTimeOffset specified cannot be converted into a Zip file timestamp'
- gci -path "$solutionPath\Output\Release" -rec -file *.dll | Where-Object {$_.LastWriteTime -lt (Get-Date).AddYears(-20)} | % { try { $_.LastWriteTime = '01/01/2000 00:00:00' } catch {} }
+ $csproj = Join-Path "$p" "Flow.Launcher/Flow.Launcher.csproj" -Resolve
+ $profile = Join-Path "$p" "Flow.Launcher/Properties/PublishProfiles/NetCore3.1-SelfContained.pubxml" -Resolve
+
+ # we call dotnet publish on the main project.
+ # The other projects should have been built in Release at this point.
+ dotnet publish -c Release $csproj /p:PublishProfile=$profile
}
function Main {
$p = Build-Path
$v = Build-Version
- Copy-Resources $p $config
+ Copy-Resources $p
if ($config -eq "Release"){
- if(IsDotNetCoreAppSelfContainedPublishEvent) {
- FixPublishLastWriteDateTimeError $p
- }
-
Delete-Unused $p $config
+
+ Publish-Self-Contained $p
+
$o = "$p\Output\Packages"
Validate-Directory $o
- # making version static as multiple versions can exist in the nuget folder and in the case a breaking change is introduced.
- New-Alias Nuget $env:USERPROFILE\.nuget\packages\NuGet.CommandLine\5.4.0\tools\NuGet.exe -Force
Pack-Squirrel-Installer $p $v $o
-
- $isInCI = $env:APPVEYOR
- if ($isInCI) {
- Zip-Release $p $v $o
- }
-
- Write-Host "List output directory"
- Get-ChildItem $o
}
}
diff --git a/SolutionAssemblyInfo.cs b/SolutionAssemblyInfo.cs
index ccbfef5d031..afd76b5d7da 100644
--- a/SolutionAssemblyInfo.cs
+++ b/SolutionAssemblyInfo.cs
@@ -16,6 +16,6 @@
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
-[assembly: AssemblyVersion("1.6.0")]
-[assembly: AssemblyFileVersion("1.6.0")]
-[assembly: AssemblyInformationalVersion("1.6.0")]
\ No newline at end of file
+[assembly: AssemblyVersion("1.7.0")]
+[assembly: AssemblyFileVersion("1.7.0")]
+[assembly: AssemblyInformationalVersion("1.7.0")]
\ No newline at end of file
diff --git a/appveyor.yml b/appveyor.yml
index 1f0937d6ddb..2c2f43b66b4 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: '1.6.0.{build}'
+version: '1.7.0.{build}'
init:
- ps: |
@@ -26,17 +26,42 @@ before_build:
build:
project: Flow.Launcher.sln
verbosity: minimal
+after_build:
+ - ps: .\Scripts\post_build.ps1
artifacts:
-- path: 'Output\Packages\Flow-Launcher-*.zip'
- name: Zip
- path: 'Output\Release\Flow.Launcher.Plugin.*.nupkg'
name: Plugin nupkg
+- path: 'Output\Packages\Flow-Launcher-*.exe'
+ name: Squirrel Installer
+- path: 'Output\Packages\FlowLauncher-*-full.nupkg'
+ name: Squirrel nupkg
+- path: 'Output\Packages\RELEASES'
+ name: Squirrel RELEASES
deploy:
- provider: NuGet
- artifact: /.*\.nupkg/
- api_key:
- secure: n80IeWR3pN81p0w4uXq4mO0TdTXoJSHHFL+yTB9YBJ0Wni2DjZGYwOFdaWzW4hRi
- on:
- branch: master
\ No newline at end of file
+ - provider: NuGet
+ artifact: Plugin nupkg
+ api_key:
+ secure: n80IeWR3pN81p0w4uXq4mO0TdTXoJSHHFL+yTB9YBJ0Wni2DjZGYwOFdaWzW4hRi
+ on:
+ branch: master
+
+ - provider: GitHub
+ release: v$(flowVersion)
+ auth_token:
+ secure: ij4UeXUYQBDJxn2YRAAhUOjklOGVKDB87Hn5J8tKIzj13yatoI7sLM666QDQFEgv
+ artifact: Squirrel Installer, Squirrel nupkg, Squirrel RELEASES
+ draft: true
+ force_update: true
+ on:
+ branch: master
+
+ - provider: GitHub
+ release: v$(flowVersion)
+ auth_token:
+ secure: ij4UeXUYQBDJxn2YRAAhUOjklOGVKDB87Hn5J8tKIzj13yatoI7sLM666QDQFEgv
+ artifact: Squirrel Installer, Squirrel nupkg, Squirrel RELEASES
+ force_update: true
+ on:
+ APPVEYOR_REPO_TAG: true
\ No newline at end of file