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