diff --git a/Flow.Launcher/Images/mainsearch.png b/Doc/mainsearch.png similarity index 100% rename from Flow.Launcher/Images/mainsearch.png rename to Doc/mainsearch.png diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj index 189a6669e..b0cc07e1a 100644 --- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj +++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj @@ -53,6 +53,7 @@ + diff --git a/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs b/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs index 6a96a94b7..13b6ac968 100644 --- a/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs +++ b/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs @@ -1,5 +1,8 @@ using System; using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.Plugin @@ -21,17 +24,17 @@ namespace Flow.Launcher.Core.Plugin }; } - protected override string ExecuteQuery(Query query) + protected override Task ExecuteQueryAsync(Query query, CancellationToken token) { JsonRPCServerRequestModel request = new JsonRPCServerRequestModel { Method = "query", - Parameters = new object[] { query.Search }, + Parameters = new object[] {query.Search}, }; _startInfo.Arguments = $"\"{request}\""; - return Execute(_startInfo); + return ExecuteAsync(_startInfo, token); } protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest) @@ -40,10 +43,12 @@ namespace Flow.Launcher.Core.Plugin return Execute(_startInfo); } - protected override string ExecuteContextMenu(Result selectedResult) { - JsonRPCServerRequestModel request = new JsonRPCServerRequestModel { + protected override string ExecuteContextMenu(Result selectedResult) + { + JsonRPCServerRequestModel request = new JsonRPCServerRequestModel + { Method = "contextmenu", - Parameters = new object[] { selectedResult.ContextData }, + Parameters = new object[] {selectedResult.ContextData}, }; _startInfo.Arguments = $"\"{request}\""; diff --git a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs index 9bc7c2472..b1c8ff6ea 100644 --- a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs +++ b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs @@ -15,6 +15,7 @@ using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.Plugin @@ -42,7 +43,10 @@ namespace Flow.Launcher.Core.Plugin public class JsonRPCQueryResponseModel : JsonRPCResponseModel { + [JsonPropertyName("result")] public new List Result { get; set; } + + public string DebugMessage { get; set; } } public class JsonRPCRequestModel : JsonRPCModelBase @@ -56,13 +60,12 @@ namespace Flow.Launcher.Core.Plugin string rpc = string.Empty; if (Parameters != null && Parameters.Length > 0) { - string parameters = Parameters.Aggregate("[", (current, o) => current + (GetParameterByType(o) + ",")); - parameters = parameters.Substring(0, parameters.Length - 1) + "]"; - rpc = string.Format(@"{{\""method\"":\""{0}\"",\""parameters\"":{1}", Method, parameters); + string parameters = $"[{string.Join(',', Parameters.Select(GetParameterByType))}]"; + rpc = $@"{{\""method\"":\""{Method}\"",\""parameters\"":{parameters}"; } else { - rpc = string.Format(@"{{\""method\"":\""{0}\"",\""parameters\"":[]", Method); + rpc = $@"{{\""method\"":\""{Method}\"",\""parameters\"":[]"; } return rpc; @@ -70,26 +73,16 @@ namespace Flow.Launcher.Core.Plugin } private string GetParameterByType(object parameter) + => parameter switch { - if (parameter == null) { - return "null"; - } - if (parameter is string) - { - return string.Format(@"\""{0}\""", ReplaceEscapes(parameter.ToString())); - } - if (parameter is int || parameter is float || parameter is double) - { - return string.Format(@"{0}", parameter); - } - if (parameter is bool) - { - return string.Format(@"{0}", parameter.ToString().ToLower()); - } - return parameter.ToString(); - } + null => "null", + string _ => $@"\""{ReplaceEscapes(parameter.ToString())}\""", + bool _ => $@"{parameter.ToString().ToLower()}", + _ => parameter.ToString() + }; - private string ReplaceEscapes(string str) + + private string ReplaceEscapes(string str) { return str.Replace(@"\", @"\\") //Escapes in ProcessStartInfo .Replace(@"\", @"\\") //Escapes itself when passed to client diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs index 31bf04286..7a088bd09 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs @@ -3,13 +3,13 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; -using Newtonsoft.Json; -using Flow.Launcher.Infrastructure.Exception; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Plugin; +using JetBrains.Annotations; namespace Flow.Launcher.Core.Plugin { @@ -17,7 +17,7 @@ namespace Flow.Launcher.Core.Plugin /// Represent the plugin that using JsonPRC /// every JsonRPC plugin should has its own plugin instance /// - internal abstract class JsonRPCPlugin : IPlugin, IContextMenu + internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu { protected PluginInitContext context; public const string JsonRPC = "JsonRPC"; @@ -27,24 +27,10 @@ namespace Flow.Launcher.Core.Plugin /// public abstract string SupportedLanguage { get; set; } - protected abstract string ExecuteQuery(Query query); + protected abstract Task ExecuteQueryAsync(Query query, CancellationToken token); protected abstract string ExecuteCallback(JsonRPCRequestModel rpcRequest); protected abstract string ExecuteContextMenu(Result selectedResult); - public List Query(Query query) - { - string output = ExecuteQuery(query); - try - { - return DeserializedResult(output); - } - catch (Exception e) - { - Log.Exception($"|JsonRPCPlugin.Query|Exception when query <{query}>", e); - return null; - } - } - public List LoadContextMenus(Result selectedResult) { string output = ExecuteContextMenu(selectedResult); @@ -59,50 +45,71 @@ namespace Flow.Launcher.Core.Plugin } } + + + private async Task> DeserializedResultAsync(Stream output) + { + if (output == Stream.Null) return null; + + JsonRPCQueryResponseModel queryResponseModel = await + JsonSerializer.DeserializeAsync(output); + + return ParseResults(queryResponseModel); + } + private List DeserializedResult(string output) { - if (!String.IsNullOrEmpty(output)) + if (string.IsNullOrEmpty(output)) return null; + + JsonRPCQueryResponseModel queryResponseModel = + JsonSerializer.Deserialize(output); + return ParseResults(queryResponseModel); + } + + private List ParseResults(JsonRPCQueryResponseModel queryResponseModel) + { + var results = new List(); + if (queryResponseModel.Result == null) return null; + + if(!string.IsNullOrEmpty(queryResponseModel.DebugMessage)) { - List results = new List(); + context.API.ShowMsg(queryResponseModel.DebugMessage); + } - JsonRPCQueryResponseModel queryResponseModel = JsonConvert.DeserializeObject(output); - if (queryResponseModel.Result == null) return null; - - foreach (JsonRPCResult result in queryResponseModel.Result) + foreach (JsonRPCResult result in queryResponseModel.Result) + { + result.Action = c => { - JsonRPCResult result1 = result; - result.Action = c => - { - if (result1.JsonRPCAction == null) return false; + if (result.JsonRPCAction == null) return false; - if (!String.IsNullOrEmpty(result1.JsonRPCAction.Method)) + if (!string.IsNullOrEmpty(result.JsonRPCAction.Method)) + { + if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher.")) { - if (result1.JsonRPCAction.Method.StartsWith("Flow.Launcher.")) + ExecuteFlowLauncherAPI(result.JsonRPCAction.Method.Substring(4), + result.JsonRPCAction.Parameters); + } + else + { + string actionReponse = ExecuteCallback(result.JsonRPCAction); + JsonRPCRequestModel jsonRpcRequestModel = + JsonSerializer.Deserialize(actionReponse); + if (jsonRpcRequestModel != null + && !string.IsNullOrEmpty(jsonRpcRequestModel.Method) + && jsonRpcRequestModel.Method.StartsWith("Flow.Launcher.")) { - ExecuteFlowLauncherAPI(result1.JsonRPCAction.Method.Substring(4), result1.JsonRPCAction.Parameters); - } - else - { - string actionReponse = ExecuteCallback(result1.JsonRPCAction); - JsonRPCRequestModel jsonRpcRequestModel = JsonConvert.DeserializeObject(actionReponse); - if (jsonRpcRequestModel != null - && !String.IsNullOrEmpty(jsonRpcRequestModel.Method) - && jsonRpcRequestModel.Method.StartsWith("Flow.Launcher.")) - { - ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method.Substring(4), jsonRpcRequestModel.Parameters); - } + ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method.Substring(4), + jsonRpcRequestModel.Parameters); } } - return !result1.JsonRPCAction.DontHideAfterAction; - }; - results.Add(result); - } - return results; - } - else - { - return null; + } + + return !result.JsonRPCAction.DontHideAfterAction; + }; + results.Add(result); } + + return results; } private void ExecuteFlowLauncherAPI(string method, object[] parameters) @@ -117,9 +124,7 @@ namespace Flow.Launcher.Core.Plugin catch (Exception) { #if (DEBUG) - { - throw; - } + throw; #endif } } @@ -130,75 +135,123 @@ namespace Flow.Launcher.Core.Plugin /// /// /// + /// Cancellation Token /// - protected string Execute(string fileName, string arguments) + protected Task ExecuteAsync(string fileName, string arguments, CancellationToken token = default) { - ProcessStartInfo start = new ProcessStartInfo(); - start.FileName = fileName; - start.Arguments = arguments; - start.UseShellExecute = false; - start.CreateNoWindow = true; - start.RedirectStandardOutput = true; - start.RedirectStandardError = true; - return Execute(start); + ProcessStartInfo start = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + return ExecuteAsync(start, token); } protected string Execute(ProcessStartInfo startInfo) { try { - using (var process = Process.Start(startInfo)) + using var process = Process.Start(startInfo); + if (process == null) return string.Empty; + + using var standardOutput = process.StandardOutput; + var result = standardOutput.ReadToEnd(); + + if (string.IsNullOrEmpty(result)) { - if (process != null) + using var standardError = process.StandardError; + var error = standardError.ReadToEnd(); + if (!string.IsNullOrEmpty(error)) { - using (var standardOutput = process.StandardOutput) - { - var result = standardOutput.ReadToEnd(); - if (string.IsNullOrEmpty(result)) - { - using (var standardError = process.StandardError) - { - var error = standardError.ReadToEnd(); - if (!string.IsNullOrEmpty(error)) - { - Log.Error($"|JsonRPCPlugin.Execute|{error}"); - return string.Empty; - } - else - { - Log.Error("|JsonRPCPlugin.Execute|Empty standard output and standard error."); - return string.Empty; - } - } - } - else if (result.StartsWith("DEBUG:")) - { - MessageBox.Show(new Form { TopMost = true }, result.Substring(6)); - return string.Empty; - } - else - { - return result; - } - } - } - else - { - Log.Error("|JsonRPCPlugin.Execute|Can't start new process"); + Log.Error($"|JsonRPCPlugin.Execute|{error}"); return string.Empty; } + + Log.Error("|JsonRPCPlugin.Execute|Empty standard output and standard error."); + return string.Empty; } + + if (result.StartsWith("DEBUG:")) + { + MessageBox.Show(new Form { TopMost = true }, result.Substring(6)); + return string.Empty; + } + + return result; + } catch (Exception e) { - Log.Exception($"|JsonRPCPlugin.Execute|Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>", e); + Log.Exception( + $"|JsonRPCPlugin.Execute|Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>", + e); return string.Empty; } } - public void Init(PluginInitContext ctx) + protected async Task ExecuteAsync(ProcessStartInfo startInfo, CancellationToken token = default) { - context = ctx; + try + { + using var process = Process.Start(startInfo); + if (process == null) + { + Log.Error("|JsonRPCPlugin.ExecuteAsync|Can't start new process"); + return Stream.Null; + } + + var result = process.StandardOutput.BaseStream; + + token.ThrowIfCancellationRequested(); + + if (!process.StandardError.EndOfStream) + { + using var standardError = process.StandardError; + var error = await standardError.ReadToEndAsync(); + + if (!string.IsNullOrEmpty(error)) + { + Log.Error($"|JsonRPCPlugin.ExecuteAsync|{error}"); + return Stream.Null; + } + + Log.Error("|JsonRPCPlugin.ExecuteAsync|Empty standard output and standard error."); + return Stream.Null; + } + + return result; + } + catch (Exception e) + { + Log.Exception( + $"|JsonRPCPlugin.ExecuteAsync|Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>", + e); + return Stream.Null; + } + } + + public async Task> QueryAsync(Query query, CancellationToken token) + { + var output = await ExecuteQueryAsync(query, token); + try + { + return await DeserializedResultAsync(output); + } + catch (Exception e) + { + Log.Exception($"|JsonRPCPlugin.Query|Exception when query <{query}>", e); + return null; + } + } + + public Task InitAsync(PluginInitContext context) + { + this.context = context; + return Task.CompletedTask; } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs b/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs index b9b878a7b..273698b86 100644 --- a/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs @@ -20,7 +20,7 @@ namespace Flow.Launcher.Core.Plugin dependencyResolver = new AssemblyDependencyResolver(assemblyFilePath); assemblyName = new AssemblyName(Path.GetFileNameWithoutExtension(assemblyFilePath)); - referencedPluginPackageDependencyResolver = + referencedPluginPackageDependencyResolver = new AssemblyDependencyResolver(Path.Combine(Constant.ProgramDirectory, "Flow.Launcher.Plugin.dll")); } @@ -38,15 +38,15 @@ namespace Flow.Launcher.Core.Plugin // that use Newtonsoft.Json if (assemblyPath == null || ExistsInReferencedPluginPackage(assemblyName)) return null; - + return LoadFromAssemblyPath(assemblyPath); } - internal Type FromAssemblyGetTypeOfInterface(Assembly assembly, Type type) + internal Type FromAssemblyGetTypeOfInterface(Assembly assembly, params Type[] types) { var allTypes = assembly.ExportedTypes; - return allTypes.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Contains(type)); + return allTypes.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Intersect(types).Any()); } internal bool ExistsInReferencedPluginPackage(AssemblyName assemblyName) diff --git a/Flow.Launcher.Core/Plugin/PluginConfig.cs b/Flow.Launcher.Core/Plugin/PluginConfig.cs index b946fa44d..46f79c60c 100644 --- a/Flow.Launcher.Core/Plugin/PluginConfig.cs +++ b/Flow.Launcher.Core/Plugin/PluginConfig.cs @@ -2,10 +2,10 @@ using System.Collections.Generic; using System.Linq; using System.IO; -using Newtonsoft.Json; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Plugin; +using System.Text.Json; namespace Flow.Launcher.Core.Plugin { @@ -61,7 +61,7 @@ namespace Flow.Launcher.Core.Plugin PluginMetadata metadata; try { - metadata = JsonConvert.DeserializeObject(File.ReadAllText(configPath)); + metadata = JsonSerializer.Deserialize(File.ReadAllText(configPath)); metadata.PluginDirectory = pluginDirectory; // for plugins which doesn't has ActionKeywords key metadata.ActionKeywords = metadata.ActionKeywords ?? new List { metadata.ActionKeyword }; diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 3b697a1ee..66eb0f0ec 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Logger; @@ -52,13 +53,14 @@ namespace Flow.Launcher.Core.Plugin } } - public static void ReloadData() + public static async Task ReloadData() { - foreach(var plugin in AllPlugins) + await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch { - var reloadablePlugin = plugin.Plugin as IReloadable; - reloadablePlugin?.ReloadData(); - } + IReloadable p => Task.Run(p.ReloadData), + IAsyncReloadable p => p.ReloadDataAsync(), + _ => Task.CompletedTask, + }).ToArray()); } static PluginManager() @@ -86,50 +88,62 @@ namespace Flow.Launcher.Core.Plugin /// Call initialize for all plugins /// /// return the list of failed to init plugins or null for none - public static void InitializePlugins(IPublicAPI api) + public static async Task InitializePlugins(IPublicAPI api) { API = api; var failedPlugins = new ConcurrentQueue(); - Parallel.ForEach(AllPlugins, pair => + + var InitTasks = AllPlugins.Select(pair => Task.Run(async delegate { try { - var milliseconds = Stopwatch.Debug($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>", () => + var milliseconds = pair.Plugin switch { - pair.Plugin.Init(new PluginInitContext - { - CurrentPluginMetadata = pair.Metadata, - API = API - }); - }); + IAsyncPlugin plugin + => await Stopwatch.DebugAsync($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>", + () => plugin.InitAsync(new PluginInitContext(pair.Metadata, API))), + IPlugin plugin + => Stopwatch.Debug($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>", + () => plugin.Init(new PluginInitContext(pair.Metadata, API))), + _ => throw new ArgumentException(), + }; pair.Metadata.InitTime += milliseconds; - Log.Info($"|PluginManager.InitializePlugins|Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>"); + Log.Info( + $"|PluginManager.InitializePlugins|Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>"); } catch (Exception e) { Log.Exception(nameof(PluginManager), $"Fail to Init plugin: {pair.Metadata.Name}", e); - pair.Metadata.Disabled = true; + pair.Metadata.Disabled = true; failedPlugins.Enqueue(pair); } - }); + })); + + await Task.WhenAll(InitTasks); _contextMenuPlugins = GetPluginsForInterface(); foreach (var plugin in AllPlugins) { - if (IsGlobalPlugin(plugin.Metadata)) - GlobalPlugins.Add(plugin); - - // Plugins may have multiple ActionKeywords, eg. WebSearch - plugin.Metadata.ActionKeywords - .Where(x => x != Query.GlobalPluginWildcardSign) - .ToList() - .ForEach(x => NonGlobalPlugins[x] = plugin); + foreach (var actionKeyword in plugin.Metadata.ActionKeywords) + { + switch (actionKeyword) + { + case Query.GlobalPluginWildcardSign: + GlobalPlugins.Add(plugin); + break; + default: + NonGlobalPlugins[actionKeyword] = plugin; + break; + } + } } if (failedPlugins.Any()) { var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name)); - API.ShowMsg($"Fail to Init Plugins", $"Plugins: {failed} - fail to load and would be disabled, please contact plugin creator for help", "", false); + API.ShowMsg($"Fail to Init Plugins", + $"Plugins: {failed} - fail to load and would be disabled, please contact plugin creator for help", + "", false); } } @@ -146,24 +160,48 @@ namespace Flow.Launcher.Core.Plugin } } - public static List QueryForPlugin(PluginPair pair, Query query) + public static async Task> QueryForPlugin(PluginPair pair, Query query, CancellationToken token) { var results = new List(); try { var metadata = pair.Metadata; - var milliseconds = Stopwatch.Debug($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}", () => + + long milliseconds = -1L; + + switch (pair.Plugin) { - results = pair.Plugin.Query(query) ?? new List(); - UpdatePluginMetadata(results, metadata, query); - }); + case IAsyncPlugin plugin: + milliseconds = await Stopwatch.DebugAsync($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}", + async () => results = await plugin.QueryAsync(query, token).ConfigureAwait(false)); + break; + case IPlugin plugin: + await Task.Run(() => milliseconds = Stopwatch.Debug($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}", + () => results = plugin.Query(query)), token).ConfigureAwait(false); + break; + default: + throw new ArgumentOutOfRangeException(); + } + token.ThrowIfCancellationRequested(); + if (results == null) + return results; + 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 null; } catch (Exception e) { - Log.Exception($"|PluginManager.QueryForPlugin|Exception for plugin <{pair.Metadata.Name}> when query <{query}>", e); + Log.Exception($"|PluginManager.QueryForPlugin|Exception for plugin <{pair.Metadata.Name}> when query <{query}>", e); } + return results; } @@ -182,11 +220,6 @@ namespace Flow.Launcher.Core.Plugin } } - private static bool IsGlobalPlugin(PluginMetadata metadata) - { - return metadata.ActionKeywords.Contains(Query.GlobalPluginWildcardSign); - } - /// /// get specified plugin, return null if not found /// @@ -222,16 +255,19 @@ namespace Flow.Launcher.Core.Plugin } catch (Exception e) { - Log.Exception($"|PluginManager.GetContextMenusForPlugin|Can't load context menus for plugin <{pluginPair.Metadata.Name}>", e); + Log.Exception( + $"|PluginManager.GetContextMenusForPlugin|Can't load context menus for plugin <{pluginPair.Metadata.Name}>", + e); } } + return results; } public static bool ActionKeywordRegistered(string actionKeyword) { return actionKeyword != Query.GlobalPluginWildcardSign - && NonGlobalPlugins.ContainsKey(actionKeyword); + && NonGlobalPlugins.ContainsKey(actionKeyword); } /// @@ -249,6 +285,7 @@ namespace Flow.Launcher.Core.Plugin { NonGlobalPlugins[newActionKeyword] = plugin; } + plugin.Metadata.ActionKeywords.Add(newActionKeyword); } @@ -262,16 +299,16 @@ namespace Flow.Launcher.Core.Plugin if (oldActionkeyword == Query.GlobalPluginWildcardSign && // Plugins may have multiple ActionKeywords that are global, eg. WebSearch plugin.Metadata.ActionKeywords - .Where(x => x == Query.GlobalPluginWildcardSign) - .ToList() - .Count == 1) + .Where(x => x == Query.GlobalPluginWildcardSign) + .ToList() + .Count == 1) { GlobalPlugins.Remove(plugin); } - + if (oldActionkeyword != Query.GlobalPluginWildcardSign) NonGlobalPlugins.Remove(oldActionkeyword); - + plugin.Metadata.ActionKeywords.Remove(oldActionkeyword); } diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index 224dbd85e..2c75f9633 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -3,13 +3,14 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; -using System.Runtime.Loader; using System.Threading.Tasks; using System.Windows.Forms; +using Droplex; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedCommands; namespace Flow.Launcher.Core.Plugin { @@ -22,7 +23,7 @@ namespace Flow.Launcher.Core.Plugin public static List Plugins(List metadatas, PluginsSettings settings) { var dotnetPlugins = DotNetPlugins(metadatas); - var pythonPlugins = PythonPlugins(metadatas, settings.PythonDirectory); + var pythonPlugins = PythonPlugins(metadatas, settings); var executablePlugins = ExecutablePlugins(metadatas); var plugins = dotnetPlugins.Concat(pythonPlugins).Concat(executablePlugins).ToList(); return plugins; @@ -37,56 +38,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,26 +99,29 @@ 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); }); } return plugins; } - public static IEnumerable PythonPlugins(List source, string pythonDirectory) + public static IEnumerable PythonPlugins(List source, PluginsSettings settings) { - // try to set Constant.PythonPath, either from + if (!source.Any(o => o.Language.ToUpper() == AllowedLanguage.Python)) + return new List(); + + // Try setting Constant.PythonPath first, either from // PATH or from the given pythonDirectory - if (string.IsNullOrEmpty(pythonDirectory)) + if (string.IsNullOrEmpty(settings.PythonDirectory)) { var paths = Environment.GetEnvironmentVariable(PATH); if (paths != null) @@ -126,47 +133,103 @@ namespace Flow.Launcher.Core.Plugin if (pythonInPath) { - Constant.PythonPath = PythonExecutable; + Constant.PythonPath = + Path.Combine(paths.Split(';').Where(p => p.ToLower().Contains(Python)).FirstOrDefault(), PythonExecutable); + settings.PythonDirectory = FilesFolders.GetPreviousExistingDirectory(FilesFolders.LocationExists, Constant.PythonPath); } else { - Log.Error("|PluginsLoader.PythonPlugins|Python can't be found in PATH."); + Log.Error("PluginsLoader","Failed to set Python path despite the environment variable PATH is found", "PythonPlugins"); } } - else - { - Log.Error("|PluginsLoader.PythonPlugins|PATH environment variable is not set."); - } } else { - var path = Path.Combine(pythonDirectory, PythonExecutable); + var path = Path.Combine(settings.PythonDirectory, PythonExecutable); if (File.Exists(path)) { Constant.PythonPath = path; } else { - Log.Error($"|PluginsLoader.PythonPlugins|Can't find python executable in {path}"); + Log.Error("PluginsLoader",$"Tried to automatically set from Settings.PythonDirectory " + + $"but can't find python executable in {path}", "PythonPlugins"); } } - // if we have a path to the python executable, - // load every python plugin pair. - if (String.IsNullOrEmpty(Constant.PythonPath)) + if (string.IsNullOrEmpty(settings.PythonDirectory)) { + if (MessageBox.Show("Flow detected you have installed Python plugins, " + + "would you like to install Python to run them? " + + Environment.NewLine + Environment.NewLine + + "Click no if it's already installed, " + + "and you will be prompted to select the folder that contains the Python executable", + string.Empty, MessageBoxButtons.YesNo) == DialogResult.No + && string.IsNullOrEmpty(settings.PythonDirectory)) + { + var dlg = new FolderBrowserDialog + { + SelectedPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + }; + + var result = dlg.ShowDialog(); + if (result == DialogResult.OK) + { + string pythonDirectory = dlg.SelectedPath; + if (!string.IsNullOrEmpty(pythonDirectory)) + { + var pythonPath = Path.Combine(pythonDirectory, PythonExecutable); + if (File.Exists(pythonPath)) + { + settings.PythonDirectory = pythonDirectory; + Constant.PythonPath = pythonPath; + } + else + { + MessageBox.Show("Can't find python in given directory"); + } + } + } + } + else + { + DroplexPackage.Drop(App.python3_9_1).Wait(); + + var installedPythonDirectory = + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python39"); + var pythonPath = Path.Combine(installedPythonDirectory, PythonExecutable); + if (FilesFolders.FileExists(pythonPath)) + { + settings.PythonDirectory = installedPythonDirectory; + Constant.PythonPath = pythonPath; + } + else + { + Log.Error("PluginsLoader", + $"Failed to set Python path after Droplex install, {pythonPath} does not exist", + "PythonPlugins"); + } + } + } + + if (string.IsNullOrEmpty(settings.PythonDirectory)) + { + MessageBox.Show("Unable to set Python executable path, please try from Flow's settings (scroll down to the bottom)."); + Log.Error("PluginsLoader", + $"Not able to successfully set Python path, the PythonDirectory variable is still an empty string.", + "PythonPlugins"); + return new List(); } - else - { - return source - .Where(o => o.Language.ToUpper() == AllowedLanguage.Python) - .Select(metadata => new PluginPair - { - Plugin = new PythonPlugin(Constant.PythonPath), - Metadata = metadata - }); - } + + return source + .Where(o => o.Language.ToUpper() == AllowedLanguage.Python) + .Select(metadata => new PluginPair + { + Plugin = new PythonPlugin(Constant.PythonPath), + Metadata = metadata + }) + .ToList(); } public static IEnumerable ExecutablePlugins(IEnumerable source) @@ -179,6 +242,5 @@ namespace Flow.Launcher.Core.Plugin Metadata = metadata }); } - } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Core/Plugin/PythonPlugin.cs b/Flow.Launcher.Core/Plugin/PythonPlugin.cs index 3c5a3a699..356a68a81 100644 --- a/Flow.Launcher.Core/Plugin/PythonPlugin.cs +++ b/Flow.Launcher.Core/Plugin/PythonPlugin.cs @@ -1,6 +1,8 @@ using System; using System.Diagnostics; using System.IO; +using System.Threading; +using System.Threading.Tasks; using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin; @@ -28,7 +30,7 @@ namespace Flow.Launcher.Core.Plugin } - protected override string ExecuteQuery(Query query) + protected override Task ExecuteQueryAsync(Query query, CancellationToken token) { JsonRPCServerRequestModel request = new JsonRPCServerRequestModel { @@ -40,13 +42,14 @@ namespace Flow.Launcher.Core.Plugin // todo happlebao why context can't be used in constructor _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; - return Execute(_startInfo); + return ExecuteAsync(_startInfo, token); } protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest) { _startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{rpcRequest}\""; _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; + // TODO: Async Action return Execute(_startInfo); } @@ -58,6 +61,7 @@ namespace Flow.Launcher.Core.Plugin _startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{request}\""; _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; + // TODO: Async Action return Execute(_startInfo); } } diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs index 20df23e40..44c34968c 100644 --- a/Flow.Launcher.Core/Updater.cs +++ b/Flow.Launcher.Core/Updater.cs @@ -8,15 +8,14 @@ using System.Threading.Tasks; using System.Windows; using JetBrains.Annotations; using Squirrel; -using Newtonsoft.Json; using Flow.Launcher.Core.Resource; using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Logger; -using System.IO; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; +using System.Text.Json.Serialization; namespace Flow.Launcher.Core { @@ -29,101 +28,84 @@ namespace Flow.Launcher.Core GitHubRepository = gitHubRepository; } - public async Task UpdateApp(IPublicAPI api , bool silentUpdate = true) + public async Task UpdateApp(IPublicAPI api, bool silentUpdate = true) { - UpdateManager updateManager; - UpdateInfo newUpdateInfo; - - if (!silentUpdate) - api.ShowMsg("Please wait...", "Checking for new update"); - try { - updateManager = await GitHubUpdateManager(GitHubRepository); - } - catch (Exception e) when (e is HttpRequestException || e is WebException || e is SocketException) - { - Log.Exception($"|Updater.UpdateApp|Please check your connection and proxy settings to api.github.com.", e); - return; - } + UpdateInfo newUpdateInfo; - try - { - // UpdateApp CheckForUpdate will return value only if the app is squirrel installed - newUpdateInfo = await updateManager.CheckForUpdate().NonNull(); - } - catch (Exception e) when (e is HttpRequestException || e is WebException || e is SocketException) - { - Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to api.github.com.", e); - updateManager.Dispose(); - return; - } - - var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString()); - var currentVersion = Version.Parse(Constant.Version); - - Log.Info($"|Updater.UpdateApp|Future Release <{newUpdateInfo.FutureReleaseEntry.Formatted()}>"); - - if (newReleaseVersion <= currentVersion) - { if (!silentUpdate) - MessageBox.Show("You already have the latest Flow Launcher version"); - updateManager.Dispose(); - return; - } + api.ShowMsg(api.GetTranslation("pleaseWait"), + api.GetTranslation("update_flowlauncher_update_check")); - if (!silentUpdate) - api.ShowMsg("Update found", "Updating..."); + using var updateManager = await GitHubUpdateManager(GitHubRepository).ConfigureAwait(false); - try - { - await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply); + + // UpdateApp CheckForUpdate will return value only if the app is squirrel installed + newUpdateInfo = await updateManager.CheckForUpdate().NonNull().ConfigureAwait(false); + + var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString()); + var currentVersion = Version.Parse(Constant.Version); + + Log.Info($"|Updater.UpdateApp|Future Release <{newUpdateInfo.FutureReleaseEntry.Formatted()}>"); + + if (newReleaseVersion <= currentVersion) + { + if (!silentUpdate) + MessageBox.Show(api.GetTranslation("update_flowlauncher_already_on_latest")); + return; + } + + if (!silentUpdate) + api.ShowMsg(api.GetTranslation("update_flowlauncher_update_found"), + api.GetTranslation("update_flowlauncher_updating")); + + await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false); + + await updateManager.ApplyReleases(newUpdateInfo).ConfigureAwait(false); + + if (DataLocation.PortableDataLocationInUse()) + { + var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{DataLocation.PortableFolderName}"; + FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination); + if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination)) + MessageBox.Show(string.Format(api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"), + DataLocation.PortableDataPath, + targetDestination)); + } + else + { + await updateManager.CreateUninstallerRegistryEntry().ConfigureAwait(false); + } + + var newVersionTips = NewVersinoTips(newReleaseVersion.ToString()); + + Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}"); + + if (MessageBox.Show(newVersionTips, api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes) + { + UpdateManager.RestartApp(Constant.ApplicationFileName); + } } catch (Exception e) when (e is HttpRequestException || e is WebException || e is SocketException) { Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e); - updateManager.Dispose(); + api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"), + api.GetTranslation("update_flowlauncher_check_connection")); return; } - - await updateManager.ApplyReleases(newUpdateInfo); - - if (DataLocation.PortableDataLocationInUse()) - { - var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{DataLocation.PortableFolderName}"; - FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination); - if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination)) - MessageBox.Show("Flow Launcher was not able to move your user profile data to the new update version. Please manually " + - $"move your profile data folder from {DataLocation.PortableDataPath} to {targetDestination}"); - } - else - { - await updateManager.CreateUninstallerRegistryEntry(); - } - - var newVersionTips = NewVersinoTips(newReleaseVersion.ToString()); - - Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}"); - - // always dispose UpdateManager - updateManager.Dispose(); - - if (MessageBox.Show(newVersionTips, "New Update", MessageBoxButton.YesNo) == MessageBoxResult.Yes) - { - UpdateManager.RestartApp(Constant.ApplicationFileName); - } } [UsedImplicitly] private class GithubRelease { - [JsonProperty("prerelease")] + [JsonPropertyName("prerelease")] public bool Prerelease { get; [UsedImplicitly] set; } - [JsonProperty("published_at")] + [JsonPropertyName("published_at")] public DateTime PublishedAt { get; [UsedImplicitly] set; } - [JsonProperty("html_url")] + [JsonPropertyName("html_url")] public string HtmlUrl { get; [UsedImplicitly] set; } } @@ -133,13 +115,13 @@ namespace Flow.Launcher.Core var uri = new Uri(repository); var api = $"https://api.github.com/repos{uri.AbsolutePath}/releases"; - var json = await Http.Get(api); + var jsonStream = await Http.GetStreamAsync(api).ConfigureAwait(false); - var releases = JsonConvert.DeserializeObject>(json); + var releases = await System.Text.Json.JsonSerializer.DeserializeAsync>(jsonStream).ConfigureAwait(false); var latest = releases.Where(r => !r.Prerelease).OrderByDescending(r => r.PublishedAt).First(); var latestUrl = latest.HtmlUrl.Replace("/tag/", "/download/"); - var client = new WebClient { Proxy = Http.WebProxy() }; + var client = new WebClient { Proxy = Http.WebProxy }; var downloader = new FileDownloader(client); var manager = new UpdateManager(latestUrl, urlDownloader: downloader); diff --git a/Flow.Launcher.Infrastructure/Constant.cs b/Flow.Launcher.Infrastructure/Constant.cs index df1464048..de6ed1b29 100644 --- a/Flow.Launcher.Infrastructure/Constant.cs +++ b/Flow.Launcher.Infrastructure/Constant.cs @@ -30,10 +30,12 @@ namespace Flow.Launcher.Infrastructure public static string PythonPath; - public static readonly string QueryTextBoxIconImagePath = $"{ProgramDirectory}\\Images\\mainsearch.png"; + public static readonly string QueryTextBoxIconImagePath = $"{ProgramDirectory}\\Images\\mainsearch.svg"; public const string DefaultTheme = "Darker"; public const string Themes = "Themes"; + + public const string Website = "https://flow-launcher.github.io"; } } diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index 28d4c0e1f..8153de6c8 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -49,7 +49,6 @@ - diff --git a/Flow.Launcher.Infrastructure/Helper.cs b/Flow.Launcher.Infrastructure/Helper.cs index fa7e18533..faa4c93b5 100644 --- a/Flow.Launcher.Infrastructure/Helper.cs +++ b/Flow.Launcher.Infrastructure/Helper.cs @@ -1,12 +1,18 @@ using System; using System.IO; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; namespace Flow.Launcher.Infrastructure { public static class Helper { + static Helper() + { + jsonFormattedSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + } + /// /// http://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy /// @@ -65,13 +71,18 @@ namespace Flow.Launcher.Infrastructure } } + private static readonly JsonSerializerOptions jsonFormattedSerializerOptions = new JsonSerializerOptions + { + WriteIndented = true + }; + public static string Formatted(this T t) { - var formatted = JsonConvert.SerializeObject( - t, - Formatting.Indented, - new StringEnumConverter() - ); + var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions + { + WriteIndented = true + }); + return formatted; } } diff --git a/Flow.Launcher.Infrastructure/Http/Http.cs b/Flow.Launcher.Infrastructure/Http/Http.cs index 1d5f240e1..3a3e770a5 100644 --- a/Flow.Launcher.Infrastructure/Http/Http.cs +++ b/Flow.Launcher.Infrastructure/Http/Http.cs @@ -6,6 +6,9 @@ using System.Threading.Tasks; using JetBrains.Annotations; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; +using System; +using System.ComponentModel; +using System.Threading; namespace Flow.Launcher.Infrastructure.Http { @@ -13,6 +16,8 @@ namespace Flow.Launcher.Infrastructure.Http { private const string UserAgent = @"Mozilla/5.0 (Trident/7.0; rv:11.0) like Gecko"; + private static HttpClient client = new HttpClient(); + static Http() { // need to be added so it would work on a win10 machine @@ -20,58 +25,118 @@ namespace Flow.Launcher.Infrastructure.Http ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12; + + client.DefaultRequestHeaders.Add("User-Agent", UserAgent); + HttpClient.DefaultProxy = WebProxy; } - public static HttpProxy Proxy { private get; set; } - public static IWebProxy WebProxy() + private static HttpProxy proxy; + + public static HttpProxy Proxy { - if (Proxy != null && Proxy.Enabled && !string.IsNullOrEmpty(Proxy.Server)) + private get { return proxy; } + set { - if (string.IsNullOrEmpty(Proxy.UserName) || string.IsNullOrEmpty(Proxy.Password)) + proxy = value; + proxy.PropertyChanged += UpdateProxy; + UpdateProxy(ProxyProperty.Enabled); + } + } + + public static WebProxy WebProxy { get; } = new WebProxy(); + + /// + /// Update the Address of the Proxy to modify the client Proxy + /// + public static void UpdateProxy(ProxyProperty property) + { + (WebProxy.Address, WebProxy.Credentials) = property switch + { + ProxyProperty.Enabled => Proxy.Enabled switch { - var webProxy = new WebProxy(Proxy.Server, Proxy.Port); - return webProxy; + true => Proxy.UserName switch + { + var userName when !string.IsNullOrEmpty(userName) => + (new Uri($"http://{Proxy.Server}:{Proxy.Port}"), null), + _ => (new Uri($"http://{Proxy.Server}:{Proxy.Port}"), + new NetworkCredential(Proxy.UserName, Proxy.Password)) + }, + false => (null, null) + }, + ProxyProperty.Server => (new Uri($"http://{Proxy.Server}:{Proxy.Port}"), WebProxy.Credentials), + ProxyProperty.Port => (new Uri($"http://{Proxy.Server}:{Proxy.Port}"), WebProxy.Credentials), + ProxyProperty.UserName => (WebProxy.Address, new NetworkCredential(Proxy.UserName, Proxy.Password)), + ProxyProperty.Password => (WebProxy.Address, new NetworkCredential(Proxy.UserName, Proxy.Password)), + _ => throw new ArgumentOutOfRangeException() + }; + } + + public static async Task DownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default) + { + try + { + using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); + if (response.StatusCode == HttpStatusCode.OK) + { + await using var fileStream = new FileStream(filePath, FileMode.CreateNew); + await response.Content.CopyToAsync(fileStream); } else { - var webProxy = new WebProxy(Proxy.Server, Proxy.Port) - { - Credentials = new NetworkCredential(Proxy.UserName, Proxy.Password) - }; - return webProxy; + throw new HttpRequestException($"Error code <{response.StatusCode}> returned from <{url}>"); } } + catch (HttpRequestException e) + { + Log.Exception("Infrastructure.Http", "Http Request Error", e, "DownloadAsync"); + throw; + } + } + + /// + /// Asynchrously get the result as string from url. + /// When supposing the result larger than 83kb, try using GetStreamAsync to avoid reading as string + /// + /// + /// The Http result as string. Null if cancellation requested + public static Task GetAsync([NotNull] string url, CancellationToken token = default) + { + Log.Debug($"|Http.Get|Url <{url}>"); + return GetAsync(new Uri(url.Replace("#", "%23")), token); + } + + /// + /// + /// + /// + /// + /// The Http result as string. Null if cancellation requested + public static async Task GetAsync([NotNull] Uri url, CancellationToken token = default) + { + Log.Debug($"|Http.Get|Url <{url}>"); + using var response = await client.GetAsync(url, token); + var content = await response.Content.ReadAsStringAsync(); + if (response.StatusCode == HttpStatusCode.OK) + { + return content; + } else { - return WebRequest.GetSystemWebProxy(); + throw new HttpRequestException( + $"Error code <{response.StatusCode}> with content <{content}> returned from <{url}>"); } } - public static void Download([NotNull] string url, [NotNull] string filePath) - { - var client = new WebClient { Proxy = WebProxy() }; - client.Headers.Add("user-agent", UserAgent); - client.DownloadFile(url, filePath); - } - - public static async Task Get([NotNull] string url, string encoding = "UTF-8") + /// + /// Asynchrously get the result as stream from url. + /// + /// + /// + public static async Task GetStreamAsync([NotNull] string url, CancellationToken token = default) { Log.Debug($"|Http.Get|Url <{url}>"); - var request = WebRequest.CreateHttp(url); - request.Method = "GET"; - request.Timeout = 1000; - request.Proxy = WebProxy(); - request.UserAgent = UserAgent; - var response = await request.GetResponseAsync() as HttpWebResponse; - response = response.NonNull(); - var stream = response.GetResponseStream().NonNull(); - - using var reader = new StreamReader(stream, Encoding.GetEncoding(encoding)); - var content = await reader.ReadToEndAsync(); - if (response.StatusCode != HttpStatusCode.OK) - throw new HttpRequestException($"Error code <{response.StatusCode}> with content <{content}> returned from <{url}>"); - - return content; + var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); + return await response.Content.ReadAsStreamAsync(); } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Infrastructure/Image/ImageCache.cs b/Flow.Launcher.Infrastructure/Image/ImageCache.cs index b1c09024f..bb7ec6817 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageCache.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageCache.cs @@ -73,8 +73,7 @@ namespace Flow.Launcher.Infrastructure.Image public bool ContainsKey(string key) { - var contains = Data.ContainsKey(key) && Data[key] != null; - return contains; + return Data.ContainsKey(key) && Data[key].imageSource != null; } public int CacheSize() diff --git a/Flow.Launcher.Infrastructure/Logger/Log.cs b/Flow.Launcher.Infrastructure/Logger/Log.cs index 289ec5d68..94132b27f 100644 --- a/Flow.Launcher.Infrastructure/Logger/Log.cs +++ b/Flow.Launcher.Infrastructure/Logger/Log.cs @@ -50,14 +50,18 @@ namespace Flow.Launcher.Infrastructure.Logger return valid; } - + [MethodImpl(MethodImplOptions.Synchronized)] public static void Exception(string className, string message, System.Exception exception, [CallerMemberName] string methodName = "") { +#if DEBUG + throw exception; +#else var classNameWithMethod = CheckClassAndMessageAndReturnFullClassWithMethod(className, message, methodName); ExceptionInternal(classNameWithMethod, message, exception); +#endif } private static string CheckClassAndMessageAndReturnFullClassWithMethod(string className, string message, @@ -128,7 +132,7 @@ namespace Flow.Launcher.Infrastructure.Logger public static void Exception(string message, System.Exception e) { #if DEBUG - throw e; + throw e; #else if (FormatValid(message)) { diff --git a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs index 80fd12820..6c2a94e82 100644 --- a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs +++ b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using System.Text; using JetBrains.Annotations; @@ -8,14 +9,109 @@ using ToolGood.Words.Pinyin; namespace Flow.Launcher.Infrastructure { + public class TranslationMapping + { + private bool constructed; + + private List originalIndexs = new List(); + private List translatedIndexs = new List(); + private int translaedLength = 0; + + public string key { get; private set; } + + public void setKey(string key) + { + this.key = key; + } + + public void AddNewIndex(int originalIndex, int translatedIndex, int length) + { + if (constructed) + throw new InvalidOperationException("Mapping shouldn't be changed after constructed"); + + originalIndexs.Add(originalIndex); + translatedIndexs.Add(translatedIndex); + translatedIndexs.Add(translatedIndex + length); + translaedLength += length - 1; + } + + public int MapToOriginalIndex(int translatedIndex) + { + if (translatedIndex > translatedIndexs.Last()) + return translatedIndex - translaedLength - 1; + + int lowerBound = 0; + int upperBound = originalIndexs.Count - 1; + + int count = 0; + + // Corner case handle + if (translatedIndex < translatedIndexs[0]) + return translatedIndex; + if (translatedIndex > translatedIndexs.Last()) + { + int indexDef = 0; + for (int k = 0; k < originalIndexs.Count; k++) + { + indexDef += translatedIndexs[k * 2 + 1] - translatedIndexs[k * 2]; + } + + return translatedIndex - indexDef - 1; + } + + // Binary Search with Range + for (int i = originalIndexs.Count / 2;; count++) + { + if (translatedIndex < translatedIndexs[i * 2]) + { + // move to lower middle + upperBound = i; + i = (i + lowerBound) / 2; + } + else if (translatedIndex > translatedIndexs[i * 2 + 1] - 1) + { + lowerBound = i; + // move to upper middle + // due to floor of integer division, move one up on corner case + i = (i + upperBound + 1) / 2; + } + else + return originalIndexs[i]; + + if (upperBound - lowerBound <= 1 && + translatedIndex > translatedIndexs[lowerBound * 2 + 1] && + translatedIndex < translatedIndexs[upperBound * 2]) + { + int indexDef = 0; + + for (int j = 0; j < upperBound; j++) + { + indexDef += translatedIndexs[j * 2 + 1] - translatedIndexs[j * 2]; + } + + return translatedIndex - indexDef - 1; + } + } + } + + public void endConstruct() + { + if (constructed) + throw new InvalidOperationException("Mapping has already been constructed"); + constructed = true; + } + } + public interface IAlphabet { - string Translate(string stringToTranslate); + public (string translation, TranslationMapping map) Translate(string stringToTranslate); } public class PinyinAlphabet : IAlphabet { - private ConcurrentDictionary _pinyinCache = new ConcurrentDictionary(); + private ConcurrentDictionary _pinyinCache = + new ConcurrentDictionary(); + private Settings _settings; public void Initialize([NotNull] Settings settings) @@ -23,7 +119,7 @@ namespace Flow.Launcher.Infrastructure _settings = settings ?? throw new ArgumentNullException(nameof(settings)); } - public string Translate(string content) + public (string translation, TranslationMapping map) Translate(string content) { if (_settings.ShouldUsePinyin) { @@ -34,14 +130,7 @@ namespace Flow.Launcher.Infrastructure var resultList = WordsHelper.GetPinyinList(content); StringBuilder resultBuilder = new StringBuilder(); - - for (int i = 0; i < resultList.Length; i++) - { - if (content[i] >= 0x3400 && content[i] <= 0x9FD5) - resultBuilder.Append(resultList[i].First()); - } - - resultBuilder.Append(' '); + TranslationMapping map = new TranslationMapping(); bool pre = false; @@ -49,6 +138,7 @@ namespace Flow.Launcher.Infrastructure { if (content[i] >= 0x3400 && content[i] <= 0x9FD5) { + map.AddNewIndex(i, resultBuilder.Length, resultList[i].Length + 1); resultBuilder.Append(' '); resultBuilder.Append(resultList[i]); pre = true; @@ -60,15 +150,21 @@ namespace Flow.Launcher.Infrastructure pre = false; resultBuilder.Append(' '); } + resultBuilder.Append(resultList[i]); } } - return _pinyinCache[content] = resultBuilder.ToString(); + map.endConstruct(); + + var key = resultBuilder.ToString(); + map.setKey(key); + + return _pinyinCache[content] = (key, map); } else { - return content; + return (content, null); } } else @@ -78,7 +174,7 @@ namespace Flow.Launcher.Infrastructure } else { - return content; + return (content, null); } } } diff --git a/Flow.Launcher.Infrastructure/Stopwatch.cs b/Flow.Launcher.Infrastructure/Stopwatch.cs index d39d90e81..dd6edaff9 100644 --- a/Flow.Launcher.Infrastructure/Stopwatch.cs +++ b/Flow.Launcher.Infrastructure/Stopwatch.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Logger; namespace Flow.Launcher.Infrastructure @@ -22,7 +23,22 @@ namespace Flow.Launcher.Infrastructure Log.Debug(info); return milliseconds; } - + + /// + /// This stopwatch will appear only in Debug mode + /// + public static async Task DebugAsync(string message, Func action) + { + var stopWatch = new System.Diagnostics.Stopwatch(); + stopWatch.Start(); + await action(); + stopWatch.Stop(); + var milliseconds = stopWatch.ElapsedMilliseconds; + string info = $"{message} <{milliseconds}ms>"; + Log.Debug(info); + return milliseconds; + } + public static long Normal(string message, Action action) { var stopWatch = new System.Diagnostics.Stopwatch(); @@ -34,6 +50,20 @@ namespace Flow.Launcher.Infrastructure Log.Info(info); return milliseconds; } + + public static async Task NormalAsync(string message, Func action) + { + var stopWatch = new System.Diagnostics.Stopwatch(); + stopWatch.Start(); + await action(); + stopWatch.Stop(); + var milliseconds = stopWatch.ElapsedMilliseconds; + string info = $"{message} <{milliseconds}ms>"; + Log.Info(info); + return milliseconds; + } + + public static void StartCount(string name, Action action) { diff --git a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs index 784c11110..37cfec252 100644 --- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs @@ -1,7 +1,7 @@ using System; using System.Globalization; using System.IO; -using Newtonsoft.Json; +using System.Text.Json; using Flow.Launcher.Infrastructure.Logger; namespace Flow.Launcher.Infrastructure.Storage @@ -9,10 +9,10 @@ namespace Flow.Launcher.Infrastructure.Storage /// /// Serialize object using json format. /// - public class JsonStrorage + public class JsonStrorage where T : new() { - private readonly JsonSerializerSettings _serializerSettings; - private T _data; + private readonly JsonSerializerOptions _serializerSettings; + protected T _data; // need a new directory name public const string DirectoryName = "Settings"; public const string FileSuffix = ".json"; @@ -24,10 +24,9 @@ namespace Flow.Launcher.Infrastructure.Storage { // use property initialization instead of DefaultValueAttribute // easier and flexible for default value of object - _serializerSettings = new JsonSerializerSettings + _serializerSettings = new JsonSerializerOptions { - ObjectCreationHandling = ObjectCreationHandling.Replace, - NullValueHandling = NullValueHandling.Ignore + IgnoreNullValues = false }; } @@ -56,7 +55,7 @@ namespace Flow.Launcher.Infrastructure.Storage { try { - _data = JsonConvert.DeserializeObject(searlized, _serializerSettings); + _data = JsonSerializer.Deserialize(searlized, _serializerSettings); } catch (JsonException e) { @@ -77,7 +76,7 @@ namespace Flow.Launcher.Infrastructure.Storage BackupOriginFile(); } - _data = JsonConvert.DeserializeObject("{}", _serializerSettings); + _data = new T(); Save(); } @@ -94,7 +93,8 @@ namespace Flow.Launcher.Infrastructure.Storage public void Save() { - string serialized = JsonConvert.SerializeObject(_data, Formatting.Indented); + string serialized = JsonSerializer.Serialize(_data, new JsonSerializerOptions() { WriteIndented = true }); + File.WriteAllText(FilePath, serialized); } } diff --git a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs index 5418e1837..ca7c454c4 100644 --- a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs @@ -15,5 +15,10 @@ namespace Flow.Launcher.Infrastructure.Storage FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}"); } + + public PluginJsonStorage(T data) : this() + { + _data = data; + } } } diff --git a/Flow.Launcher.Infrastructure/StringMatcher.cs b/Flow.Launcher.Infrastructure/StringMatcher.cs index 2a4270fb4..3ffa9f7b1 100644 --- a/Flow.Launcher.Infrastructure/StringMatcher.cs +++ b/Flow.Launcher.Infrastructure/StringMatcher.cs @@ -1,8 +1,7 @@ +using Flow.Launcher.Plugin.SharedModels; using System; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; -using static Flow.Launcher.Infrastructure.StringMatcher; namespace Flow.Launcher.Infrastructure { @@ -32,7 +31,20 @@ namespace Flow.Launcher.Infrastructure } /// - /// Current method: + /// Current method has two parts, Acronym Match and Fuzzy Search: + /// + /// Acronym Match: + /// Charater listed below will be considered as acronym + /// 1. Character on index 0 + /// 2. Character appears after a space + /// 3. Character that is UpperCase + /// 4. Character that is number + /// + /// Acronym Match will succeed when all query characters match with acronyms in stringToCompare. + /// If any of the characters in the query isn't matched with stringToCompare, Acronym Match will fail. + /// Score will be calculated based the percentage of all query characters matched with total acronyms in stringToCompare. + /// + /// Fuzzy Search: /// Character matching + substring matching; /// 1. Query search string is split into substrings, separator is whitespace. /// 2. Check each query substring's characters against full compare string, @@ -44,20 +56,21 @@ namespace Flow.Launcher.Infrastructure /// public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption opt) { - if (string.IsNullOrEmpty(stringToCompare) || string.IsNullOrEmpty(query)) return new MatchResult (false, UserSettingSearchPrecision); - - query = query.Trim(); + if (string.IsNullOrEmpty(stringToCompare) || string.IsNullOrEmpty(query)) + return new MatchResult(false, UserSettingSearchPrecision); - if (_alphabet != null) - { - query = _alphabet.Translate(query); - stringToCompare = _alphabet.Translate(stringToCompare); - } + query = query.Trim(); + TranslationMapping translationMapping; + (stringToCompare, translationMapping) = _alphabet?.Translate(stringToCompare) ?? (stringToCompare, null); + + var currentAcronymQueryIndex = 0; + var acronymMatchData = new List(); + int acronymsTotalCount = 0; + int acronymsMatched = 0; var fullStringToCompareWithoutCase = opt.IgnoreCase ? stringToCompare.ToLower() : stringToCompare; - var queryWithoutCase = opt.IgnoreCase ? query.ToLower() : query; - + var querySubstrings = queryWithoutCase.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); int currentQuerySubstringIndex = 0; var currentQuerySubstring = querySubstrings[currentQuerySubstringIndex]; @@ -75,17 +88,44 @@ namespace Flow.Launcher.Infrastructure for (var compareStringIndex = 0; compareStringIndex < fullStringToCompareWithoutCase.Length; compareStringIndex++) { + // If acronyms matching successfully finished, this gets the remaining not matched acronyms for score calculation + if (currentAcronymQueryIndex >= query.Length && acronymsMatched == query.Length) + { + if (IsAcronymCount(stringToCompare, compareStringIndex)) + acronymsTotalCount++; + continue; + } + + if (currentAcronymQueryIndex >= query.Length || + currentAcronymQueryIndex >= query.Length && allQuerySubstringsMatched) + break; // To maintain a list of indices which correspond to spaces in the string to compare // To populate the list only for the first query substring - if (fullStringToCompareWithoutCase[compareStringIndex].Equals(' ') && currentQuerySubstringIndex == 0) - { + if (fullStringToCompareWithoutCase[compareStringIndex] == ' ' && currentQuerySubstringIndex == 0) spaceIndices.Add(compareStringIndex); + + // Acronym Match + if (IsAcronym(stringToCompare, compareStringIndex)) + { + if (fullStringToCompareWithoutCase[compareStringIndex] == + queryWithoutCase[currentAcronymQueryIndex]) + { + acronymMatchData.Add(compareStringIndex); + acronymsMatched++; + + currentAcronymQueryIndex++; + } } - if (fullStringToCompareWithoutCase[compareStringIndex] != currentQuerySubstring[currentQuerySubstringCharacterIndex]) + if (IsAcronymCount(stringToCompare, compareStringIndex)) + acronymsTotalCount++; + + if (allQuerySubstringsMatched || fullStringToCompareWithoutCase[compareStringIndex] != + currentQuerySubstring[currentQuerySubstringCharacterIndex]) { matchFoundInPreviousLoop = false; + continue; } @@ -107,14 +147,16 @@ namespace Flow.Launcher.Infrastructure // in order to do so we need to verify all previous chars are part of the pattern var startIndexToVerify = compareStringIndex - currentQuerySubstringCharacterIndex; - if (AllPreviousCharsMatched(startIndexToVerify, currentQuerySubstringCharacterIndex, fullStringToCompareWithoutCase, currentQuerySubstring)) + if (AllPreviousCharsMatched(startIndexToVerify, currentQuerySubstringCharacterIndex, + fullStringToCompareWithoutCase, currentQuerySubstring)) { matchFoundInPreviousLoop = true; // if it's the beginning character of the first query substring that is matched then we need to update start index firstMatchIndex = currentQuerySubstringIndex == 0 ? startIndexToVerify : firstMatchIndex; - indexList = GetUpdatedIndexList(startIndexToVerify, currentQuerySubstringCharacterIndex, firstMatchIndexInWord, indexList); + indexList = GetUpdatedIndexList(startIndexToVerify, currentQuerySubstringCharacterIndex, + firstMatchIndexInWord, indexList); } } @@ -127,49 +169,96 @@ namespace Flow.Launcher.Infrastructure if (currentQuerySubstringCharacterIndex == currentQuerySubstring.Length) { // if any of the substrings was not matched then consider as all are not matched - allSubstringsContainedInCompareString = matchFoundInPreviousLoop && allSubstringsContainedInCompareString; + allSubstringsContainedInCompareString = + matchFoundInPreviousLoop && allSubstringsContainedInCompareString; currentQuerySubstringIndex++; - allQuerySubstringsMatched = AllQuerySubstringsMatched(currentQuerySubstringIndex, querySubstrings.Length); + allQuerySubstringsMatched = + AllQuerySubstringsMatched(currentQuerySubstringIndex, querySubstrings.Length); + if (allQuerySubstringsMatched) - break; + continue; // otherwise move to the next query substring currentQuerySubstring = querySubstrings[currentQuerySubstringIndex]; currentQuerySubstringCharacterIndex = 0; } } - + + // return acronym match if all query char matched + if (acronymsMatched > 0 && acronymsMatched == query.Length) + { + int acronymScore = acronymsMatched * 100 / acronymsTotalCount; + + if (acronymScore >= (int)UserSettingSearchPrecision) + { + acronymMatchData = acronymMatchData.Select(x => translationMapping?.MapToOriginalIndex(x) ?? x).Distinct().ToList(); + return new MatchResult(true, UserSettingSearchPrecision, acronymMatchData, acronymScore); + } + } + // proceed to calculate score if every char or substring without whitespaces matched if (allQuerySubstringsMatched) { var nearestSpaceIndex = CalculateClosestSpaceIndex(spaceIndices, firstMatchIndex); - var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1, lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString); + var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1, + lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString); - return new MatchResult(true, UserSettingSearchPrecision, indexList, score); + var resultList = indexList.Select(x => translationMapping?.MapToOriginalIndex(x) ?? x).Distinct().ToList(); + return new MatchResult(true, UserSettingSearchPrecision, resultList, score); } return new MatchResult(false, UserSettingSearchPrecision); } + private bool IsAcronym(string stringToCompare, int compareStringIndex) + { + if (IsAcronymChar(stringToCompare, compareStringIndex) || IsAcronymNumber(stringToCompare, compareStringIndex)) + return true; + + return false; + } + + // When counting acronyms, treat a set of numbers as one acronym ie. Visual 2019 as 2 acronyms instead of 5 + private bool IsAcronymCount(string stringToCompare, int compareStringIndex) + { + if (IsAcronymChar(stringToCompare, compareStringIndex)) + return true; + + if (IsAcronymNumber(stringToCompare, compareStringIndex)) + return compareStringIndex == 0 || char.IsWhiteSpace(stringToCompare[compareStringIndex - 1]); + + return false; + } + + private bool IsAcronymChar(string stringToCompare, int compareStringIndex) + => char.IsUpper(stringToCompare[compareStringIndex]) || + compareStringIndex == 0 || // 0 index means char is the start of the compare string, which is an acronym + char.IsWhiteSpace(stringToCompare[compareStringIndex - 1]); + + private bool IsAcronymNumber(string stringToCompare, int compareStringIndex) + => stringToCompare[compareStringIndex] >= 0 && stringToCompare[compareStringIndex] <= 9; + // To get the index of the closest space which preceeds the first matching index private int CalculateClosestSpaceIndex(List spaceIndices, int firstMatchIndex) { - if (spaceIndices.Count == 0) + var closestSpaceIndex = -1; + + // spaceIndices should be ordered asc + foreach (var index in spaceIndices) { - return -1; - } - else - { - int? ind = spaceIndices.OrderBy(item => (firstMatchIndex - item)).Where(item => firstMatchIndex > item).FirstOrDefault(); - int closestSpaceIndex = ind ?? -1; - return closestSpaceIndex; + if (index < firstMatchIndex) + closestSpaceIndex = index; + else + break; } + + return closestSpaceIndex; } private static bool AllPreviousCharsMatched(int startIndexToVerify, int currentQuerySubstringCharacterIndex, - string fullStringToCompareWithoutCase, string currentQuerySubstring) + string fullStringToCompareWithoutCase, string currentQuerySubstring) { var allMatch = true; for (int indexToCheck = 0; indexToCheck < currentQuerySubstringCharacterIndex; indexToCheck++) @@ -183,8 +272,9 @@ namespace Flow.Launcher.Infrastructure return allMatch; } - - private static List GetUpdatedIndexList(int startIndexToVerify, int currentQuerySubstringCharacterIndex, int firstMatchIndexInWord, List indexList) + + private static List GetUpdatedIndexList(int startIndexToVerify, int currentQuerySubstringCharacterIndex, + int firstMatchIndexInWord, List indexList) { var updatedList = new List(); @@ -202,10 +292,12 @@ namespace Flow.Launcher.Infrastructure private static bool AllQuerySubstringsMatched(int currentQuerySubstringIndex, int querySubstringsLength) { + // Acronym won't utilize the substring to match return currentQuerySubstringIndex >= querySubstringsLength; } - private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, int matchLen, bool allSubstringsContainedInCompareString) + private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, int matchLen, + bool allSubstringsContainedInCompareString) { // A match found near the beginning of a string is scored more than a match found near the end // A match is scored more if the characters in the patterns are closer to each other, @@ -239,74 +331,6 @@ namespace Flow.Launcher.Infrastructure return score; } - - public enum SearchPrecisionScore - { - Regular = 50, - Low = 20, - None = 0 - } - } - - public class MatchResult - { - public MatchResult(bool success, SearchPrecisionScore searchPrecision) - { - Success = success; - SearchPrecision = searchPrecision; - } - - public MatchResult(bool success, SearchPrecisionScore searchPrecision, List matchData, int rawScore) - { - Success = success; - SearchPrecision = searchPrecision; - MatchData = matchData; - RawScore = rawScore; - } - - public bool Success { get; set; } - - /// - /// The final score of the match result with search precision filters applied. - /// - public int Score { get; private set; } - - /// - /// The raw calculated search score without any search precision filtering applied. - /// - private int _rawScore; - - public int RawScore - { - get { return _rawScore; } - set - { - _rawScore = value; - Score = ScoreAfterSearchPrecisionFilter(_rawScore); - } - } - - /// - /// Matched data to highlight. - /// - public List MatchData { get; set; } - - public SearchPrecisionScore SearchPrecision { get; set; } - - public bool IsSearchPrecisionScoreMet() - { - return IsSearchPrecisionScoreMet(_rawScore); - } - - private bool IsSearchPrecisionScoreMet(int rawScore) - { - return rawScore >= (int)SearchPrecision; - } - - private int ScoreAfterSearchPrecisionFilter(int rawScore) - { - return IsSearchPrecisionScoreMet(rawScore) ? rawScore : 0; - } } public class MatchOption diff --git a/Flow.Launcher.Infrastructure/UserSettings/HttpProxy.cs b/Flow.Launcher.Infrastructure/UserSettings/HttpProxy.cs index c1b0c1dd7..213193526 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/HttpProxy.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/HttpProxy.cs @@ -1,11 +1,80 @@ -namespace Flow.Launcher.Infrastructure.UserSettings +using System.ComponentModel; + +namespace Flow.Launcher.Infrastructure.UserSettings { + public enum ProxyProperty + { + Enabled, + Server, + Port, + UserName, + Password + } + public class HttpProxy { - public bool Enabled { get; set; } = false; - public string Server { get; set; } - public int Port { get; set; } - public string UserName { get; set; } - public string Password { get; set; } + private bool _enabled = false; + private string _server; + private int _port; + private string _userName; + private string _password; + + public bool Enabled + { + get => _enabled; + set + { + _enabled = value; + OnPropertyChanged(ProxyProperty.Enabled); + } + } + + public string Server + { + get => _server; + set + { + _server = value; + OnPropertyChanged(ProxyProperty.Server); + } + } + + public int Port + { + get => _port; + set + { + _port = value; + OnPropertyChanged(ProxyProperty.Port); + } + } + + public string UserName + { + get => _userName; + set + { + _userName = value; + OnPropertyChanged(ProxyProperty.UserName); + } + } + + public string Password + { + get => _password; + set + { + _password = value; + OnPropertyChanged(ProxyProperty.Password); + } + } + + public delegate void ProxyPropertyChangedHandler(ProxyProperty property); + public event ProxyPropertyChangedHandler PropertyChanged; + + private void OnPropertyChanged(ProxyProperty property) + { + PropertyChanged?.Invoke(property); + } } } \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs index ccd9beb86..29bc11480 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs @@ -31,6 +31,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings metadata.ActionKeyword = settings.ActionKeywords[0]; } metadata.Disabled = settings.Disabled; + metadata.Priority = settings.Priority; } else { @@ -40,7 +41,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings Name = metadata.Name, Version = metadata.Version, ActionKeywords = metadata.ActionKeywords, - Disabled = metadata.Disabled + Disabled = metadata.Disabled, + Priority = metadata.Priority }; } } @@ -52,6 +54,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings public string Name { get; set; } public string Version { get; set; } public List ActionKeywords { get; set; } // a reference of the action keywords from plugin manager + public int Priority { get; set; } /// /// Used only to save the state of the plugin in settings diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 832b6fbfa..76a370978 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -1,9 +1,9 @@ using System; using System.Collections.ObjectModel; using System.Drawing; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; +using System.Text.Json.Serialization; using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedModels; namespace Flow.Launcher.Infrastructure.UserSettings { @@ -16,7 +16,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings public bool ShowOpenResultHotkey { get; set; } = true; public string Language { - get => language; set { + get => language; set + { language = value; OnPropertyChanged(); } @@ -38,7 +39,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings /// public bool ShouldUsePinyin { get; set; } = false; - internal StringMatcher.SearchPrecisionScore QuerySearchPrecision { get; private set; } = StringMatcher.SearchPrecisionScore.Regular; + internal SearchPrecisionScore QuerySearchPrecision { get; private set; } = SearchPrecisionScore.Regular; [JsonIgnore] public string QuerySearchPrecisionString @@ -48,8 +49,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings { try { - var precisionScore = (StringMatcher.SearchPrecisionScore)Enum - .Parse(typeof(StringMatcher.SearchPrecisionScore), value); + var precisionScore = (SearchPrecisionScore)Enum + .Parse(typeof(SearchPrecisionScore), value); QuerySearchPrecision = precisionScore; StringMatcher.Instance.UserSettingSearchPrecision = precisionScore; @@ -58,8 +59,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings { Logger.Log.Exception(nameof(Settings), "Failed to load QuerySearchPrecisionString value from Settings file", e); - QuerySearchPrecision = StringMatcher.SearchPrecisionScore.Regular; - StringMatcher.Instance.UserSettingSearchPrecision = StringMatcher.SearchPrecisionScore.Regular; + QuerySearchPrecision = SearchPrecisionScore.Regular; + StringMatcher.Instance.UserSettingSearchPrecision = SearchPrecisionScore.Regular; throw; } @@ -73,9 +74,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings public int MaxResultsToShow { get; set; } = 5; public int ActivateTimes { get; set; } - // Order defaults to 0 or -1, so 1 will let this property appear last - [JsonProperty(Order = 1)] - public PluginsSettings PluginSettings { get; set; } = new PluginsSettings(); + public ObservableCollection CustomPluginHotkeys { get; set; } = new ObservableCollection(); public bool DontPromptUpdateMsg { get; set; } @@ -100,8 +99,12 @@ namespace Flow.Launcher.Infrastructure.UserSettings public HttpProxy Proxy { get; set; } = new HttpProxy(); - [JsonConverter(typeof(StringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public LastQueryMode LastQueryMode { get; set; } = LastQueryMode.Selected; + + + // This needs to be loaded last by staying at the bottom + public PluginsSettings PluginSettings { get; set; } = new PluginsSettings(); } public enum LastQueryMode diff --git a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj index 0f6450d18..0eefe5c4f 100644 --- a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj +++ b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.1 @@ -14,10 +14,10 @@ - 1.3.0 - 1.3.0 - 1.3.0 - 1.3.0 + 1.4.0 + 1.4.0 + 1.4.0 + 1.4.0 Flow.Launcher.Plugin Flow-Launcher MIT @@ -62,7 +62,6 @@ - - \ No newline at end of file + diff --git a/Flow.Launcher.Plugin/IAsyncPlugin.cs b/Flow.Launcher.Plugin/IAsyncPlugin.cs new file mode 100644 index 000000000..b0b41cc22 --- /dev/null +++ b/Flow.Launcher.Plugin/IAsyncPlugin.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Flow.Launcher.Plugin +{ + /// + /// Asynchronous Plugin Model for Flow Launcher + /// + public interface IAsyncPlugin + { + /// + /// Asynchronous Querying + /// + /// + /// If the Querying or Init method requires high IO transmission + /// or performing CPU intense jobs (performing better with cancellation), please use this IAsyncPlugin interface + /// + /// Query to search + /// Cancel when querying job is obsolete + /// + Task> QueryAsync(Query query, CancellationToken token); + + /// + /// Initialize plugin asynchrously (will still wait finish to continue) + /// + /// + /// + Task InitAsync(PluginInitContext context); + } +} diff --git a/Flow.Launcher.Plugin/IPlugin.cs b/Flow.Launcher.Plugin/IPlugin.cs index 8f7d279fa..203dc9af7 100644 --- a/Flow.Launcher.Plugin/IPlugin.cs +++ b/Flow.Launcher.Plugin/IPlugin.cs @@ -2,9 +2,30 @@ namespace Flow.Launcher.Plugin { + /// + /// Synchronous Plugin Model for Flow Launcher + /// + /// If the Querying or Init method requires high IO transmission + /// or performaing CPU intense jobs (performing better with cancellation), please try the IAsyncPlugin interface + /// + /// public interface IPlugin { + /// + /// Querying when user's search changes + /// + /// This method will be called within a Task.Run, + /// so please avoid synchrously wait for long. + /// + /// + /// Query to search + /// List Query(Query query); + + /// + /// Initialize plugin + /// + /// void Init(PluginInitContext context); } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Plugin/IPublicAPI.cs b/Flow.Launcher.Plugin/IPublicAPI.cs index ccc00d5e9..07e2b2700 100644 --- a/Flow.Launcher.Plugin/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/IPublicAPI.cs @@ -1,5 +1,11 @@ -using System; +using Flow.Launcher.Plugin.SharedModels; +using JetBrains.Annotations; +using System; using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; namespace Flow.Launcher.Plugin { @@ -34,7 +40,7 @@ namespace Flow.Launcher.Plugin /// Plugin's in memory data with new content /// added by user. /// - void ReloadAllPluginData(); + Task ReloadAllPluginData(); /// /// Check for new Flow Launcher update @@ -82,5 +88,87 @@ namespace Flow.Launcher.Plugin /// if you want to hook something like Ctrl+R, you should use this event /// event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent; + + /// + /// Fuzzy Search the string with the given query. This is the core search mechanism Flow uses + /// + /// Query string + /// The string that will be compared against the query + /// Match results + MatchResult FuzzySearch(string query, string stringToCompare); + + /// + /// Http download the spefic url and return as string + /// + /// URL to call Http Get + /// Cancellation Token + /// Task to get string result + Task HttpGetStringAsync(string url, CancellationToken token = default); + + /// + /// Http download the spefic url and return as stream + /// + /// URL to call Http Get + /// Cancellation Token + /// Task to get stream result + Task HttpGetStreamAsync(string url, CancellationToken token = default); + + /// + /// Download the specific url to a cretain file path + /// + /// URL to download file + /// place to store file + /// Task showing the progress + Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default); + + /// + /// Add ActionKeyword for specific plugin + /// + /// ID for plugin that needs to add action keyword + /// The actionkeyword that is supposed to be added + void AddActionKeyword(string pluginId, string newActionKeyword); + + /// + /// Remove ActionKeyword for specific plugin + /// + /// ID for plugin that needs to remove action keyword + /// The actionkeyword that is supposed to be removed + void RemoveActionKeyword(string pluginId, string oldActionKeyword); + + /// + /// Log debug message + /// Message will only be logged in Debug mode + /// + void LogDebug(string className, string message, [CallerMemberName] string methodName = ""); + + /// + /// Log info message + /// + void LogInfo(string className, string message, [CallerMemberName] string methodName = ""); + + /// + /// Log warning message + /// + void LogWarn(string className, string message, [CallerMemberName] string methodName = ""); + + /// + /// Log an Exception. Will throw if in debug mode so developer will be aware, + /// otherwise logs the eror message. This is the primary logging method used for Flow + /// + void LogException(string className, string message, Exception e, [CallerMemberName] string methodName = ""); + + /// + /// Load JsonStorage for current plugin. This is the method used to load settings from json in Flow + /// + /// Type for deserialization + /// + T LoadJsonStorage() where T : new(); + + /// + /// Save JsonStorage for current plugin. This is the method used to save settings to json in Flow + /// + /// Type for Serialization + /// + void SaveJsonStorage(T setting) where T : new(); } } diff --git a/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs new file mode 100644 index 000000000..fc4ac4715 --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; + +namespace Flow.Launcher.Plugin +{ + /// + /// This interface is to indicate and allow plugins to asyncronously reload their + /// in memory data cache or other mediums when user makes a new change + /// that is not immediately captured. For example, for BrowserBookmark and Program + /// plugin does not automatically detect when a user added a new bookmark or program, + /// so this interface's function is exposed to allow user manually do the reloading after + /// those new additions. + /// + /// The command that allows user to manual reload is exposed via Plugin.Sys, and + /// it will call the plugins that have implemented this interface. + /// + public interface IAsyncReloadable + { + Task ReloadDataAsync(); + } +} diff --git a/Flow.Launcher.Plugin/Interfaces/IReloadable.cs b/Flow.Launcher.Plugin/Interfaces/IReloadable.cs index 29b3c15c9..31611519c 100644 --- a/Flow.Launcher.Plugin/Interfaces/IReloadable.cs +++ b/Flow.Launcher.Plugin/Interfaces/IReloadable.cs @@ -1,7 +1,7 @@ namespace Flow.Launcher.Plugin { /// - /// This interface is to indicate and allow plugins to reload their + /// This interface is to indicate and allow plugins to synchronously reload their /// in memory data cache or other mediums when user makes a new change /// that is not immediately captured. For example, for BrowserBookmark and Program /// plugin does not automatically detect when a user added a new bookmark or program, @@ -10,6 +10,10 @@ /// /// The command that allows user to manual reload is exposed via Plugin.Sys, and /// it will call the plugins that have implemented this interface. + /// + /// + /// If requiring reloading data asynchronously, please use the IAsyncReloadable interface + /// /// public interface IReloadable { diff --git a/Flow.Launcher.Infrastructure/Storage/ISavable.cs b/Flow.Launcher.Plugin/Interfaces/ISavable.cs similarity index 81% rename from Flow.Launcher.Infrastructure/Storage/ISavable.cs rename to Flow.Launcher.Plugin/Interfaces/ISavable.cs index 8294a3df8..7c1110e0e 100644 --- a/Flow.Launcher.Infrastructure/Storage/ISavable.cs +++ b/Flow.Launcher.Plugin/Interfaces/ISavable.cs @@ -1,4 +1,4 @@ -namespace Flow.Launcher.Infrastructure.Storage +namespace Flow.Launcher.Plugin { /// /// Save plugin settings/cache, diff --git a/Flow.Launcher.Plugin/PluginInitContext.cs b/Flow.Launcher.Plugin/PluginInitContext.cs index 49366a5c6..04f20e984 100644 --- a/Flow.Launcher.Plugin/PluginInitContext.cs +++ b/Flow.Launcher.Plugin/PluginInitContext.cs @@ -4,6 +4,16 @@ namespace Flow.Launcher.Plugin { public class PluginInitContext { + public PluginInitContext() + { + } + + public PluginInitContext(PluginMetadata currentPluginMetadata, IPublicAPI api) + { + CurrentPluginMetadata = currentPluginMetadata; + API = api; + } + public PluginMetadata CurrentPluginMetadata { get; internal set; } /// diff --git a/Flow.Launcher.Plugin/PluginMetadata.cs b/Flow.Launcher.Plugin/PluginMetadata.cs index d81b442e2..e8f5cf744 100644 --- a/Flow.Launcher.Plugin/PluginMetadata.cs +++ b/Flow.Launcher.Plugin/PluginMetadata.cs @@ -1,11 +1,10 @@ using System; using System.Collections.Generic; using System.IO; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Flow.Launcher.Plugin { - [JsonObject(MemberSerialization.OptOut)] public class PluginMetadata : BaseModel { private string _pluginDirectory; @@ -37,12 +36,15 @@ namespace Flow.Launcher.Plugin public List ActionKeywords { get; set; } public string IcoPath { get; set;} - + public override string ToString() { return Name; } + [JsonIgnore] + public int Priority { get; set; } + /// /// Init time include both plugin load time and init time /// diff --git a/Flow.Launcher.Plugin/PluginPair.cs b/Flow.Launcher.Plugin/PluginPair.cs index 910367ec6..e8954b7a0 100644 --- a/Flow.Launcher.Plugin/PluginPair.cs +++ b/Flow.Launcher.Plugin/PluginPair.cs @@ -2,7 +2,7 @@ { public class PluginPair { - public IPlugin Plugin { get; internal set; } + public object Plugin { get; internal set; } public PluginMetadata Metadata { get; internal set; } diff --git a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs index 27cd1a558..be33bd86c 100644 --- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs +++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs @@ -121,7 +121,7 @@ namespace Flow.Launcher.Plugin.SharedCommands public static void OpenPath(string fileOrFolderPath) { - var psi = new ProcessStartInfo { FileName = FileExplorerProgramName, UseShellExecute = true, Arguments = fileOrFolderPath }; + var psi = new ProcessStartInfo { FileName = FileExplorerProgramName, UseShellExecute = true, Arguments = '"' + fileOrFolderPath + '"' }; try { if (LocationExists(fileOrFolderPath) || FileExists(fileOrFolderPath)) @@ -146,31 +146,23 @@ namespace Flow.Launcher.Plugin.SharedCommands /// This checks whether a given string is a directory path or network location string. /// It does not check if location actually exists. /// - public static bool IsLocationPathString(string querySearchString) + public static bool IsLocationPathString(this string querySearchString) { - if (string.IsNullOrEmpty(querySearchString)) + if (string.IsNullOrEmpty(querySearchString) || querySearchString.Length < 3) return false; // // shared folder location, and not \\\location\ - if (querySearchString.Length >= 3 - && querySearchString.StartsWith(@"\\") - && char.IsLetter(querySearchString[2])) + if (querySearchString.StartsWith(@"\\") + && querySearchString[2] != '\\') return true; // c:\ - if (querySearchString.Length == 3 - && char.IsLetter(querySearchString[0]) + if (char.IsLetter(querySearchString[0]) && querySearchString[1] == ':' && querySearchString[2] == '\\') - return true; - - // c:\\ - if (querySearchString.Length >= 4 - && char.IsLetter(querySearchString[0]) - && querySearchString[1] == ':' - && querySearchString[2] == '\\' - && char.IsLetter(querySearchString[3])) - return true; + { + return querySearchString.Length == 3 || querySearchString[3] != '\\'; + } return false; } diff --git a/Flow.Launcher.Plugin/SharedModels/MatchResult.cs b/Flow.Launcher.Plugin/SharedModels/MatchResult.cs new file mode 100644 index 000000000..5144eb61d --- /dev/null +++ b/Flow.Launcher.Plugin/SharedModels/MatchResult.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; + +namespace Flow.Launcher.Plugin.SharedModels +{ + public class MatchResult + { + public MatchResult(bool success, SearchPrecisionScore searchPrecision) + { + Success = success; + SearchPrecision = searchPrecision; + } + + public MatchResult(bool success, SearchPrecisionScore searchPrecision, List matchData, int rawScore) + { + Success = success; + SearchPrecision = searchPrecision; + MatchData = matchData; + RawScore = rawScore; + } + + public bool Success { get; set; } + + /// + /// The final score of the match result with search precision filters applied. + /// + public int Score { get; private set; } + + /// + /// The raw calculated search score without any search precision filtering applied. + /// + private int _rawScore; + + public int RawScore + { + get { return _rawScore; } + set + { + _rawScore = value; + Score = ScoreAfterSearchPrecisionFilter(_rawScore); + } + } + + /// + /// Matched data to highlight. + /// + public List MatchData { get; set; } + + public SearchPrecisionScore SearchPrecision { get; set; } + + public bool IsSearchPrecisionScoreMet() + { + return IsSearchPrecisionScoreMet(_rawScore); + } + + private bool IsSearchPrecisionScoreMet(int rawScore) + { + return rawScore >= (int)SearchPrecision; + } + + private int ScoreAfterSearchPrecisionFilter(int rawScore) + { + return IsSearchPrecisionScoreMet(rawScore) ? rawScore : 0; + } + } + + public enum SearchPrecisionScore + { + Regular = 50, + Low = 20, + None = 0 + } +} diff --git a/Flow.Launcher.Test/FuzzyMatcherTest.cs b/Flow.Launcher.Test/FuzzyMatcherTest.cs index 468b94457..bbddcbd2a 100644 --- a/Flow.Launcher.Test/FuzzyMatcherTest.cs +++ b/Flow.Launcher.Test/FuzzyMatcherTest.cs @@ -5,6 +5,7 @@ using System.Linq; using NUnit.Framework; using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedModels; namespace Flow.Launcher.Test { @@ -37,8 +38,8 @@ namespace Flow.Launcher.Test { var listToReturn = new List(); - Enum.GetValues(typeof(StringMatcher.SearchPrecisionScore)) - .Cast() + Enum.GetValues(typeof(SearchPrecisionScore)) + .Cast() .ToList() .ForEach(x => listToReturn.Add((int)x)); @@ -92,7 +93,8 @@ namespace Flow.Launcher.Test [TestCase("cand")] [TestCase("cpywa")] [TestCase("ccs")] - public void GivenQueryString_WhenAppliedPrecisionFiltering_ThenShouldReturnGreaterThanPrecisionScoreResults(string searchTerm) + public void GivenQueryString_WhenAppliedPrecisionFiltering_ThenShouldReturnGreaterThanPrecisionScoreResults( + string searchTerm) { var results = new List(); var matcher = new StringMatcher(); @@ -108,9 +110,9 @@ namespace Flow.Launcher.Test foreach (var precisionScore in GetPrecisionScores()) { var filteredResult = results.Where(result => result.Score >= precisionScore) - .Select(result => result) - .OrderByDescending(x => x.Score) - .ToList(); + .Select(result => result) + .OrderByDescending(x => x.Score) + .ToList(); Debug.WriteLine(""); Debug.WriteLine("###############################################"); @@ -119,6 +121,7 @@ namespace Flow.Launcher.Test { Debug.WriteLine("SCORE: " + item.Score.ToString() + ", FoundString: " + item.Title); } + Debug.WriteLine("###############################################"); Debug.WriteLine(""); @@ -128,37 +131,47 @@ namespace Flow.Launcher.Test [TestCase(Chrome, Chrome, 157)] [TestCase(Chrome, LastIsChrome, 147)] - [TestCase(Chrome, HelpCureHopeRaiseOnMindEntityChrome, 25)] + [TestCase("chro", HelpCureHopeRaiseOnMindEntityChrome, 50)] + [TestCase("chr", HelpCureHopeRaiseOnMindEntityChrome, 30)] [TestCase(Chrome, UninstallOrChangeProgramsOnYourComputer, 21)] [TestCase(Chrome, CandyCrushSagaFromKing, 0)] [TestCase("sql", MicrosoftSqlServerManagementStudio, 110)] - [TestCase("sql manag", MicrosoftSqlServerManagementStudio, 121)]//double spacing intended + [TestCase("sql manag", MicrosoftSqlServerManagementStudio, 121)] //double spacing intended public void WhenGivenQueryString_ThenShouldReturn_TheDesiredScoring( string queryString, string compareString, int expectedScore) { // When, Given - var matcher = new StringMatcher(); + var matcher = new StringMatcher {UserSettingSearchPrecision = SearchPrecisionScore.Regular}; var rawScore = matcher.FuzzyMatch(queryString, compareString).RawScore; // Should - Assert.AreEqual(expectedScore, rawScore, + Assert.AreEqual(expectedScore, rawScore, $"Expected score for compare string '{compareString}': {expectedScore}, Actual: {rawScore}"); } - [TestCase("goo", "Google Chrome", StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("chr", "Google Chrome", StringMatcher.SearchPrecisionScore.Low, true)] - [TestCase("chr", "Chrome", StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("chr", "Help cure hope raise on mind entity Chrome", StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("chr", "Help cure hope raise on mind entity Chrome", StringMatcher.SearchPrecisionScore.Low, true)] - [TestCase("chr", "Candy Crush Saga from King", StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("chr", "Candy Crush Saga from King", StringMatcher.SearchPrecisionScore.None, true)] - [TestCase("ccs", "Candy Crush Saga from King", StringMatcher.SearchPrecisionScore.Low, true)] - [TestCase("cand", "Candy Crush Saga from King",StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("cand", "Help cure hope raise on mind entity Chrome", StringMatcher.SearchPrecisionScore.Regular, false)] + [TestCase("goo", "Google Chrome", SearchPrecisionScore.Regular, true)] + [TestCase("chr", "Google Chrome", SearchPrecisionScore.Low, true)] + [TestCase("chr", "Chrome", SearchPrecisionScore.Regular, true)] + [TestCase("chr", "Help cure hope raise on mind entity Chrome", SearchPrecisionScore.Regular, false)] + [TestCase("chr", "Help cure hope raise on mind entity Chrome", SearchPrecisionScore.Low, true)] + [TestCase("chr", "Candy Crush Saga from King", SearchPrecisionScore.Regular, false)] + [TestCase("chr", "Candy Crush Saga from King", SearchPrecisionScore.None, true)] + [TestCase("ccs", "Candy Crush Saga from King", SearchPrecisionScore.Low, true)] + [TestCase("cand", "Candy Crush Saga from King", SearchPrecisionScore.Regular, true)] + [TestCase("cand", "Help cure hope raise on mind entity Chrome", SearchPrecisionScore.Regular, false)] + [TestCase("vsc", VisualStudioCode, SearchPrecisionScore.Regular, true)] + [TestCase("vs", VisualStudioCode, SearchPrecisionScore.Regular, true)] + [TestCase("vc", VisualStudioCode, SearchPrecisionScore.Regular, true)] + [TestCase("vts", VisualStudioCode, SearchPrecisionScore.Regular, false)] + [TestCase("vcs", VisualStudioCode, SearchPrecisionScore.Regular, false)] + [TestCase("wt", "Windows Terminal From Microsoft Store", SearchPrecisionScore.Regular, false)] + [TestCase("vsp", "Visual Studio 2019 Preview", SearchPrecisionScore.Regular, true)] + [TestCase("vsp", "2019 Visual Studio Preview", SearchPrecisionScore.Regular, true)] + [TestCase("2019p", "Visual Studio 2019 Preview", SearchPrecisionScore.Regular, true)] public void WhenGivenDesiredPrecision_ThenShouldReturn_AllResultsGreaterOrEqual( string queryString, string compareString, - StringMatcher.SearchPrecisionScore expectedPrecisionScore, + SearchPrecisionScore expectedPrecisionScore, bool expectedPrecisionResult) { // When @@ -170,48 +183,50 @@ namespace Flow.Launcher.Test Debug.WriteLine(""); Debug.WriteLine("###############################################"); Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}"); - Debug.WriteLine($"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int)expectedPrecisionScore})"); + Debug.WriteLine( + $"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})"); Debug.WriteLine("###############################################"); Debug.WriteLine(""); // Should Assert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(), - $"Query:{queryString}{Environment.NewLine} " + - $"Compare:{compareString}{Environment.NewLine}" + + $"Query: {queryString}{Environment.NewLine} " + + $"Compare: {compareString}{Environment.NewLine}" + $"Raw Score: {matchResult.RawScore}{Environment.NewLine}" + $"Precision Score: {(int)expectedPrecisionScore}"); } - [TestCase("exce", "OverLeaf-Latex: An online LaTeX editor", StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("term", "Windows Terminal (Preview)", StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("sql s managa", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("sql' s manag", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("sql s manag", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("sql manag", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("sql", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("sql serv", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("servez", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("sql servz", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("sql serv man", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("sql studio", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("mic", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("chr", "Shutdown", StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("mssms", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("chr", "Change settings for text-to-speech and for speech recognition (if installed).", StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("ch r", "Change settings for text-to-speech and for speech recognition (if installed).", StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("a test", "This is a test", StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("test", "This is a test", StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("cod", VisualStudioCode, StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("code", VisualStudioCode, StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("codes", "Visual Studio Codes", StringMatcher.SearchPrecisionScore.Regular, true)] + [TestCase("exce", "OverLeaf-Latex: An online LaTeX editor", SearchPrecisionScore.Regular, false)] + [TestCase("term", "Windows Terminal (Preview)", SearchPrecisionScore.Regular, true)] + [TestCase("sql s managa", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, false)] + [TestCase("sql' s manag", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, false)] + [TestCase("sql s manag", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] + [TestCase("sql manag", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] + [TestCase("sql", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] + [TestCase("sql serv", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] + [TestCase("servez", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, false)] + [TestCase("sql servz", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, false)] + [TestCase("sql serv man", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] + [TestCase("sql studio", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] + [TestCase("mic", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] + [TestCase("mssms", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] + [TestCase("msms", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] + [TestCase("chr", "Shutdown", SearchPrecisionScore.Regular, false)] + [TestCase("chr", "Change settings for text-to-speech and for speech recognition (if installed).", SearchPrecisionScore.Regular, false)] + [TestCase("ch r", "Change settings for text-to-speech and for speech recognition (if installed).", SearchPrecisionScore.Regular, true)] + [TestCase("a test", "This is a test", SearchPrecisionScore.Regular, true)] + [TestCase("test", "This is a test", SearchPrecisionScore.Regular, true)] + [TestCase("cod", VisualStudioCode, SearchPrecisionScore.Regular, true)] + [TestCase("code", VisualStudioCode, SearchPrecisionScore.Regular, true)] + [TestCase("codes", "Visual Studio Codes", SearchPrecisionScore.Regular, true)] public void WhenGivenQuery_ShouldReturnResults_ContainingAllQuerySubstrings( string queryString, string compareString, - StringMatcher.SearchPrecisionScore expectedPrecisionScore, + SearchPrecisionScore expectedPrecisionScore, bool expectedPrecisionResult) { // When - var matcher = new StringMatcher { UserSettingSearchPrecision = expectedPrecisionScore }; + var matcher = new StringMatcher {UserSettingSearchPrecision = expectedPrecisionScore}; // Given var matchResult = matcher.FuzzyMatch(queryString, compareString); @@ -219,7 +234,8 @@ namespace Flow.Launcher.Test Debug.WriteLine(""); Debug.WriteLine("###############################################"); Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}"); - Debug.WriteLine($"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int)expectedPrecisionScore})"); + Debug.WriteLine( + $"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})"); Debug.WriteLine("###############################################"); Debug.WriteLine(""); @@ -238,7 +254,7 @@ namespace Flow.Launcher.Test string queryString, string compareString1, string compareString2) { // When - var matcher = new StringMatcher { UserSettingSearchPrecision = StringMatcher.SearchPrecisionScore.Regular }; + var matcher = new StringMatcher {UserSettingSearchPrecision = SearchPrecisionScore.Regular}; // Given var compareString1Result = matcher.FuzzyMatch(queryString, compareString1); @@ -247,8 +263,10 @@ namespace Flow.Launcher.Test Debug.WriteLine(""); Debug.WriteLine("###############################################"); Debug.WriteLine($"QueryString: \"{queryString}\"{Environment.NewLine}"); - Debug.WriteLine($"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}"); - Debug.WriteLine($"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}"); + Debug.WriteLine( + $"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}"); + Debug.WriteLine( + $"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}"); Debug.WriteLine("###############################################"); Debug.WriteLine(""); @@ -256,13 +274,13 @@ namespace Flow.Launcher.Test Assert.True(compareString1Result.Score > compareString2Result.Score, $"Query: \"{queryString}\"{Environment.NewLine} " + $"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" + - $"Should be greater than{ Environment.NewLine}" + + $"Should be greater than{Environment.NewLine}" + $"CompareString2: \"{compareString2}\", Score: {compareString1Result.Score}{Environment.NewLine}"); } [TestCase("vim", "Vim", "ignoreDescription", "ignore.exe", "Vim Diff", "ignoreDescription", "ignore.exe")] public void WhenMultipleResults_ExactMatchingResult_ShouldHaveGreatestScore( - string queryString, string firstName, string firstDescription, string firstExecutableName, + string queryString, string firstName, string firstDescription, string firstExecutableName, string secondName, string secondDescription, string secondExecutableName) { // Act @@ -275,15 +293,39 @@ namespace Flow.Launcher.Test var secondDescriptionMatch = matcher.FuzzyMatch(queryString, secondDescription).RawScore; var secondExecutableNameMatch = matcher.FuzzyMatch(queryString, secondExecutableName).RawScore; - var firstScore = new[] { firstNameMatch, firstDescriptionMatch, firstExecutableNameMatch }.Max(); - var secondScore = new[] { secondNameMatch, secondDescriptionMatch, secondExecutableNameMatch }.Max(); + var firstScore = new[] {firstNameMatch, firstDescriptionMatch, firstExecutableNameMatch}.Max(); + var secondScore = new[] {secondNameMatch, secondDescriptionMatch, secondExecutableNameMatch}.Max(); // Assert Assert.IsTrue(firstScore > secondScore, $"Query: \"{queryString}\"{Environment.NewLine} " + $"Name of first: \"{firstName}\", Final Score: {firstScore}{Environment.NewLine}" + - $"Should be greater than{ Environment.NewLine}" + + $"Should be greater than{Environment.NewLine}" + $"Name of second: \"{secondName}\", Final Score: {secondScore}{Environment.NewLine}"); } + + [TestCase("vsc", "Visual Studio Code", 100)] + [TestCase("jbr", "JetBrain Rider", 100)] + [TestCase("jr", "JetBrain Rider", 66)] + [TestCase("vs", "Visual Studio", 100)] + [TestCase("vs", "Visual Studio Preview", 66)] + [TestCase("vsp", "Visual Studio Preview", 100)] + [TestCase("pc", "postman canary", 100)] + [TestCase("psc", "Postman super canary", 100)] + [TestCase("psc", "Postman super Canary", 100)] + [TestCase("vsp", "Visual Studio", 0)] + [TestCase("vps", "Visual Studio", 0)] + [TestCase(Chrome, HelpCureHopeRaiseOnMindEntityChrome, 75)] + public void WhenGivenAnAcronymQuery_ShouldReturnAcronymScore(string queryString, string compareString, + int desiredScore) + { + var matcher = new StringMatcher(); + var score = matcher.FuzzyMatch(queryString, compareString).Score; + Assert.IsTrue(score == desiredScore, + $@"Query: ""{queryString}"" + CompareString: ""{compareString}"" + Score: {score} + Desired Score: {desiredScore}"); + } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs index c91144825..3d0a9a64f 100644 --- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs +++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs @@ -7,6 +7,8 @@ using Flow.Launcher.Plugin.SharedCommands; using NUnit.Framework; using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace Flow.Launcher.Test.Plugins { @@ -17,15 +19,15 @@ namespace Flow.Launcher.Test.Plugins [TestFixture] public class ExplorerTest { - private List MethodWindowsIndexSearchReturnsZeroResults(Query dummyQuery, string dummyString) + private async Task> MethodWindowsIndexSearchReturnsZeroResultsAsync(Query dummyQuery, string dummyString, CancellationToken dummyToken) { return new List(); } - private List MethodDirectoryInfoClassSearchReturnsTwoResults(Query dummyQuery, string dummyString) + private List MethodDirectoryInfoClassSearchReturnsTwoResults(Query dummyQuery, string dummyString, CancellationToken token) { - return new List - { + return new List + { new Result { Title="Result 1" @@ -58,16 +60,16 @@ namespace Flow.Launcher.Test.Plugins $"Actual: {result}{Environment.NewLine}"); } - [TestCase("C:\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\'")] - [TestCase("C:\\SomeFolder\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\'")] + [TestCase("C:\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\' ORDER BY System.FileName")] + [TestCase("C:\\SomeFolder\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\' ORDER BY System.FileName")] public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_ThenQueryShouldUseExpectedString(string folderPath, string expectedString) { // Given var queryConstructor = new QueryConstructor(new Settings()); - + //When var queryString = queryConstructor.QueryForTopLevelDirectorySearch(folderPath); - + // Then Assert.IsTrue(queryString == expectedString, $"Expected string: {expectedString}{Environment.NewLine} " + @@ -77,7 +79,7 @@ namespace Flow.Launcher.Test.Plugins [TestCase("C:\\SomeFolder\\flow.launcher.sln", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " + "FROM SystemIndex WHERE (System.FileName LIKE 'flow.launcher.sln%' " + "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033))" + - " AND directory='file:C:\\SomeFolder'")] + " AND directory='file:C:\\SomeFolder' ORDER BY System.FileName")] public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryShouldUseExpectedString( string userSearchString, string expectedString) { @@ -112,13 +114,10 @@ namespace Flow.Launcher.Test.Plugins } [TestCase("scope='file:'")] - public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryWhereRestrictionsShouldUseScopeString(string expectedString) + public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryWhereRestrictionsShouldUseScopeString(string expectedString) { - // Given - var queryConstructor = new QueryConstructor(new Settings()); - //When - var resultString = queryConstructor.QueryWhereRestrictionsForAllFilesAndFoldersSearch(); + var resultString = QueryConstructor.QueryWhereRestrictionsForAllFilesAndFoldersSearch; // Then Assert.IsTrue(resultString == expectedString, @@ -128,9 +127,9 @@ namespace Flow.Launcher.Test.Plugins [TestCase("flow.launcher.sln", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" " + "FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " + - "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:'")] + "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY System.FileName")] public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShouldUseExpectedString( - string userSearchString, string expectedString) + string userSearchString, string expectedString) { // Given var queryConstructor = new QueryConstructor(new Settings()); @@ -145,18 +144,19 @@ namespace Flow.Launcher.Test.Plugins } [TestCase] - public void GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldContinueDirectoryInfoClassSearch() + public async Task GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldContinueDirectoryInfoClassSearch() { // Given var searchManager = new SearchManager(new Settings(), new PluginInitContext()); - + // When - var results = searchManager.TopLevelDirectorySearchBehaviour( - MethodWindowsIndexSearchReturnsZeroResults, - MethodDirectoryInfoClassSearchReturnsTwoResults, - false, + var results = await searchManager.TopLevelDirectorySearchBehaviourAsync( + MethodWindowsIndexSearchReturnsZeroResultsAsync, + MethodDirectoryInfoClassSearchReturnsTwoResults, + false, new Query(), - "string not used"); + "string not used", + default); // Then Assert.IsTrue(results.Count == 2, @@ -165,18 +165,19 @@ namespace Flow.Launcher.Test.Plugins } [TestCase] - public void GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldNotContinueDirectoryInfoClassSearch() + public async Task GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldNotContinueDirectoryInfoClassSearch() { // Given var searchManager = new SearchManager(new Settings(), new PluginInitContext()); // When - var results = searchManager.TopLevelDirectorySearchBehaviour( - MethodWindowsIndexSearchReturnsZeroResults, + var results = await searchManager.TopLevelDirectorySearchBehaviourAsync( + MethodWindowsIndexSearchReturnsZeroResultsAsync, MethodDirectoryInfoClassSearchReturnsTwoResults, true, new Query(), - "string not used"); + "string not used", + default); // Then Assert.IsTrue(results.Count == 0, @@ -201,7 +202,7 @@ namespace Flow.Launcher.Test.Plugins } [TestCase("some words", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " + - "FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:'")] + "FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY System.FileName")] public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseExpectedString( string userSearchString, string expectedString) { @@ -223,7 +224,7 @@ namespace Flow.Launcher.Test.Plugins var query = new Query { ActionKeyword = "doc:", Search = "search term" }; var searchManager = new SearchManager(new Settings(), new PluginInitContext()); - + // When var result = searchManager.IsFileContentSearch(query.ActionKeyword); @@ -239,6 +240,9 @@ namespace Flow.Launcher.Test.Plugins [TestCase(@"cc:\", false)] [TestCase(@"\\\SomeNetworkLocation\", false)] [TestCase("RandomFile", false)] + [TestCase(@"c:\>*", true)] + [TestCase(@"c:\>", true)] + [TestCase(@"c:\SomeLocation\SomeOtherLocation\>", true)] public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString(string querySearchString, bool expectedResult) { // When, Given @@ -250,7 +254,7 @@ namespace Flow.Launcher.Test.Plugins $"Actual check result is {result} {Environment.NewLine}"); } - + [TestCase(@"C:\SomeFolder\SomeApp", true, @"C:\SomeFolder\")] [TestCase(@"C:\SomeFolder\SomeApp\SomeFile", true, @"C:\SomeFolder\SomeApp\")] [TestCase(@"C:\NonExistentFolder\SomeApp", false, "")] @@ -291,10 +295,10 @@ namespace Flow.Launcher.Test.Plugins } [TestCase("c:\\SomeFolder\\>", "scope='file:c:\\SomeFolder'")] - [TestCase("c:\\SomeFolder\\>SomeName", "(System.FileName LIKE 'SomeName%' " + - "OR CONTAINS(System.FileName,'\"SomeName*\"',1033)) AND " + - "scope='file:c:\\SomeFolder'")] - public void GivenWindowsIndexSearch_WhenSearchPatternHotKeyIsSearchAll_ThenQueryWhereRestrictionsShouldUseScopeString(string path, string expectedString) + [TestCase("c:\\SomeFolder\\>SomeName", "(System.FileName LIKE 'SomeName%' " + + "OR CONTAINS(System.FileName,'\"SomeName*\"',1033)) AND " + + "scope='file:c:\\SomeFolder'")] + public void GivenWindowsIndexSearch_WhenSearchPatternHotKeyIsSearchAll_ThenQueryWhereRestrictionsShouldUseScopeString(string path, string expectedString) { // Given var queryConstructor = new QueryConstructor(new Settings()); @@ -308,16 +312,14 @@ 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) { - // Given - var criteriaConstructor = new DirectoryInfoSearch(new PluginInitContext()); //When - var resultString = criteriaConstructor.ConstructSearchCriteria(path); + var resultString = DirectoryInfoSearch.ConstructSearchCriteria(path); // Then Assert.IsTrue(resultString == expectedString, diff --git a/Flow.Launcher.sln b/Flow.Launcher.sln index 4d8997177..21c3b47dc 100644 --- a/Flow.Launcher.sln +++ b/Flow.Launcher.sln @@ -20,7 +20,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher", "Flow.Launc {F9C4C081-4CC3-4146-95F1-E102B4E10A5F} = {F9C4C081-4CC3-4146-95F1-E102B4E10A5F} {59BD9891-3837-438A-958D-ADC7F91F6F7E} = {59BD9891-3837-438A-958D-ADC7F91F6F7E} {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0} = {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0} - {F35190AA-4758-4D9E-A193-E3BDF6AD3567} = {F35190AA-4758-4D9E-A193-E3BDF6AD3567} {9B130CC5-14FB-41FF-B310-0A95B6894C37} = {9B130CC5-14FB-41FF-B310-0A95B6894C37} {FDED22C8-B637-42E8-824A-63B5B6E05A3A} = {FDED22C8-B637-42E8-824A-63B5B6E05A3A} {A3DCCBCA-ACC1-421D-B16E-210896234C26} = {A3DCCBCA-ACC1-421D-B16E-210896234C26} @@ -44,8 +43,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Sys", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Url", "Plugins\Flow.Launcher.Plugin.Url\Flow.Launcher.Plugin.Url.csproj", "{A3DCCBCA-ACC1-421D-B16E-210896234C26}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Color", "Plugins\Flow.Launcher.Plugin.Color\Flow.Launcher.Plugin.Color.csproj", "{F35190AA-4758-4D9E-A193-E3BDF6AD3567}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FFD651C7-0546-441F-BC8C-D4EE8FD01EA7}" ProjectSection(SolutionItems) = preProject .gitattributes = .gitattributes @@ -214,18 +211,6 @@ Global {A3DCCBCA-ACC1-421D-B16E-210896234C26}.Release|x64.Build.0 = Release|Any CPU {A3DCCBCA-ACC1-421D-B16E-210896234C26}.Release|x86.ActiveCfg = Release|Any CPU {A3DCCBCA-ACC1-421D-B16E-210896234C26}.Release|x86.Build.0 = Release|Any CPU - {F35190AA-4758-4D9E-A193-E3BDF6AD3567}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F35190AA-4758-4D9E-A193-E3BDF6AD3567}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F35190AA-4758-4D9E-A193-E3BDF6AD3567}.Debug|x64.ActiveCfg = Debug|Any CPU - {F35190AA-4758-4D9E-A193-E3BDF6AD3567}.Debug|x64.Build.0 = Debug|Any CPU - {F35190AA-4758-4D9E-A193-E3BDF6AD3567}.Debug|x86.ActiveCfg = Debug|Any CPU - {F35190AA-4758-4D9E-A193-E3BDF6AD3567}.Debug|x86.Build.0 = Debug|Any CPU - {F35190AA-4758-4D9E-A193-E3BDF6AD3567}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F35190AA-4758-4D9E-A193-E3BDF6AD3567}.Release|Any CPU.Build.0 = Release|Any CPU - {F35190AA-4758-4D9E-A193-E3BDF6AD3567}.Release|x64.ActiveCfg = Release|Any CPU - {F35190AA-4758-4D9E-A193-E3BDF6AD3567}.Release|x64.Build.0 = Release|Any CPU - {F35190AA-4758-4D9E-A193-E3BDF6AD3567}.Release|x86.ActiveCfg = Release|Any CPU - {F35190AA-4758-4D9E-A193-E3BDF6AD3567}.Release|x86.Build.0 = Release|Any CPU {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}.Debug|Any CPU.Build.0 = Debug|Any CPU {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -309,7 +294,6 @@ Global {FDED22C8-B637-42E8-824A-63B5B6E05A3A} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} {0B9DE348-9361-4940-ADB6-F5953BFFCCEC} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} {A3DCCBCA-ACC1-421D-B16E-210896234C26} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} - {F35190AA-4758-4D9E-A193-E3BDF6AD3567} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} {9B130CC5-14FB-41FF-B310-0A95B6894C37} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} {59BD9891-3837-438A-958D-ADC7F91F6F7E} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} diff --git a/Flow.Launcher/App.xaml b/Flow.Launcher/App.xaml index f3347d7fb..18addac73 100644 --- a/Flow.Launcher/App.xaml +++ b/Flow.Launcher/App.xaml @@ -3,7 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ui="http://schemas.modernwpf.com/2019" ShutdownMode="OnMainWindowClose" - Startup="OnStartup"> + Startup="OnStartupAsync"> diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 59bdbc896..7c4c6a367 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -45,9 +45,9 @@ namespace Flow.Launcher } } - private void OnStartup(object sender, StartupEventArgs e) + private async void OnStartupAsync(object sender, StartupEventArgs e) { - Stopwatch.Normal("|App.OnStartup|Startup cost", () => + await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => { _portable.PreStartCleanUpAfterPortabilityUpdate(); @@ -61,6 +61,8 @@ namespace Flow.Launcher _settingsVM = new SettingWindowViewModel(_updater, _portable); _settings = _settingsVM.Settings; + Http.Proxy = _settings.Proxy; + _alphabet.Initialize(_settings); _stringMatcher = new StringMatcher(_alphabet); StringMatcher.Instance = _stringMatcher; @@ -68,9 +70,10 @@ 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; @@ -84,8 +87,6 @@ namespace Flow.Launcher ThemeManager.Instance.Settings = _settings; ThemeManager.Instance.ChangeTheme(_settings.Theme); - Http.Proxy = _settings.Proxy; - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); RegisterExitEvents(); diff --git a/Flow.Launcher/CustomQueryHotkeySetting.xaml b/Flow.Launcher/CustomQueryHotkeySetting.xaml index 5f4cdff19..a97f90733 100644 --- a/Flow.Launcher/CustomQueryHotkeySetting.xaml +++ b/Flow.Launcher/CustomQueryHotkeySetting.xaml @@ -5,7 +5,7 @@ Icon="Images\app.png" ResizeMode="NoResize" WindowStartupLocation="CenterScreen" - Title="Custom Plugin Hotkey" Height="200" Width="674.766"> + Title="{DynamicResource customeQueryHotkeyTitle}" Height="200" Width="674.766"> diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index 8548ba39e..20e847f82 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -60,6 +60,15 @@ Designer PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + @@ -78,7 +87,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + @@ -87,116 +97,7 @@ - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - - - - - \ No newline at end of file diff --git a/Flow.Launcher/Images/app.ico b/Flow.Launcher/Images/app.ico new file mode 100644 index 000000000..36b1d22d0 Binary files /dev/null and b/Flow.Launcher/Images/app.ico differ diff --git a/Flow.Launcher/Images/mainsearch.svg b/Flow.Launcher/Images/mainsearch.svg new file mode 100644 index 000000000..5d28abdb3 --- /dev/null +++ b/Flow.Launcher/Images/mainsearch.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index adb49b65d..a036949fc 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -17,6 +17,7 @@ Flow Launcher Settings General + Portable Mode Start Flow Launcher on system startup Hide Flow Launcher when focus is lost Do not show new version notifications @@ -39,10 +40,13 @@ Plugin Find more plugins + Enable Disable Action keyword: Current action keyword: New action keyword: + Current Priority: + New Priority: Plugin Directory Author Init time: @@ -72,7 +76,7 @@ Are you sure you want to delete {0} plugin hotkey? Query window shadow effect Shadow effect has a substantial usage of GPU. - Not recommended if you computer performance is limited. + Not recommended if your computer performance is limited. HTTP Proxy @@ -104,6 +108,10 @@ Release Notes + + Greater the number, the higher the result will be ranked. Try setting it as 5. If you want the results to be lower than any other plugin's, provide a negative number + Please provide an valid integer for Priority! + Old Action Keyword New Action Keyword @@ -113,9 +121,11 @@ New Action Keyword can't be empty This new Action Keyword is already assigned to another plugin, please choose a different one Success + Completed successfully Use * if you don't want to specify an action keyword + Custom Plugin Hotkey Preview Hotkey is unavailable, please select a new hotkey Invalid plugin hotkey @@ -140,11 +150,23 @@ Failed to send report Flow Launcher got an error + + Please wait... + + Checking for new update + You already have the latest Flow Launcher version + Update found + Updating... + Flow Launcher was not able to move your user profile data to the new update version. + Please manually move your profile data folder from {0} to {1} + New Update New Flow Launcher release {0} is now available An error occurred while trying to install software updates Update Cancel + Update Failed + Check your connection and try updating proxy settings to github-cloud.s3.amazonaws.com. This upgrade will restart Flow Launcher Following files will be updated Update files diff --git a/Flow.Launcher/Languages/sk.xaml b/Flow.Launcher/Languages/sk.xaml index bf001d507..7f1b1739d 100644 --- a/Flow.Launcher/Languages/sk.xaml +++ b/Flow.Launcher/Languages/sk.xaml @@ -17,6 +17,7 @@ Nastavenia Flow Launchera Všeobecné + Prenosný režim Spustiť Flow Launcher po štarte systému Schovať Flow Launcher po strate fokusu Nezobrazovať upozornenia na novú verziu @@ -39,14 +40,17 @@ Plugin Nájsť ďalšie pluginy - Zakázať + Povolené + Zakázané Skratka akcie Aktuálna akcia skratky: Nová akcia skratky: + Aktuálna priorita: + Nová priorita: Priečinok s pluginmi Autor - Príprava: {0}ms - Čas dopytu: {0}ms + Príprava: + Čas dopytu: Motív @@ -104,6 +108,10 @@ Poznámky k vydaniu + + Vyššie číslo znamená, že výsledok bude vyššie. Skúste nastaviť napr. 5. Ak chcete, aby boli výsledky nižšie ako ktorékoľvek iné doplnky, zadajte záporné číslo + Prosím, zadajte platné číslo pre prioritu! + Stará skratka akcie Nová skratka akcie @@ -116,6 +124,7 @@ Použite * ak nechcete určiť skratku pre akciu + Vlastná klávesová skratka pre plugin Náhľad Klávesová skratka je nedostupná, prosím, zadajte novú Neplatná klávesová skratka pluginu @@ -140,11 +149,23 @@ Odoslanie hlásenia zlyhalo Flow Launcher zaznamenal chybu + + Čakajte, prosím… + - Je dostupná nová verzia Flow Launcher {0} + Kontrolujú sa akutalizácie + Už máte najnovšiu verizu Flow Launchera + Bola nájdená aktualizácia + Aktualizuje sa… + Flow Launcher nedokázal presunúť používateľské údaje do aktualizovanej verzie. + Prosím, presuňte profilový priečinok „data“ z {0} do {1} + Nová aktualizácia + Je dostupná nová verzia Flow Launchera {0} Počas inštalácie aktualizácií došlo k chybe Aktualizovať Zrušiť + Aktualizácia zlyhala + Skontrolujte pripojenie a skúste aktualizovať nastavenia servera proxy na github-cloud.s3.amazonaws.com. Tento upgrade reštartuje Flow Launcher Nasledujúce súbory budú aktualizované Aktualizovať súbory diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 07bb96339..aa0240dd4 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -6,6 +6,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:converters="clr-namespace:Flow.Launcher.Converters" + xmlns:svgc="http://sharpvectors.codeplex.com/svgc/" mc:Ignorable="d" Title="Flow Launcher" Topmost="True" @@ -33,6 +34,7 @@ + @@ -92,10 +94,11 @@ - + diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 3812b4e1f..04a1063f8 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -26,6 +26,7 @@ namespace Flow.Launcher #region Private Fields private readonly Storyboard _progressBarStoryboard = new Storyboard(); + private bool isProgressBarStoryboardPaused; private Settings _settings; private NotifyIcon _notifyIcon; private MainViewModel _viewModel; @@ -52,7 +53,7 @@ namespace Flow.Launcher private void OnInitialized(object sender, EventArgs e) { - + } private void OnLoaded(object sender, RoutedEventArgs _) @@ -73,7 +74,7 @@ namespace Flow.Launcher { if (e.PropertyName == nameof(MainViewModel.MainWindowVisibility)) { - if (Visibility == Visibility.Visible) + if (_viewModel.MainWindowVisibility == Visibility.Visible) { Activate(); QueryTextBox.Focus(); @@ -84,7 +85,34 @@ namespace Flow.Launcher QueryTextBox.SelectAll(); _viewModel.LastQuerySelected = true; } + + if (_viewModel.ProgressBarVisibility == Visibility.Visible && isProgressBarStoryboardPaused) + { + _progressBarStoryboard.Resume(); + isProgressBarStoryboardPaused = false; + } } + else if (!isProgressBarStoryboardPaused) + { + _progressBarStoryboard.Pause(); + isProgressBarStoryboardPaused = true; + } + } + else if (e.PropertyName == nameof(MainViewModel.ProgressBarVisibility)) + { + Dispatcher.Invoke(() => + { + if (_viewModel.ProgressBarVisibility == Visibility.Hidden && !isProgressBarStoryboardPaused) + { + _progressBarStoryboard.Pause(); + isProgressBarStoryboardPaused = true; + } + else if (_viewModel.MainWindowVisibility == Visibility.Visible && isProgressBarStoryboardPaused) + { + _progressBarStoryboard.Resume(); + isProgressBarStoryboardPaused = false; + } + }, System.Windows.Threading.DispatcherPriority.Render); } }; _settings.PropertyChanged += (o, e) => @@ -170,6 +198,7 @@ namespace Flow.Launcher _progressBarStoryboard.RepeatBehavior = RepeatBehavior.Forever; ProgressBar.BeginStoryboard(_progressBarStoryboard); _viewModel.ProgressBarVisibility = Visibility.Hidden; + isProgressBarStoryboardPaused = true; } private void OnMouseDown(object sender, MouseButtonEventArgs e) diff --git a/Flow.Launcher/Msg.xaml.cs b/Flow.Launcher/Msg.xaml.cs index 4129ce28b..6bb2fc2dc 100644 --- a/Flow.Launcher/Msg.xaml.cs +++ b/Flow.Launcher/Msg.xaml.cs @@ -66,7 +66,7 @@ namespace Flow.Launcher } if (!File.Exists(iconPath)) { - imgIco.Source = ImageLoader.Load(Path.Combine(Infrastructure.Constant.ProgramDirectory, "Images\\app.png")); + imgIco.Source = ImageLoader.Load(Path.Combine(Constant.ProgramDirectory, "Images\\app.png")); } else { imgIco.Source = ImageLoader.Load(iconPath); diff --git a/Flow.Launcher/PriorityChangeWindow.xaml b/Flow.Launcher/PriorityChangeWindow.xaml new file mode 100644 index 000000000..68b5a49b7 --- /dev/null +++ b/Flow.Launcher/PriorityChangeWindow.xaml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/PriorityChangeWindow.xaml.cs b/Flow.Launcher/PriorityChangeWindow.xaml.cs new file mode 100644 index 000000000..0adb1f080 --- /dev/null +++ b/Flow.Launcher/PriorityChangeWindow.xaml.cs @@ -0,0 +1,69 @@ +using Flow.Launcher.Core.Plugin; +using Flow.Launcher.Core.Resource; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Flow.Launcher.ViewModel; +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Shapes; + +namespace Flow.Launcher +{ + /// + /// Interaction Logic of PriorityChangeWindow.xaml + /// + public partial class PriorityChangeWindow : Window + { + private readonly PluginPair plugin; + private Settings settings; + private readonly Internationalization translater = InternationalizationManager.Instance; + private readonly PluginViewModel pluginViewModel; + + public PriorityChangeWindow(string pluginId, Settings settings, PluginViewModel pluginViewModel) + { + InitializeComponent(); + plugin = PluginManager.GetPluginForId(pluginId); + this.settings = settings; + this.pluginViewModel = pluginViewModel; + if (plugin == null) + { + MessageBox.Show(translater.GetTranslation("cannotFindSpecifiedPlugin")); + Close(); + } + } + + private void BtnCancel_OnClick(object sender, RoutedEventArgs e) + { + Close(); + } + + private void btnDone_OnClick(object sender, RoutedEventArgs e) + { + if (int.TryParse(tbAction.Text.Trim(), out var newPriority)) + { + pluginViewModel.ChangePriority(newPriority); + Close(); + } + else + { + string msg = translater.GetTranslation("invalidPriority"); + MessageBox.Show(msg); + } + + } + + private void PriorityChangeWindow_Loaded(object sender, RoutedEventArgs e) + { + OldPriority.Text = pluginViewModel.Priority.ToString(); + tbAction.Focus(); + } + } +} \ No newline at end of file diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index 90d4fff63..9b41c2c05 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -5,7 +5,6 @@ using System.Net; using System.Threading.Tasks; using System.Windows; using Squirrel; -using Flow.Launcher.Core; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Core.Resource; using Flow.Launcher.Helper; @@ -14,6 +13,14 @@ using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Plugin; using Flow.Launcher.ViewModel; +using Flow.Launcher.Plugin.SharedModels; +using System.Threading; +using System.IO; +using Flow.Launcher.Infrastructure.Http; +using JetBrains.Annotations; +using System.Runtime.CompilerServices; +using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.Storage; namespace Flow.Launcher { @@ -57,18 +64,15 @@ namespace Flow.Launcher // which will cause ungraceful exit SaveAppAllSettings(); + // Restart requires Squirrel's Update.exe to be present in the parent folder, + // it is only published from the project's release pipeline. When debugging without it, + // the project may not restart or just terminates. This is expected. UpdateManager.RestartApp(Constant.ApplicationFileName); } - public void RestarApp() - { - RestartApp(); - } + public void RestarApp() => RestartApp(); - public void CheckForNewUpdate() - { - _settingsVM.UpdateApp(); - } + public void CheckForNewUpdate() => _settingsVM.UpdateApp(); public void SaveAppAllSettings() { @@ -78,21 +82,15 @@ namespace Flow.Launcher ImageLoader.Save(); } - public void ReloadAllPluginData() - { - PluginManager.ReloadData(); - } + public Task ReloadAllPluginData() => PluginManager.ReloadData(); - public void ShowMsg(string title, string subTitle = "", string iconPath = "") - { - ShowMsg(title, subTitle, iconPath, true); - } + public void ShowMsg(string title, string subTitle = "", string iconPath = "") => ShowMsg(title, subTitle, iconPath, true); public void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true) { 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); }); } @@ -105,25 +103,37 @@ namespace Flow.Launcher }); } - public void StartLoadingBar() - { - _mainVM.ProgressBarVisibility = Visibility.Visible; - } + public void StartLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Visible; - public void StopLoadingBar() - { - _mainVM.ProgressBarVisibility = Visibility.Collapsed; - } + public void StopLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Collapsed; - public string GetTranslation(string key) - { - return InternationalizationManager.Instance.GetTranslation(key); - } + public string GetTranslation(string key) => InternationalizationManager.Instance.GetTranslation(key); - public List GetAllPlugins() - { - return PluginManager.AllPlugins.ToList(); - } + public List GetAllPlugins() => PluginManager.AllPlugins.ToList(); + + public MatchResult FuzzySearch(string query, string stringToCompare) => StringMatcher.FuzzySearch(query, stringToCompare); + + public Task HttpGetStringAsync(string url, CancellationToken token = default) => Http.GetAsync(url); + + public Task HttpGetStreamAsync(string url, CancellationToken token = default) => Http.GetStreamAsync(url); + + public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default) => Http.DownloadAsync(url, filePath, token); + + public void AddActionKeyword(string pluginId, string newActionKeyword) => PluginManager.AddActionKeyword(pluginId, newActionKeyword); + + public void RemoveActionKeyword(string pluginId, string oldActionKeyword) => PluginManager.RemoveActionKeyword(pluginId, oldActionKeyword); + + public void LogDebug(string className, string message, [CallerMemberName] string methodName = "") => Log.Debug(className, message, methodName); + + public void LogInfo(string className, string message, [CallerMemberName] string methodName = "") => Log.Info(className, message, methodName); + + public void LogWarn(string className, string message, [CallerMemberName] string methodName = "") => Log.Warn(className, message, methodName); + + public void LogException(string className, string message, Exception e, [CallerMemberName] string methodName = "") => Log.Exception(className, message, e, methodName); + + public T LoadJsonStorage() where T : new() => new PluginJsonStorage().Load(); + + public void SaveJsonStorage(T setting) where T : new() => new PluginJsonStorage(setting).Save(); public event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent; @@ -139,6 +149,7 @@ namespace Flow.Launcher } return true; } + #endregion } } diff --git a/Flow.Launcher/ResultListBox.xaml b/Flow.Launcher/ResultListBox.xaml index 072196605..2f9d06d81 100644 --- a/Flow.Launcher/ResultListBox.xaml +++ b/Flow.Launcher/ResultListBox.xaml @@ -9,7 +9,7 @@ d:DataContext="{d:DesignInstance vm:ResultsViewModel}" MaxHeight="{Binding MaxHeight}" SelectedIndex="{Binding SelectedIndex, Mode=TwoWay}" - SelectedItem="{Binding SelectedItem, Mode=OneWayToSource}" + SelectedItem="{Binding SelectedItem, Mode=TwoWay}" HorizontalContentAlignment="Stretch" ItemsSource="{Binding Results}" Margin="{Binding Margin}" Visibility="{Binding Visbility}" diff --git a/Flow.Launcher/SettingWindow.xaml b/Flow.Launcher/SettingWindow.xaml index 32f9e9a6e..cb0ef2def 100644 --- a/Flow.Launcher/SettingWindow.xaml +++ b/Flow.Launcher/SettingWindow.xaml @@ -8,9 +8,10 @@ xmlns:userSettings="clr-namespace:Flow.Launcher.Infrastructure.UserSettings;assembly=Flow.Launcher.Infrastructure" xmlns:sys="clr-namespace:System;assembly=mscorlib" xmlns:ui="http://schemas.modernwpf.com/2019" + xmlns:svgc="http://sharpvectors.codeplex.com/svgc/" x:Class="Flow.Launcher.SettingWindow" mc:Ignorable="d" - Icon="Images\app.png" + Icon="Images\app.ico" Title="{DynamicResource flowlauncher_settings}" ResizeMode="NoResize" WindowStartupLocation="CenterScreen" @@ -36,7 +37,7 @@ - + @@ -165,17 +166,21 @@ - - + + + Margin="5 0 0 0"/> - + @@ -429,8 +435,8 @@ - - + + diff --git a/Flow.Launcher/SettingWindow.xaml.cs b/Flow.Launcher/SettingWindow.xaml.cs index e5583da33..a922b4d67 100644 --- a/Flow.Launcher/SettingWindow.xaml.cs +++ b/Flow.Launcher/SettingWindow.xaml.cs @@ -206,7 +206,16 @@ namespace Flow.Launcher { var id = viewModel.SelectedPlugin.PluginPair.Metadata.ID; // used to sync the current status from the plugin manager into the setting to keep consistency after save - settings.PluginSettings.Plugins[id].Disabled = viewModel.SelectedPlugin.PluginPair.Metadata.Disabled; + settings.PluginSettings.Plugins[id].Disabled = viewModel.SelectedPlugin.PluginPair.Metadata.Disabled; + } + + private void OnPluginPriorityClick(object sender, MouseButtonEventArgs e) + { + if (e.ChangedButton == MouseButton.Left) + { + PriorityChangeWindow priorityChangeWindow = new PriorityChangeWindow(viewModel.SelectedPlugin.PluginPair.Metadata.ID, settings, viewModel.SelectedPlugin); + priorityChangeWindow.ShowDialog(); + } } private void OnPluginActionKeywordsClick(object sender, MouseButtonEventArgs e) @@ -281,5 +290,6 @@ namespace Flow.Launcher { FilesFolders.OpenPath(Path.Combine(DataLocation.DataDirectory(), Constant.Themes)); } + } -} +} \ No newline at end of file diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs index de3bcaa22..2b2103605 100644 --- a/Flow.Launcher/Storage/QueryHistory.cs +++ b/Flow.Launcher/Storage/QueryHistory.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json; using Flow.Launcher.Plugin; namespace Flow.Launcher.Storage diff --git a/Flow.Launcher/Storage/TopMostRecord.cs b/Flow.Launcher/Storage/TopMostRecord.cs index c110bdf92..c92ef4956 100644 --- a/Flow.Launcher/Storage/TopMostRecord.cs +++ b/Flow.Launcher/Storage/TopMostRecord.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json; +using System.Text.Json; +using System.Text.Json.Serialization; using Flow.Launcher.Plugin; namespace Flow.Launcher.Storage @@ -8,21 +9,24 @@ namespace Flow.Launcher.Storage // todo this class is not thread safe.... but used from multiple threads. public class TopMostRecord { - [JsonProperty] - private Dictionary records = new Dictionary(); + /// + /// You should not directly access this field + /// + /// It is public due to System.Text.Json limitation in version 3.1 + /// + /// + /// TODO: Set it to private + public Dictionary records { get; set; } = new Dictionary(); internal bool IsTopMost(Result result) { - if (records.Count == 0) + if (records.Count == 0 || !records.ContainsKey(result.OriginQuery.RawQuery)) { return false; } - // since this dictionary should be very small (or empty) going over it should be pretty fast. - return records.Any(o => o.Value.Title == result.Title - && o.Value.SubTitle == result.SubTitle - && o.Value.PluginID == result.PluginID - && o.Key == result.OriginQuery.RawQuery); + // since this dictionary should be very small (or empty) going over it should be pretty fast. + return records[result.OriginQuery.RawQuery].Equals(result); } internal void Remove(Result result) @@ -54,5 +58,12 @@ namespace Flow.Launcher.Storage public string Title { get; set; } public string SubTitle { get; set; } public string PluginID { get; set; } + + public bool Equals(Result r) + { + return Title == r.Title + && SubTitle == r.SubTitle + && PluginID == r.PluginID; + } } } diff --git a/Flow.Launcher/Storage/UserSelectedRecord.cs b/Flow.Launcher/Storage/UserSelectedRecord.cs index 1fda04e9b..bc7a2da73 100644 --- a/Flow.Launcher/Storage/UserSelectedRecord.cs +++ b/Flow.Launcher/Storage/UserSelectedRecord.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Plugin; @@ -7,15 +7,27 @@ namespace Flow.Launcher.Storage { public class UserSelectedRecord { - [JsonProperty] - private Dictionary records = new Dictionary(); + /// + /// You should not directly access this field + /// + /// It is public due to System.Text.Json limitation in version 3.1 + /// + /// + /// TODO: Set it to private + [JsonPropertyName("records")] + public Dictionary records { get; set; } + + public UserSelectedRecord() + { + records = new Dictionary(); + } public void Add(Result result) { var key = result.ToString(); - if (records.TryGetValue(key, out int value)) + if (records.ContainsKey(key)) { - records[key] = value + 1; + records[key]++; } else { diff --git a/Flow.Launcher/Themes/BlurBlack Darker.xaml b/Flow.Launcher/Themes/BlurBlack Darker.xaml new file mode 100644 index 000000000..5c615d500 --- /dev/null +++ b/Flow.Launcher/Themes/BlurBlack Darker.xaml @@ -0,0 +1,63 @@ + + + + + + True + + + + + + + + + + + + + + + + + #356ef3 + + + + + + diff --git a/Flow.Launcher/Themes/BlurWhite.xaml b/Flow.Launcher/Themes/BlurWhite.xaml index 1c1f2f9ec..6a130bb39 100644 --- a/Flow.Launcher/Themes/BlurWhite.xaml +++ b/Flow.Launcher/Themes/BlurWhite.xaml @@ -17,7 +17,7 @@ @@ -26,7 +26,7 @@ - + diff --git a/Flow.Launcher/Themes/Gray.xaml b/Flow.Launcher/Themes/Gray.xaml index 16a1db274..1fbaa959a 100644 --- a/Flow.Launcher/Themes/Gray.xaml +++ b/Flow.Launcher/Themes/Gray.xaml @@ -4,21 +4,19 @@ @@ -31,15 +29,15 @@ - #00AAF6 + #787878 @@ -38,7 +38,7 @@ - #3875D7 + #909090 + + + + + + + + + + + #5e81ac + + + + diff --git a/Flow.Launcher/Themes/Nord.xaml b/Flow.Launcher/Themes/Nord.xaml new file mode 100644 index 000000000..2253b3410 --- /dev/null +++ b/Flow.Launcher/Themes/Nord.xaml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + #5e81ac + + + + diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 7a3aa9f2f..c1382e51e 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; using System.Windows; using System.Windows.Input; using NHotkey; @@ -19,8 +18,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.Channels; namespace Flow.Launcher.ViewModel { @@ -48,6 +47,9 @@ namespace Flow.Launcher.ViewModel private readonly Internationalization _translator = InternationalizationManager.Instance; + private ChannelWriter _resultsUpdateChannelWriter; + private Task _resultsViewUpdateTask; + #endregion #region Constructor @@ -74,6 +76,7 @@ namespace Flow.Launcher.ViewModel _selectedResults = Results; InitializeKeyCommands(); + RegisterViewUpdate(); RegisterResultsUpdatedEvent(); SetHotkey(_settings.Hotkey, OnHotkey); @@ -81,6 +84,47 @@ namespace Flow.Launcher.ViewModel SetOpenResultModifiers(); } + private void RegisterViewUpdate() + { + var resultUpdateChannel = Channel.CreateUnbounded(); + _resultsUpdateChannelWriter = resultUpdateChannel.Writer; + _resultsViewUpdateTask = + Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted); + + async Task updateAction() + { + var queue = new Dictionary(); + var channelReader = resultUpdateChannel.Reader; + + // it is not supposed to be false because it won't be complete + while (await channelReader.WaitToReadAsync()) + { + await Task.Delay(20); + while (channelReader.TryRead(out var item)) + { + if (!item.Token.IsCancellationRequested) + queue[item.ID] = item; + } + + UpdateResultView(queue.Values); + queue.Clear(); + } + + Log.Error("MainViewModel", "Unexpected ResultViewUpdate ends"); + }; + + void continueAction(Task t) + { +#if DEBUG + throw t.Exception; +#else + Log.Error($"Error happen in task dealing with viewupdate for results. {t.Exception}"); + _resultsViewUpdateTask = + Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted); +#endif + } + } + private void RegisterResultsUpdatedEvent() { foreach (var pair in PluginManager.GetPluginsForInterface()) @@ -88,11 +132,14 @@ namespace Flow.Launcher.ViewModel var plugin = (IResultUpdated)pair.Plugin; plugin.ResultsUpdated += (s, e) => { - Task.Run(() => + if (e.Query.RawQuery == QueryText) // TODO: allow cancellation { PluginManager.UpdatePluginMetadata(e.Results, pair.Metadata, e.Query); - UpdateResultView(e.Results, pair.Metadata, e.Query); - }, _updateToken); + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(e.Results, pair.Metadata, e.Query, _updateToken))) + { + Log.Error("MainViewModel", "Unable to add item to Result Update Queue"); + }; + } }; } } @@ -112,25 +159,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()); @@ -197,6 +232,25 @@ namespace Flow.Launcher.ViewModel SelectedResults = Results; } }); + + ReloadPluginDataCommand = new RelayCommand(_ => + { + var msg = new Msg { Owner = Application.Current.MainWindow }; + + MainWindowVisibility = Visibility.Collapsed; + + PluginManager + .ReloadData() + .ContinueWith(_ => + Application.Current.Dispatcher.Invoke(() => + { + msg.Show( + InternationalizationManager.Instance.GetTranslation("success"), + InternationalizationManager.Instance.GetTranslation("completedSuccessfully"), + ""); + })) + .ConfigureAwait(false); + }); } #endregion @@ -208,9 +262,10 @@ namespace Flow.Launcher.ViewModel public ResultsViewModel History { get; private set; } private string _queryText; + public string QueryText { - get { return _queryText; } + get => _queryText; set { _queryText = value; @@ -228,10 +283,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; } @@ -263,6 +320,7 @@ namespace Flow.Launcher.ViewModel QueryText = string.Empty; } } + _selectedResults.Visbility = Visibility.Visible; } } @@ -281,10 +339,11 @@ namespace Flow.Launcher.ViewModel public ICommand LoadContextMenuCommand { get; set; } public ICommand LoadHistoryCommand { get; set; } public ICommand OpenResultCommand { get; set; } + public ICommand ReloadPluginDataCommand { get; set; } public string OpenResultCommandModifiers { get; private set; } - public ImageSource Image => ImageLoader.Load(Constant.QueryTextBoxIconImagePath); + public string Image => Constant.QueryTextBoxIconImagePath; #endregion @@ -322,9 +381,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 @@ -378,90 +448,131 @@ 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) - { - var results = PluginManager.QueryForPlugin(plugin, query); - UpdateResultView(results, plugin.Metadata, query); - } - }); + tasks[i] = QueryTask(plugins[i]); + } + else + { + tasks[i] = Task.CompletedTask; // Avoid Null + } } - catch (OperationCanceledException) - { - // nothing to do here - } - - // this should happen once after all queries are done so progress bar should continue - // until the end of all querying - _isQueryRunning = false; - if (currentUpdateSource == _updateSource) - { // update to hidden if this is still the current query - ProgressBarVisibility = Visibility.Hidden; - } - }, currentCancellationToken); - } - } - else - { - Results.Clear(); - Results.Visbility = Visibility.Collapsed; - } + // Check the code, WhenAll will translate all type of IEnumerable or Collection to Array, so make an array at first + await Task.WhenAll(tasks); + } + catch (OperationCanceledException) + { + // nothing to do here + } + + if (currentCancellationToken.IsCancellationRequested) + return; + + // this should happen once after all queries are done so progress bar should continue + // until the end of all querying + _isQueryRunning = false; + if (!currentCancellationToken.IsCancellationRequested) + { + // update to hidden if this is still the current query + ProgressBarVisibility = Visibility.Hidden; + } + + // Local function + async Task QueryTask(PluginPair plugin) + { + // Since it is wrapped within a Task.Run, the synchronous context is null + // Task.Yield will force it to run in ThreadPool + await Task.Yield(); + + var results = await PluginManager.QueryForPlugin(plugin, query, currentCancellationToken); + if (currentCancellationToken.IsCancellationRequested || results == null) return; + + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, plugin.Metadata, query, currentCancellationToken))) + { + Log.Error("MainViewModel", "Unable to add item to Result Update Queue"); + }; + } + }, currentCancellationToken) + .ContinueWith(t => Log.Exception("|MainViewModel|Plugins Query Exceptions", t.Exception), + TaskContinuationOptions.OnlyOnFaulted); } + private void RemoveOldQueryResults(Query query) { string lastKeyword = _lastQuery.ActionKeyword; + string keyword = query.ActionKeyword; if (string.IsNullOrEmpty(lastKeyword)) { if (!string.IsNullOrEmpty(keyword)) { - Results.RemoveResultsExcept(PluginManager.NonGlobalPlugins[keyword].Metadata); + Results.KeepResultsFor(PluginManager.NonGlobalPlugins[keyword].Metadata); } } else { if (string.IsNullOrEmpty(keyword)) { - Results.RemoveResultsFor(PluginManager.NonGlobalPlugins[lastKeyword].Metadata); + Results.KeepResultsExcept(PluginManager.NonGlobalPlugins[lastKeyword].Metadata); } else if (lastKeyword != keyword) { - Results.RemoveResultsExcept(PluginManager.NonGlobalPlugins[keyword].Metadata); + Results.KeepResultsFor(PluginManager.NonGlobalPlugins[keyword].Metadata); } } } @@ -499,6 +610,7 @@ namespace Flow.Launcher.ViewModel } }; } + return menu; } @@ -538,12 +650,12 @@ namespace Flow.Launcher.ViewModel return selected; } - private bool HistorySelected() { var selected = SelectedResults == History; return selected; } + #region Hotkey private void SetHotkey(string hotkeyStr, EventHandler action) @@ -562,7 +674,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); } } @@ -612,7 +725,6 @@ namespace Flow.Launcher.ViewModel { if (!ShouldIgnoreHotkeys()) { - if (_settings.LastQueryMode == LastQueryMode.Empty) { ChangeQueryText(string.Empty); @@ -666,29 +778,47 @@ namespace Flow.Launcher.ViewModel /// /// To avoid deadlock, this method should not called from main thread /// - public void UpdateResultView(List list, PluginMetadata metadata, Query originQuery) + public void UpdateResultView(IEnumerable resultsForUpdates) { - foreach (var result in list) + if (!resultsForUpdates.Any()) + return; + CancellationToken token; + + try { - if (_topMostRecord.IsTopMost(result)) + // Don't know why sometimes even resultsForUpdates is empty, the method won't return; + token = resultsForUpdates.Select(r => r.Token).Distinct().SingleOrDefault(); + } +#if DEBUG + catch + { + throw new ArgumentException("Unacceptable token"); + } +#else + catch + { + token = default; + } +#endif + + + foreach (var metaResults in resultsForUpdates) + { + foreach (var result in metaResults.Results) { - result.Score = int.MaxValue; - } - else - { - result.Score += _userSelectedRecord.GetSelectedCount(result) * 5; + if (_topMostRecord.IsTopMost(result)) + { + result.Score = int.MaxValue; + } + else + { + var priorityScore = metaResults.Metadata.Priority * 150; + result.Score += _userSelectedRecord.GetSelectedCount(result) * 5 + priorityScore; + } } } - if (originQuery.RawQuery == _lastQuery.RawQuery) - { - Results.AddResults(list, metadata.ID); - } - - if (Results.Visbility != Visibility.Visible && list.Count > 0) - { - Results.Visbility = Visibility.Visible; - } + Results.AddResults(resultsForUpdates, token); } #endregion diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs index eb7e0054d..7c8814b41 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -26,6 +26,7 @@ namespace Flow.Launcher.ViewModel public string InitilizaTime => PluginPair.Metadata.InitTime.ToString() + "ms"; public string QueryTime => PluginPair.Metadata.AvgQueryTime + "ms"; public string ActionKeywordsText => string.Join(Query.ActionKeywordSeperater, PluginPair.Metadata.ActionKeywords); + public int Priority => PluginPair.Metadata.Priority; public void ChangeActionKeyword(string newActionKeyword, string oldActionKeyword) { @@ -34,6 +35,12 @@ namespace Flow.Launcher.ViewModel OnPropertyChanged(nameof(ActionKeywordsText)); } + public void ChangePriority(int newPriority) + { + PluginPair.Metadata.Priority = newPriority; + OnPropertyChanged(nameof(Priority)); + } + public bool IsActionKeywordRegistered(string newActionKeyword) => PluginManager.ActionKeywordRegistered(newActionKeyword); } } diff --git a/Flow.Launcher/ViewModel/ResultViewModel.cs b/Flow.Launcher/ViewModel/ResultViewModel.cs index 00a0e1ae5..c91bbb107 100644 --- a/Flow.Launcher/ViewModel/ResultViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultViewModel.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using System.Windows; using System.Windows.Media; -using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; @@ -12,9 +11,9 @@ namespace Flow.Launcher.ViewModel { public class ResultViewModel : BaseModel { - public class LazyAsync : Lazy> + public class LazyAsync : Lazy> { - private T defaultValue; + private readonly T defaultValue; private readonly Action _updateCallback; public new T Value @@ -23,21 +22,27 @@ namespace Flow.Launcher.ViewModel { if (!IsValueCreated) { - base.Value.ContinueWith(_ => - { - _updateCallback(); - }); + _ = Exercute(); // manually use callback strategy return defaultValue; } - - if (!base.Value.IsCompleted || base.Value.IsFaulted) + + if (!base.Value.IsCompletedSuccessfully) return defaultValue; return base.Value.Result; + + // If none of the variables captured by the local function are captured by other lambdas, + // the compiler can avoid heap allocations. + async ValueTask Exercute() + { + await base.Value.ConfigureAwait(false); + _updateCallback(); + } + } } - public LazyAsync(Func> factory, T defaultValue, Action updateCallback) : base(factory) + public LazyAsync(Func> factory, T defaultValue, Action updateCallback) : base(factory) { if (defaultValue != null) { @@ -55,13 +60,13 @@ namespace Flow.Launcher.ViewModel Result = result; Image = new LazyAsync( - SetImage, + SetImage, ImageLoader.DefaultImage, () => { OnPropertyChanged(nameof(Image)); }); - } + } Settings = settings; } @@ -82,7 +87,7 @@ namespace Flow.Launcher.ViewModel public LazyAsync Image { get; set; } - private async Task SetImage() + private async ValueTask SetImage() { var imagePath = Result.IcoPath; if (string.IsNullOrEmpty(imagePath) && Result.Icon != null) @@ -94,19 +99,15 @@ namespace Flow.Launcher.ViewModel catch (Exception e) { Log.Exception($"|ResultViewModel.Image|IcoPath is empty and exception when calling Icon() for result <{Result.Title}> of plugin <{Result.PluginDirectory}>", e); - imagePath = Constant.MissingImgIcon; + return ImageLoader.DefaultImage; } } if (ImageLoader.CacheContainImage(imagePath)) - { // will get here either when icoPath has value\icon delegate is null\when had exception in delegate return ImageLoader.Load(imagePath); - } - else - { - return await Task.Run(() => ImageLoader.Load(imagePath)); - } + + return await Task.Run(() => ImageLoader.Load(imagePath)); } public Result Result { get; } diff --git a/Flow.Launcher/ViewModel/ResultsForUpdate.cs b/Flow.Launcher/ViewModel/ResultsForUpdate.cs new file mode 100644 index 000000000..87d526fd6 --- /dev/null +++ b/Flow.Launcher/ViewModel/ResultsForUpdate.cs @@ -0,0 +1,28 @@ +using Flow.Launcher.Plugin; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; + +namespace Flow.Launcher.ViewModel +{ + public struct ResultsForUpdate + { + public List Results { get; } + + public PluginMetadata Metadata { get; } + public string ID { get; } + + public Query Query { get; } + public CancellationToken Token { get; } + + public ResultsForUpdate(List results, PluginMetadata metadata, Query query, CancellationToken token) + { + Results = results; + Metadata = metadata; + Query = query; + Token = token; + ID = metadata.ID; + } + } +} diff --git a/Flow.Launcher/ViewModel/ResultsViewModel.cs b/Flow.Launcher/ViewModel/ResultsViewModel.cs index d30854180..41f16f4f2 100644 --- a/Flow.Launcher/ViewModel/ResultsViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultsViewModel.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Collections.Specialized; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; @@ -17,7 +20,6 @@ namespace Flow.Launcher.ViewModel public ResultCollection Results { get; } - private readonly object _addResultsLock = new object(); private readonly object _collectionLock = new object(); private readonly Settings _settings; private int MaxResults => _settings?.MaxResultsToShow ?? 6; @@ -116,17 +118,20 @@ namespace Flow.Launcher.ViewModel public void Clear() { - Results.Clear(); + lock (_collectionLock) + Results.RemoveAll(); } - public void RemoveResultsExcept(PluginMetadata metadata) + public void KeepResultsFor(PluginMetadata metadata) { - Results.RemoveAll(r => r.Result.PluginID != metadata.ID); + lock (_collectionLock) + Results.Update(Results.Where(r => r.Result.PluginID == metadata.ID).ToList()); } - public void RemoveResultsFor(PluginMetadata metadata) + public void KeepResultsExcept(PluginMetadata metadata) { - Results.RemoveAll(r => r.Result.PluginID == metadata.ID); + lock (_collectionLock) + Results.Update(Results.Where(r => r.Result.PluginID != metadata.ID).ToList()); } /// @@ -134,70 +139,70 @@ namespace Flow.Launcher.ViewModel /// public void AddResults(List newRawResults, string resultId) { - lock (_addResultsLock) + var newResults = NewResults(newRawResults, resultId); + + UpdateResults(newResults); + } + /// + /// To avoid deadlock, this method should not called from main thread + /// + public void AddResults(IEnumerable resultsForUpdates, CancellationToken token) + { + var newResults = NewResults(resultsForUpdates); + + if (token.IsCancellationRequested) + return; + + UpdateResults(newResults, token); + } + + private void UpdateResults(List newResults, CancellationToken token = default) + { + lock (_collectionLock) { - var newResults = NewResults(newRawResults, resultId); - // update UI in one run, so it can avoid UI flickering - Results.Update(newResults); + Results.Update(newResults, token); + if (Results.Any()) + SelectedItem = Results[0]; + } - if (Results.Count > 0) - { + switch (Visbility) + { + case Visibility.Collapsed when Results.Count > 0: Margin = new Thickness { Top = 8 }; SelectedIndex = 0; - } - else - { + Visbility = Visibility.Visible; + break; + case Visibility.Visible when Results.Count == 0: Margin = new Thickness { Top = 0 }; - } + Visbility = Visibility.Collapsed; + break; } } private List NewResults(List newRawResults, string resultId) { - var results = Results.ToList(); - var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings)).ToList(); - var oldResults = results.Where(r => r.Result.PluginID == resultId).ToList(); + if (newRawResults.Count == 0) + return Results; - // 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(); - // remove result of relative complement of B in A - foreach (var result in oldResults.Except(sameResults)) - { - results.Remove(result); - } + var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings)); - // update result with B's score and index position - foreach (var sameResult in sameResults) - { - int oldIndex = results.IndexOf(sameResult); - int oldScore = results[oldIndex].Result.Score; - var newResult = newResults[newResults.IndexOf(sameResult)]; - int newScore = newResult.Result.Score; - if (newScore != oldScore) - { - var oldResult = results[oldIndex]; + return Results.Where(r => r.Result.PluginID != resultId) + .Concat(newResults) + .OrderByDescending(r => r.Result.Score) + .ToList(); + } - oldResult.Result.Score = newScore; - oldResult.Result.OriginQuery = newResult.Result.OriginQuery; + private List NewResults(IEnumerable resultsForUpdates) + { + if (!resultsForUpdates.Any()) + return Results; - 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.ID == r.Result.PluginID)) + .Concat(resultsForUpdates.SelectMany(u => u.Results, (u, r) => new ResultViewModel(r, _settings))) + .OrderByDescending(rv => rv.Result.Score) + .ToList(); } #endregion @@ -232,60 +237,78 @@ namespace Flow.Launcher.ViewModel } #endregion - public class ResultCollection : ObservableCollection + public class ResultCollection : List, INotifyCollectionChanged { + private long editTime = 0; - public void RemoveAll(Predicate predicate) + private CancellationToken _token; + + public event NotifyCollectionChangedEventHandler CollectionChanged; + + + protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { - CheckReentrancy(); + CollectionChanged?.Invoke(this, e); + } - for (int i = Count - 1; i >= 0; i--) + public void BulkAddAll(List resultViews) + { + AddRange(resultViews); + + // can return because the list will be cleared next time updated, which include a reset event + if (_token.IsCancellationRequested) + return; + + // manually update event + // wpf use directx / double buffered already, so just reset all won't cause ui flickering + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + private void AddAll(List Items) + { + for (int i = 0; i < Items.Count; i++) { - if (predicate(this[i])) - { - RemoveAt(i); - } + var item = Items[i]; + if (_token.IsCancellationRequested) + return; + Add(item); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, i)); } } + public void RemoveAll(int Capacity = 512) + { + Clear(); + if (this.Capacity > 8000 && Capacity < this.Capacity) + this.Capacity = Capacity; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } /// /// Update the results collection with new results, try to keep identical results /// /// - public void Update(List newItems) + public void Update(List newItems, CancellationToken token = default) { - int newCount = newItems.Count; - int oldCount = Items.Count; - int location = newCount > oldCount ? oldCount : newCount; + _token = token; + if (Count == 0 && newItems.Count == 0 || _token.IsCancellationRequested) + return; - for (int i = 0; i < location; i++) + if (editTime < 10 || newItems.Count < 30) { - ResultViewModel oldResult = this[i]; - ResultViewModel newResult = newItems[i]; - if (!oldResult.Equals(newResult)) - { // result is not the same update it in the current index - this[i] = newResult; - } - else if (oldResult.Result.Score != newResult.Result.Score) - { - this[i].Result.Score = newResult.Result.Score; - } - } - - - if (newCount >= oldCount) - { - for (int i = oldCount; i < newCount; i++) - { - Add(newItems[i]); - } + if (Count != 0) RemoveAll(newItems.Count); + AddAll(newItems); + editTime++; + return; } else { - for (int i = oldCount - 1; i >= newCount; i--) + Clear(); + BulkAddAll(newItems); + if (Capacity > 8000 && newItems.Count < 3000) { - RemoveAt(i); + Capacity = newItems.Count; } + editTime++; } } } diff --git a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs index 853925852..98685dc1b 100644 --- a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs +++ b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs @@ -17,6 +17,7 @@ using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedModels; namespace Flow.Launcher.ViewModel { @@ -88,6 +89,7 @@ namespace Flow.Launcher.ViewModel var id = vm.PluginPair.Metadata.ID; Settings.PluginSettings.Plugins[id].Disabled = vm.PluginPair.Metadata.Disabled; + Settings.PluginSettings.Plugins[id].Priority = vm.Priority; } PluginManager.Save(); @@ -152,7 +154,7 @@ namespace Flow.Launcher.ViewModel { var precisionStrings = new List(); - var enumList = Enum.GetValues(typeof(StringMatcher.SearchPrecisionScore)).Cast().ToList(); + var enumList = Enum.GetValues(typeof(SearchPrecisionScore)).Cast().ToList(); enumList.ForEach(x => precisionStrings.Add(x.ToString())); @@ -213,7 +215,7 @@ namespace Flow.Launcher.ViewModel #region plugin - public static string Plugin => "http://www.wox.one/plugin"; + public static string Plugin => @"https://github.com/Flow-Launcher/Flow.Launcher.PluginsManifest"; public PluginViewModel SelectedPlugin { get; set; } public IList PluginViewModels @@ -438,7 +440,7 @@ namespace Flow.Launcher.ViewModel } } - public ImageSource ThemeImage => ImageLoader.Load(Constant.QueryTextBoxIconImagePath); + public string ThemeImage => Constant.QueryTextBoxIconImagePath; #endregion @@ -450,7 +452,7 @@ namespace Flow.Launcher.ViewModel #region about - public string Github => _updater.GitHubRepository; + public string Website => Constant.Website; public string ReleaseNotes => _updater.GitHubRepository + @"/releases/latest"; public static string Version => Constant.Version; public string ActivatedTimes => string.Format(_translater.GetTranslation("about_activate_times"), Settings.ActivateTimes); diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/Bookmarks.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/Bookmarks.cs index c7013aa67..60c4a0ee6 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/Bookmarks.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/Bookmarks.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Flow.Launcher.Infrastructure; +using Flow.Launcher.Plugin.SharedModels; namespace Flow.Launcher.Plugin.BrowserBookmark.Commands { diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj index 85b745a6b..d2a8736a6 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj @@ -40,14 +40,6 @@ - - Always - - - Designer - MSBuild:Compile - PreserveNewest - Always @@ -60,13 +52,16 @@ - + - + MSBuild:Compile Designer PreserveNewest + + PreserveNewest + diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Images/copylink.png b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Images/copylink.png new file mode 100644 index 000000000..0dee870b6 Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Images/copylink.png differ diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml index 3beccb5e7..f456e7495 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml @@ -12,4 +12,6 @@ New tab Set browser from path: Choose + Copy url + Copy the bookmark's url to clipboard \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs index 47493654f..b889bb0d0 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs @@ -1,6 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using System.Windows; using System.Windows.Controls; +using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Plugin.BrowserBookmark.Commands; using Flow.Launcher.Plugin.BrowserBookmark.Models; @@ -9,7 +12,7 @@ using Flow.Launcher.Plugin.SharedCommands; namespace Flow.Launcher.Plugin.BrowserBookmark { - public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, ISavable + public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, ISavable, IContextMenu { private PluginInitContext context; @@ -60,7 +63,8 @@ namespace Flow.Launcher.Plugin.BrowserBookmark } return true; - } + }, + ContextData = new BookmarkAttributes { Url = c.Url } }).Where(r => r.Score > 0); return returnList.ToList(); } @@ -84,7 +88,8 @@ namespace Flow.Launcher.Plugin.BrowserBookmark } return true; - } + }, + ContextData = new BookmarkAttributes { Url = c.Url } }).ToList(); } } @@ -115,5 +120,39 @@ namespace Flow.Launcher.Plugin.BrowserBookmark { _storage.Save(); } + + public List LoadContextMenus(Result selectedResult) + { + return new List() { + new Result + { + Title = context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copyurl_title"), + SubTitle = context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copyurl_subtitle"), + Action = _ => + { + try + { + Clipboard.SetDataObject(((BookmarkAttributes)selectedResult.ContextData).Url); + + return true; + } + catch (Exception e) + { + var message = "Failed to set url in clipboard"; + Log.Exception("Main",message, e, "LoadContextMenus"); + + context.API.ShowMsg(message); + + return false; + } + }, + IcoPath = "Images\\copylink.png" + }}; + } + + internal class BookmarkAttributes + { + internal string Url { get; set; } + } } } diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json index de4f3849b..62311f514 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json @@ -4,7 +4,7 @@ "Name": "Browser Bookmarks", "Description": "Search your browser bookmarks", "Author": "qianlifeng, Ioannis G.", - "Version": "1.3.1", + "Version": "1.4.0", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.BrowserBookmark.dll", diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj b/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj index 9e1fefdb3..1090926fc 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj @@ -11,6 +11,7 @@ true false false + en @@ -43,57 +44,14 @@ - + - - PreserveNewest - - - - - + MSBuild:Compile Designer PreserveNewest - - - - - MSBuild:Compile - Designer - PreserveNewest - - - - - - MSBuild:Compile - Designer - PreserveNewest - - - - - - MSBuild:Compile - Designer - PreserveNewest - - - - - - MSBuild:Compile - Designer - PreserveNewest - - - - - - MSBuild:Compile - Designer + PreserveNewest diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/sk.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/sk.xaml index c08f0265c..b0d84f45d 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/sk.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/sk.xaml @@ -6,7 +6,7 @@ Spracúva matematické operácie.(Skúste 5*3-2 vo flowlauncheri) Nie je číslo (NaN) Nesprávny alebo neúplný výraz (Nezabudli ste na zátvorky?) - Kopírovať toto číslo do schránky + Kopírovať výsledok do schránky Oddeľovač des. miest Oddeľovač desatinných miest použitý vo výsledku. Použiť podľa systému diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index 949911229..5b23ceacc 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -91,7 +91,7 @@ namespace Flow.Launcher.Plugin.Caculator }; } } - catch + catch (Exception) { // ignored } diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json b/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json index 709757d1a..7d9ca58d5 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json @@ -4,7 +4,7 @@ "Name": "Calculator", "Description": "Provide mathematical calculations.(Try 5*3-2 in Flow Launcher)", "Author": "cxfksword", - "Version": "1.1.3", + "Version": "1.1.4", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.Caculator.dll", diff --git a/Plugins/Flow.Launcher.Plugin.Color/Flow.Launcher.Plugin.Color.csproj b/Plugins/Flow.Launcher.Plugin.Color/Flow.Launcher.Plugin.Color.csproj deleted file mode 100644 index c7fe8271a..000000000 --- a/Plugins/Flow.Launcher.Plugin.Color/Flow.Launcher.Plugin.Color.csproj +++ /dev/null @@ -1,101 +0,0 @@ - - - - Library - netcoreapp3.1 - {F35190AA-4758-4D9E-A193-E3BDF6AD3567} - Properties - Flow.Launcher.Plugin.Color - Flow.Launcher.Plugin.Color - true - false - false - - - - true - full - false - ..\..\Output\Debug\Plugins\Flow.Launcher.Plugin.Color\ - DEBUG;TRACE - prompt - 4 - false - - - - pdbonly - true - ..\..\Output\Release\Plugins\Flow.Launcher.Plugin.Color\ - TRACE - prompt - 4 - false - - - - - PreserveNewest - - - - - - PreserveNewest - - - - - - - - - - - MSBuild:Compile - Designer - PreserveNewest - - - - - - MSBuild:Compile - Designer - PreserveNewest - - - - - - MSBuild:Compile - Designer - PreserveNewest - - - - - - MSBuild:Compile - Designer - PreserveNewest - - - - - - MSBuild:Compile - Designer - PreserveNewest - - - - - - MSBuild:Compile - Designer - PreserveNewest - - - - \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Color/Images/color.png b/Plugins/Flow.Launcher.Plugin.Color/Images/color.png deleted file mode 100644 index da28583b1..000000000 Binary files a/Plugins/Flow.Launcher.Plugin.Color/Images/color.png and /dev/null differ diff --git a/Plugins/Flow.Launcher.Plugin.Color/Languages/de.xaml b/Plugins/Flow.Launcher.Plugin.Color/Languages/de.xaml deleted file mode 100644 index 3244dee14..000000000 --- a/Plugins/Flow.Launcher.Plugin.Color/Languages/de.xaml +++ /dev/null @@ -1,8 +0,0 @@ - - - Farben - Stellt eine HEX-Farben Vorschau bereit. (Versuche #000 in Flow Launcher) - - \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Color/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Color/Languages/en.xaml deleted file mode 100644 index 85e2830db..000000000 --- a/Plugins/Flow.Launcher.Plugin.Color/Languages/en.xaml +++ /dev/null @@ -1,8 +0,0 @@ - - - Colors - Allows to preview colors using hex values.(Try #000 in Flow Launcher) - - \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Color/Languages/pl.xaml b/Plugins/Flow.Launcher.Plugin.Color/Languages/pl.xaml deleted file mode 100644 index 15525cfe9..000000000 --- a/Plugins/Flow.Launcher.Plugin.Color/Languages/pl.xaml +++ /dev/null @@ -1,8 +0,0 @@ - - - Kolory - Podgląd kolorów po wpisaniu ich kodu szesnastkowego. (Spróbuj wpisać #000 w oknie Flow Launchera) - - \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Color/Languages/sk.xaml b/Plugins/Flow.Launcher.Plugin.Color/Languages/sk.xaml deleted file mode 100644 index 4b208691a..000000000 --- a/Plugins/Flow.Launcher.Plugin.Color/Languages/sk.xaml +++ /dev/null @@ -1,8 +0,0 @@ - - - Farby - Zobrazuje náhľad farieb v HEX formáte. (Skúste #000 vo Flow Launcheri) - - \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Color/Languages/tr.xaml b/Plugins/Flow.Launcher.Plugin.Color/Languages/tr.xaml deleted file mode 100644 index f56e73526..000000000 --- a/Plugins/Flow.Launcher.Plugin.Color/Languages/tr.xaml +++ /dev/null @@ -1,8 +0,0 @@ - - - Renkler - Hex kodunu girdiğiniz renkleri görüntülemeye yarar.(#000 yazmayı deneyin) - - \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Color/Languages/zh-cn.xaml b/Plugins/Flow.Launcher.Plugin.Color/Languages/zh-cn.xaml deleted file mode 100644 index 39ede4844..000000000 --- a/Plugins/Flow.Launcher.Plugin.Color/Languages/zh-cn.xaml +++ /dev/null @@ -1,8 +0,0 @@ - - - 颜色 - 提供在Flow Launcher查询hex颜色。(尝试在Flow Launcher中输入#000) - - \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Color/Languages/zh-tw.xaml b/Plugins/Flow.Launcher.Plugin.Color/Languages/zh-tw.xaml deleted file mode 100644 index 4e7062a22..000000000 --- a/Plugins/Flow.Launcher.Plugin.Color/Languages/zh-tw.xaml +++ /dev/null @@ -1,7 +0,0 @@ - - - 顏色 - 提供在 Flow Launcher 查詢 hex 顏色。(試著在 Flow Launcher 中輸入 #000) - diff --git a/Plugins/Flow.Launcher.Plugin.Color/Main.cs b/Plugins/Flow.Launcher.Plugin.Color/Main.cs deleted file mode 100644 index a15483ebc..000000000 --- a/Plugins/Flow.Launcher.Plugin.Color/Main.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Imaging; -using System.IO; -using System.Linq; -using System.Windows; - -namespace Flow.Launcher.Plugin.Color -{ - public sealed class ColorsPlugin : IPlugin, IPluginI18n - { - private string DIR_PATH = Path.Combine(Path.GetTempPath(), @"Plugins\Colors\"); - private PluginInitContext context; - private const int IMG_SIZE = 32; - - private DirectoryInfo ColorsDirectory { get; set; } - - public ColorsPlugin() - { - if (!Directory.Exists(DIR_PATH)) - { - ColorsDirectory = Directory.CreateDirectory(DIR_PATH); - } - else - { - ColorsDirectory = new DirectoryInfo(DIR_PATH); - } - } - - public List Query(Query query) - { - var raw = query.Search; - if (!IsAvailable(raw)) return new List(0); - try - { - var cached = Find(raw); - if (cached.Length == 0) - { - var path = CreateImage(raw); - return new List - { - new Result - { - Title = raw, - IcoPath = path, - Action = _ => - { - Clipboard.SetText(raw); - return true; - } - } - }; - } - return cached.Select(x => new Result - { - Title = raw, - IcoPath = x.FullName, - Action = _ => - { - Clipboard.SetText(raw); - return true; - } - }).ToList(); - } - catch (Exception exception) - { - // todo: log - return new List(0); - } - } - - private bool IsAvailable(string query) - { - // todo: rgb, names - var length = query.Length - 1; // minus `#` sign - return query.StartsWith("#") && (length == 3 || length == 6); - } - - public FileInfo[] Find(string name) - { - var file = string.Format("{0}.png", name.Substring(1)); - return ColorsDirectory.GetFiles(file, SearchOption.TopDirectoryOnly); - } - - private string CreateImage(string name) - { - using (var bitmap = new Bitmap(IMG_SIZE, IMG_SIZE)) - using (var graphics = Graphics.FromImage(bitmap)) - { - var color = ColorTranslator.FromHtml(name); - graphics.Clear(color); - - var path = CreateFileName(name); - bitmap.Save(path, ImageFormat.Png); - return path; - } - } - - private string CreateFileName(string name) - { - return string.Format("{0}{1}.png", ColorsDirectory.FullName, name.Substring(1)); - } - - public void Init(PluginInitContext context) - { - this.context = context; - } - - - public string GetTranslatedPluginTitle() - { - return context.API.GetTranslation("flowlauncher_plugin_color_plugin_name"); - } - - public string GetTranslatedPluginDescription() - { - return context.API.GetTranslation("flowlauncher_plugin_color_plugin_description"); - } - } -} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Color/plugin.json b/Plugins/Flow.Launcher.Plugin.Color/plugin.json deleted file mode 100644 index 8c0c483ba..000000000 --- a/Plugins/Flow.Launcher.Plugin.Color/plugin.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "ID": "9B36CE6181FC47FBB597AA2C29CD9B0A", - "ActionKeyword": "*", - "Name": "Colors", - "Description": "Provide hex color preview.(Try #000 in Flow Launcher)", - "Author": "qianlifeng", - "Version": "1.1.1", - "Language": "csharp", - "Website": "https://github.com/Flow-Launcher/Flow.Launcher", - "ExecuteFileName": "Flow.Launcher.Plugin.Color.dll", - "IcoPath": "Images\\color.png" -} diff --git a/Plugins/Flow.Launcher.Plugin.ControlPanel/ControlPanelList.cs b/Plugins/Flow.Launcher.Plugin.ControlPanel/ControlPanelList.cs index fdcffb0b3..70afda536 100644 --- a/Plugins/Flow.Launcher.Plugin.ControlPanel/ControlPanelList.cs +++ b/Plugins/Flow.Launcher.Plugin.ControlPanel/ControlPanelList.cs @@ -38,7 +38,7 @@ namespace Flow.Launcher.Plugin.ControlPanel int cxDesired, int cyDesired, uint fuLoad); [DllImport("user32.dll", CharSet = CharSet.Auto)] - extern static bool DestroyIcon(IntPtr handle); + static extern bool DestroyIcon(IntPtr handle); [DllImport("kernel32.dll")] static extern IntPtr FindResource(IntPtr hModule, IntPtr lpName, IntPtr lpType); diff --git a/Plugins/Flow.Launcher.Plugin.ControlPanel/Flow.Launcher.Plugin.ControlPanel.csproj b/Plugins/Flow.Launcher.Plugin.ControlPanel/Flow.Launcher.Plugin.ControlPanel.csproj index 699737634..06969a135 100644 --- a/Plugins/Flow.Launcher.Plugin.ControlPanel/Flow.Launcher.Plugin.ControlPanel.csproj +++ b/Plugins/Flow.Launcher.Plugin.ControlPanel/Flow.Launcher.Plugin.ControlPanel.csproj @@ -45,53 +45,10 @@ - - PreserveNewest - - - - - - MSBuild:Compile - Designer + PreserveNewest - - - - - MSBuild:Compile - Designer - PreserveNewest - - - - - - MSBuild:Compile - Designer - PreserveNewest - - - - - - MSBuild:Compile - Designer - PreserveNewest - - - - - - MSBuild:Compile - Designer - PreserveNewest - - - - - + MSBuild:Compile Designer PreserveNewest diff --git a/Plugins/Flow.Launcher.Plugin.ControlPanel/plugin.json b/Plugins/Flow.Launcher.Plugin.ControlPanel/plugin.json index 4f552a014..23f35e9ac 100644 --- a/Plugins/Flow.Launcher.Plugin.ControlPanel/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.ControlPanel/plugin.json @@ -4,7 +4,7 @@ "Name": "Control Panel", "Description": "Search within the Control Panel.", "Author": "CoenraadS", - "Version": "1.1.1", + "Version": "1.1.2", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.ControlPanel.dll", diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs index c9a0b7303..21eb844b4 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs @@ -7,12 +7,13 @@ using System.Windows; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.Explorer.Search; -using Flow.Launcher.Plugin.Explorer.Search.FolderLinks; +using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; using System.Linq; using MessageBox = System.Windows.Forms.MessageBox; using MessageBoxIcon = System.Windows.Forms.MessageBoxIcon; using MessageBoxButton = System.Windows.Forms.MessageBoxButtons; using DialogResult = System.Windows.Forms.DialogResult; +using Flow.Launcher.Plugin.Explorer.ViewModels; namespace Flow.Launcher.Plugin.Explorer { @@ -22,10 +23,13 @@ namespace Flow.Launcher.Plugin.Explorer private Settings Settings { get; set; } - public ContextMenu(PluginInitContext context, Settings settings) + private SettingsViewModel ViewModel { get; set; } + + public ContextMenu(PluginInitContext context, Settings settings, SettingsViewModel vm) { Context = context; Settings = settings; + ViewModel = vm; } public List LoadContextMenus(Result selectedResult) @@ -50,6 +54,58 @@ namespace Flow.Launcher.Plugin.Explorer var icoPath = (record.Type == ResultType.File) ? Constants.FileImagePath : Constants.FolderImagePath; var fileOrFolder = (record.Type == ResultType.File) ? "file" : "folder"; + + if (!Settings.QuickAccessLinks.Any(x => x.Path == record.FullPath)) + { + contextMenus.Add(new Result + { + Title = Context.API.GetTranslation("plugin_explorer_add_to_quickaccess_title"), + SubTitle = string.Format(Context.API.GetTranslation("plugin_explorer_add_to_quickaccess_subtitle"), fileOrFolder), + Action = (context) => + { + Settings.QuickAccessLinks.Add(new AccessLink { Path = record.FullPath, Type = record.Type }); + + Context.API.ShowMsg(Context.API.GetTranslation("plugin_explorer_addfilefoldersuccess"), + string.Format( + Context.API.GetTranslation("plugin_explorer_addfilefoldersuccess_detail"), + fileOrFolder), + Constants.ExplorerIconImageFullPath); + + ViewModel.Save(); + + return true; + }, + SubTitleToolTip = Context.API.GetTranslation("plugin_explorer_contextmenu_titletooltip"), + TitleToolTip = Context.API.GetTranslation("plugin_explorer_contextmenu_titletooltip"), + IcoPath = Constants.QuickAccessImagePath + }); + } + else + { + contextMenus.Add(new Result + { + Title = Context.API.GetTranslation("plugin_explorer_remove_from_quickaccess_title"), + SubTitle = string.Format(Context.API.GetTranslation("plugin_explorer_remove_from_quickaccess_subtitle"), fileOrFolder), + Action = (context) => + { + Settings.QuickAccessLinks.Remove(Settings.QuickAccessLinks.FirstOrDefault(x => x.Path == record.FullPath)); + + Context.API.ShowMsg(Context.API.GetTranslation("plugin_explorer_removefilefoldersuccess"), + string.Format( + Context.API.GetTranslation("plugin_explorer_removefilefoldersuccess_detail"), + fileOrFolder), + Constants.ExplorerIconImageFullPath); + + ViewModel.Save(); + + return true; + }, + SubTitleToolTip = Context.API.GetTranslation("plugin_explorer_contextmenu_remove_titletooltip"), + TitleToolTip = Context.API.GetTranslation("plugin_explorer_contextmenu_remove_titletooltip"), + IcoPath = Constants.RemoveQuickAccessImagePath + }); + } + contextMenus.Add(new Result { Title = Context.API.GetTranslation("plugin_explorer_copypath"), @@ -228,7 +284,7 @@ namespace Flow.Launcher.Plugin.Explorer Action = _ => { if(!Settings.IndexSearchExcludedSubdirectoryPaths.Any(x => x.Path == record.FullPath)) - Settings.IndexSearchExcludedSubdirectoryPaths.Add(new FolderLink { Path = record.FullPath }); + Settings.IndexSearchExcludedSubdirectoryPaths.Add(new AccessLink { Path = record.FullPath }); Task.Run(() => { diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj index a1a08843a..9f0b46d93 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj @@ -7,6 +7,7 @@ true true false + en @@ -26,73 +27,10 @@ - - PreserveNewest - - - - PreserveNewest - - - - PreserveNewest - - - - PreserveNewest - - - - Always - - - + PreserveNewest - - - PreserveNewest - - - - PreserveNewest - - - - PreserveNewest - - - - MSBuild:Compile - Designer - PreserveNewest - - - - MSBuild:Compile - Designer - PreserveNewest - - - - MSBuild:Compile - Designer - PreserveNewest - - - - MSBuild:Compile - Designer - PreserveNewest - - - - MSBuild:Compile - Designer - PreserveNewest - - - + MSBuild:Compile Designer PreserveNewest diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/quickaccess.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/quickaccess.png new file mode 100644 index 000000000..470a6782f Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/quickaccess.png differ diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/removequickaccess.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/removequickaccess.png new file mode 100644 index 000000000..fbfb0b960 Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/removequickaccess.png differ diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml index 2fb16e0e1..9ba0da3f6 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml @@ -16,7 +16,7 @@ Edit Add Customise Action Keywords - Quick Folder Access Paths + Quick Access Links Index Search Excluded Paths Indexing Options Search Activation: @@ -42,5 +42,15 @@ Open Windows Indexing Options Manage indexed files and folders Failed to open Windows Indexing Options + Add to Quick Access + Add the current {0} to Quick Access + Successfully Added + Successfully added to Quick Access + Successfully Removed + Successfully removed from Quick Access + Add to Quick Access so it can be opened with Explorer's Search Activation action keyword + Remove from Quick Access + Remove from Quick Access + Remove the current {0} from Quick Access \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs index 30a06e882..ae7bf57d2 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs @@ -1,13 +1,17 @@ using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Plugin.Explorer.Search; +using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; using Flow.Launcher.Plugin.Explorer.ViewModels; using Flow.Launcher.Plugin.Explorer.Views; using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using System.Windows.Controls; namespace Flow.Launcher.Plugin.Explorer { - public class Main : ISettingProvider, IPlugin, ISavable, IContextMenu, IPluginI18n + public class Main : ISettingProvider, IAsyncPlugin, ISavable, IContextMenu, IPluginI18n { internal PluginInitContext Context { get; set; } @@ -17,17 +21,30 @@ 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); + + // as at v1.7.0 this is to maintain backwards compatibility, need to be removed afterwards. + if (Settings.QuickFolderAccessLinks.Any()) + { + Settings.QuickAccessLinks = Settings.QuickFolderAccessLinks; + Settings.QuickFolderAccessLinks = new List(); + } + + contextMenu = new ContextMenu(Context, Settings, viewModel); + searchManager = new SearchManager(Settings, Context); + ResultManager.Init(Context); } public List LoadContextMenus(Result selectedResult) @@ -35,9 +52,9 @@ namespace Flow.Launcher.Plugin.Explorer return contextMenu.LoadContextMenus(selectedResult); } - public List Query(Query query) + public async Task> QueryAsync(Query query, CancellationToken token) { - return new SearchManager(Settings, Context).Search(query); + return await searchManager.SearchAsync(query, token); } public void Save() diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs index 38939e244..78c7c98a5 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs @@ -15,6 +15,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search internal const string ExplorerIconImagePath = "Images\\explorer.png"; internal const string DifferentUserIconImagePath = "Images\\user.png"; internal const string IndexingOptionsIconImagePath = "Images\\windowsindexingoptions.png"; + internal const string QuickAccessImagePath = "Images\\quickaccess.png"; + internal const string RemoveQuickAccessImagePath = "Images\\removequickaccess.png"; internal const string ToolTipOpenDirectory = "Ctrl + Enter to open the directory"; diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs index 02de0eeae..acd960ef1 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs @@ -4,29 +4,27 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo { - public class DirectoryInfoSearch + public static class DirectoryInfoSearch { - private readonly ResultManager resultManager; - - public DirectoryInfoSearch(PluginInitContext context) - { - resultManager = new ResultManager(context); - } - - internal List TopLevelDirectorySearch(Query query, string search) + internal static List TopLevelDirectorySearch(Query query, string search, CancellationToken token) { var criteria = ConstructSearchCriteria(search); - if (search.LastIndexOf(Constants.AllFilesFolderSearchWildcard) > search.LastIndexOf(Constants.DirectorySeperator)) - return DirectorySearch(SearchOption.AllDirectories, query, search, criteria); - - return DirectorySearch(SearchOption.TopDirectoryOnly, query, search, criteria); + if (search.LastIndexOf(Constants.AllFilesFolderSearchWildcard) > + search.LastIndexOf(Constants.DirectorySeperator)) + return DirectorySearch(new EnumerationOptions + { + RecurseSubdirectories = true + }, query, search, criteria, token); + + return DirectorySearch(new EnumerationOptions(), query, search, criteria, token); // null will be passed as default } - public string ConstructSearchCriteria(string search) + public static string ConstructSearchCriteria(string search) { string incompleteName = ""; @@ -45,7 +43,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo return incompleteName; } - private List DirectorySearch(SearchOption searchOption, Query query, string search, string searchCriteria) + private static List DirectorySearch(EnumerationOptions enumerationOption, Query query, string search, + string searchCriteria, CancellationToken token) { var results = new List(); @@ -57,40 +56,39 @@ 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, enumerationOption)) { - if ((fileSystemInfo.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) continue; - if (fileSystemInfo is System.IO.DirectoryInfo) { - folderList.Add(resultManager.CreateFolderResult(fileSystemInfo.Name, fileSystemInfo.FullName, fileSystemInfo.FullName, query, true, false)); + folderList.Add(ResultManager.CreateFolderResult(fileSystemInfo.Name, fileSystemInfo.FullName, + fileSystemInfo.FullName, query, true, false)); } else { - fileList.Add(resultManager.CreateFileResult(fileSystemInfo.FullName, query, true, false)); + fileList.Add(ResultManager.CreateFileResult(fileSystemInfo.FullName, query, true, false)); } + + token.ThrowIfCancellationRequested(); } } catch (Exception e) { - if (e is UnauthorizedAccessException || e is ArgumentException) - { - results.Add(new Result { Title = e.Message, Score = 501 }); + if (!(e is ArgumentException)) + throw e; + + results.Add(new Result {Title = e.Message, Score = 501}); - return results; - } + return results; #if DEBUG // Please investigate and handle error from DirectoryInfo search - throw e; #else Log.Exception($"|Flow.Launcher.Plugin.Explorer.DirectoryInfoSearch|Error from performing DirectoryInfoSearch", e); -#endif +#endif } - // Intial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection. + // Initial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection. return results.Concat(folderList.OrderBy(x => x.Title)).Concat(fileList.OrderBy(x => x.Title)).ToList(); } } -} +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs index 6a870f149..1e9815cb9 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs @@ -76,7 +76,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search { var expandedPath = environmentVariables[search]; - results.Add(new ResultManager(context).CreateFolderResult($"%{search}%", expandedPath, expandedPath, query)); + results.Add(ResultManager.CreateFolderResult($"%{search}%", expandedPath, expandedPath, query)); return results; } @@ -95,7 +95,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search { if (p.Key.StartsWith(search, StringComparison.InvariantCultureIgnoreCase)) { - results.Add(new ResultManager(context).CreateFolderResult($"%{p.Key}%", p.Value, p.Value, query)); + results.Add(ResultManager.CreateFolderResult($"%{p.Key}%", p.Value, p.Value, query)); } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/QuickFolderAccess.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/QuickFolderAccess.cs deleted file mode 100644 index 8bd19956e..000000000 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/QuickFolderAccess.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Flow.Launcher.Plugin.Explorer.Search.FolderLinks -{ - public class QuickFolderAccess - { - internal List FolderListMatched(Query query, List folderLinks, PluginInitContext context) - { - if (string.IsNullOrEmpty(query.Search)) - return new List(); - - string search = query.Search.ToLower(); - - var queriedFolderLinks = folderLinks.Where(x => x.Nickname.StartsWith(search, StringComparison.OrdinalIgnoreCase)); - - return queriedFolderLinks.Select(item => - new ResultManager(context) - .CreateFolderResult(item.Nickname, item.Path, item.Path, query)) - .ToList(); - } - - internal List FolderListAll(Query query, List folderLinks, PluginInitContext context) - => folderLinks - .Select(item => - new ResultManager(context).CreateFolderResult(item.Nickname, item.Path, item.Path, query)) - .ToList(); - } -} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/FolderLink.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/AccessLink.cs similarity index 71% rename from Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/FolderLink.cs rename to Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/AccessLink.cs index 379b5848f..f623cc2ca 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/FolderLink.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/AccessLink.cs @@ -1,15 +1,16 @@ -using Newtonsoft.Json; -using System; +using System; using System.Linq; +using System.Text.Json.Serialization; -namespace Flow.Launcher.Plugin.Explorer.Search.FolderLinks +namespace Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks { - [JsonObject(MemberSerialization.OptIn)] - public class FolderLink + public class AccessLink { - [JsonProperty] public string Path { get; set; } + public ResultType Type { get; set; } = ResultType.Folder; + + [JsonIgnore] public string Nickname { get diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/QuickAccess.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/QuickAccess.cs new file mode 100644 index 000000000..d71e9ab49 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/QuickAccess.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks +{ + internal static class QuickAccess + { + internal static List AccessLinkListMatched(Query query, List accessLinks) + { + if (string.IsNullOrEmpty(query.Search)) + return new List(); + + string search = query.Search.ToLower(); + + var queriedAccessLinks = + accessLinks + .Where(x => x.Nickname.StartsWith(search, StringComparison.OrdinalIgnoreCase)) + .OrderBy(x => x.Type) + .ThenBy(x => x.Nickname); + + return queriedAccessLinks.Select(l => l.Type switch + { + ResultType.Folder => ResultManager.CreateFolderResult(l.Nickname, l.Path, l.Path, query), + ResultType.File => ResultManager.CreateFileResult(l.Path, query), + _ => throw new ArgumentOutOfRangeException() + + }).ToList(); + } + + internal static List AccessLinkListAll(Query query, List accessLinks) + => accessLinks + .OrderBy(x => x.Type) + .ThenBy(x => x.Nickname) + .Select(l => l.Type switch + { + ResultType.Folder => ResultManager.CreateFolderResult(l.Nickname, l.Path, l.Path, query), + ResultType.File => ResultManager.CreateFileResult(l.Path, query), + _ => throw new ArgumentOutOfRangeException() + + }).ToList(); + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs index 6a336c59a..6872f04b4 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs @@ -1,22 +1,22 @@ using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin.SharedCommands; using System; -using System.Diagnostics; using System.IO; using System.Linq; using System.Windows; namespace Flow.Launcher.Plugin.Explorer.Search { - public class ResultManager + public static class ResultManager { - private readonly PluginInitContext context; + private static PluginInitContext Context; - public ResultManager(PluginInitContext context) + public static void Init(PluginInitContext context) { - this.context = context; + Context = context; } - internal Result CreateFolderResult(string title, string subtitle, string path, Query query, bool showIndexState = false, bool windowsIndexed = false) + + internal static Result CreateFolderResult(string title, string subtitle, string path, Query query, bool showIndexState = false, bool windowsIndexed = false) { return new Result { @@ -41,7 +41,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search } string changeTo = path.EndsWith(Constants.DirectorySeperator) ? path : path + Constants.DirectorySeperator; - context.API.ChangeQuery(string.IsNullOrEmpty(query.ActionKeyword) ? + Context.API.ChangeQuery(string.IsNullOrEmpty(query.ActionKeyword) ? changeTo : query.ActionKeyword + " " + changeTo); return false; @@ -52,7 +52,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search }; } - internal Result CreateOpenCurrentFolderResult(string path, bool windowsIndexed = false) + internal static Result CreateOpenCurrentFolderResult(string path, bool windowsIndexed = false) { var retrievedDirectoryPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path); @@ -94,7 +94,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search }; } - internal Result CreateFileResult(string filePath, Query query, bool showIndexState = false, bool windowsIndexed = false) + internal static Result CreateFileResult(string filePath, Query query, bool showIndexState = false, bool windowsIndexed = false) { var result = new Result { @@ -140,7 +140,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search public bool ShowIndexState { get; set; } } - internal enum ResultType + public enum ResultType { Volume, Folder, diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs index 5b50b7fad..2af09bf2c 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs @@ -1,10 +1,12 @@ using Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo; -using Flow.Launcher.Plugin.Explorer.Search.FolderLinks; +using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; using Flow.Launcher.Plugin.Explorer.Search.WindowsIndex; using Flow.Launcher.Plugin.SharedCommands; using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace Flow.Launcher.Plugin.Explorer.Search { @@ -12,41 +14,31 @@ namespace Flow.Launcher.Plugin.Explorer.Search { private readonly PluginInitContext context; - private readonly IndexSearch indexSearch; - - private readonly QuickFolderAccess quickFolderAccess = new QuickFolderAccess(); - - private readonly ResultManager resultManager; - private readonly Settings settings; public SearchManager(Settings settings, PluginInitContext context) { this.context = context; - indexSearch = new IndexSearch(context); - resultManager = new ResultManager(context); this.settings = settings; } - internal List Search(Query query) + internal async Task> SearchAsync(Query query, CancellationToken token) { var results = new List(); var querySearch = query.Search; if (IsFileContentSearch(query.ActionKeyword)) - return WindowsIndexFileContentSearch(query, querySearch); + return await WindowsIndexFileContentSearchAsync(query, querySearch, token).ConfigureAwait(false); // This allows the user to type the assigned action keyword and only see the list of quick folder links - if (settings.QuickFolderAccessLinks.Count > 0 - && query.ActionKeyword == settings.SearchActionKeyword - && string.IsNullOrEmpty(query.Search)) - return quickFolderAccess.FolderListAll(query, settings.QuickFolderAccessLinks, context); + if (string.IsNullOrEmpty(query.Search)) + return QuickAccess.AccessLinkListAll(query, settings.QuickAccessLinks); - var quickFolderLinks = quickFolderAccess.FolderListMatched(query, settings.QuickFolderAccessLinks, context); + var quickaccessLinks = QuickAccess.AccessLinkListMatched(query, settings.QuickAccessLinks); - if (quickFolderLinks.Count > 0) - results.AddRange(quickFolderLinks); + if (quickaccessLinks.Count > 0) + results.AddRange(quickaccessLinks); var isEnvironmentVariable = EnvironmentVariables.IsEnvironmentVariableSearch(querySearch); @@ -54,11 +46,11 @@ 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 +60,42 @@ 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, - DirectoryInfoClassSearch, - useIndexSearch, - query, - locationPath)); + results.Add(ResultManager.CreateOpenCurrentFolderResult(locationPath, useIndexSearch)); + + token.ThrowIfCancellationRequested(); + + var directoryResult = await TopLevelDirectorySearchBehaviourAsync(WindowsIndexTopLevelFolderSearchAsync, + DirectoryInfoClassSearch, + useIndexSearch, + query, + locationPath, + token).ConfigureAwait(false); + + token.ThrowIfCancellationRequested(); + + results.AddRange(directoryResult); return results; } - private List WindowsIndexFileContentSearch(Query query, string querySearchString) + private async Task> WindowsIndexFileContentSearchAsync(Query query, string querySearchString, CancellationToken token) { var queryConstructor = new QueryConstructor(settings); if (string.IsNullOrEmpty(querySearchString)) return new List(); - return indexSearch.WindowsIndexSearch(querySearchString, + return await IndexSearch.WindowsIndexSearchAsync(querySearchString, queryConstructor.CreateQueryHelper().ConnectionString, queryConstructor.QueryForFileContentSearch, - query); + query, + token).ConfigureAwait(false); } public bool IsFileContentSearch(string actionKeyword) @@ -102,44 +103,45 @@ namespace Flow.Launcher.Plugin.Explorer.Search return actionKeyword == settings.FileContentSearchActionKeyword; } - private List DirectoryInfoClassSearch(Query query, string querySearch) + private List DirectoryInfoClassSearch(Query query, string querySearch, CancellationToken token) { - var directoryInfoSearch = new DirectoryInfoSearch(context); - - return directoryInfoSearch.TopLevelDirectorySearch(query, querySearch); + return DirectoryInfoSearch.TopLevelDirectorySearch(query, querySearch, token); } - public List TopLevelDirectorySearchBehaviour( - Func> windowsIndexSearch, - Func> directoryInfoClassSearch, + public async Task> TopLevelDirectorySearchBehaviourAsync( + Func>> windowsIndexSearch, + Func> directoryInfoClassSearch, bool useIndexSearch, Query query, - string querySearchString) + string querySearchString, + CancellationToken token) { if (!useIndexSearch) - return directoryInfoClassSearch(query, querySearchString); + return directoryInfoClassSearch(query, querySearchString, token); - return windowsIndexSearch(query, querySearchString); + return await windowsIndexSearch(query, querySearchString, token); } - private List WindowsIndexFilesAndFoldersSearch(Query query, string querySearchString) + private async Task> WindowsIndexFilesAndFoldersSearchAsync(Query query, string querySearchString, CancellationToken token) { var queryConstructor = new QueryConstructor(settings); - return indexSearch.WindowsIndexSearch(querySearchString, + return await IndexSearch.WindowsIndexSearchAsync(querySearchString, queryConstructor.CreateQueryHelper().ConnectionString, queryConstructor.QueryForAllFilesAndFolders, - query); + query, + token).ConfigureAwait(false); } - - private List WindowsIndexTopLevelFolderSearch(Query query, string path) + + private async Task> WindowsIndexTopLevelFolderSearchAsync(Query query, string path, CancellationToken token) { var queryConstructor = new QueryConstructor(settings); - return indexSearch.WindowsIndexSearch(path, + return await IndexSearch.WindowsIndexSearchAsync(path, queryConstructor.CreateQueryHelper().ConnectionString, queryConstructor.QueryForTopLevelDirectorySearch, - query); + query, + token).ConfigureAwait(false); } private bool UseWindowsIndexForDirectorySearch(string locationPath) @@ -154,7 +156,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search .StartsWith(x.Path, StringComparison.OrdinalIgnoreCase))) return false; - return indexSearch.PathIsIndexed(pathToDirectory); + return IndexSearch.PathIsIndexed(pathToDirectory); } } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs index 4f9325c77..b1e1d7622 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs @@ -1,82 +1,71 @@ -using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.Logger; using Microsoft.Search.Interop; using System; using System.Collections.Generic; using System.Data.OleDb; using System.Linq; using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex { - internal class IndexSearch + internal static class IndexSearch { - private readonly object _lock = new object(); - - private OleDbConnection conn; - - private OleDbCommand command; - - private OleDbDataReader dataReaderResults; - - private readonly ResultManager resultManager; // Reserved keywords in oleDB - private readonly string reservedStringPattern = @"^[\/\\\$\%_]+$"; + private const string reservedStringPattern = @"^[`\@\#\^,\&\/\\\$\%_]+$"; - internal IndexSearch(PluginInitContext context) + internal async static Task> ExecuteWindowsIndexSearchAsync(string indexQueryString, string connectionString, Query query, CancellationToken token) { - resultManager = new ResultManager(context); - } - - internal List ExecuteWindowsIndexSearch(string indexQueryString, string connectionString, Query query) - { - var folderResults = new List(); - var fileResults = new List(); var results = new List(); + var fileResults = new List(); try { - using (conn = new OleDbConnection(connectionString)) + using var conn = new OleDbConnection(connectionString); + await conn.OpenAsync(token); + token.ThrowIfCancellationRequested(); + + using var command = new OleDbCommand(indexQueryString, conn); + // Results return as an OleDbDataReader. + using var dataReaderResults = await command.ExecuteReaderAsync(token) as OleDbDataReader; + token.ThrowIfCancellationRequested(); + + if (dataReaderResults.HasRows) { - conn.Open(); - - using (command = new OleDbCommand(indexQueryString, conn)) + while (await dataReaderResults.ReadAsync(token)) { - // Results return as an OleDbDataReader. - using (dataReaderResults = command.ExecuteReader()) + token.ThrowIfCancellationRequested(); + if (dataReaderResults.GetValue(0) != DBNull.Value && dataReaderResults.GetValue(1) != DBNull.Value) { - if (dataReaderResults.HasRows) - { - while (dataReaderResults.Read()) - { - if (dataReaderResults.GetValue(0) != DBNull.Value && dataReaderResults.GetValue(1) != DBNull.Value) - { - // # is URI syntax for the fragment component, need to be encoded so LocalPath returns complete path - var encodedFragmentPath = dataReaderResults - .GetString(1) - .Replace("#", "%23", StringComparison.OrdinalIgnoreCase); - - var path = new Uri(encodedFragmentPath).LocalPath; + // # is URI syntax for the fragment component, need to be encoded so LocalPath returns complete path + var encodedFragmentPath = dataReaderResults + .GetString(1) + .Replace("#", "%23", StringComparison.OrdinalIgnoreCase); - if (dataReaderResults.GetString(2) == "Directory") - { - folderResults.Add(resultManager.CreateFolderResult( - dataReaderResults.GetString(0), - path, - path, - query, true, true)); - } - else - { - fileResults.Add(resultManager.CreateFileResult(path, query, true, true)); - } - } - } + var path = new Uri(encodedFragmentPath).LocalPath; + + if (dataReaderResults.GetString(2) == "Directory") + { + results.Add(ResultManager.CreateFolderResult( + dataReaderResults.GetString(0), + path, + path, + query, true, true)); + } + else + { + fileResults.Add(ResultManager.CreateFileResult(path, query, true, true)); } } } } } + catch (OperationCanceledException) + { + return new List(); // The source code indicates that without adding members, it won't allocate an array + } catch (InvalidOperationException e) { // Internal error from ExecuteReader(): Connection closed. @@ -87,32 +76,34 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex LogException("General error from performing index search", e); } + results.AddRange(fileResults); + // Intial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection. - return results.Concat(folderResults.OrderBy(x => x.Title)).Concat(fileResults.OrderBy(x => x.Title)).ToList(); ; + return results; } - internal List WindowsIndexSearch(string searchString, string connectionString, Func constructQuery, Query query) + internal async static Task> WindowsIndexSearchAsync(string searchString, string connectionString, + Func constructQuery, Query query, + CancellationToken token) { var regexMatch = Regex.Match(searchString, reservedStringPattern); if (regexMatch.Success) return new List(); - lock (_lock) - { - var constructedQuery = constructQuery(searchString); - return ExecuteWindowsIndexSearch(constructedQuery, connectionString, query); - } + var constructedQuery = constructQuery(searchString); + return await ExecuteWindowsIndexSearchAsync(constructedQuery, connectionString, query, token); + } - internal bool PathIsIndexed(string path) + internal static bool PathIsIndexed(string path) { var csm = new CSearchManager(); var indexManager = csm.GetCatalog("SystemIndex").GetCrawlScopeManager(); return indexManager.IncludedInCrawlScope(path) > 0; } - private void LogException(string message, Exception e) + private static void LogException(string message, Exception e) { #if DEBUG // Please investigate and handle error from index search throw e; diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs index 5718fdb0a..20e85bbb5 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs @@ -42,7 +42,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex // Get the ISearchQueryHelper which will help us to translate AQS --> SQL necessary to query the indexer var queryHelper = catalogManager.GetQueryHelper(); - + return queryHelper; } @@ -81,11 +81,9 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex var previousLevelDirectory = path.Substring(0, indexOfSeparator); if (string.IsNullOrEmpty(itemName)) - return searchDepth + $"{previousLevelDirectory}'"; + return $"{searchDepth}{previousLevelDirectory}'"; - return $"(System.FileName LIKE '{itemName}%' " + - $"OR CONTAINS(System.FileName,'\"{itemName}*\"',1033)) AND " + - searchDepth + $"{previousLevelDirectory}'"; + return $"(System.FileName LIKE '{itemName}%' OR CONTAINS(System.FileName,'\"{itemName}*\"',1033)) AND {searchDepth}{previousLevelDirectory}'"; } /// @@ -96,9 +94,9 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex string query = "SELECT TOP " + settings.MaxResult + $" {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE "; if (path.LastIndexOf(Constants.AllFilesFolderSearchWildcard) > path.LastIndexOf(Constants.DirectorySeperator)) - return query + QueryWhereRestrictionsForTopLevelDirectoryAllFilesAndFoldersSearch(path); + return query + QueryWhereRestrictionsForTopLevelDirectoryAllFilesAndFoldersSearch(path) + QueryOrderByFileNameRestriction; - return query + QueryWhereRestrictionsForTopLevelDirectorySearch(path); + return query + QueryWhereRestrictionsForTopLevelDirectorySearch(path) + QueryOrderByFileNameRestriction; } /// @@ -107,16 +105,17 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex public string QueryForAllFilesAndFolders(string userSearchString) { // Generate SQL from constructed parameters, converting the userSearchString from AQS->WHERE clause - return CreateBaseQuery().GenerateSQLFromUserQuery(userSearchString) + " AND " + QueryWhereRestrictionsForAllFilesAndFoldersSearch(); + return CreateBaseQuery().GenerateSQLFromUserQuery(userSearchString) + " AND " + QueryWhereRestrictionsForAllFilesAndFoldersSearch + + QueryOrderByFileNameRestriction; } /// /// Set the required WHERE clause restriction to search for all files and folders. /// - public string QueryWhereRestrictionsForAllFilesAndFoldersSearch() - { - return $"scope='file:'"; - } + public const string QueryWhereRestrictionsForAllFilesAndFoldersSearch = "scope='file:'"; + + public const string QueryOrderByFileNameRestriction = " ORDER BY System.FileName"; + /// /// Search will be performed on all indexed file contents for the specified search keywords. @@ -125,7 +124,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex { string query = "SELECT TOP " + settings.MaxResult + $" {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE "; - return query + QueryWhereRestrictionsForFileContentSearch(userSearchString) + " AND " + QueryWhereRestrictionsForAllFilesAndFoldersSearch(); + return query + QueryWhereRestrictionsForFileContentSearch(userSearchString) + " AND " + QueryWhereRestrictionsForAllFilesAndFoldersSearch + + QueryOrderByFileNameRestriction; } /// diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs index 5b12870c8..a8eac986d 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs @@ -1,28 +1,24 @@ using Flow.Launcher.Plugin.Explorer.Search; -using Flow.Launcher.Plugin.Explorer.Search.FolderLinks; -using Newtonsoft.Json; +using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; using System.Collections.Generic; namespace Flow.Launcher.Plugin.Explorer { public class Settings { - [JsonProperty] public int MaxResult { get; set; } = 100; - [JsonProperty] - public List QuickFolderAccessLinks { get; set; } = new List(); + public List QuickAccessLinks { get; set; } = new List(); + + // as at v1.7.0 this is to maintain backwards compatibility, need to be removed afterwards. + public List QuickFolderAccessLinks { get; set; } = new List(); - [JsonProperty] public bool UseWindowsIndexForDirectorySearch { get; set; } = true; - [JsonProperty] - public List IndexSearchExcludedSubdirectoryPaths { get; set; } = new List(); + public List IndexSearchExcludedSubdirectoryPaths { get; set; } = new List(); - [JsonProperty] public string SearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign; - [JsonProperty] public string FileContentSearchActionKeyword { get; set; } = Constants.DefaultContentSearchActionKeyword; } } \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs index 7fcd77f07..791c06b66 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs @@ -1,8 +1,9 @@ using Flow.Launcher.Core.Plugin; using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Plugin.Explorer.Search; -using Flow.Launcher.Plugin.Explorer.Search.FolderLinks; +using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; using System.Diagnostics; +using System.Threading.Tasks; namespace Flow.Launcher.Plugin.Explorer.ViewModels { @@ -21,14 +22,19 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels Settings = storage.Load(); } + public Task LoadStorage() + { + return Task.Run(() => Settings = storage.Load()); + } + public void Save() { storage.Save(); } - internal void RemoveFolderLinkFromQuickFolders(FolderLink selectedRow) => Settings.QuickFolderAccessLinks.Remove(selectedRow); + internal void RemoveLinkFromQuickAccess(AccessLink selectedRow) => Settings.QuickAccessLinks.Remove(selectedRow); - internal void RemoveFolderLinkFromExcludedIndexPaths(FolderLink selectedRow) => Settings.IndexSearchExcludedSubdirectoryPaths.Remove(selectedRow); + internal void RemoveAccessLinkFromExcludedIndexPaths(AccessLink selectedRow) => Settings.IndexSearchExcludedSubdirectoryPaths.Remove(selectedRow); internal void OpenWindowsIndexingOptions() { diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml index 9d6f4976e..13d46394c 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml @@ -7,7 +7,7 @@ mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> - + @@ -40,22 +40,22 @@ - + x:Name="lbxAccessLinks" AllowDrop="True" + Drop="lbxAccessLinks_Drop" + DragEnter="lbxAccessLinks_DragEnter" + ItemTemplate="{StaticResource ListViewTemplateAccessLinks}"/> diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml.cs index 3b67b408d..5d2980c55 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml.cs @@ -1,4 +1,4 @@ -using Flow.Launcher.Plugin.Explorer.Search.FolderLinks; +using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; using Flow.Launcher.Plugin.Explorer.ViewModels; using System; using System.Collections.Generic; @@ -29,7 +29,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views this.viewModel = viewModel; - lbxFolderLinks.ItemsSource = this.viewModel.Settings.QuickFolderAccessLinks; + lbxAccessLinks.ItemsSource = this.viewModel.Settings.QuickAccessLinks; lbxExcludedPaths.ItemsSource = this.viewModel.Settings.IndexSearchExcludedSubdirectoryPaths; @@ -54,7 +54,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views public void RefreshView() { - lbxFolderLinks.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending)); + lbxAccessLinks.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending)); lbxExcludedPaths.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending)); @@ -62,7 +62,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views btnEdit.Visibility = Visibility.Hidden; btnAdd.Visibility = Visibility.Hidden; - if (expFolderLinks.IsExpanded || expExcludedPaths.IsExpanded || expActionKeywords.IsExpanded) + if (expAccessLinks.IsExpanded || expExcludedPaths.IsExpanded || expActionKeywords.IsExpanded) { if (!expActionKeywords.IsExpanded) btnAdd.Visibility = Visibility.Visible; @@ -71,7 +71,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views && btnEdit.Visibility == Visibility.Hidden) btnEdit.Visibility = Visibility.Visible; - if ((lbxFolderLinks.Items.Count == 0 && lbxExcludedPaths.Items.Count == 0) + if ((lbxAccessLinks.Items.Count == 0 && lbxExcludedPaths.Items.Count == 0) && btnDelete.Visibility == Visibility.Visible && btnEdit.Visibility == Visibility.Visible) { @@ -79,8 +79,8 @@ namespace Flow.Launcher.Plugin.Explorer.Views btnEdit.Visibility = Visibility.Hidden; } - if (expFolderLinks.IsExpanded - && lbxFolderLinks.Items.Count > 0 + if (expAccessLinks.IsExpanded + && lbxAccessLinks.Items.Count > 0 && btnDelete.Visibility == Visibility.Hidden && btnEdit.Visibility == Visibility.Hidden) { @@ -98,7 +98,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views } } - lbxFolderLinks.Items.Refresh(); + lbxAccessLinks.Items.Refresh(); lbxExcludedPaths.Items.Refresh(); @@ -113,8 +113,8 @@ namespace Flow.Launcher.Plugin.Explorer.Views if (expExcludedPaths.IsExpanded) expExcludedPaths.IsExpanded = false; - if (expFolderLinks.IsExpanded) - expFolderLinks.IsExpanded = false; + if (expAccessLinks.IsExpanded) + expAccessLinks.IsExpanded = false; RefreshView(); } @@ -125,10 +125,10 @@ namespace Flow.Launcher.Plugin.Explorer.Views expActionKeywords.Height = Double.NaN; } - private void expFolderLinks_Click(object sender, RoutedEventArgs e) + private void expAccessLinks_Click(object sender, RoutedEventArgs e) { - if (expFolderLinks.IsExpanded) - expFolderLinks.Height = 215; + if (expAccessLinks.IsExpanded) + expAccessLinks.Height = 215; if (expExcludedPaths.IsExpanded) expExcludedPaths.IsExpanded = false; @@ -139,19 +139,19 @@ namespace Flow.Launcher.Plugin.Explorer.Views RefreshView(); } - private void expFolderLinks_Collapsed(object sender, RoutedEventArgs e) + private void expAccessLinks_Collapsed(object sender, RoutedEventArgs e) { - if (!expFolderLinks.IsExpanded) - expFolderLinks.Height = Double.NaN; + if (!expAccessLinks.IsExpanded) + expAccessLinks.Height = Double.NaN; } private void expExcludedPaths_Click(object sender, RoutedEventArgs e) { if (expExcludedPaths.IsExpanded) - expFolderLinks.Height = Double.NaN; + expAccessLinks.Height = Double.NaN; - if (expFolderLinks.IsExpanded) - expFolderLinks.IsExpanded = false; + if (expAccessLinks.IsExpanded) + expAccessLinks.IsExpanded = false; if (expActionKeywords.IsExpanded) expActionKeywords.IsExpanded = false; @@ -161,7 +161,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views private void btnDelete_Click(object sender, RoutedEventArgs e) { - var selectedRow = lbxFolderLinks.SelectedItem as FolderLink?? lbxExcludedPaths.SelectedItem as FolderLink; + var selectedRow = lbxAccessLinks.SelectedItem as AccessLink?? lbxExcludedPaths.SelectedItem as AccessLink; if (selectedRow != null) { @@ -169,11 +169,11 @@ namespace Flow.Launcher.Plugin.Explorer.Views if (MessageBox.Show(msg, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes) { - if (expFolderLinks.IsExpanded) - viewModel.RemoveFolderLinkFromQuickFolders(selectedRow); + if (expAccessLinks.IsExpanded) + viewModel.RemoveLinkFromQuickAccess(selectedRow); if (expExcludedPaths.IsExpanded) - viewModel.RemoveFolderLinkFromExcludedIndexPaths(selectedRow); + viewModel.RemoveAccessLinkFromExcludedIndexPaths(selectedRow); RefreshView(); } @@ -199,7 +199,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views } else { - var selectedRow = lbxFolderLinks.SelectedItem as FolderLink ?? lbxExcludedPaths.SelectedItem as FolderLink; + var selectedRow = lbxAccessLinks.SelectedItem as AccessLink ?? lbxExcludedPaths.SelectedItem as AccessLink; if (selectedRow != null) { @@ -207,9 +207,9 @@ namespace Flow.Launcher.Plugin.Explorer.Views folderBrowserDialog.SelectedPath = selectedRow.Path; if (folderBrowserDialog.ShowDialog() == DialogResult.OK) { - if (expFolderLinks.IsExpanded) + if (expAccessLinks.IsExpanded) { - var link = viewModel.Settings.QuickFolderAccessLinks.First(x => x.Path == selectedRow.Path); + var link = viewModel.Settings.QuickAccessLinks.First(x => x.Path == selectedRow.Path); link.Path = folderBrowserDialog.SelectedPath; } @@ -235,36 +235,36 @@ namespace Flow.Launcher.Plugin.Explorer.Views var folderBrowserDialog = new FolderBrowserDialog(); if (folderBrowserDialog.ShowDialog() == DialogResult.OK) { - var newFolderLink = new FolderLink + var newAccessLink = new AccessLink { Path = folderBrowserDialog.SelectedPath }; - AddFolderLink(newFolderLink); + AddAccessLink(newAccessLink); } RefreshView(); } - private void lbxFolders_Drop(object sender, DragEventArgs e) + private void lbxAccessLinks_Drop(object sender, DragEventArgs e) { string[] files = (string[])e.Data.GetData(DataFormats.FileDrop); if (files != null && files.Count() > 0) { - if (expFolderLinks.IsExpanded && viewModel.Settings.QuickFolderAccessLinks == null) - viewModel.Settings.QuickFolderAccessLinks = new List(); + if (expAccessLinks.IsExpanded && viewModel.Settings.QuickAccessLinks == null) + viewModel.Settings.QuickAccessLinks = new List(); foreach (string s in files) { if (Directory.Exists(s)) { - var newFolderLink = new FolderLink + var newFolderLink = new AccessLink { Path = s }; - AddFolderLink(newFolderLink); + AddAccessLink(newFolderLink); } RefreshView(); @@ -272,28 +272,28 @@ namespace Flow.Launcher.Plugin.Explorer.Views } } - private void AddFolderLink(FolderLink newFolderLink) + private void AddAccessLink(AccessLink newAccessLink) { - if (expFolderLinks.IsExpanded - && !viewModel.Settings.QuickFolderAccessLinks.Any(x => x.Path == newFolderLink.Path)) + if (expAccessLinks.IsExpanded + && !viewModel.Settings.QuickAccessLinks.Any(x => x.Path == newAccessLink.Path)) { - if (viewModel.Settings.QuickFolderAccessLinks == null) - viewModel.Settings.QuickFolderAccessLinks = new List(); + if (viewModel.Settings.QuickAccessLinks == null) + viewModel.Settings.QuickAccessLinks = new List(); - viewModel.Settings.QuickFolderAccessLinks.Add(newFolderLink); + viewModel.Settings.QuickAccessLinks.Add(newAccessLink); } if (expExcludedPaths.IsExpanded - && !viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.Any(x => x.Path == newFolderLink.Path)) + && !viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.Any(x => x.Path == newAccessLink.Path)) { if (viewModel.Settings.IndexSearchExcludedSubdirectoryPaths == null) - viewModel.Settings.IndexSearchExcludedSubdirectoryPaths = new List(); + viewModel.Settings.IndexSearchExcludedSubdirectoryPaths = new List(); - viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.Add(newFolderLink); + viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.Add(newAccessLink); } } - private void lbxFolders_DragEnter(object sender, DragEventArgs e) + private void lbxAccessLinks_DragEnter(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.FileDrop)) { diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json b/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json index 2c57ac668..9aa54fb83 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json @@ -7,7 +7,7 @@ "Name": "Explorer", "Description": "Search and manage files and folders. Explorer utilises Windows Index Search", "Author": "Jeremy Wu", - "Version": "1.2.5", + "Version": "1.7.0", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.Explorer.dll", diff --git a/Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj b/Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj index e6bfa7aa3..cc280b9a9 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj +++ b/Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj @@ -10,6 +10,7 @@ true false false + en @@ -46,55 +47,12 @@ - - PreserveNewest - - - - - + MSBuild:Compile Designer PreserveNewest - - - - - MSBuild:Compile - Designer - PreserveNewest - - - - - - MSBuild:Compile - Designer - PreserveNewest - - - - - - MSBuild:Compile - Designer - PreserveNewest - - - - - - MSBuild:Compile - Designer - PreserveNewest - - - - - - MSBuild:Compile - Designer + PreserveNewest diff --git a/Plugins/Flow.Launcher.Plugin.PluginIndicator/plugin.json b/Plugins/Flow.Launcher.Plugin.PluginIndicator/plugin.json index 80900a445..7f73263a8 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginIndicator/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.PluginIndicator/plugin.json @@ -4,7 +4,7 @@ "Name": "Plugin Indicator", "Description": "Provide plugin actionword suggestion", "Author": "qianlifeng", - "Version": "1.1.1", + "Version": "1.1.2", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.PluginIndicator.dll", diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs index d26c1ead4..85f918a03 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs @@ -1,4 +1,5 @@ using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin.PluginsManager.Models; using System; using System.Collections.Generic; using System.Text; @@ -9,21 +10,67 @@ namespace Flow.Launcher.Plugin.PluginsManager { private PluginInitContext Context { get; set; } - private Settings Settings { get; set; } - - public ContextMenu(PluginInitContext context, Settings settings) + public ContextMenu(PluginInitContext context) { Context = context; - Settings = settings; } public List LoadContextMenus(Result selectedResult) { - // Open website - // Go to source code - // Report an issue? - // Request a feature? - return new List(); + var pluginManifestInfo = selectedResult.ContextData as UserPlugin; + + return new List + { + new Result + { + Title = Context.API.GetTranslation("plugin_pluginsmanager_plugin_contextmenu_openwebsite_title"), + SubTitle = Context.API.GetTranslation("plugin_pluginsmanager_plugin_contextmenu_openwebsite_subtitle"), + IcoPath = selectedResult.IcoPath, + Action = _ => + { + SharedCommands.SearchWeb.NewTabInBrowser(pluginManifestInfo.Website); + return true; + } + }, + new Result + { + Title = Context.API.GetTranslation("plugin_pluginsmanager_plugin_contextmenu_gotosourcecode_title"), + SubTitle = Context.API.GetTranslation("plugin_pluginsmanager_plugin_contextmenu_gotosourcecode_subtitle"), + IcoPath = "Images\\sourcecode.png", + Action = _ => + { + SharedCommands.SearchWeb.NewTabInBrowser(pluginManifestInfo.UrlSourceCode); + return true; + } + }, + new Result + { + Title = Context.API.GetTranslation("plugin_pluginsmanager_plugin_contextmenu_newissue_title"), + SubTitle = Context.API.GetTranslation("plugin_pluginsmanager_plugin_contextmenu_newissue_subtitle"), + IcoPath = "Images\\request.png", + Action = _ => + { + // standard UrlSourceCode format in PluginsManifest's plugins.json file: https://github.com/jjw24/WoxDictionary/tree/master + var link = pluginManifestInfo.UrlSourceCode.StartsWith("https://github.com") + ? pluginManifestInfo.UrlSourceCode.Replace("/tree/master", "/issues/new/choose") + : pluginManifestInfo.UrlSourceCode; + + SharedCommands.SearchWeb.NewTabInBrowser(link); + return true; + } + }, + new Result + { + Title = Context.API.GetTranslation("plugin_pluginsmanager_plugin_contextmenu_pluginsmanifest_title"), + SubTitle = Context.API.GetTranslation("plugin_pluginsmanager_plugin_contextmenu_pluginsmanifest_subtitle"), + IcoPath = "Images\\manifestsite.png", + Action = _ => + { + SharedCommands.SearchWeb.NewTabInBrowser("https://github.com/Flow-Launcher/Flow.Launcher.PluginsManifest"); + return true; + } + } + }; } } } diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj b/Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj index cc1a931ce..f94027bcc 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj @@ -1,5 +1,4 @@  - Library netcoreapp3.1 @@ -19,6 +18,7 @@ + @@ -27,15 +27,12 @@ PreserveNewest - - - - PreserveNewest - - - + + PreserveNewest + + PreserveNewest diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Images/manifestsite.png b/Plugins/Flow.Launcher.Plugin.PluginsManager/Images/manifestsite.png new file mode 100644 index 000000000..f96ba15b2 Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.PluginsManager/Images/manifestsite.png differ diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Images/request.png b/Plugins/Flow.Launcher.Plugin.PluginsManager/Images/request.png new file mode 100644 index 000000000..a9126cb9b Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.PluginsManager/Images/request.png differ diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Images/sourcecode.png b/Plugins/Flow.Launcher.Plugin.PluginsManager/Images/sourcecode.png new file mode 100644 index 000000000..8efbdaa48 Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.PluginsManager/Images/sourcecode.png differ diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml index fc13f0c2f..3017f39c3 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml @@ -1,4 +1,4 @@ - @@ -6,13 +6,20 @@ Downloading plugin Please wait... Successfully downloaded - Do you want to uninstall the following plugin?{0}{1}{2} by {3} - Do you want to install the following plugin?{0}{1}{2} by {3} + {0} by {1} {2}{3}Would you like to uninstall this plugin? After the uninstallation Flow will automatically restart. + {0} by {1} {2}{3}Would you like to install this plugin? After the installation Flow will automatically restart. Plugin Install Plugin Uninstall Install failed: unable to find the plugin.json metadata file from the new plugin - You have installed plugin {0} successfully.{1}Would you like to restart Flow Launcher to take effect? - You have uninstalled plugin {0} successfully.{1}Would you like to restart Flow Launcher to take effect? + Error installing plugin + Error occured while trying to install {0} + No update available + All plugins are up to date + {0} by {1} {2}{3}Would you like to update this plugin? After the update Flow will automatically restart. + Plugin Update + This plugin has an update, would you like to see it? + This plugin is already installed + @@ -20,5 +27,13 @@ Management of installing, uninstalling or updating Flow Launcher plugins + Open website + Visit the plugin's website + See source code + See the plugin's source code + Suggest an enhancement or submit an issue + Suggest an enhancement or submit an issue to the plugin developer + Go to Flow's plugins repository + Visit the PluginsManifest repository to see comunity-made plugin submissions \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/sk.xaml b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/sk.xaml new file mode 100644 index 000000000..fe5afa317 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/sk.xaml @@ -0,0 +1,39 @@ + + + + Sťahovanie pluginu + Čakajte, prosím… + Úspešne stiahnuté + {0} od {1} {2}{3}Chcete odinštalovať tento plugin? Po odinštalovaní sa Flow automaticky reštartuje. + {0} by {1} {2}{3}Chcete nainštalovať tento plugin? Po nainštalovaní sa Flow automaticky reštartuje. + Inštalovať plugin + Odinštalovať plugin + Inštalácia zlyhala: nepodarilo sa nájsť metadáta súboru plugin.json nového pluginu + Chyba inštalácie pluginu + Nastala chyba počas inštaláciu pluginu {0} + Nie je k dispozícii žiadna aktualizácia + Všetky pluginy sú aktuálne + {0} od {1} {2}{3}Chcete aktualizovať tento plugin? Po odinštalovaní sa Flow automaticky reštartuje. + Aktualizácia pluginu + Tento plugin má dostupnú aktualizáciu, chcete ju zobraziť? + Tento plugin je už nainštalovaný + + + + + Správca pluginov + Správa inštalácie, odinštalácie alebo aktualizácie pluginov programu Flow Launcher + + + Prejsť na webovú stránku + Prejsť na webovú stránku pluginu + Zobraziť zdrojový kód + Zobraziť zdrojový kód pluginu + Navrhnúť vylepšenie alebo nahlásiť chybu + Navrhnúť vylepšenie alebo nahlásiť chybu vývojárovi pluginu + Prejsť na repozitár pluginov spúšťača Flow + Prejsť na repozitár pluginov spúšťača Flow a zobraziť príspevky komunity + + \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs index 58ec5005f..3707beee2 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs @@ -1,13 +1,17 @@ using Flow.Launcher.Infrastructure.Storage; -using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin.PluginsManager.ViewModels; using Flow.Launcher.Plugin.PluginsManager.Views; using System.Collections.Generic; +using System.Linq; 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 + public class Main : ISettingProvider, IAsyncPlugin, ISavable, IContextMenu, IPluginI18n, IAsyncReloadable { internal PluginInitContext Context { get; set; } @@ -17,17 +21,28 @@ namespace Flow.Launcher.Plugin.PluginsManager private IContextMenu contextMenu; + internal PluginsManager pluginManager; + + private DateTime lastUpdateTime = DateTime.MinValue; + public Control CreateSettingPanel() { return new PluginsManagerSettings(viewModel); } - public void Init(PluginInitContext context) + public Task InitAsync(PluginInitContext context) { Context = context; viewModel = new SettingsViewModel(context); Settings = viewModel.Settings; - contextMenu = new ContextMenu(Context, Settings); + contextMenu = new ContextMenu(Context); + pluginManager = new PluginsManager(Context, Settings); + _ = pluginManager.UpdateManifest().ContinueWith(_ => + { + lastUpdateTime = DateTime.Now; + }, TaskContinuationOptions.OnlyOnRanToCompletion); + + return Task.CompletedTask; } public List LoadContextMenus(Result selectedResult) @@ -35,17 +50,32 @@ namespace Flow.Launcher.Plugin.PluginsManager return contextMenu.LoadContextMenus(selectedResult); } - public List Query(Query query) + public async Task> QueryAsync(Query query, CancellationToken token) { var search = query.Search.ToLower(); - var pluginManager = new PluginsManager(Context, Settings); + if (string.IsNullOrWhiteSpace(search)) + return pluginManager.GetDefaultHotKeys(); - if (!string.IsNullOrEmpty(search) - && ($"{Settings.UninstallHotkey} ".StartsWith(search) || search.StartsWith($"{Settings.UninstallHotkey} "))) - return pluginManager.RequestUninstall(search); - - return pluginManager.RequestInstallOrUpdate(search); + if ((DateTime.Now - lastUpdateTime).TotalHours > 12) // 12 hours + { + _ = pluginManager.UpdateManifest().ContinueWith(t => + { + lastUpdateTime = DateTime.Now; + }, TaskContinuationOptions.OnlyOnRanToCompletion); + } + + return search switch + { + 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) => await pluginManager.RequestUpdate(s, token), + _ => pluginManager.GetDefaultHotKeys().Where(hotkey => + { + hotkey.Score = StringMatcher.FuzzySearch(search, hotkey.Title).Score; + return hotkey.Score > 0; + }).ToList() + }; } public void Save() @@ -62,5 +92,11 @@ namespace Flow.Launcher.Plugin.PluginsManager { return Context.API.GetTranslation("plugin_pluginsmanager_plugin_description"); } + + public async Task ReloadDataAsync() + { + await pluginManager.UpdateManifest(); + lastUpdateTime = DateTime.Now; + } } -} +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Models/PluginsManifest.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Models/PluginsManifest.cs index 290221710..145aadc98 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Models/PluginsManifest.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Models/PluginsManifest.cs @@ -1,32 +1,24 @@ using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Logger; -using Newtonsoft.Json; using System; using System.Collections.Generic; +using System.Text.Json; using System.Threading.Tasks; namespace Flow.Launcher.Plugin.PluginsManager.Models { internal class PluginsManifest { - internal List UserPlugins { get; private set; } - internal PluginsManifest() - { - DownloadManifest(); - } + internal List UserPlugins { get; private set; } = new List(); - private void DownloadManifest() + internal async Task DownloadManifest() { - var json = string.Empty; try { - var t = Task.Run( - async () => - json = await Http.Get("https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/main/plugins.json")); + await using var jsonStream = await Http.GetStreamAsync("https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/main/plugins.json") + .ConfigureAwait(false); - t.Wait(); - - UserPlugins = JsonConvert.DeserializeObject>(json); + UserPlugins = await JsonSerializer.DeserializeAsync>(jsonStream).ConfigureAwait(false); } catch (Exception e) { @@ -34,7 +26,6 @@ namespace Flow.Launcher.Plugin.PluginsManager.Models UserPlugins = new List(); } - } } -} +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Models/UserPlugin.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Models/UserPlugin.cs index 3bc44e0f6..c1af3014b 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Models/UserPlugin.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Models/UserPlugin.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - + namespace Flow.Launcher.Plugin.PluginsManager.Models { public class UserPlugin diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs index fc4abc800..c2e392b51 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs @@ -1,25 +1,45 @@ +using Flow.Launcher.Core.Plugin; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin.PluginsManager.Models; +using Flow.Launcher.Plugin.SharedCommands; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using System.Windows; namespace Flow.Launcher.Plugin.PluginsManager { internal class PluginsManager { - private readonly PluginsManifest pluginsManifest; + private PluginsManifest pluginsManifest; private PluginInitContext Context { get; set; } private Settings Settings { get; set; } - private readonly string icoPath = "Images\\pluginsmanager.png"; + private bool shouldHideWindow = true; + + private bool ShouldHideWindow + { + set { shouldHideWindow = value; } + get + { + var setValue = shouldHideWindow; + // Default value for hide main window is true. Revert after get call. + // This ensures when set by another method to false, it is only used once. + shouldHideWindow = true; + + return setValue; + } + } + + internal readonly string icoPath = "Images\\pluginsmanager.png"; internal PluginsManager(PluginInitContext context, Settings settings) { @@ -27,47 +47,232 @@ namespace Flow.Launcher.Plugin.PluginsManager Context = context; Settings = settings; } - internal void InstallOrUpdate(UserPlugin plugin) + + private Task _downloadManifestTask = Task.CompletedTask; + + + internal Task UpdateManifest() + { + if (_downloadManifestTask.Status == TaskStatus.Running) + { + return _downloadManifestTask; + } + else + { + return _downloadManifestTask = pluginsManifest.DownloadManifest().ContinueWith(t => + 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.", icoPath, false), + TaskContinuationOptions.OnlyOnFaulted); + } + } + + internal List GetDefaultHotKeys() + { + return new List() + { + new Result() + { + Title = Settings.HotKeyInstall, + IcoPath = icoPath, + Action = _ => + { + Context.API.ChangeQuery("pm install "); + return false; + } + }, + new Result() + { + 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; + } + } + }; + } + + internal async Task InstallOrUpdate(UserPlugin plugin) { if (PluginExists(plugin.ID)) { - Context.API.ShowMsg("Plugin already installed"); + if (Context.API.GetAllPlugins() + .Any(x => x.Metadata.ID == plugin.ID && x.Metadata.Version.CompareTo(plugin.Version) < 0)) + { + if (MessageBox.Show(Context.API.GetTranslation("plugin_pluginsmanager_update_exists"), + Context.API.GetTranslation("plugin_pluginsmanager_update_title"), + MessageBoxButton.YesNo) == MessageBoxResult.Yes) + Context + .API + .ChangeQuery( + $"{Context.CurrentPluginMetadata.ActionKeywords.FirstOrDefault()} {Settings.HotkeyUpdate} {plugin.Name}"); + + Application.Current.MainWindow.Show(); + shouldHideWindow = false; + + return; + } + + Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_update_alreadyexists")); return; } var message = string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_prompt"), - Environment.NewLine, Environment.NewLine, - plugin.Name, plugin.Author); + plugin.Name, plugin.Author, + Environment.NewLine, Environment.NewLine); - if (MessageBox.Show(message, Context.API.GetTranslation("plugin_pluginsmanager_install_title"), MessageBoxButton.YesNo) == MessageBoxResult.No) + if (MessageBox.Show(message, Context.API.GetTranslation("plugin_pluginsmanager_install_title"), + MessageBoxButton.YesNo) == MessageBoxResult.No) return; - var filePath = Path.Combine(DataLocation.PluginsDirectory, $"{plugin.Name}{plugin.ID}.zip"); + var filePath = Path.Combine(DataLocation.PluginsDirectory, $"{plugin.Name}-{plugin.Version}.zip"); try { Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), - Context.API.GetTranslation("plugin_pluginsmanager_please_wait")); + Context.API.GetTranslation("plugin_pluginsmanager_please_wait")); - Http.Download(plugin.UrlDownload, filePath); + await Http.DownloadAsync(plugin.UrlDownload, filePath).ConfigureAwait(false); Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), - Context.API.GetTranslation("plugin_pluginsmanager_download_success")); + Context.API.GetTranslation("plugin_pluginsmanager_download_success")); + + Install(plugin, filePath); } catch (Exception e) { - Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), - Context.API.GetTranslation("plugin_pluginsmanager_download_success")); + Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"), + string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"), + plugin.Name)); - Log.Exception("PluginsManager", "An error occured while downloading plugin", e, "PluginDownload"); + Log.Exception("PluginsManager", "An error occured while downloading plugin", e, "InstallOrUpdate"); + + return; } - Application.Current.Dispatcher.Invoke(() => Install(plugin, filePath)); + Context.API.RestartApp(); } - internal void Update() + internal async ValueTask> RequestUpdate(string search, CancellationToken token) { - throw new NotImplementedException(); + if (!pluginsManifest.UserPlugins.Any()) + { + await UpdateManifest(); + } + + token.ThrowIfCancellationRequested(); + + var autocompletedResults = AutoCompleteReturnAllResults(search, + Settings.HotkeyUpdate, + "Update", + "Select a plugin to update"); + + if (autocompletedResults.Any()) + return autocompletedResults; + + var uninstallSearch = search.Replace(Settings.HotkeyUpdate, string.Empty).TrimStart(); + + + var resultsForUpdate = + 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 + select + new + { + pluginFromManifest.Name, + pluginFromManifest.Author, + CurrentVersion = existingPlugin.Metadata.Version, + NewVersion = pluginFromManifest.Version, + existingPlugin.Metadata.IcoPath, + PluginExistingMetadata = existingPlugin.Metadata, + PluginNewUserPlugin = pluginFromManifest + }; + + if (!resultsForUpdate.Any()) + return new List + { + new Result + { + Title = Context.API.GetTranslation("plugin_pluginsmanager_update_noresult_title"), + SubTitle = Context.API.GetTranslation("plugin_pluginsmanager_update_noresult_subtitle"), + IcoPath = icoPath + } + }; + + + var results = resultsForUpdate + .Select(x => + new Result + { + Title = $"{x.Name} by {x.Author}", + SubTitle = $"Update from version {x.CurrentVersion} to {x.NewVersion}", + IcoPath = x.IcoPath, + Action = e => + { + string message = string.Format( + Context.API.GetTranslation("plugin_pluginsmanager_update_prompt"), + x.Name, x.Author, + Environment.NewLine, Environment.NewLine); + + if (MessageBox.Show(message, + Context.API.GetTranslation("plugin_pluginsmanager_update_title"), + MessageBoxButton.YesNo) == MessageBoxResult.Yes) + { + Uninstall(x.PluginExistingMetadata); + + var downloadToFilePath = Path.Combine(DataLocation.PluginsDirectory, + $"{x.Name}-{x.NewVersion}.zip"); + + Task.Run(async delegate + { + Context.API.ShowMsg( + Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), + Context.API.GetTranslation("plugin_pluginsmanager_please_wait")); + + await Http.DownloadAsync(x.PluginNewUserPlugin.UrlDownload, downloadToFilePath) + .ConfigureAwait(false); + + Context.API.ShowMsg( + Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), + Context.API.GetTranslation("plugin_pluginsmanager_download_success")); + + Install(x.PluginNewUserPlugin, downloadToFilePath); + + Context.API.RestartApp(); + }).ContinueWith(t => + { + Log.Exception("PluginsManager", $"Update failed for {x.Name}", + t.Exception.InnerException, "RequestUpdate"); + Context.API.ShowMsg( + Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"), + string.Format( + Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"), + x.Name)); + }, TaskContinuationOptions.OnlyOnFaulted); + + return true; + } + + return false; + } + }); + + return Search(results, uninstallSearch); } internal bool PluginExists(string id) @@ -75,51 +280,53 @@ namespace Flow.Launcher.Plugin.PluginsManager return Context.API.GetAllPlugins().Any(x => x.Metadata.ID == id); } - internal void PluginsManifestSiteOpen() - { - //Open from context menu https://git.vcmq.workers.dev/Flow-Launcher/Flow.Launcher.PluginsManifest - throw new NotImplementedException(); - } - - internal List Search(List results, string searchName) + internal List Search(IEnumerable results, string searchName) { if (string.IsNullOrEmpty(searchName)) - return results; + return results.ToList(); return results - .Where(x => - { - var matchResult = StringMatcher.FuzzySearch(searchName, x.Title); - if (matchResult.IsSearchPrecisionScoreMet()) - x.Score = matchResult.Score; + .Where(x => + { + var matchResult = StringMatcher.FuzzySearch(searchName, x.Title); + if (matchResult.IsSearchPrecisionScoreMet()) + x.Score = matchResult.Score; - return matchResult.IsSearchPrecisionScoreMet(); - }) - .ToList(); + return matchResult.IsSearchPrecisionScoreMet(); + }) + .ToList(); } - internal List RequestInstallOrUpdate(string searchName) + internal async ValueTask> RequestInstallOrUpdate(string searchName, CancellationToken token) { + if (!pluginsManifest.UserPlugins.Any()) + { + await UpdateManifest(); + } + + token.ThrowIfCancellationRequested(); + + var searchNameWithoutKeyword = searchName.Replace(Settings.HotKeyInstall, string.Empty).Trim(); + var results = pluginsManifest - .UserPlugins - .Select(x => - new Result - { - Title = $"{x.Name} by {x.Author}", - SubTitle = x.Description, - IcoPath = icoPath, - Action = e => + .UserPlugins + .Select(x => + new Result { - Application.Current.MainWindow.Hide(); - InstallOrUpdate(x); + Title = $"{x.Name} by {x.Author}", + SubTitle = x.Description, + IcoPath = icoPath, + Action = e => + { + Application.Current.MainWindow.Hide(); + _ = InstallOrUpdate(x); // No need to wait + return ShouldHideWindow; + }, + ContextData = x + }); - return true; - } - }) - .ToList(); - - return Search(results, searchName); + return Search(results, searchNameWithoutKeyword); } private void Install(UserPlugin plugin, string downloadedFilePath) @@ -137,7 +344,9 @@ namespace Flow.Launcher.Plugin.PluginsManager var zipFilePath = Path.Combine(tempFolderPath, Path.GetFileName(downloadedFilePath)); - File.Move(downloadedFilePath, zipFilePath); + File.Copy(downloadedFilePath, zipFilePath); + + File.Delete(downloadedFilePath); Utilities.UnZip(zipFilePath, tempFolderPluginPath, true); @@ -153,36 +362,87 @@ namespace Flow.Launcher.Plugin.PluginsManager return; } - string newPluginPath = Path.Combine(DataLocation.PluginsDirectory, $"{plugin.Name}{plugin.ID}"); + string newPluginPath = Path.Combine(DataLocation.PluginsDirectory, $"{plugin.Name}-{plugin.Version}"); - Directory.Move(pluginFolderPath, newPluginPath); + FilesFolders.CopyAll(pluginFolderPath, newPluginPath); - if (MessageBox.Show(string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_successandrestart"), - plugin.Name, Environment.NewLine), - Context.API.GetTranslation("plugin_pluginsmanager_install_title"), - MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes) - Context.API.RestartApp(); + Directory.Delete(pluginFolderPath, true); } internal List RequestUninstall(string search) + { + var autocompletedResults = AutoCompleteReturnAllResults(search, + Settings.HotkeyUninstall, + "Uninstall", + "Select a plugin to uninstall"); + + if (autocompletedResults.Any()) + return autocompletedResults; + + var uninstallSearch = search.Replace(Settings.HotkeyUninstall, string.Empty).TrimStart(); + + var results = Context.API + .GetAllPlugins() + .Select(x => + new Result + { + Title = $"{x.Metadata.Name} by {x.Metadata.Author}", + SubTitle = x.Metadata.Description, + IcoPath = x.Metadata.IcoPath, + Action = e => + { + string message = string.Format( + Context.API.GetTranslation("plugin_pluginsmanager_uninstall_prompt"), + x.Metadata.Name, x.Metadata.Author, + Environment.NewLine, Environment.NewLine); + + if (MessageBox.Show(message, + Context.API.GetTranslation("plugin_pluginsmanager_uninstall_title"), + MessageBoxButton.YesNo) == MessageBoxResult.Yes) + { + Application.Current.MainWindow.Hide(); + Uninstall(x.Metadata); + Context.API.RestartApp(); + + return true; + } + + return false; + } + }); + + return Search(results, uninstallSearch); + } + + private void Uninstall(PluginMetadata plugin) + { + PluginManager.Settings.Plugins.Remove(plugin.ID); + PluginManager.AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID); + + // Marked for deletion. Will be deleted on next start up + using var _ = File.CreateText(Path.Combine(plugin.PluginDirectory, "NeedDelete.txt")); + } + + private List AutoCompleteReturnAllResults(string search, string hotkey, string title, string subtitle) { if (!string.IsNullOrEmpty(search) - && Settings.UninstallHotkey.StartsWith(search) - && (Settings.UninstallHotkey != search || !search.StartsWith(Settings.UninstallHotkey))) + && hotkey.StartsWith(search) + && (hotkey != search || !search.StartsWith(hotkey))) { - return + return new List { new Result { - Title = "Uninstall", + Title = title, IcoPath = icoPath, - SubTitle = "Select a plugin to uninstall", + SubTitle = subtitle, Action = e => { Context - .API - .ChangeQuery($"{Context.CurrentPluginMetadata.ActionKeywords.FirstOrDefault()} {Settings.UninstallHotkey} "); + .API + .ChangeQuery( + $"{Context.CurrentPluginMetadata.ActionKeywords.FirstOrDefault()} {hotkey} "); return false; } @@ -190,46 +450,7 @@ namespace Flow.Launcher.Plugin.PluginsManager }; } - var uninstallSearch = search.Replace(Settings.UninstallHotkey, string.Empty).TrimStart(); - - var results= Context.API - .GetAllPlugins() - .Select(x => - new Result - { - Title = $"{x.Metadata.Name} by {x.Metadata.Author}", - SubTitle = x.Metadata.Description, - IcoPath = x.Metadata.IcoPath, - Action = e => - { - Application.Current.MainWindow.Hide(); - Uninstall(x.Metadata); - - return true; - } - }) - .ToList(); - - return Search(results, uninstallSearch); - } - - private void Uninstall(PluginMetadata plugin) - { - string message = string.Format(Context.API.GetTranslation("plugin_pluginsmanager_uninstall_prompt"), - Environment.NewLine, Environment.NewLine, - plugin.Name, plugin.Author); - - if (MessageBox.Show(message, Context.API.GetTranslation("plugin_pluginsmanager_uninstall_title"), - MessageBoxButton.YesNo) == MessageBoxResult.Yes) - { - using var _ = File.CreateText(Path.Combine(plugin.PluginDirectory, "NeedDelete.txt")); - - if (MessageBox.Show(string.Format(Context.API.GetTranslation("plugin_pluginsmanager_uninstall_successandrestart"), - plugin.Name, Environment.NewLine), - Context.API.GetTranslation("plugin_pluginsmanager_uninstall_title"), - MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes) - Context.API.RestartApp(); - } + return new List(); } } -} +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs index 0c647e6ae..9c5b0d29f 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs @@ -6,6 +6,9 @@ namespace Flow.Launcher.Plugin.PluginsManager { internal class Settings { - internal string UninstallHotkey { get; set; } = "uninstall"; + internal string HotKeyInstall { get; set; } = "install"; + internal string HotkeyUninstall { get; set; } = "uninstall"; + + internal string HotkeyUpdate { get; set; } = "update"; } } diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json b/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json index 327011ac3..f95c0d60d 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json @@ -6,7 +6,7 @@ "Name": "Plugins Manager", "Description": "Management of installing, uninstalling or updating Flow Launcher plugins", "Author": "Jeremy Wu", - "Version": "1.0.0", + "Version": "1.7.0", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.PluginsManager.dll", diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj index cf9c96294..a643ebf86 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj @@ -36,14 +36,14 @@ - - PreserveNewest - - + Designer MSBuild:Compile PreserveNewest + + PreserveNewest + Always diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json b/Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json index d769397a8..2bb40c644 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json @@ -4,7 +4,7 @@ "Name":"Process Killer", "Description":"kill running processes from Flow", "Author":"Flow-Launcher", - "Version":"1.2.1", + "Version":"1.2.2", "Language":"csharp", "Website":"https://github.com/Flow-Launcher/Flow.Launcher.Plugin.ProcessKiller", "IcoPath":"Images\\app.png", diff --git a/Plugins/Flow.Launcher.Plugin.Program/AppxPackagingTlb.dll b/Plugins/Flow.Launcher.Plugin.Program/AppxPackagingTlb.dll deleted file mode 100644 index 183cfc085..000000000 Binary files a/Plugins/Flow.Launcher.Plugin.Program/AppxPackagingTlb.dll and /dev/null differ diff --git a/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj b/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj index 3802297c7..a2d7b7fef 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj +++ b/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj @@ -40,65 +40,14 @@ PreserveNewest - - - - .\AppxPackagingTlb.dll - True - - - .\ShObjIdlTlb.dll - True - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - Always - - + MSBuild:Compile Designer PreserveNewest - - - - - MSBuild:Compile - Designer - PreserveNewest - - - MSBuild:Compile - Designer - PreserveNewest - - - MSBuild:Compile - Designer - PreserveNewest - - - MSBuild:Compile - Designer - PreserveNewest - - - MSBuild:Compile - Designer + PreserveNewest diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index 8f124f3a4..bd09cd9c6 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; using System.Windows.Controls; using Flow.Launcher.Infrastructure.Logger; @@ -12,9 +13,8 @@ using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; namespace Flow.Launcher.Plugin.Program { - public class Main : ISettingProvider, IPlugin, IPluginI18n, IContextMenu, ISavable, IReloadable + public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, ISavable, IAsyncReloadable { - private static readonly object IndexLock = new object(); internal static Win32[] _win32s { get; set; } internal static UWP.Application[] _uwps { get; set; } internal static Settings _settings { get; set; } @@ -30,33 +30,6 @@ namespace Flow.Launcher.Plugin.Program public Main() { _settingsStorage = new PluginJsonStorage(); - _settings = _settingsStorage.Load(); - - Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Preload programs cost", () => - { - _win32Storage = new BinaryStorage("Win32"); - _win32s = _win32Storage.TryLoad(new Win32[] { }); - _uwpStorage = new BinaryStorage("UWP"); - _uwps = _uwpStorage.TryLoad(new UWP.Application[] { }); - }); - Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload win32 programs <{_win32s.Length}>"); - Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload uwps <{_uwps.Length}>"); - - var a = Task.Run(() => - { - if (IsStartupIndexProgramsRequired || !_win32s.Any()) - Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexWin32Programs); - }); - - var b = Task.Run(() => - { - if (IsStartupIndexProgramsRequired || !_uwps.Any()) - Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexUWPPrograms); - }); - - Task.WaitAll(a, b); - - _settings.LastIndexTime = DateTime.Today; } public void Save() @@ -66,28 +39,92 @@ namespace Flow.Launcher.Plugin.Program _uwpStorage.Save(_uwps); } - public List Query(Query query) + public async Task> QueryAsync(Query query, CancellationToken token) { + if (IsStartupIndexProgramsRequired) + _ = IndexPrograms(); + Win32[] win32; UWP.Application[] uwps; win32 = _win32s; uwps = _uwps; - var result = win32.Cast() - .Concat(uwps) - .AsParallel() - .Where(p => p.Enabled) - .Select(p => p.Result(query.Search, _context.API)) - .Where(r => r?.Score > 0) - .ToList(); + try + { + var result = await Task.Run(delegate + { + try + { + return win32.Cast() + .Concat(uwps) + .AsParallel() + .WithCancellation(token) + .Where(p => p.Enabled) + .Select(p => p.Result(query.Search, _context.API)) + .Where(r => r?.Score > 0) + .ToList(); + } + catch (OperationCanceledException) + { + return null; + } + }, token).ConfigureAwait(false); - return result; + token.ThrowIfCancellationRequested(); + + return result; + } + catch (OperationCanceledException) + { + return null; + } } - public void Init(PluginInitContext context) + public async Task InitAsync(PluginInitContext context) { _context = context; + + await Task.Run(() => + { + _settings = _settingsStorage.Load(); + + Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Preload programs cost", () => + { + _win32Storage = new BinaryStorage("Win32"); + _win32s = _win32Storage.TryLoad(new Win32[] { }); + _uwpStorage = new BinaryStorage("UWP"); + _uwps = _uwpStorage.TryLoad(new UWP.Application[] { }); + }); + Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload win32 programs <{_win32s.Length}>"); + Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload uwps <{_uwps.Length}>"); + }); + + bool indexedWinApps = false; + bool indexedUWPApps = false; + + var a = Task.Run(() => + { + if (IsStartupIndexProgramsRequired || !_win32s.Any()) + { + Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexWin32Programs); + indexedWinApps = true; + } + }); + + var b = Task.Run(() => + { + if (IsStartupIndexProgramsRequired || !_uwps.Any()) + { + Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexUwpPrograms); + indexedUWPApps = true; + } + }); + + await Task.WhenAll(a, b); + + if (indexedWinApps && indexedUWPApps) + _settings.LastIndexTime = DateTime.Today; } public static void IndexWin32Programs() @@ -95,10 +132,9 @@ namespace Flow.Launcher.Plugin.Program var win32S = Win32.All(_settings); _win32s = win32S; - } - public static void IndexUWPPrograms() + public static void IndexUwpPrograms() { var windows10 = new Version(10, 0); var support = Environment.OSVersion.Version.Major >= windows10.Major; @@ -106,16 +142,15 @@ namespace Flow.Launcher.Plugin.Program var applications = support ? UWP.All() : new UWP.Application[] { }; _uwps = applications; - } - public static void IndexPrograms() + public static async Task IndexPrograms() { - var t1 = Task.Run(() => IndexWin32Programs()); + var t1 = Task.Run(IndexWin32Programs); - var t2 = Task.Run(() => IndexUWPPrograms()); + var t2 = Task.Run(IndexUwpPrograms); - Task.WaitAll(t1, t2); + await Task.WhenAll(t1, t2).ConfigureAwait(false); _settings.LastIndexTime = DateTime.Today; } @@ -145,19 +180,21 @@ namespace Flow.Launcher.Plugin.Program } menuOptions.Add( - new Result - { - Title = _context.API.GetTranslation("flowlauncher_plugin_program_disable_program"), - Action = c => - { - DisableProgram(program); - _context.API.ShowMsg(_context.API.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_success"), - _context.API.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_success_message")); - return false; - }, - IcoPath = "Images/disable.png" - } - ); + new Result + { + Title = _context.API.GetTranslation("flowlauncher_plugin_program_disable_program"), + Action = c => + { + DisableProgram(program); + _context.API.ShowMsg( + _context.API.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_success"), + _context.API.GetTranslation( + "flowlauncher_plugin_program_disable_dlgtitle_success_message")); + return false; + }, + IcoPath = "Images/disable.png" + } + ); return menuOptions; } @@ -168,26 +205,29 @@ namespace Flow.Launcher.Plugin.Program return; if (_uwps.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier)) - _uwps.Where(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier).FirstOrDefault().Enabled = false; + _uwps.Where(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier) + .FirstOrDefault() + .Enabled = false; if (_win32s.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier)) - _win32s.Where(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier).FirstOrDefault().Enabled = false; + _win32s.Where(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier) + .FirstOrDefault() + .Enabled = false; _settings.DisabledProgramSources - .Add( - new Settings.DisabledProgramSource - { - Name = programToDelete.Name, - Location = programToDelete.Location, - UniqueIdentifier = programToDelete.UniqueIdentifier, - Enabled = false - } - ); + .Add( + new Settings.DisabledProgramSource + { + Name = programToDelete.Name, + Location = programToDelete.Location, + UniqueIdentifier = programToDelete.UniqueIdentifier, + Enabled = false + } + ); } public static void StartProcess(Func runProcess, ProcessStartInfo info) { - bool hide; try { runProcess(info); @@ -200,9 +240,9 @@ namespace Flow.Launcher.Plugin.Program } } - public void ReloadData() + public async Task ReloadDataAsync() { - IndexPrograms(); + await IndexPrograms(); } } -} \ No newline at end of file +} diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/ApplicationActivationHelper.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/ApplicationActivationHelper.cs new file mode 100644 index 000000000..4b8423ed6 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/ApplicationActivationHelper.cs @@ -0,0 +1,44 @@ +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; + +namespace Flow.Launcher.Plugin.Program.Programs +{ + public class ApplicationActivationHelper + { + // Reference : https://github.com/MicrosoftEdge/edge-launcher/blob/108e63df0b4cb5cd9d5e45aa7a264690851ec51d/MIcrosoftEdgeLauncherCsharp/Program.cs + public enum ActivateOptions + { + None = 0x00000000, + DesignMode = 0x00000001, + NoErrorUI = 0x00000002, + NoSplashScreen = 0x00000004, + } + + /// ApplicationActivationManager + [ComImport, Guid("2e941141-7f97-4756-ba1d-9decde894a3d"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + interface IApplicationActivationManager + { + IntPtr ActivateApplication([In] string appUserModelId, [In] string arguments, [In] ActivateOptions options, [Out] out uint processId); + IntPtr ActivateForFile([In] string appUserModelId, [In] IntPtr /*IShellItemArray* */ itemArray, [In] string verb, [Out] out uint processId); + IntPtr ActivateForProtocol([In] string appUserModelId, [In] IntPtr /* IShellItemArray* */itemArray, [Out] out uint processId); + } + + // Application Activation Manager Class + [ComImport, Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C")] + public class ApplicationActivationManager : IApplicationActivationManager + { + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)/*, PreserveSig*/] + public extern IntPtr ActivateApplication([In] string appUserModelId, [In] string arguments, [In] ActivateOptions options, [Out] out uint processId); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + public extern IntPtr ActivateForFile([In] string appUserModelId, [In] IntPtr /*IShellItemArray* */ itemArray, [In] string verb, [Out] out uint processId); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + public extern IntPtr ActivateForProtocol([In] string appUserModelId, [In] IntPtr /* IShellItemArray* */itemArray, [Out] out uint processId); + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/AppxPackageHelper.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/AppxPackageHelper.cs new file mode 100644 index 000000000..e48f30757 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/AppxPackageHelper.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using Windows.Storage; + +namespace Flow.Launcher.Plugin.Program.Programs +{ + public class AppxPackageHelper + { + // This function returns a list of attributes of applications + public List getAppsFromManifest(IStream stream) + { + List apps = new List(); + var appxFactory = new AppxFactory(); + var reader = ((IAppxFactory)appxFactory).CreateManifestReader(stream); + var manifestApps = reader.GetApplications(); + while (manifestApps.GetHasCurrent()) + { + string appListEntry; + var manifestApp = manifestApps.GetCurrent(); + manifestApp.GetStringValue("AppListEntry", out appListEntry); + if (appListEntry != "none") + { + apps.Add(manifestApp); + } + manifestApps.MoveNext(); + } + return apps; + } + + // Reference : https://stackoverflow.com/questions/32122679/getting-icon-of-modern-windows-app-from-a-desktop-application + [Guid("5842a140-ff9f-4166-8f5c-62f5b7b0c781"), ComImport] + public class AppxFactory + { + } + + [Guid("BEB94909-E451-438B-B5A7-D79E767B75D8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IAppxFactory + { + void _VtblGap0_2(); // skip 2 methods + IAppxManifestReader CreateManifestReader(IStream inputStream); + } + + [Guid("4E1BD148-55A0-4480-A3D1-15544710637C"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IAppxManifestReader + { + void _VtblGap0_1(); // skip 1 method + IAppxManifestProperties GetProperties(); + void _VtblGap1_5(); // skip 5 methods + IAppxManifestApplicationsEnumerator GetApplications(); + } + + [Guid("9EB8A55A-F04B-4D0D-808D-686185D4847A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IAppxManifestApplicationsEnumerator + { + IAppxManifestApplication GetCurrent(); + bool GetHasCurrent(); + bool MoveNext(); + } + + [Guid("5DA89BF4-3773-46BE-B650-7E744863B7E8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IAppxManifestApplication + { + [PreserveSig] + int GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string name, [MarshalAs(UnmanagedType.LPWStr)] out string value); + + [PreserveSig] + int GetAppUserModelId([MarshalAs(UnmanagedType.LPWStr)] out string value); + } + + [Guid("03FAF64D-F26F-4B2C-AAF7-8FE7789B8BCA"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IAppxManifestProperties + { + [PreserveSig] + int GetBoolValue([MarshalAs(UnmanagedType.LPWStr)]string name, out bool value); + [PreserveSig] + int GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string name, [MarshalAs(UnmanagedType.LPWStr)] out string value); + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs new file mode 100644 index 000000000..4ded3412a --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Runtime.InteropServices; +using System.IO; +using Accessibility; +using System.Runtime.InteropServices.ComTypes; +using System.Security.Policy; + +namespace Flow.Launcher.Plugin.Program.Programs +{ + class ShellLinkHelper + { + [Flags()] + public enum SLGP_FLAGS + { + SLGP_SHORTPATH = 0x1, + SLGP_UNCPRIORITY = 0x2, + SLGP_RAWPATH = 0x4 + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct WIN32_FIND_DATAW + { + public uint dwFileAttributes; + public long ftCreationTime; + public long ftLastAccessTime; + public long ftLastWriteTime; + public uint nFileSizeHigh; + public uint nFileSizeLow; + public uint dwReserved0; + public uint dwReserved1; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] + public string cFileName; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] + public string cAlternateFileName; + } + + [Flags()] + public enum SLR_FLAGS + { + SLR_NO_UI = 0x1, + SLR_ANY_MATCH = 0x2, + SLR_UPDATE = 0x4, + SLR_NOUPDATE = 0x8, + SLR_NOSEARCH = 0x10, + SLR_NOTRACK = 0x20, + SLR_NOLINKINFO = 0x40, + SLR_INVOKE_MSI = 0x80 + } + + + // Reference : http://www.pinvoke.net/default.aspx/Interfaces.IShellLinkW + /// The IShellLink interface allows Shell links to be created, modified, and resolved + [ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("000214F9-0000-0000-C000-000000000046")] + interface IShellLinkW + { + /// Retrieves the path and file name of a Shell link object + void GetPath([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, ref WIN32_FIND_DATAW pfd, SLGP_FLAGS fFlags); + /// Retrieves the list of item identifiers for a Shell link object + void GetIDList(out IntPtr ppidl); + /// Sets the pointer to an item identifier list (PIDL) for a Shell link object. + void SetIDList(IntPtr pidl); + /// Retrieves the description string for a Shell link object + void GetDescription([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName); + /// Sets the description for a Shell link object. The description can be any application-defined string + void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName); + /// Retrieves the name of the working directory for a Shell link object + void GetWorkingDirectory([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath); + /// Sets the name of the working directory for a Shell link object + void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir); + /// Retrieves the command-line arguments associated with a Shell link object + void GetArguments([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath); + /// Sets the command-line arguments for a Shell link object + void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs); + /// Retrieves the hot key for a Shell link object + void GetHotkey(out short pwHotkey); + /// Sets a hot key for a Shell link object + void SetHotkey(short wHotkey); + /// Retrieves the show command for a Shell link object + void GetShowCmd(out int piShowCmd); + /// Sets the show command for a Shell link object. The show command sets the initial show state of the window. + void SetShowCmd(int iShowCmd); + /// Retrieves the location (path and index) of the icon for a Shell link object + void GetIconLocation([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, + int cchIconPath, out int piIcon); + /// Sets the location (path and index) of the icon for a Shell link object + void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon); + /// Sets the relative path to the Shell link object + void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved); + /// Attempts to find the target of a Shell link, even if it has been moved or renamed + void Resolve(ref Accessibility._RemotableHandle hwnd, SLR_FLAGS fFlags); + /// Sets the path and file name of a Shell link object + void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile); + } + + [ComImport(), Guid("00021401-0000-0000-C000-000000000046")] + public class ShellLink + { + } + + // To initialize the app description + public String description = String.Empty; + + + // Retrieve the target path using Shell Link + public string retrieveTargetPath(string path) + { + var link = new ShellLink(); + const int STGM_READ = 0; + ((IPersistFile)link).Load(path, STGM_READ); + var hwnd = new _RemotableHandle(); + ((IShellLinkW)link).Resolve(ref hwnd, 0); + + const int MAX_PATH = 260; + StringBuilder buffer = new StringBuilder(MAX_PATH); + + var data = new WIN32_FIND_DATAW(); + ((IShellLinkW)link).GetPath(buffer, buffer.Capacity, ref data, SLGP_FLAGS.SLGP_SHORTPATH); + var target = buffer.ToString(); + + // To set the app description + if (!String.IsNullOrEmpty(target)) + { + buffer = new StringBuilder(MAX_PATH); + ((IShellLinkW)link).GetDescription(buffer, MAX_PATH); + description = buffer.ToString(); + } + return target; + } + } +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs index 3ea78156d..855a11c86 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; using System.Security.Principal; using System.Text; using System.Threading.Tasks; @@ -12,12 +13,10 @@ using System.Windows.Media.Imaging; using System.Xml.Linq; using Windows.ApplicationModel; using Windows.Management.Deployment; -using AppxPackaing; -using Shell; using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin.Program.Logger; -using IStream = AppxPackaing.IStream; using Rect = System.Windows.Rect; +using Flow.Launcher.Plugin.SharedModels; namespace Flow.Launcher.Plugin.Program.Programs { @@ -51,33 +50,27 @@ namespace Flow.Launcher.Plugin.Program.Programs private void InitializeAppInfo() { + AppxPackageHelper _helper = new AppxPackageHelper(); var path = Path.Combine(Location, "AppxManifest.xml"); var namespaces = XmlNamespaces(path); InitPackageVersion(namespaces); - var appxFactory = new AppxFactory(); - IStream stream; const uint noAttribute = 0x80; const Stgm exclusiveRead = Stgm.Read | Stgm.ShareExclusive; - var hResult = SHCreateStreamOnFileEx(path, exclusiveRead, noAttribute, false, null, out stream); + var hResult = SHCreateStreamOnFileEx(path, exclusiveRead, noAttribute, false, null, out IStream stream); if (hResult == Hresult.Ok) { - var reader = appxFactory.CreateManifestReader(stream); - var manifestApps = reader.GetApplications(); var apps = new List(); - while (manifestApps.GetHasCurrent() != 0) + + List _apps = _helper.getAppsFromManifest(stream); + foreach(var _app in _apps) { - var manifestApp = manifestApps.GetCurrent(); - var appListEntry = manifestApp.GetStringValue("AppListEntry"); - if (appListEntry != "none") - { - var app = new Application(manifestApp, this); - apps.Add(app); - } - manifestApps.MoveNext(); + var app = new Application(_app, this); + apps.Add(app); } + Apps = apps.Where(a => a.AppListEntry != "none").ToArray(); } else @@ -206,12 +199,11 @@ namespace Flow.Launcher.Plugin.Program.Programs } catch (Exception e) { - ProgramLogger.LogException("UWP" ,"CurrentUserPackages", $"id","An unexpected error occured and " + ProgramLogger.LogException("UWP", "CurrentUserPackages", $"id", "An unexpected error occured and " + $"unable to verify if package is valid", e); return false; } - - + return valid; }); return ps; @@ -254,6 +246,7 @@ namespace Flow.Launcher.Plugin.Program.Programs public string UserModelId { get; set; } public string BackgroundColor { get; set; } + public string EntryPoint { get; set; } public string Name => DisplayName; public string Location => Package.Location; @@ -263,21 +256,40 @@ namespace Flow.Launcher.Plugin.Program.Programs public string LogoPath { get; set; } public UWP Package { get; set; } - public Application(){} + public Application() { } public Result Result(string query, IPublicAPI api) { - var title = (Name, Description) switch - { - (var n, null) => n, - (var n, var d) when d.StartsWith(n) => d, - (var n, var d) when n.StartsWith(d) => n, - (var n, var d) when !string.IsNullOrEmpty(d) => $"{n}: {d}", - _ => Name - }; + string title; + MatchResult matchResult; - var matchResult = StringMatcher.FuzzySearch(query, title); + // We suppose Name won't be null + if (Description == null || Name.StartsWith(Description)) + { + title = Name; + matchResult = StringMatcher.FuzzySearch(query, title); + } + else if (Description.StartsWith(Name)) + { + title = Description; + matchResult = StringMatcher.FuzzySearch(query, Description); + } + else + { + title = $"{Name}: {Description}"; + var nameMatch = StringMatcher.FuzzySearch(query, Name); + var desciptionMatch = StringMatcher.FuzzySearch(query, Description); + if (desciptionMatch.Score > nameMatch.Score) + { + for (int i = 0; i < desciptionMatch.MatchData.Count; i++) + { + desciptionMatch.MatchData[i] += Name.Length + 2; // 2 is ": " + } + matchResult = desciptionMatch; + } + else matchResult = nameMatch; + } if (!matchResult.Success) return null; @@ -311,7 +323,7 @@ namespace Flow.Launcher.Plugin.Program.Programs Action = _ => { - Main.StartProcess(Process.Start, + Main.StartProcess(Process.Start, new ProcessStartInfo( !string.IsNullOrEmpty(Main._settings.CustomizedExplorer) ? Main._settings.CustomizedExplorer @@ -331,15 +343,14 @@ namespace Flow.Launcher.Plugin.Program.Programs private async void Launch(IPublicAPI api) { - var appManager = new ApplicationActivationManager(); - uint unusedPid; + var appManager = new ApplicationActivationHelper.ApplicationActivationManager(); const string noArgs = ""; - const ACTIVATEOPTIONS noFlags = ACTIVATEOPTIONS.AO_NONE; + const ApplicationActivationHelper.ActivateOptions noFlags = ApplicationActivationHelper.ActivateOptions.None; await Task.Run(() => { try { - appManager.ActivateApplication(UserModelId, noArgs, noFlags, out unusedPid); + appManager.ActivateApplication(UserModelId, noArgs, noFlags, out _); } catch (Exception) { @@ -350,13 +361,24 @@ namespace Flow.Launcher.Plugin.Program.Programs }); } - public Application(IAppxManifestApplication manifestApp, UWP package) + public Application(AppxPackageHelper.IAppxManifestApplication manifestApp, UWP package) { - UserModelId = manifestApp.GetAppUserModelId(); - UniqueIdentifier = manifestApp.GetAppUserModelId(); - DisplayName = manifestApp.GetStringValue("DisplayName"); - Description = manifestApp.GetStringValue("Description"); - BackgroundColor = manifestApp.GetStringValue("BackgroundColor"); + // This is done because we cannot use the keyword 'out' along with a property + + manifestApp.GetAppUserModelId(out string tmpUserModelId); + manifestApp.GetAppUserModelId(out string tmpUniqueIdentifier); + manifestApp.GetStringValue("DisplayName", out string tmpDisplayName); + manifestApp.GetStringValue("Description", out string tmpDescription); + manifestApp.GetStringValue("BackgroundColor", out string tmpBackgroundColor); + manifestApp.GetStringValue("EntryPoint", out string tmpEntryPoint); + + UserModelId = tmpUserModelId; + UniqueIdentifier = tmpUniqueIdentifier; + DisplayName = tmpDisplayName; + Description = tmpDescription; + BackgroundColor = tmpBackgroundColor; + EntryPoint = tmpEntryPoint; + Package = package; DisplayName = ResourceFromPri(package.FullName, package.Name, DisplayName); @@ -403,14 +425,14 @@ namespace Flow.Launcher.Plugin.Program.Programs public string FormattedPriReferenceValue(string packageName, string rawPriReferenceValue) { const string prefix = "ms-resource:"; - + if (string.IsNullOrWhiteSpace(rawPriReferenceValue) || !rawPriReferenceValue.StartsWith(prefix)) return rawPriReferenceValue; string key = rawPriReferenceValue.Substring(prefix.Length); if (key.StartsWith("//")) return $"{prefix}{key}"; - + if (!key.StartsWith("/")) { key = $"/{key}"; @@ -424,7 +446,7 @@ namespace Flow.Launcher.Plugin.Program.Programs return $"{prefix}//{packageName}{key}"; } - internal string LogoUriFromManifest(IAppxManifestApplication app) + internal string LogoUriFromManifest(AppxPackageHelper.IAppxManifestApplication app) { var logoKeyFromVersion = new Dictionary { @@ -435,7 +457,7 @@ namespace Flow.Launcher.Plugin.Program.Programs if (logoKeyFromVersion.ContainsKey(Package.Version)) { var key = logoKeyFromVersion[Package.Version]; - var logoUri = app.GetStringValue(key); + app.GetStringValue(key, out string logoUri); return logoUri; } else diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs index 77278330a..372d36524 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; @@ -8,10 +7,13 @@ using System.Security; using System.Text; using System.Threading.Tasks; using Microsoft.Win32; -using Shell; using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin.Program.Logger; using Flow.Launcher.Plugin.SharedCommands; +using Flow.Launcher.Plugin.SharedModels; +using Flow.Launcher.Infrastructure.Logger; +using System.Diagnostics; +using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; namespace Flow.Launcher.Plugin.Program.Programs { @@ -22,6 +24,7 @@ namespace Flow.Launcher.Plugin.Program.Programs public string UniqueIdentifier { get; set; } public string IcoPath { get; set; } public string FullPath { get; set; } + public string LnkResolvedPath { get; set; } public string ParentDirectory { get; set; } public string ExecutableName { get; set; } public string Description { get; set; } @@ -30,39 +33,73 @@ namespace Flow.Launcher.Plugin.Program.Programs public string Location => ParentDirectory; private const string ShortcutExtension = "lnk"; - private const string ApplicationReferenceExtension = "appref-ms"; private const string ExeExtension = "exe"; public Result Result(string query, IPublicAPI api) { - var title = (Name, Description) switch + string title; + + var nameMatchResult = StringMatcher.FuzzySearch(query, Name); + var descriptionMatchResult = StringMatcher.FuzzySearch(query, Description); + + var pathMatchResult = new MatchResult(false, 0, new List(), 0); + if (ExecutableName != null) // only lnk program will need this one + pathMatchResult = StringMatcher.FuzzySearch(query, ExecutableName); + + MatchResult matchResult = nameMatchResult; + + if (nameMatchResult.Score < descriptionMatchResult.Score) + matchResult = descriptionMatchResult; + + if (!matchResult.IsSearchPrecisionScoreMet()) { - (var n, null) => n, - (var n, var d) when d.StartsWith(n) => d, - (var n, var d) when n.StartsWith(d) => n, - (var n, var d) when !string.IsNullOrEmpty(d) => $"{n}: {d}", - _ => Name - }; + if (pathMatchResult.IsSearchPrecisionScoreMet()) + matchResult = pathMatchResult; + else return null; + } - var matchResult = StringMatcher.FuzzySearch(query, title); + // We suppose Name won't be null + if (Description == null || Name.StartsWith(Description)) + { + title = Name; + } + else if (Description.StartsWith(Name)) + { + title = Description; + } + else + { + title = $"{Name}: {Description}"; - if (!matchResult.Success) - return null; + if (matchResult == descriptionMatchResult) + { + for (int i = 0; i < descriptionMatchResult.MatchData.Count; i++) + { + matchResult.MatchData[i] += Name.Length + 2; // 2 is ": " + } + } + } + + if (matchResult == pathMatchResult) + { + // path Match won't have valid highlight data + matchResult.MatchData = new List(); + } var result = new Result { Title = title, - SubTitle = FullPath, + SubTitle = LnkResolvedPath ?? FullPath, IcoPath = IcoPath, Score = matchResult.Score, TitleHighlightData = matchResult.MatchData, ContextData = this, - Action = e => + Action = _ => { var info = new ProcessStartInfo { - FileName = FullPath, + FileName = LnkResolvedPath ?? FullPath, WorkingDirectory = ParentDirectory, UseShellExecute = true }; @@ -124,19 +161,19 @@ namespace Flow.Launcher.Plugin.Program.Programs Action = _ => { var args = !string.IsNullOrWhiteSpace(Main._settings.CustomizedArgs) - ? Main._settings.CustomizedArgs - .Replace("%s",$"\"{ParentDirectory}\"") - .Replace("%f",$"\"{FullPath}\"") - : Main._settings.CustomizedExplorer==Settings.Explorer - ? $"/select,\"{FullPath}\"" - : Settings.ExplorerArgs; + ? Main._settings.CustomizedArgs + .Replace("%s", $"\"{ParentDirectory}\"") + .Replace("%f", $"\"{FullPath}\"") + : Main._settings.CustomizedExplorer == Settings.Explorer + ? $"/select,\"{FullPath}\"" + : Settings.ExplorerArgs; Main.StartProcess(Process.Start, - new ProcessStartInfo( - !string.IsNullOrWhiteSpace(Main._settings.CustomizedExplorer) - ? Main._settings.CustomizedExplorer - : Settings.Explorer, - args)); + new ProcessStartInfo( + !string.IsNullOrWhiteSpace(Main._settings.CustomizedExplorer) + ? Main._settings.CustomizedExplorer + : Settings.Explorer, + args)); return true; }, @@ -147,10 +184,9 @@ namespace Flow.Launcher.Plugin.Program.Programs } - public override string ToString() { - return ExecutableName; + return Name; } private static Win32 Win32Program(string path) @@ -173,7 +209,7 @@ namespace Flow.Launcher.Plugin.Program.Programs catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) { ProgramLogger.LogException($"|Win32|Win32Program|{path}" + - $"|Permission denied when trying to load the program from {path}", e); + $"|Permission denied when trying to load the program from {path}", e); return new Win32() { Valid = false, Enabled = false }; } @@ -184,27 +220,21 @@ namespace Flow.Launcher.Plugin.Program.Programs var program = Win32Program(path); try { - var link = new ShellLink(); - const uint STGM_READ = 0; - ((IPersistFile)link).Load(path, STGM_READ); - var hwnd = new _RemotableHandle(); - link.Resolve(ref hwnd, 0); - const int MAX_PATH = 260; StringBuilder buffer = new StringBuilder(MAX_PATH); + ShellLinkHelper _helper = new ShellLinkHelper(); + string target = _helper.retrieveTargetPath(path); - var data = new _WIN32_FIND_DATAW(); - const uint SLGP_SHORTPATH = 1; - link.GetPath(buffer, buffer.Capacity, ref data, SLGP_SHORTPATH); - var target = buffer.ToString(); if (!string.IsNullOrEmpty(target)) { var extension = Extension(target); if (extension == ExeExtension && File.Exists(target)) { - buffer = new StringBuilder(MAX_PATH); - link.GetDescription(buffer, MAX_PATH); - var description = buffer.ToString(); + program.LnkResolvedPath = program.FullPath; + program.FullPath = Path.GetFullPath(target).ToLower(); + program.ExecutableName = Path.GetFileName(target); + + var description = _helper.description; if (!string.IsNullOrEmpty(description)) { program.Description = description; @@ -219,13 +249,15 @@ namespace Flow.Launcher.Plugin.Program.Programs } } } + return program; } catch (COMException e) { // C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\MiracastView.lnk always cause exception ProgramLogger.LogException($"|Win32|LnkProgram|{path}" + - "|Error caused likely due to trying to get the description of the program", e); + "|Error caused likely due to trying to get the description of the program", + e); program.Valid = false; return program; @@ -255,7 +287,7 @@ namespace Flow.Launcher.Plugin.Program.Programs catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) { ProgramLogger.LogException($"|Win32|ExeProgram|{path}" + - $"|Permission denied when trying to load the program from {path}", e); + $"|Permission denied when trying to load the program from {path}", e); return new Win32() { Valid = false, Enabled = false }; } @@ -264,28 +296,13 @@ namespace Flow.Launcher.Plugin.Program.Programs private static IEnumerable ProgramPaths(string directory, string[] suffixes) { if (!Directory.Exists(directory)) - return new string[] { }; - try - { - var paths = Directory.EnumerateFiles(directory, "*", new EnumerationOptions - { - IgnoreInaccessible = true, - RecurseSubdirectories = true - }) - .Where(x => suffixes.Contains(Extension(x))); + return Enumerable.Empty(); - return paths; - } - catch (DirectoryNotFoundException e) + return Directory.EnumerateFiles(directory, "*", new EnumerationOptions { - ProgramLogger.LogException($"Directory not found {directory}", e); - return new string[] { }; - } - catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) - { - ProgramLogger.LogException($"Permission denied {directory}", e); - return new string[] { }; - } + IgnoreInaccessible = true, + RecurseSubdirectories = true + }).Where(x => suffixes.Contains(Extension(x))); } private static string Extension(string path) @@ -301,14 +318,13 @@ namespace Flow.Launcher.Plugin.Program.Programs } } - private static ParallelQuery UnregisteredPrograms(List sources, string[] suffixes) + private static IEnumerable UnregisteredPrograms(List sources, string[] suffixes) { - var paths = sources.Where(s => Directory.Exists(s.Location) && s.Enabled) - .SelectMany(s => ProgramPaths(s.Location, suffixes)) - .Where(t1 => !Main._settings.DisabledProgramSources.Any(x => t1 == x.UniqueIdentifier)) + var paths = ExceptDisabledSource(sources.Where(s => Directory.Exists(s.Location) && s.Enabled) + .SelectMany(s => ProgramPaths(s.Location, suffixes)), x => x) .Distinct(); - var programs = paths.AsParallel().Select(x => Extension(x) switch + var programs = paths.Select(x => Extension(x) switch { ExeExtension => ExeProgram(x), ShortcutExtension => LnkProgram(x), @@ -319,7 +335,7 @@ namespace Flow.Launcher.Plugin.Program.Programs return programs; } - private static ParallelQuery StartMenuPrograms(string[] suffixes) + private static IEnumerable StartMenuPrograms(string[] suffixes) { var disabledProgramsList = Main._settings.DisabledProgramSources; @@ -330,53 +346,49 @@ namespace Flow.Launcher.Plugin.Program.Programs var toFilter = paths1.Concat(paths2); - var programs = toFilter - .AsParallel() - .Where(t1 => !disabledProgramsList.Any(x => x.UniqueIdentifier == t1)) - .Distinct() - .Select(x => Extension(x) switch - { - ShortcutExtension => LnkProgram(x), - _ => Win32Program(x) - }).Where(x => x.Valid); + var programs = ExceptDisabledSource(toFilter.Distinct()) + .Select(x => Extension(x) switch + { + ShortcutExtension => LnkProgram(x), + _ => Win32Program(x) + }).Where(x => x.Valid); return programs; } - private static ParallelQuery AppPathsPrograms(string[] suffixes) + private static IEnumerable AppPathsPrograms(string[] suffixes) { // https://msdn.microsoft.com/en-us/library/windows/desktop/ee872121 const string appPaths = @"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths"; - var programs = new List(); - using (var root = Registry.LocalMachine.OpenSubKey(appPaths)) + + IEnumerable toFilter = Enumerable.Empty(); + + using var rootMachine = Registry.LocalMachine.OpenSubKey(appPaths); + using var rootUser = Registry.CurrentUser.OpenSubKey(appPaths); + + if (rootMachine != null) { - if (root != null) - { - programs.AddRange(GetProgramsFromRegistry(root)); - } - } - using (var root = Registry.CurrentUser.OpenSubKey(appPaths)) - { - if (root != null) - { - programs.AddRange(GetProgramsFromRegistry(root)); - } + toFilter = toFilter.Concat(GetPathFromRegistry(rootMachine)); } - var disabledProgramsList = Main._settings.DisabledProgramSources; - var toFilter = programs.AsParallel().Where(p => suffixes.Contains(Extension(p.ExecutableName))); + if (rootUser != null) + { + toFilter = toFilter.Concat(GetPathFromRegistry(rootUser)); + } - var filtered = toFilter.Where(t1 => !disabledProgramsList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier)).Select(t1 => t1); - return filtered; + toFilter = toFilter.Distinct().Where(p => suffixes.Contains(Extension(p))); + + var filtered = ExceptDisabledSource(toFilter); + + return filtered.Select(GetProgramFromPath).ToList(); // ToList due to disposing issue } - private static IEnumerable GetProgramsFromRegistry(RegistryKey root) + private static IEnumerable GetPathFromRegistry(RegistryKey root) { return root - .GetSubKeyNames() - .Select(x => GetProgramPathFromRegistrySubKeys(root, x)) - .Distinct() - .Select(x => GetProgramFromPath(x)); + .GetSubKeyNames() + .Select(x => GetProgramPathFromRegistrySubKeys(root, x)) + .Distinct(); } private static string GetProgramPathFromRegistrySubKeys(RegistryKey root, string subkey) @@ -402,7 +414,7 @@ namespace Flow.Launcher.Plugin.Program.Programs catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) { ProgramLogger.LogException($"|Win32|GetProgramPathFromRegistrySubKeys|{path}" + - $"|Permission denied when trying to load the program from {path}", e); + $"|Permission denied when trying to load the program from {path}", e); return string.Empty; } @@ -411,27 +423,74 @@ namespace Flow.Launcher.Plugin.Program.Programs private static Win32 GetProgramFromPath(string path) { if (string.IsNullOrEmpty(path)) - return new Win32(); + return null; path = Environment.ExpandEnvironmentVariables(path); if (!File.Exists(path)) - return new Win32(); + return null; var entry = Win32Program(path); - entry.ExecutableName = Path.GetFileName(path); return entry; } + public static IEnumerable ExceptDisabledSource(IEnumerable sources) + { + return ExceptDisabledSource(sources, x => x); + } + + public static IEnumerable ExceptDisabledSource(IEnumerable sources, + Func keySelector) + { + return Main._settings.DisabledProgramSources.Count == 0 + ? sources + : ExceptDisabledSourceEnumerable(sources, keySelector); + + static IEnumerable ExceptDisabledSourceEnumerable(IEnumerable elements, + Func selector) + { + var set = Main._settings.DisabledProgramSources.Select(x => x.UniqueIdentifier).ToHashSet(); + + foreach (var element in elements) + { + if (!set.Contains(selector(element))) + yield return element; + } + } + } + + public static IEnumerable DistinctBy(IEnumerable source, Func selector) + { + var set = new HashSet(); + foreach (var item in source) + { + if (set.Add(selector(item))) + yield return item; + } + } + + private static Win32[] ProgramsHasher(IEnumerable programs) + { + return programs.GroupBy(p => p.FullPath.ToLower()) + .SelectMany(g => + { + if (g.Count() > 1) + return DistinctBy(g.Where(p => !string.IsNullOrEmpty(p.Description)), x => x.Description); + return g; + }).ToArray(); + } + + public static Win32[] All(Settings settings) { try { - var programs = new List().AsParallel(); + var programs = Enumerable.Empty(); var unregistered = UnregisteredPrograms(settings.ProgramSources, settings.ProgramSuffixes); programs = programs.Concat(unregistered); + if (settings.EnableRegistrySource) { var appPaths = AppPathsPrograms(settings.ProgramSuffixes); @@ -444,7 +503,8 @@ namespace Flow.Launcher.Plugin.Program.Programs programs = programs.Concat(startMenu); } - return programs.ToArray(); + + return ProgramsHasher(programs.Where(p => p != null)); } #if DEBUG //This is to make developer aware of any unhandled exception and add in handling. catch (Exception e) @@ -458,9 +518,9 @@ namespace Flow.Launcher.Plugin.Program.Programs { ProgramLogger.LogException("|Win32|All|Not available|An unexpected error occurred", e); - return new Win32[0]; + return Array.Empty(); } #endif } } -} +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Program/ShObjIdlTlb.dll b/Plugins/Flow.Launcher.Plugin.Program/ShObjIdlTlb.dll deleted file mode 100644 index 83815e40a..000000000 Binary files a/Plugins/Flow.Launcher.Plugin.Program/ShObjIdlTlb.dll and /dev/null differ diff --git a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs index e4e92b9bc..d66ca345e 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs @@ -51,7 +51,7 @@ namespace Flow.Launcher.Plugin.Program.Views private void ViewRefresh() { - if(programSourceView.Items.Count == 0 + if (programSourceView.Items.Count == 0 && btnProgramSourceStatus.Visibility == Visibility.Visible && btnEditProgramSource.Visibility == Visibility.Visible) { @@ -70,21 +70,19 @@ namespace Flow.Launcher.Plugin.Program.Views programSourceView.Items.Refresh(); } - private void ReIndexing() + private async void ReIndexing() { ViewRefresh(); - Task.Run(() => - { - Dispatcher.Invoke(() => { indexingPanel.Visibility = Visibility.Visible; }); - Main.IndexPrograms(); - Dispatcher.Invoke(() => { indexingPanel.Visibility = Visibility.Hidden; }); - }); + + indexingPanel.Visibility = Visibility.Visible; + await Main.IndexPrograms(); + indexingPanel.Visibility = Visibility.Hidden; } private void btnAddProgramSource_OnClick(object sender, RoutedEventArgs e) { var add = new AddProgramSource(context, _settings); - if(add.ShowDialog() ?? false) + if (add.ShowDialog() ?? false) { ReIndexing(); } @@ -165,14 +163,14 @@ namespace Flow.Launcher.Plugin.Program.Views UniqueIdentifier = directory }; - directoriesToAdd.Add(source); + directoriesToAdd.Add(source); } } if (directoriesToAdd.Count() > 0) { directoriesToAdd.ForEach(x => _settings.ProgramSources.Add(x)); - directoriesToAdd.ForEach(x => ProgramSettingDisplayList.Add(x)); + directoriesToAdd.ForEach(x => ProgramSettingDisplayList.Add(x)); ViewRefresh(); ReIndexing(); @@ -238,8 +236,8 @@ namespace Flow.Launcher.Plugin.Program.Views ProgramSettingDisplayList.SetProgramSourcesStatus(selectedItems, true); ProgramSettingDisplayList.RemoveDisabledFromSettings(); - } - + } + if (selectedItems.IsReindexRequired()) ReIndexing(); @@ -282,7 +280,7 @@ namespace Flow.Launcher.Plugin.Program.Views var sortBy = columnBinding?.Path.Path ?? headerClicked.Column.Header as string; Sort(sortBy, direction); - + _lastHeaderClicked = headerClicked; _lastDirection = direction; } diff --git a/Plugins/Flow.Launcher.Plugin.Program/plugin.json b/Plugins/Flow.Launcher.Plugin.Program/plugin.json index 7d7a42e03..d110124ff 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Program/plugin.json @@ -4,7 +4,7 @@ "Name": "Program", "Description": "Search programs in Flow.Launcher", "Author": "qianlifeng", - "Version": "1.2.2", + "Version": "1.4.2", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.Program.dll", diff --git a/Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj b/Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj index 178d95010..d3042722b 100644 --- a/Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj +++ b/Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj @@ -33,32 +33,6 @@ 4 false - - - - Always - - - MSBuild:Compile - Designer - PreserveNewest - - - MSBuild:Compile - Designer - PreserveNewest - - - MSBuild:Compile - Designer - PreserveNewest - - - MSBuild:Compile - Designer - PreserveNewest - - @@ -72,26 +46,18 @@ - - PreserveNewest - - - - - + MSBuild:Compile Designer PreserveNewest - - MSBuild:Compile - Designer + PreserveNewest - - MSBuild:Compile - Designer - + + MSBuild:Compile + Designer + diff --git a/Plugins/Flow.Launcher.Plugin.Shell/plugin.json b/Plugins/Flow.Launcher.Plugin.Shell/plugin.json index 63e74d678..4ad572cf6 100644 --- a/Plugins/Flow.Launcher.Plugin.Shell/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Shell/plugin.json @@ -4,7 +4,7 @@ "Name": "Shell", "Description": "Provide executing commands from Flow Launcher. Commands should start with >", "Author": "qianlifeng", - "Version": "1.1.1", + "Version": "1.1.2", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.Shell.dll", diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj b/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj index bdab40457..c25e759d3 100644 --- a/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj +++ b/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj @@ -40,52 +40,18 @@ - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - + MSBuild:Compile Designer PreserveNewest - - MSBuild:Compile - Designer + PreserveNewest - - MSBuild:Compile - Designer - PreserveNewest - - - MSBuild:Compile - Designer - PreserveNewest - - - MSBuild:Compile - Designer - PreserveNewest - - - MSBuild:Compile - Designer - PreserveNewest - - - MSBuild:Compile - Designer - + + MSBuild:Compile + Designer + @@ -93,39 +59,4 @@ PreserveNewest - - - - PreserveNewest - - - - - - PreserveNewest - - - - - - PreserveNewest - - - - - - PreserveNewest - - - - - - PreserveNewest - - - - - - - \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs index 5642b62ed..0aa37cdf5 100644 --- a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs @@ -67,13 +67,15 @@ namespace Flow.Launcher.Plugin.Sys { c.TitleHighlightData = titleMatch.MatchData; } - else + else { c.SubTitleHighlightData = subTitleMatch.MatchData; } + results.Add(c); } } + return results; } @@ -94,13 +96,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 +115,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 +170,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 +236,13 @@ namespace Flow.Launcher.Plugin.Sys { // Hide the window first then show msg after done because sometimes the reload could take a while, so not to make user think it's frozen. Application.Current.MainWindow.Hide(); - context.API.ReloadAllPluginData(); - context.API.ShowMsg(context.API.GetTranslation("flowlauncher_plugin_sys_dlgtitle_success"), - context.API.GetTranslation("flowlauncher_plugin_sys_dlgtext_all_applicableplugins_reloaded")); + + context.API.ReloadAllPluginData().ContinueWith(_ => + context.API.ShowMsg( + context.API.GetTranslation("flowlauncher_plugin_sys_dlgtitle_success"), + context.API.GetTranslation( + "flowlauncher_plugin_sys_dlgtext_all_applicableplugins_reloaded"))); + return true; } }, diff --git a/Plugins/Flow.Launcher.Plugin.Sys/plugin.json b/Plugins/Flow.Launcher.Plugin.Sys/plugin.json index 8d4b9a238..75d7073b8 100644 --- a/Plugins/Flow.Launcher.Plugin.Sys/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Sys/plugin.json @@ -4,7 +4,7 @@ "Name": "System Commands", "Description": "Provide System related commands. e.g. shutdown,lock,setting etc.", "Author": "qianlifeng", - "Version": "1.1.1", + "Version": "1.2.0", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.Sys.dll", diff --git a/Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj b/Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj index 7d802d815..671a8b1c2 100644 --- a/Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj +++ b/Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj @@ -44,49 +44,14 @@ - + - - PreserveNewest - - - - - + MSBuild:Compile Designer PreserveNewest - - - - - MSBuild:Compile - Designer - PreserveNewest - - - - - - MSBuild:Compile - Designer - PreserveNewest - - - - - - MSBuild:Compile - Designer - PreserveNewest - - - - - - MSBuild:Compile - Designer + PreserveNewest diff --git a/Plugins/Flow.Launcher.Plugin.Url/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Url/Languages/en.xaml index 452be00ee..eff1ac263 100644 --- a/Plugins/Flow.Launcher.Plugin.Url/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Url/Languages/en.xaml @@ -2,6 +2,10 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:system="clr-namespace:System;assembly=mscorlib"> + Open search in: + New Window + New Tab + Open url:{0} Can't open url:{0} diff --git a/Plugins/Flow.Launcher.Plugin.Url/Languages/sk.xaml b/Plugins/Flow.Launcher.Plugin.Url/Languages/sk.xaml index 69640735e..97568be5a 100644 --- a/Plugins/Flow.Launcher.Plugin.Url/Languages/sk.xaml +++ b/Plugins/Flow.Launcher.Plugin.Url/Languages/sk.xaml @@ -2,6 +2,10 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:system="clr-namespace:System;assembly=mscorlib"> + Otvoriť vyhľadávanie v: + Nové okno + Nová karta + Otvoriť url:{0} Adresa URL sa nedá otvoriť: {0} diff --git a/Plugins/Flow.Launcher.Plugin.Url/SettingsControl.xaml b/Plugins/Flow.Launcher.Plugin.Url/SettingsControl.xaml index f54aea878..9219a0009 100644 --- a/Plugins/Flow.Launcher.Plugin.Url/SettingsControl.xaml +++ b/Plugins/Flow.Launcher.Plugin.Url/SettingsControl.xaml @@ -10,11 +10,11 @@ - - -