diff --git a/.github/ISSUE_TEMPLATE/discussion.md b/.github/ISSUE_TEMPLATE/discussion.md.not_used similarity index 100% rename from .github/ISSUE_TEMPLATE/discussion.md rename to .github/ISSUE_TEMPLATE/discussion.md.not_used diff --git a/Doc/Default Icons/app_missing_img.png b/Artworks/Default Icons/app_missing_img.png similarity index 100% rename from Doc/Default Icons/app_missing_img.png rename to Artworks/Default Icons/app_missing_img.png diff --git a/Doc/Default Icons/app_missing_img_01.png b/Artworks/Default Icons/app_missing_img_01.png similarity index 100% rename from Doc/Default Icons/app_missing_img_01.png rename to Artworks/Default Icons/app_missing_img_01.png diff --git a/Doc/Default Icons/app_missing_img_01.svg b/Artworks/Default Icons/app_missing_img_01.svg similarity index 100% rename from Doc/Default Icons/app_missing_img_01.svg rename to Artworks/Default Icons/app_missing_img_01.svg diff --git a/Doc/Default Icons/app_missing_img_02.png b/Artworks/Default Icons/app_missing_img_02.png similarity index 100% rename from Doc/Default Icons/app_missing_img_02.png rename to Artworks/Default Icons/app_missing_img_02.png diff --git a/Doc/Default Icons/app_missing_img_02.svg b/Artworks/Default Icons/app_missing_img_02.svg similarity index 100% rename from Doc/Default Icons/app_missing_img_02.svg rename to Artworks/Default Icons/app_missing_img_02.svg diff --git a/Doc/Default Icons/app_missing_img_03.png b/Artworks/Default Icons/app_missing_img_03.png similarity index 100% rename from Doc/Default Icons/app_missing_img_03.png rename to Artworks/Default Icons/app_missing_img_03.png diff --git a/Doc/Default Icons/app_missing_img_03.svg b/Artworks/Default Icons/app_missing_img_03.svg similarity index 100% rename from Doc/Default Icons/app_missing_img_03.svg rename to Artworks/Default Icons/app_missing_img_03.svg diff --git a/Doc/Default Icons/app_missing_img_buttons.png b/Artworks/Default Icons/app_missing_img_buttons.png similarity index 100% rename from Doc/Default Icons/app_missing_img_buttons.png rename to Artworks/Default Icons/app_missing_img_buttons.png diff --git a/Doc/Default Icons/app_missing_img_buttons.svg b/Artworks/Default Icons/app_missing_img_buttons.svg similarity index 100% rename from Doc/Default Icons/app_missing_img_buttons.svg rename to Artworks/Default Icons/app_missing_img_buttons.svg diff --git a/Doc/Default Icons/app_missing_img_fluent.png b/Artworks/Default Icons/app_missing_img_fluent.png similarity index 100% rename from Doc/Default Icons/app_missing_img_fluent.png rename to Artworks/Default Icons/app_missing_img_fluent.png diff --git a/Doc/Default Icons/app_missing_img_fluent.svg b/Artworks/Default Icons/app_missing_img_fluent.svg similarity index 100% rename from Doc/Default Icons/app_missing_img_fluent.svg rename to Artworks/Default Icons/app_missing_img_fluent.svg diff --git a/Doc/Default Icons/app_missing_img_huge.png b/Artworks/Default Icons/app_missing_img_huge.png similarity index 100% rename from Doc/Default Icons/app_missing_img_huge.png rename to Artworks/Default Icons/app_missing_img_huge.png diff --git a/Doc/Default Icons/app_missing_img_huge.svg b/Artworks/Default Icons/app_missing_img_huge.svg similarity index 100% rename from Doc/Default Icons/app_missing_img_huge.svg rename to Artworks/Default Icons/app_missing_img_huge.svg diff --git a/Doc/Default Icons/app_missing_img_lightblue.png b/Artworks/Default Icons/app_missing_img_lightblue.png similarity index 100% rename from Doc/Default Icons/app_missing_img_lightblue.png rename to Artworks/Default Icons/app_missing_img_lightblue.png diff --git a/Doc/Default Icons/app_missing_img_lightblue.svg b/Artworks/Default Icons/app_missing_img_lightblue.svg similarity index 100% rename from Doc/Default Icons/app_missing_img_lightblue.svg rename to Artworks/Default Icons/app_missing_img_lightblue.svg diff --git a/Doc/Default Icons/app_missing_img_lightblue_buttons.png b/Artworks/Default Icons/app_missing_img_lightblue_buttons.png similarity index 100% rename from Doc/Default Icons/app_missing_img_lightblue_buttons.png rename to Artworks/Default Icons/app_missing_img_lightblue_buttons.png diff --git a/Doc/Default Icons/app_missing_img_lightblue_buttons.svg b/Artworks/Default Icons/app_missing_img_lightblue_buttons.svg similarity index 100% rename from Doc/Default Icons/app_missing_img_lightblue_buttons.svg rename to Artworks/Default Icons/app_missing_img_lightblue_buttons.svg diff --git a/Doc/Default Icons/app_missing_img_minimal.png b/Artworks/Default Icons/app_missing_img_minimal.png similarity index 100% rename from Doc/Default Icons/app_missing_img_minimal.png rename to Artworks/Default Icons/app_missing_img_minimal.png diff --git a/Doc/Default Icons/app_missing_img_minimal.svg b/Artworks/Default Icons/app_missing_img_minimal.svg similarity index 100% rename from Doc/Default Icons/app_missing_img_minimal.svg rename to Artworks/Default Icons/app_missing_img_minimal.svg diff --git a/Doc/Default Icons/app_missing_img_minimal_buttons.png b/Artworks/Default Icons/app_missing_img_minimal_buttons.png similarity index 100% rename from Doc/Default Icons/app_missing_img_minimal_buttons.png rename to Artworks/Default Icons/app_missing_img_minimal_buttons.png diff --git a/Doc/Default Icons/app_missing_img_minimal_buttons.svg b/Artworks/Default Icons/app_missing_img_minimal_buttons.svg similarity index 100% rename from Doc/Default Icons/app_missing_img_minimal_buttons.svg rename to Artworks/Default Icons/app_missing_img_minimal_buttons.svg diff --git a/Doc/Logo/app_error.png b/Artworks/Logo/app_error.png similarity index 100% rename from Doc/Logo/app_error.png rename to Artworks/Logo/app_error.png diff --git a/Doc/Logo/logo.ico b/Artworks/Logo/logo.ico similarity index 100% rename from Doc/Logo/logo.ico rename to Artworks/Logo/logo.ico diff --git a/Doc/Logo/logo.png b/Artworks/Logo/logo.png similarity index 100% rename from Doc/Logo/logo.png rename to Artworks/Logo/logo.png diff --git a/Doc/Logo/logo.svg b/Artworks/Logo/logo.svg similarity index 100% rename from Doc/Logo/logo.svg rename to Artworks/Logo/logo.svg diff --git a/Doc/Logo/logo128.png b/Artworks/Logo/logo128.png similarity index 100% rename from Doc/Logo/logo128.png rename to Artworks/Logo/logo128.png diff --git a/Doc/Logo/logo16.png b/Artworks/Logo/logo16.png similarity index 100% rename from Doc/Logo/logo16.png rename to Artworks/Logo/logo16.png diff --git a/Doc/Logo/logo256.png b/Artworks/Logo/logo256.png similarity index 100% rename from Doc/Logo/logo256.png rename to Artworks/Logo/logo256.png diff --git a/Doc/Logo/logo32.png b/Artworks/Logo/logo32.png similarity index 100% rename from Doc/Logo/logo32.png rename to Artworks/Logo/logo32.png diff --git a/Doc/Logo/logo48.png b/Artworks/Logo/logo48.png similarity index 100% rename from Doc/Logo/logo48.png rename to Artworks/Logo/logo48.png diff --git a/Doc/Logo/logo512.png b/Artworks/Logo/logo512.png similarity index 100% rename from Doc/Logo/logo512.png rename to Artworks/Logo/logo512.png diff --git a/Doc/Logo/logo64.png b/Artworks/Logo/logo64.png similarity index 100% rename from Doc/Logo/logo64.png rename to Artworks/Logo/logo64.png diff --git a/Doc/Logo/resources/flow-header-landscape-transparent.png b/Artworks/Logo/resources/flow-header-landscape-transparent.png similarity index 100% rename from Doc/Logo/resources/flow-header-landscape-transparent.png rename to Artworks/Logo/resources/flow-header-landscape-transparent.png diff --git a/Doc/Logo/resources/flow-header-landscape.png b/Artworks/Logo/resources/flow-header-landscape.png similarity index 100% rename from Doc/Logo/resources/flow-header-landscape.png rename to Artworks/Logo/resources/flow-header-landscape.png diff --git a/Doc/Logo/resources/flow-header-square-transparent.png b/Artworks/Logo/resources/flow-header-square-transparent.png similarity index 100% rename from Doc/Logo/resources/flow-header-square-transparent.png rename to Artworks/Logo/resources/flow-header-square-transparent.png diff --git a/Doc/Logo/resources/flow-header-square.png b/Artworks/Logo/resources/flow-header-square.png similarity index 100% rename from Doc/Logo/resources/flow-header-square.png rename to Artworks/Logo/resources/flow-header-square.png diff --git a/Doc/Logo/resources/flow-logo.ai b/Artworks/Logo/resources/flow-logo.ai similarity index 100% rename from Doc/Logo/resources/flow-logo.ai rename to Artworks/Logo/resources/flow-logo.ai diff --git a/Doc/Logo/resources/preview.pdf b/Artworks/Logo/resources/preview.pdf similarity index 100% rename from Doc/Logo/resources/preview.pdf rename to Artworks/Logo/resources/preview.pdf diff --git a/Doc/app.ico b/Artworks/app.ico similarity index 100% rename from Doc/app.ico rename to Artworks/app.ico diff --git a/Doc/app.png b/Artworks/app.png similarity index 100% rename from Doc/app.png rename to Artworks/app.png diff --git a/Doc/app_error.png b/Artworks/app_error.png similarity index 100% rename from Doc/app_error.png rename to Artworks/app_error.png diff --git a/Doc/app_missing_img.png b/Artworks/app_missing_img.png similarity index 100% rename from Doc/app_missing_img.png rename to Artworks/app_missing_img.png diff --git a/Doc/mainsearch.png b/Artworks/mainsearch.png similarity index 100% rename from Doc/mainsearch.png rename to Artworks/mainsearch.png diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj index 189a6669e..d7df11f83 100644 --- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj +++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj @@ -1,7 +1,7 @@ - + - netcoreapp3.1 + net5.0-windows true true Library @@ -16,7 +16,7 @@ true - full + portable false ..\Output\Debug\ DEBUG;TRACE @@ -53,14 +53,11 @@ + - - - - 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 247fe1889..5232e46da 100644 --- a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs +++ b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs @@ -1,5 +1,4 @@ - -/* We basically follow the Json-RPC 2.0 spec (http://www.jsonrpc.org/specification) to invoke methods between Flow Launcher and other plugins, +/* We basically follow the Json-RPC 2.0 spec (http://www.jsonrpc.org/specification) to invoke methods between Flow Launcher and other plugins, * like python or other self-execute program. But, we added addtional infos (proxy and so on) into rpc request. Also, we didn't use the * "id" and "jsonrpc" in the request, since it's not so useful in our request model. * @@ -13,10 +12,12 @@ * */ +using Flow.Launcher.Core.Resource; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; using Flow.Launcher.Plugin; +using System.Text.Json; namespace Flow.Launcher.Core.Plugin { @@ -29,12 +30,8 @@ namespace Flow.Launcher.Core.Plugin public string Data { get; set; } } - public class JsonRPCModelBase - { - public int Id { get; set; } - } - public class JsonRPCResponseModel : JsonRPCModelBase + public class JsonRPCResponseModel { public string Result { get; set; } @@ -45,57 +42,23 @@ namespace Flow.Launcher.Core.Plugin { [JsonPropertyName("result")] public new List Result { get; set; } - } - public class JsonRPCRequestModel : JsonRPCModelBase + public string DebugMessage { get; set; } + } + + public class JsonRPCRequestModel { public string Method { get; set; } public object[] Parameters { get; set; } + private static readonly JsonSerializerOptions options = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; public override string ToString() { - 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); - } - else - { - rpc = string.Format(@"{{\""method\"":\""{0}\"",\""parameters\"":[]", Method); - } - - return rpc; - - } - - private string GetParameterByType(object parameter) - { - 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(); - } - - private string ReplaceEscapes(string str) - { - return str.Replace(@"\", @"\\") //Escapes in ProcessStartInfo - .Replace(@"\", @"\\") //Escapes itself when passed to client - .Replace(@"""", @"\\"""""); + return JsonSerializer.Serialize(this, options); } } @@ -104,11 +67,7 @@ namespace Flow.Launcher.Core.Plugin /// public class JsonRPCServerRequestModel : JsonRPCRequestModel { - public override string ToString() - { - string rpc = base.ToString(); - return rpc + "}"; - } + } /// @@ -117,12 +76,6 @@ namespace Flow.Launcher.Core.Plugin public class JsonRPCClientRequestModel : JsonRPCRequestModel { public bool DontHideAfterAction { get; set; } - - public override string ToString() - { - string rpc = base.ToString(); - return rpc + "}"; - } } /// @@ -134,4 +87,4 @@ namespace Flow.Launcher.Core.Plugin { public JsonRPCClientRequestModel JsonRPCAction { get; set; } } -} +} \ No newline at end of file diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs index c7ad70391..431458881 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs @@ -1,15 +1,17 @@ -using System; +using Flow.Launcher.Core.Resource; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Reflection; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; -using Flow.Launcher.Infrastructure.Exception; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Plugin; +using JetBrains.Annotations; namespace Flow.Launcher.Core.Plugin { @@ -17,7 +19,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 +29,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,55 +47,94 @@ namespace Flow.Launcher.Core.Plugin } } + private static readonly JsonSerializerOptions options = new() + { + PropertyNameCaseInsensitive = true, + IgnoreNullValues = true, + Converters = + { + new JsonObjectConverter() + } + }; + + private async Task> DeserializedResultAsync(Stream output) + { + if (output == Stream.Null) return null; + + var queryResponseModel = await + JsonSerializer.DeserializeAsync(output, options); + + await output.DisposeAsync(); + + return ParseResults(queryResponseModel); + } + private List DeserializedResult(string output) { - if (!String.IsNullOrEmpty(output)) + if (string.IsNullOrEmpty(output)) return null; + + var queryResponseModel = + JsonSerializer.Deserialize(output, options); + 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 = JsonSerializer.Deserialize(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)) + { + return !result.JsonRPCAction.DontHideAfterAction; + } + + if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher.")) + { + ExecuteFlowLauncherAPI(result.JsonRPCAction.Method["Flow.Launcher.".Length..], + result.JsonRPCAction.Parameters); + } + else + { + var actionResponse = ExecuteCallback(result.JsonRPCAction); + + if (string.IsNullOrEmpty(actionResponse)) { - if (result1.JsonRPCAction.Method.StartsWith("Flow.Launcher.")) - { - ExecuteFlowLauncherAPI(result1.JsonRPCAction.Method.Substring(4), result1.JsonRPCAction.Parameters); - } - else - { - string actionReponse = ExecuteCallback(result1.JsonRPCAction); - JsonRPCRequestModel jsonRpcRequestModel = JsonSerializer.Deserialize(actionReponse); - if (jsonRpcRequestModel != null - && !String.IsNullOrEmpty(jsonRpcRequestModel.Method) - && jsonRpcRequestModel.Method.StartsWith("Flow.Launcher.")) - { - ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method.Substring(4), jsonRpcRequestModel.Parameters); - } - } + return !result.JsonRPCAction.DontHideAfterAction; } - return !result1.JsonRPCAction.DontHideAfterAction; - }; - results.Add(result); - } - return results; - } - else - { - return null; + + var jsonRpcRequestModel = JsonSerializer.Deserialize(actionResponse, options); + + if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false) + { + ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..], + jsonRpcRequestModel.Parameters); + } + } + + return !result.JsonRPCAction.DontHideAfterAction; + }; + results.Add(result); } + + return results; } private void ExecuteFlowLauncherAPI(string method, object[] parameters) { - MethodInfo methodInfo = PluginManager.API.GetType().GetMethod(method); + var parametersTypeArray = parameters.Select(param => param.GetType()).ToArray(); + MethodInfo methodInfo = PluginManager.API.GetType().GetMethod(method, parametersTypeArray); if (methodInfo != null) { try @@ -117,9 +144,7 @@ namespace Flow.Launcher.Core.Plugin catch (Exception) { #if (DEBUG) - { - throw; - } + throw; #endif } } @@ -130,17 +155,20 @@ 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) @@ -148,52 +176,104 @@ namespace Flow.Launcher.Core.Plugin try { using var process = Process.Start(startInfo); - if (process == null) - { - Log.Error("|JsonRPCPlugin.Execute|Can't start new process"); - return string.Empty; - } + if (process == null) return string.Empty; using var standardOutput = process.StandardOutput; var result = standardOutput.ReadToEnd(); + if (string.IsNullOrEmpty(result)) { - using (var standardError = process.StandardError) + using var standardError = process.StandardError; + var error = standardError.ReadToEnd(); + if (!string.IsNullOrEmpty(error)) { - 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; - } + Log.Error($"|JsonRPCPlugin.Execute|{error}"); + return string.Empty; } - } - else if (result.StartsWith("DEBUG:")) - { - MessageBox.Show(new Form { TopMost = true }, result.Substring(6)); + + Log.Error("|JsonRPCPlugin.Execute|Empty standard output and standard error."); return string.Empty; } - else + + if (result.StartsWith("DEBUG:")) { - return result; + 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 virtual 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 273698b86..515b0bedc 100644 --- a/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs @@ -1,5 +1,7 @@ using Flow.Launcher.Infrastructure; using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -11,17 +13,26 @@ namespace Flow.Launcher.Core.Plugin { private readonly AssemblyDependencyResolver dependencyResolver; - private readonly AssemblyDependencyResolver referencedPluginPackageDependencyResolver; - private readonly AssemblyName assemblyName; + private static readonly ConcurrentDictionary loadedAssembly; + + static PluginAssemblyLoader() + { + var currentAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + loadedAssembly = new ConcurrentDictionary( + currentAssemblies.Select(x => new KeyValuePair(x.FullName, default))); + + AppDomain.CurrentDomain.AssemblyLoad += (sender, args) => + { + loadedAssembly[args.LoadedAssembly.FullName] = default; + }; + } + internal PluginAssemblyLoader(string assemblyFilePath) { dependencyResolver = new AssemblyDependencyResolver(assemblyFilePath); assemblyName = new AssemblyName(Path.GetFileNameWithoutExtension(assemblyFilePath)); - - referencedPluginPackageDependencyResolver = - new AssemblyDependencyResolver(Path.Combine(Constant.ProgramDirectory, "Flow.Launcher.Plugin.dll")); } internal Assembly LoadAssemblyAndDependencies() @@ -33,25 +44,24 @@ namespace Flow.Launcher.Core.Plugin { string assemblyPath = dependencyResolver.ResolveAssemblyToPath(assemblyName); - // When resolving dependencies, ignore assembly depenedencies that already exits with Flow.Launcher.Plugin - // Otherwise will get unexpected behaviour with plugins, e.g. JsonIgnore attribute not honored in WebSearch or other plugins - // that use Newtonsoft.Json - if (assemblyPath == null || ExistsInReferencedPluginPackage(assemblyName)) + // When resolving dependencies, ignore assembly depenedencies that already exits with Flow.Launcher + // Otherwise duplicate assembly will be loaded and some weird behavior will occur, such as WinRT.Runtime.dll + // will fail due to loading multiple versions in process, each with their own static instance of registration state + if (assemblyPath == null || ExistsInReferencedPackage(assemblyName)) return null; return LoadFromAssemblyPath(assemblyPath); } - internal Type FromAssemblyGetTypeOfInterface(Assembly assembly, params Type[] types) + internal Type FromAssemblyGetTypeOfInterface(Assembly assembly, Type type) { var allTypes = assembly.ExportedTypes; - - return allTypes.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Intersect(types).Any()); + return allTypes.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Any(t => t == type)); } - internal bool ExistsInReferencedPluginPackage(AssemblyName assemblyName) + internal bool ExistsInReferencedPackage(AssemblyName assemblyName) { - return referencedPluginPackageDependencyResolver.ResolveAssemblyToPath(assemblyName) != null; + return loadedAssembly.ContainsKey(assemblyName.FullName); } } -} +} \ No newline at end of file diff --git a/Flow.Launcher.Core/Plugin/PluginConfig.cs b/Flow.Launcher.Core/Plugin/PluginConfig.cs index 46f79c60c..ea2119e60 100644 --- a/Flow.Launcher.Core/Plugin/PluginConfig.cs +++ b/Flow.Launcher.Core/Plugin/PluginConfig.cs @@ -20,7 +20,7 @@ namespace Flow.Launcher.Core.Plugin public static List Parse(string[] pluginDirectories) { var allPluginMetadata = new List(); - var directories = pluginDirectories.SelectMany(Directory.GetDirectories); + var directories = pluginDirectories.SelectMany(Directory.EnumerateDirectories); // todo use linq when diable plugin is implmented since parallel.foreach + list is not thread saft foreach (var directory in directories) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 700a7d509..134c3c002 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -7,9 +7,9 @@ using System.Threading; using System.Threading.Tasks; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Logger; -using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; +using ISavable = Flow.Launcher.Plugin.ISavable; namespace Flow.Launcher.Core.Plugin { @@ -21,8 +21,8 @@ namespace Flow.Launcher.Core.Plugin private static IEnumerable _contextMenuPlugins; public static List AllPlugins { get; private set; } - public static readonly List GlobalPlugins = new List(); - public static readonly Dictionary NonGlobalPlugins = new Dictionary(); + public static readonly HashSet GlobalPlugins = new(); + public static readonly Dictionary NonGlobalPlugins = new(); public static IPublicAPI API { private set; get; } @@ -33,7 +33,10 @@ namespace Flow.Launcher.Core.Plugin /// /// Directories that will hold Flow Launcher plugin directory /// - private static readonly string[] Directories = { Constant.PreinstalledDirectory, DataLocation.PluginsDirectory }; + private static readonly string[] Directories = + { + Constant.PreinstalledDirectory, DataLocation.PluginsDirectory + }; private static void DeletePythonBinding() { @@ -51,6 +54,24 @@ namespace Flow.Launcher.Core.Plugin var savable = plugin.Plugin as ISavable; savable?.Save(); } + + API.SavePluginSettings(); + } + + public static async ValueTask DisposePluginsAsync() + { + foreach (var pluginPair in AllPlugins) + { + switch (pluginPair.Plugin) + { + case IDisposable disposable: + disposable.Dispose(); + break; + case IAsyncDisposable asyncDisposable: + await asyncDisposable.DisposeAsync(); + break; + } + } } public static async Task ReloadData() @@ -97,16 +118,9 @@ namespace Flow.Launcher.Core.Plugin { try { - var milliseconds = pair.Plugin switch - { - 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(), - }; + var milliseconds = await Stopwatch.DebugAsync($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>", + () => pair.Plugin.InitAsync(new PluginInitContext(pair.Metadata, API))); + pair.Metadata.InitTime += milliseconds; Log.Info( $"|PluginManager.InitializePlugins|Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>"); @@ -124,7 +138,9 @@ namespace Flow.Launcher.Core.Plugin _contextMenuPlugins = GetPluginsForInterface(); foreach (var plugin in AllPlugins) { - foreach (var actionKeyword in plugin.Metadata.ActionKeywords) + // set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin + // has multiple global and action keywords because we will only add them here once. + foreach (var actionKeyword in plugin.Metadata.ActionKeywords.Distinct()) { switch (actionKeyword) { @@ -147,12 +163,15 @@ namespace Flow.Launcher.Core.Plugin } } - public static List ValidPluginsForQuery(Query query) + public static ICollection ValidPluginsForQuery(Query query) { if (NonGlobalPlugins.ContainsKey(query.ActionKeyword)) { var plugin = NonGlobalPlugins[query.ActionKeyword]; - return new List { plugin }; + return new List + { + plugin + }; } else { @@ -160,7 +179,7 @@ namespace Flow.Launcher.Core.Plugin } } - public static async Task> QueryForPlugin(PluginPair pair, Query query, CancellationToken token) + public static async Task> QueryForPluginAsync(PluginPair pair, Query query, CancellationToken token) { var results = new List(); try @@ -169,22 +188,12 @@ namespace Flow.Launcher.Core.Plugin long milliseconds = -1L; - switch (pair.Plugin) - { - 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(); - } + milliseconds = await Stopwatch.DebugAsync($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}", + async () => results = await pair.Plugin.QueryAsync(query, token).ConfigureAwait(false)); + token.ThrowIfCancellationRequested(); if (results == null) - return results; + return null; UpdatePluginMetadata(results, metadata, query); metadata.QueryCount += 1; @@ -195,11 +204,7 @@ namespace Flow.Launcher.Core.Plugin catch (OperationCanceledException) { // null will be fine since the results will only be added into queue if the token hasn't been cancelled - return results = null; - } - catch (Exception e) - { - Log.Exception($"|PluginManager.QueryForPlugin|Exception for plugin <{pair.Metadata.Name}> when query <{query}>", e); + return null; } return results; @@ -245,7 +250,7 @@ namespace Flow.Launcher.Core.Plugin try { - results = plugin.LoadContextMenus(result); + results = plugin.LoadContextMenus(result) ?? results; foreach (var r in results) { r.PluginDirectory = pluginPair.Metadata.PluginDirectory; @@ -266,6 +271,8 @@ namespace Flow.Launcher.Core.Plugin public static bool ActionKeywordRegistered(string actionKeyword) { + // this method is only checking for action keywords (defined as not '*') registration + // hence the actionKeyword != Query.GlobalPluginWildcardSign logic return actionKeyword != Query.GlobalPluginWildcardSign && NonGlobalPlugins.ContainsKey(actionKeyword); } @@ -290,7 +297,7 @@ namespace Flow.Launcher.Core.Plugin } /// - /// used to add action keyword for multiple action keyword plugin + /// used to remove action keyword for multiple action keyword plugin /// e.g. web search /// public static void RemoveActionKeyword(string id, string oldActionkeyword) @@ -299,9 +306,7 @@ 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) + .Count(x => x == Query.GlobalPluginWildcardSign) == 1) { GlobalPlugins.Remove(plugin); } @@ -322,4 +327,4 @@ namespace Flow.Launcher.Core.Plugin } } } -} +} \ No newline at end of file diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index fcf178445..1b78c68ae 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -3,26 +3,27 @@ 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; +using System.Diagnostics; +using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; namespace Flow.Launcher.Core.Plugin { public static class PluginsLoader { - public const string PATH = "PATH"; - public const string Python = "python"; public const string PythonExecutable = "pythonw.exe"; 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; @@ -43,23 +44,23 @@ namespace Flow.Launcher.Core.Plugin #if DEBUG var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath); var assembly = assemblyLoader.LoadAssemblyAndDependencies(); - var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, typeof(IPlugin), + var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, typeof(IAsyncPlugin)); - var plugin = Activator.CreateInstance(type); + var plugin = Activator.CreateInstance(type) as IAsyncPlugin; #else Assembly assembly = null; - object plugin = null; + IAsyncPlugin plugin = null; try { var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath); assembly = assemblyLoader.LoadAssemblyAndDependencies(); - var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, typeof(IPlugin), + var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, typeof(IAsyncPlugin)); - plugin = Activator.CreateInstance(type); + plugin = Activator.CreateInstance(type) as IAsyncPlugin; } catch (Exception e) when (assembly == null) { @@ -77,18 +78,14 @@ namespace Flow.Launcher.Core.Plugin { Log.Exception($"|PluginsLoader.DotNetPlugins|The following plugin has errored and can not be loaded: <{metadata.Name}>", e); } - +#endif 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; } @@ -113,73 +110,100 @@ namespace Flow.Launcher.Core.Plugin 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 - // PATH or from the given pythonDirectory - if (string.IsNullOrEmpty(pythonDirectory)) - { - var paths = Environment.GetEnvironmentVariable(PATH); - if (paths != null) - { - var pythonInPath = paths - .Split(';') - .Where(p => p.ToLower().Contains(Python)) - .Any(); + if (!source.Any(o => o.Language.ToUpper() == AllowedLanguage.Python)) + return new List(); - if (pythonInPath) - { - Constant.PythonPath = PythonExecutable; - } - else - { - Log.Error("|PluginsLoader.PythonPlugins|Python can't be found in PATH."); - } - } - else + if (!string.IsNullOrEmpty(settings.PythonDirectory) && FilesFolders.LocationExists(settings.PythonDirectory)) + return SetPythonPathForPluginPairs(source, Path.Combine(settings.PythonDirectory, PythonExecutable)); + + var pythonPath = string.Empty; + + 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 { - Log.Error("|PluginsLoader.PythonPlugins|PATH environment variable is not set."); + SelectedPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + }; + + var result = dlg.ShowDialog(); + if (result == DialogResult.OK) + { + string pythonDirectory = dlg.SelectedPath; + if (!string.IsNullOrEmpty(pythonDirectory)) + { + 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 { - var path = Path.Combine(pythonDirectory, PythonExecutable); - if (File.Exists(path)) + var installedPythonDirectory = Path.Combine(DataLocation.DataDirectory(), "PythonEmbeddable"); + + // Python 3.8.9 is used for Windows 7 compatibility + DroplexPackage.Drop(App.python_3_8_9_embeddable, installedPythonDirectory).Wait(); + + pythonPath = Path.Combine(installedPythonDirectory, PythonExecutable); + if (FilesFolders.FileExists(pythonPath)) { - Constant.PythonPath = path; + settings.PythonDirectory = installedPythonDirectory; + Constant.PythonPath = pythonPath; } else { - Log.Error($"|PluginsLoader.PythonPlugins|Can't find python executable in {path}"); + Log.Error("PluginsLoader", + $"Failed to set Python path after Droplex install, {pythonPath} does not exist", + "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) || string.IsNullOrEmpty(pythonPath)) { + 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 SetPythonPathForPluginPairs(source, pythonPath); } - public static IEnumerable ExecutablePlugins(IEnumerable source) + private static IEnumerable SetPythonPathForPluginPairs(List source, string pythonPath) + => source + .Where(o => o.Language.ToUpper() == AllowedLanguage.Python) + .Select(metadata => new PluginPair + { + Plugin = new PythonPlugin(pythonPath), + Metadata = metadata + }) + .ToList(); + + public static IEnumerable ExecutablePlugins(IEnumerable source) { return source .Where(o => o.Language.ToUpper() == AllowedLanguage.Executable) .Select(metadata => new PluginPair { - Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), - Metadata = metadata + Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), Metadata = metadata }); } } diff --git a/Flow.Launcher.Core/Plugin/PythonPlugin.cs b/Flow.Launcher.Core/Plugin/PythonPlugin.cs index 3c5a3a699..5d2d8d51d 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; @@ -26,39 +28,52 @@ namespace Flow.Launcher.Core.Plugin var path = Path.Combine(Constant.ProgramDirectory, JsonRPC); _startInfo.EnvironmentVariables["PYTHONPATH"] = path; + //Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable + _startInfo.ArgumentList.Add("-B"); } - 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 }, + Method = "query", Parameters = new object[] {query.Search}, }; - //Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable - _startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{request}\""; + + _startInfo.ArgumentList[2] = request.ToString(); + // 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.ArgumentList[2] = rpcRequest.ToString(); _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; + // TODO: Async Action return Execute(_startInfo); } - protected override string ExecuteContextMenu(Result selectedResult) { - JsonRPCServerRequestModel request = new JsonRPCServerRequestModel { - Method = "context_menu", - Parameters = new object[] { selectedResult.ContextData }, + protected override string ExecuteContextMenu(Result selectedResult) + { + JsonRPCServerRequestModel request = new JsonRPCServerRequestModel + { + Method = "context_menu", Parameters = new object[] {selectedResult.ContextData}, }; - _startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{request}\""; + _startInfo.ArgumentList[2] = request.ToString(); _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; + // TODO: Async Action return Execute(_startInfo); } + + public override Task InitAsync(PluginInitContext context) + { + this.context = context; + _startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); + _startInfo.ArgumentList.Add(""); + return Task.CompletedTask; + } } } \ No newline at end of file diff --git a/Flow.Launcher.Core/Properties/AssemblyInfo.cs b/Flow.Launcher.Core/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..40017c46c --- /dev/null +++ b/Flow.Launcher.Core/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Flow.Launcher.Test")] \ No newline at end of file diff --git a/Flow.Launcher.Core/Resource/JsonObjectConverter.cs b/Flow.Launcher.Core/Resource/JsonObjectConverter.cs new file mode 100644 index 000000000..5a891ae30 --- /dev/null +++ b/Flow.Launcher.Core/Resource/JsonObjectConverter.cs @@ -0,0 +1,42 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Flow.Launcher.Core.Resource +{ + public class JsonObjectConverter : JsonConverter + { + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.True: + return true; + case JsonTokenType.False: + return false; + case JsonTokenType.Number when reader.TryGetInt32(out var i): + return i; + case JsonTokenType.Number when reader.TryGetInt64(out var l): + return l; + case JsonTokenType.Number: + return reader.GetDouble(); + case JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime): + return datetime; + case JsonTokenType.String: + return reader.GetString(); + default: + // Use JsonElement as fallback. + // Newtonsoft uses JArray or JObject. + using (var document = JsonDocument.ParseValue(ref reader)) + { + return document.RootElement.Clone(); + } + } + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + throw new InvalidOperationException("Should not get here."); + } + } +} \ No newline at end of file diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index a5f12bbb9..e70de673e 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -17,6 +17,8 @@ namespace Flow.Launcher.Core.Resource { public class Theme { + private const int ShadowExtraMargin = 12; + private readonly List _themeDirectories = new List(); private ResourceDictionary _oldResource; private string _oldTheme; @@ -26,6 +28,10 @@ namespace Flow.Launcher.Core.Resource private string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder); private string UserDirectoryPath => Path.Combine(DataLocation.DataDirectory(), Folder); + public bool BlurEnabled { get; set; } + + private double mainWindowWidth; + public Theme() { _themeDirectories.Add(DirectoryPath); @@ -86,8 +92,12 @@ namespace Flow.Launcher.Core.Resource _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); } - if (Settings.UseDropShadowEffect) + BlurEnabled = IsBlurTheme(); + + if (Settings.UseDropShadowEffect && !BlurEnabled) AddDropShadowEffectToCurrentTheme(); + + SetBlurForWindow(); } catch (DirectoryNotFoundException e) { @@ -179,6 +189,21 @@ namespace Flow.Launcher.Core.Resource Array.ForEach(new[] { resultItemStyle, resultSubItemStyle, resultItemSelectedStyle, resultSubItemSelectedStyle }, o => Array.ForEach(setters, p => o.Setters.Add(p))); } + var windowStyle = dict["WindowStyle"] as Style; + + var width = windowStyle?.Setters.OfType().Where(x => x.Property.Name == "Width") + .Select(x => x.Value).FirstOrDefault(); + + if (width == null) + { + windowStyle = dict["BaseWindowStyle"] as Style; + + width = windowStyle?.Setters.OfType().Where(x => x.Property.Name == "Width") + .Select(x => x.Value).FirstOrDefault(); + } + + mainWindowWidth = (double)width; + return dict; } @@ -224,17 +249,54 @@ namespace Flow.Launcher.Core.Resource BlurRadius = 15 }; + var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter; + if (marginSetter == null) + { + marginSetter = new Setter() + { + Property = Border.MarginProperty, + Value = new Thickness(ShadowExtraMargin), + }; + windowBorderStyle.Setters.Add(marginSetter); + } + else + { + var baseMargin = (Thickness) marginSetter.Value; + var newMargin = new Thickness( + baseMargin.Left + ShadowExtraMargin, + baseMargin.Top + ShadowExtraMargin, + baseMargin.Right + ShadowExtraMargin, + baseMargin.Bottom + ShadowExtraMargin); + marginSetter.Value = newMargin; + } + windowBorderStyle.Setters.Add(effectSetter); UpdateResourceDictionary(dict); } - public void RemoveDropShadowEffectToCurrentTheme() + public void RemoveDropShadowEffectFromCurrentTheme() { var dict = CurrentThemeResourceDictionary(); var windowBorderStyle = dict["WindowBorderStyle"] as Style; - dict.Remove(Border.EffectProperty); + var effectSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.EffectProperty) as Setter; + var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter; + + if(effectSetter != null) + { + windowBorderStyle.Setters.Remove(effectSetter); + } + if (marginSetter != null) + { + var currentMargin = (Thickness)marginSetter.Value; + var newMargin = new Thickness( + currentMargin.Left - ShadowExtraMargin, + currentMargin.Top - ShadowExtraMargin, + currentMargin.Right - ShadowExtraMargin, + currentMargin.Bottom - ShadowExtraMargin); + marginSetter.Value = newMargin; + } UpdateResourceDictionary(dict); } @@ -281,35 +343,39 @@ namespace Flow.Launcher.Core.Resource /// public void SetBlurForWindow() { + if (BlurEnabled) + { + SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_ENABLE_BLURBEHIND); + } + else + { + SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_DISABLED); + } + } - // Exception of FindResource can't be cathed if global exception handle is set + private bool IsBlurTheme() + { if (Environment.OSVersion.Version >= new Version(6, 2)) { var resource = Application.Current.TryFindResource("ThemeBlurEnabled"); - bool blur; - if (resource is bool) - { - blur = (bool)resource; - } - else - { - blur = false; - } - if (blur) - { - SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_ENABLE_BLURBEHIND); - } - else - { - SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_DISABLED); - } + if (resource is bool) + return (bool)resource; + + return false; } + + return false; } private void SetWindowAccent(Window w, AccentState state) { var windowHelper = new WindowInteropHelper(w); + + // this determines the width of the main query window + w.Width = mainWindowWidth; + windowHelper.EnsureHandle(); + var accent = new AccentPolicy { AccentState = state }; var accentStructSize = Marshal.SizeOf(accent); diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs index 8f0de798c..76713ce2a 100644 --- a/Flow.Launcher.Core/Updater.cs +++ b/Flow.Launcher.Core/Updater.cs @@ -87,7 +87,7 @@ namespace Flow.Launcher.Core UpdateManager.RestartApp(Constant.ApplicationFileName); } } - catch (Exception e) when (e is HttpRequestException || e is WebException || e is SocketException || e is TaskCanceledException) + catch (Exception e) when (e is HttpRequestException || e is WebException || e is SocketException || e.InnerException is TimeoutException) { Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e); api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"), diff --git a/Flow.Launcher.Infrastructure/Constant.cs b/Flow.Launcher.Infrastructure/Constant.cs index de6ed1b29..a5b89c300 100644 --- a/Flow.Launcher.Infrastructure/Constant.cs +++ b/Flow.Launcher.Infrastructure/Constant.cs @@ -21,6 +21,7 @@ namespace Flow.Launcher.Infrastructure public static readonly string PreinstalledDirectory = Path.Combine(ProgramDirectory, Plugins); public const string Issue = "https://github.com/Flow-Launcher/Flow.Launcher/issues/new"; public static readonly string Version = FileVersionInfo.GetVersionInfo(Assembly.Location.NonNull()).ProductVersion; + public const string Documentation = "https://flow-launcher.github.io/docs/#/usage-tips"; public static readonly int ThumbnailSize = 64; private static readonly string ImagesDirectory = Path.Combine(ProgramDirectory, "Images"); diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index 8153de6c8..aea43506e 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -1,7 +1,7 @@ - + - netcoreapp3.1 + net5.0-windows {4FD29318-A8AB-4D8F-AA47-60BC241B8DA3} Library true @@ -15,7 +15,7 @@ true - full + portable false ..\Output\Debug\ DEBUG;TRACE @@ -49,8 +49,11 @@ - - + + + + + diff --git a/Flow.Launcher.Infrastructure/Http/Http.cs b/Flow.Launcher.Infrastructure/Http/Http.cs index b3a507c90..b45b6adcd 100644 --- a/Flow.Launcher.Infrastructure/Http/Http.cs +++ b/Flow.Launcher.Infrastructure/Http/Http.cs @@ -90,7 +90,7 @@ namespace Flow.Launcher.Infrastructure.Http { try { - using var response = await client.GetAsync(url, token); + using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); if (response.StatusCode == HttpStatusCode.OK) { await using var fileStream = new FileStream(filePath, FileMode.CreateNew); @@ -150,7 +150,7 @@ namespace Flow.Launcher.Infrastructure.Http public static async Task GetStreamAsync([NotNull] string url, CancellationToken token = default) { Log.Debug($"|Http.Get|Url <{url}>"); - var response = await client.GetAsync(url, token); + var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); return await response.Content.ReadAsStreamAsync(); } } diff --git a/Flow.Launcher.Infrastructure/Logger/Log.cs b/Flow.Launcher.Infrastructure/Logger/Log.cs index 94132b27f..26e305ace 100644 --- a/Flow.Launcher.Infrastructure/Logger/Log.cs +++ b/Flow.Launcher.Infrastructure/Logger/Log.cs @@ -5,6 +5,11 @@ using NLog; using NLog.Config; using NLog.Targets; using Flow.Launcher.Infrastructure.UserSettings; +using JetBrains.Annotations; +using NLog.Fluent; +using NLog.Targets.Wrappers; +using System.Runtime.ExceptionServices; +using System.Text; namespace Flow.Launcher.Infrastructure.Logger { @@ -23,15 +28,37 @@ namespace Flow.Launcher.Infrastructure.Logger } var configuration = new LoggingConfiguration(); - var target = new FileTarget(); - configuration.AddTarget("file", target); - target.FileName = CurrentLogDirectory.Replace(@"\", "/") + "/${shortdate}.txt"; + + const string layout = + @"${date:format=HH\:mm\:ss.ffffK} - " + + @"${level:uppercase=true:padding=-5} - ${logger} - ${message:l}" + + @"${onexception:${newline}" + + @"EXCEPTION OCCURS\: ${exception:format=tostring}${newline}}"; + + var fileTarget = new FileTarget + { + FileName = CurrentLogDirectory.Replace(@"\", "/") + "/${shortdate}.txt", + Layout = layout + }; + + var fileTargetASyncWrapper = new AsyncTargetWrapper(fileTarget); + + var debugTarget = new OutputDebugStringTarget + { + Layout = layout + }; + + configuration.AddTarget("file", fileTargetASyncWrapper); + configuration.AddTarget("debug", debugTarget); + #if DEBUG - var rule = new LoggingRule("*", LogLevel.Debug, target); + var fileRule = new LoggingRule("*", LogLevel.Debug, fileTargetASyncWrapper); + var debugRule = new LoggingRule("*", LogLevel.Debug, debugTarget); + configuration.LoggingRules.Add(debugRule); #else - var rule = new LoggingRule("*", LogLevel.Info, target); + var fileRule = new LoggingRule("*", LogLevel.Info, fileTargetASyncWrapper); #endif - configuration.LoggingRules.Add(rule); + configuration.LoggingRules.Add(fileRule); LogManager.Configuration = configuration; } @@ -39,7 +66,6 @@ namespace Flow.Launcher.Infrastructure.Logger { var logger = LogManager.GetLogger("FaultyLogger"); message = $"Wrong logger message format <{message}>"; - System.Diagnostics.Debug.WriteLine($"FATAL|{message}"); logger.Fatal(message); } @@ -51,12 +77,11 @@ namespace Flow.Launcher.Infrastructure.Logger } - - [MethodImpl(MethodImplOptions.Synchronized)] public static void Exception(string className, string message, System.Exception exception, [CallerMemberName] string methodName = "") { + exception = exception.Demystify(); #if DEBUG - throw exception; + ExceptionDispatchInfo.Capture(exception).Throw(); #else var classNameWithMethod = CheckClassAndMessageAndReturnFullClassWithMethod(className, message, methodName); @@ -90,23 +115,9 @@ namespace Flow.Launcher.Infrastructure.Logger { var logger = LogManager.GetLogger(classAndMethod); - System.Diagnostics.Debug.WriteLine($"ERROR|{message}"); + var messageBuilder = new StringBuilder(); - logger.Error("-------------------------- Begin exception --------------------------"); - logger.Error(message); - - do - { - logger.Error($"Exception full name:\n <{e.GetType().FullName}>"); - logger.Error($"Exception message:\n <{e.Message}>"); - logger.Error($"Exception stack trace:\n <{e.StackTrace}>"); - logger.Error($"Exception source:\n <{e.Source}>"); - logger.Error($"Exception target site:\n <{e.TargetSite}>"); - logger.Error($"Exception HResult:\n <{e.HResult}>"); - e = e.InnerException; - } while (e != null); - - logger.Error("-------------------------- End exception --------------------------"); + logger.Error(e, message); } private static void LogInternal(string message, LogLevel level) @@ -117,8 +128,6 @@ namespace Flow.Launcher.Infrastructure.Logger var prefix = parts[1]; var unprefixed = parts[2]; var logger = LogManager.GetLogger(prefix); - - System.Diagnostics.Debug.WriteLine($"{level.Name}|{message}"); logger.Log(level, unprefixed); } else @@ -128,11 +137,12 @@ namespace Flow.Launcher.Infrastructure.Logger } /// example: "|prefix|unprefixed" - [MethodImpl(MethodImplOptions.Synchronized)] + /// Exception public static void Exception(string message, System.Exception e) { + e = e.Demystify(); #if DEBUG - throw e; + ExceptionDispatchInfo.Capture(e).Throw(); #else if (FormatValid(message)) { @@ -165,7 +175,6 @@ namespace Flow.Launcher.Infrastructure.Logger var logger = LogManager.GetLogger(classNameWithMethod); - System.Diagnostics.Debug.WriteLine($"{level.Name}|{message}"); logger.Log(level, message); } diff --git a/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs index 03a0206bb..ec7a38804 100644 --- a/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs @@ -8,7 +8,7 @@ using Flow.Launcher.Infrastructure.UserSettings; namespace Flow.Launcher.Infrastructure.Storage { - public class FlowLauncherJsonStorage : JsonStrorage where T : new() + public class FlowLauncherJsonStorage : JsonStorage where T : new() { public FlowLauncherJsonStorage() { diff --git a/Flow.Launcher.Infrastructure/Storage/ISavable.cs b/Flow.Launcher.Infrastructure/Storage/ISavable.cs index 8294a3df8..ba2b58c6a 100644 --- a/Flow.Launcher.Infrastructure/Storage/ISavable.cs +++ b/Flow.Launcher.Infrastructure/Storage/ISavable.cs @@ -1,11 +1,8 @@ -namespace Flow.Launcher.Infrastructure.Storage +using System; + +namespace Flow.Launcher.Infrastructure.Storage { - /// - /// Save plugin settings/cache, - /// todo should be merged into a abstract class intead of seperate interface - /// - public interface ISavable - { - void Save(); - } -} + [Obsolete("Deprecated as of Flow Launcher v1.8.0, on 2021.06.21. " + + "This is used only for Everything plugin v1.4.9 or below backwards compatibility")] + public interface ISavable : Plugin.ISavable { } +} \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs index f0e4a79fc..0083ccb87 100644 --- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs @@ -9,10 +9,9 @@ namespace Flow.Launcher.Infrastructure.Storage /// /// Serialize object using json format. /// - public class JsonStrorage where T : new() + public class JsonStorage where T : new() { - private readonly JsonSerializerOptions _serializerSettings; - private T _data; + protected T _data; // need a new directory name public const string DirectoryName = "Settings"; public const string FileSuffix = ".json"; @@ -20,24 +19,14 @@ namespace Flow.Launcher.Infrastructure.Storage public string DirectoryPath { get; set; } - internal JsonStrorage() - { - // use property initialization instead of DefaultValueAttribute - // easier and flexible for default value of object - _serializerSettings = new JsonSerializerOptions - { - IgnoreNullValues = false - }; - } - public T Load() { if (File.Exists(FilePath)) { - var searlized = File.ReadAllText(FilePath); - if (!string.IsNullOrWhiteSpace(searlized)) + var serialized = File.ReadAllText(FilePath); + if (!string.IsNullOrWhiteSpace(serialized)) { - Deserialize(searlized); + Deserialize(serialized); } else { @@ -51,16 +40,16 @@ namespace Flow.Launcher.Infrastructure.Storage return _data.NonNull(); } - private void Deserialize(string searlized) + private void Deserialize(string serialized) { try { - _data = JsonSerializer.Deserialize(searlized, _serializerSettings); + _data = JsonSerializer.Deserialize(serialized); } catch (JsonException e) { LoadDefault(); - Log.Exception($"|JsonStrorage.Deserialize|Deserialize error for json <{FilePath}>", e); + Log.Exception($"|JsonStorage.Deserialize|Deserialize error for json <{FilePath}>", e); } if (_data == null) @@ -98,4 +87,8 @@ namespace Flow.Launcher.Infrastructure.Storage File.WriteAllText(FilePath, serialized); } } + + [Obsolete("Deprecated as of Flow Launcher v1.8.0, on 2021.06.21. " + + "This is used only for Everything plugin v1.4.9 or below backwards compatibility")] + public class JsonStrorage : JsonStorage where T : new() { } } diff --git a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs index 5418e1837..923a1a6b5 100644 --- a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs @@ -3,17 +3,23 @@ using Flow.Launcher.Infrastructure.UserSettings; namespace Flow.Launcher.Infrastructure.Storage { - public class PluginJsonStorage :JsonStrorage where T : new() + public class PluginJsonStorage :JsonStorage where T : new() { public PluginJsonStorage() { - // C# releated, add python releated below + // C# related, add python related below var dataType = typeof(T); - var assemblyName = typeof(T).Assembly.GetName().Name; + var assemblyName = dataType.Assembly.GetName().Name; DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Plugins, assemblyName); Helper.ValidateDirectory(DirectoryPath); FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}"); } + + public PluginJsonStorage(T data) : this() + { + _data = data; + } } } + diff --git a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs index 29bc11480..abce8b41e 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs @@ -16,11 +16,13 @@ namespace Flow.Launcher.Infrastructure.UserSettings { var settings = Plugins[metadata.ID]; - // TODO: Remove. This is one off for 1.2.0 release. - // Introduced a new action keyword in Explorer, so need to update plugin setting in the UserData folder. - // This kind of plugin meta update should be handled by a dedicated method trigger by version bump. + // TODO: Remove. This is backwards compatibility for 1.8.0 release. + // Introduced two new action keywords in Explorer, so need to update plugin setting in the UserData folder. if (metadata.ID == "572be03c74c642baae319fc283e561a8" && metadata.ActionKeywords.Count != settings.ActionKeywords.Count) - settings.ActionKeywords = metadata.ActionKeywords; + { + settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for index search + settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for path search + } if (string.IsNullOrEmpty(settings.Version)) settings.Version = metadata.Version; @@ -30,6 +32,11 @@ namespace Flow.Launcher.Infrastructure.UserSettings metadata.ActionKeywords = settings.ActionKeywords; metadata.ActionKeyword = settings.ActionKeywords[0]; } + else + { + metadata.ActionKeywords = new List(); + metadata.ActionKeyword = string.Empty; + } metadata.Disabled = settings.Disabled; metadata.Priority = settings.Priority; } diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 76a370978..e7e7902d4 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -97,6 +97,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings public bool RememberLastLaunchLocation { get; set; } public bool IgnoreHotkeysOnFullscreen { get; set; } + public bool AutoHideScrollBar { get; set; } + public HttpProxy Proxy { get; set; } = new HttpProxy(); [JsonConverter(typeof(JsonStringEnumConverter))] diff --git a/Flow.Launcher.Plugin/Feature.cs b/Flow.Launcher.Plugin/Feature.cs deleted file mode 100644 index 81839d816..000000000 --- a/Flow.Launcher.Plugin/Feature.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; - -namespace Flow.Launcher.Plugin -{ - public interface IFeatures { } - - public interface IContextMenu : IFeatures - { - List LoadContextMenus(Result selectedResult); - } - - /// - /// Represent plugins that support internationalization - /// - public interface IPluginI18n : IFeatures - { - string GetTranslatedPluginTitle(); - - string GetTranslatedPluginDescription(); - } - - public interface IResultUpdated : IFeatures - { - event ResultUpdatedEventHandler ResultsUpdated; - } - - public delegate void ResultUpdatedEventHandler(IResultUpdated sender, ResultUpdatedEventArgs e); - - public class ResultUpdatedEventArgs : EventArgs - { - public List Results; - public Query Query; - } -} diff --git a/Flow.Launcher.Plugin/Features.cs b/Flow.Launcher.Plugin/Features.cs new file mode 100644 index 000000000..5b9c6a7b9 --- /dev/null +++ b/Flow.Launcher.Plugin/Features.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Threading; + +namespace Flow.Launcher.Plugin +{ + /// + /// Base Interface for Flow's special plugin feature interface + /// + public interface IFeatures + { + } +} \ No newline at end of file diff --git a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj index 0eefe5c4f..664373434 100644 --- a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj +++ b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj @@ -1,7 +1,7 @@ - + - netcoreapp3.1 + net5.0-windows {8451ECDD-2EA4-4966-BB0A-7BBC40138E80} true Library @@ -14,10 +14,10 @@ - 1.4.0 - 1.4.0 - 1.4.0 - 1.4.0 + 2.0.0 + 2.0.0 + 2.0.0 + 2.0.0 Flow.Launcher.Plugin Flow-Launcher MIT @@ -34,7 +34,7 @@ true - full + portable false ..\Output\Debug\ DEBUG;TRACE diff --git a/Flow.Launcher.Plugin/IPublicAPI.cs b/Flow.Launcher.Plugin/IPublicAPI.cs deleted file mode 100644 index dd73eb0e5..000000000 --- a/Flow.Launcher.Plugin/IPublicAPI.cs +++ /dev/null @@ -1,104 +0,0 @@ -using Flow.Launcher.Plugin.SharedModels; -using JetBrains.Annotations; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Flow.Launcher.Plugin -{ - /// - /// Public APIs that plugin can use - /// - public interface IPublicAPI - { - /// - /// Change Flow.Launcher query - /// - /// query text - /// - /// force requery By default, Flow Launcher will not fire query if your query is same with existing one. - /// Set this to true to force Flow Launcher requerying - /// - void ChangeQuery(string query, bool requery = false); - - /// - /// Restart Flow Launcher - /// - void RestartApp(); - - /// - /// Save all Flow Launcher settings - /// - void SaveAppAllSettings(); - - /// - /// Reloads any Plugins that have the - /// IReloadable implemented. It refeshes - /// Plugin's in memory data with new content - /// added by user. - /// - Task ReloadAllPluginData(); - - /// - /// Check for new Flow Launcher update - /// - void CheckForNewUpdate(); - - /// - /// Show message box - /// - /// Message title - /// Message subtitle - /// Message icon path (relative path to your plugin folder) - void ShowMsg(string title, string subTitle = "", string iconPath = ""); - - /// - /// Show message box - /// - /// Message title - /// Message subtitle - /// Message icon path (relative path to your plugin folder) - /// when true will use main windows as the owner - void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true); - - /// - /// Open setting dialog - /// - void OpenSettingDialog(); - - /// - /// Get translation of current language - /// You need to implement IPluginI18n if you want to support multiple languages for your plugin - /// - /// - /// - string GetTranslation(string key); - - /// - /// Get all loaded plugins - /// - /// - List GetAllPlugins(); - - /// - /// Fired after global keyboard events - /// if you want to hook something like Ctrl+R, you should use this event - /// - event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent; - - MatchResult FuzzySearch(string query, string stringToCompare); - - Task HttpGetStringAsync(string url, CancellationToken token = default); - - Task HttpGetStreamAsync(string url, CancellationToken token = default); - - Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath); - - void AddActionKeyword(string pluginId, string newActionKeyword); - - void RemoveActionKeyword(string pluginId, string oldActionKeyword); - - } -} diff --git a/Flow.Launcher.Plugin/IAsyncPlugin.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncPlugin.cs similarity index 100% rename from Flow.Launcher.Plugin/IAsyncPlugin.cs rename to Flow.Launcher.Plugin/Interfaces/IAsyncPlugin.cs diff --git a/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs index fc4ac4715..bd4500a7e 100644 --- a/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs +++ b/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs @@ -13,8 +13,8 @@ namespace Flow.Launcher.Plugin /// 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 + public interface IAsyncReloadable : IFeatures { Task ReloadDataAsync(); } -} +} \ No newline at end of file diff --git a/Flow.Launcher.Plugin/Interfaces/IContextMenu.cs b/Flow.Launcher.Plugin/Interfaces/IContextMenu.cs new file mode 100644 index 000000000..5befbf5c9 --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IContextMenu.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Flow.Launcher.Plugin +{ + public interface IContextMenu : IFeatures + { + List LoadContextMenus(Result selectedResult); + } +} \ No newline at end of file diff --git a/Flow.Launcher.Plugin/IPlugin.cs b/Flow.Launcher.Plugin/Interfaces/IPlugin.cs similarity index 74% rename from Flow.Launcher.Plugin/IPlugin.cs rename to Flow.Launcher.Plugin/Interfaces/IPlugin.cs index 203dc9af7..bac93d090 100644 --- a/Flow.Launcher.Plugin/IPlugin.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPlugin.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace Flow.Launcher.Plugin { @@ -9,7 +11,7 @@ namespace Flow.Launcher.Plugin /// or performaing CPU intense jobs (performing better with cancellation), please try the IAsyncPlugin interface /// /// - public interface IPlugin + public interface IPlugin : IAsyncPlugin { /// /// Querying when user's search changes @@ -27,5 +29,9 @@ namespace Flow.Launcher.Plugin /// /// void Init(PluginInitContext context); + + Task IAsyncPlugin.InitAsync(PluginInitContext context) => Task.Run(() => Init(context)); + + Task> IAsyncPlugin.QueryAsync(Query query, CancellationToken token) => Task.Run(() => Query(query)); } } diff --git a/Flow.Launcher.Plugin/Interfaces/IPluginI18n.cs b/Flow.Launcher.Plugin/Interfaces/IPluginI18n.cs new file mode 100644 index 000000000..e332d450e --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IPluginI18n.cs @@ -0,0 +1,12 @@ +namespace Flow.Launcher.Plugin +{ + /// + /// Represent plugins that support internationalization + /// + public interface IPluginI18n : IFeatures + { + string GetTranslatedPluginTitle(); + + string GetTranslatedPluginDescription(); + } +} \ No newline at end of file diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs new file mode 100644 index 000000000..d9cdf5581 --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -0,0 +1,189 @@ +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 +{ + /// + /// Public APIs that plugin can use + /// + public interface IPublicAPI + { + /// + /// Change Flow.Launcher query + /// + /// query text + /// + /// force requery By default, Flow Launcher will not fire query if your query is same with existing one. + /// Set this to true to force Flow Launcher requerying + /// + void ChangeQuery(string query, bool requery = false); + + /// + /// Restart Flow Launcher + /// + void RestartApp(); + + /// + /// Save everything, all of Flow Launcher and plugins' data and settings + /// + void SaveAppAllSettings(); + + /// + /// Save all Flow's plugins settings + /// + void SavePluginSettings(); + + /// + /// Reloads any Plugins that have the + /// IReloadable implemented. It refeshes + /// Plugin's in memory data with new content + /// added by user. + /// + Task ReloadAllPluginData(); + + /// + /// Check for new Flow Launcher update + /// + void CheckForNewUpdate(); + + /// + /// Show the error message using Flow's standard error icon. + /// + /// Message title + /// Optional message subtitle + void ShowMsgError(string title, string subTitle = ""); + + /// + /// Show message box + /// + /// Message title + /// Message subtitle + /// Message icon path (relative path to your plugin folder) + void ShowMsg(string title, string subTitle = "", string iconPath = ""); + + /// + /// Show message box + /// + /// Message title + /// Message subtitle + /// Message icon path (relative path to your plugin folder) + /// when true will use main windows as the owner + void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true); + + /// + /// Open setting dialog + /// + void OpenSettingDialog(); + + /// + /// Get translation of current language + /// You need to implement IPluginI18n if you want to support multiple languages for your plugin + /// + /// + /// + string GetTranslation(string key); + + /// + /// Get all loaded plugins + /// + /// + List GetAllPlugins(); + + /// + /// Fired after global keyboard events + /// 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's setting. This is the method used to load settings from json in Flow. + /// When the file is not exist, it will create a new instance for the specific type. + /// + /// Type for deserialization + /// + T LoadSettingJsonStorage() where T : new(); + + /// + /// Save JsonStorage for current plugin's setting. This is the method used to save settings to json in Flow.Launcher + /// This method will save the original instance loaded with LoadJsonStorage. + /// This API call is for manually Save. Flow will automatically save all setting type that has called LoadSettingJsonStorage or SaveSettingJsonStorage previously. + /// + /// Type for Serialization + /// + void SaveSettingJsonStorage() where T : new(); + } +} diff --git a/Flow.Launcher.Plugin/Interfaces/IReloadable.cs b/Flow.Launcher.Plugin/Interfaces/IReloadable.cs index 31611519c..bd1ad406e 100644 --- a/Flow.Launcher.Plugin/Interfaces/IReloadable.cs +++ b/Flow.Launcher.Plugin/Interfaces/IReloadable.cs @@ -15,8 +15,8 @@ /// If requiring reloading data asynchronously, please use the IAsyncReloadable interface /// /// - public interface IReloadable + public interface IReloadable : IFeatures { void ReloadData(); } -} +} \ No newline at end of file diff --git a/Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs b/Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs new file mode 100644 index 000000000..fd21460ac --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Threading; + +namespace Flow.Launcher.Plugin +{ + public interface IResultUpdated : IFeatures + { + event ResultUpdatedEventHandler ResultsUpdated; + } + + public delegate void ResultUpdatedEventHandler(IResultUpdated sender, ResultUpdatedEventArgs e); + + public class ResultUpdatedEventArgs : EventArgs + { + public List Results; + public Query Query; + public CancellationToken Token { get; init; } + } +} \ No newline at end of file diff --git a/Flow.Launcher.Plugin/Interfaces/ISavable.cs b/Flow.Launcher.Plugin/Interfaces/ISavable.cs new file mode 100644 index 000000000..2d13eaa6e --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/ISavable.cs @@ -0,0 +1,12 @@ +namespace Flow.Launcher.Plugin +{ + /// + /// Save addtional plugin data. Inherit this interface if additional data e.g. cache needs to be saved, + /// Otherwise if LoadSettingJsonStorage or SaveSettingJsonStorage has been callded, + /// plugin settings will be automatically saved (see Flow.Launcher/PublicAPIInstance.SavePluginSettings) by Flow + /// + public interface ISavable : IFeatures + { + void Save(); + } +} \ No newline at end of file diff --git a/Flow.Launcher.Plugin/ISettingProvider.cs b/Flow.Launcher.Plugin/Interfaces/ISettingProvider.cs similarity index 100% rename from Flow.Launcher.Plugin/ISettingProvider.cs rename to Flow.Launcher.Plugin/Interfaces/ISettingProvider.cs diff --git a/Flow.Launcher.Plugin/PluginPair.cs b/Flow.Launcher.Plugin/PluginPair.cs index e8954b7a0..7bf634691 100644 --- a/Flow.Launcher.Plugin/PluginPair.cs +++ b/Flow.Launcher.Plugin/PluginPair.cs @@ -2,7 +2,7 @@ { public class PluginPair { - public object Plugin { get; internal set; } + public IAsyncPlugin Plugin { get; internal set; } public PluginMetadata Metadata { get; internal set; } diff --git a/Flow.Launcher.Plugin/README.md b/Flow.Launcher.Plugin/README.md index 3b4d1598a..5c5b7c3ed 100644 --- a/Flow.Launcher.Plugin/README.md +++ b/Flow.Launcher.Plugin/README.md @@ -3,4 +3,4 @@ * Defines base objects and interfaces for plugins * Plugin authors making C# plugins should reference this DLL via nuget -* Contains base commands used by all plugins +* Contains commands and models that can be used by plugins diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index 70be1e9dc..ac9c10df0 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -10,9 +10,18 @@ namespace Flow.Launcher.Plugin { private string _pluginDirectory; + private string _icoPath; + + /// + /// Provides the title of the result. This is always required. + /// public string Title { get; set; } - public string SubTitle { get; set; } + + /// + /// Provides additional details for the result. This is optional + /// + public string SubTitle { get; set; } = string.Empty; /// /// This holds the action keyword that triggered the result. diff --git a/Flow.Launcher.Test/Flow.Launcher.Test.csproj b/Flow.Launcher.Test/Flow.Launcher.Test.csproj index e970c47b9..84c9137b7 100644 --- a/Flow.Launcher.Test/Flow.Launcher.Test.csproj +++ b/Flow.Launcher.Test/Flow.Launcher.Test.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net5.0-windows10.0.19041.0 {FF742965-9A80-41A5-B042-D6C7D3A21708} Library Properties @@ -15,7 +15,7 @@ true - full + portable false bin\Debug\ DEBUG;TRACE @@ -49,12 +49,12 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + \ No newline at end of file diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs index 3d0a9a64f..9d7fccad9 100644 --- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs +++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs @@ -133,6 +133,13 @@ namespace Flow.Launcher.Test.Plugins { // Given var queryConstructor = new QueryConstructor(new Settings()); + var baseQuery = queryConstructor.CreateBaseQuery(); + + // system running this test could have different locale than the hard-coded 1033 LCID en-US. + var queryKeywordLocale = baseQuery.QueryKeywordLocale; + expectedString = expectedString.Replace("1033", queryKeywordLocale.ToString()); + + //When var resultString = queryConstructor.QueryForAllFilesAndFolders(userSearchString); diff --git a/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs new file mode 100644 index 000000000..2b9512692 --- /dev/null +++ b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs @@ -0,0 +1,105 @@ +using NUnit; +using NUnit.Framework; +using Flow.Launcher.Core.Plugin; +using Flow.Launcher.Plugin; +using System.Threading.Tasks; +using System.IO; +using System.Threading; +using System.Text; +using System.Text.Json; +using System.Linq; +using System.Collections.Generic; + +namespace Flow.Launcher.Test.Plugins +{ + [TestFixture] + // ReSharper disable once InconsistentNaming + internal class JsonRPCPluginTest : JsonRPCPlugin + { + public override string SupportedLanguage { get; set; } = AllowedLanguage.Executable; + + protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest) + { + throw new System.NotImplementedException(); + } + + protected override string ExecuteContextMenu(Result selectedResult) + { + throw new System.NotImplementedException(); + } + + protected override Task ExecuteQueryAsync(Query query, CancellationToken token) + { + var byteInfo = Encoding.UTF8.GetBytes(query.RawQuery); + + var resultStream = new MemoryStream(byteInfo); + return Task.FromResult((Stream)resultStream); + } + + [TestCase("{\"result\":[],\"DebugMessage\":null}", Description = "Empty Result")] + [TestCase("{\"result\":[{\"JsonRPCAction\":null,\"Title\":\"something\",\"SubTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null}],\"DebugMessage\":null}", Description = "One Result with Pascal Case")] + [TestCase("{\"result\":[{\"jsonRPCAction\":null,\"title\":\"something\",\"subTitle\":\"\",\"actionKeywordAssigned\":null,\"icoPath\":null}],\"debugMessage\":null}", Description = "One Result with camel Case")] + [TestCase("{\"result\":[{\"JsonRPCAction\":null,\"Title\":\"iii\",\"SubTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null},{\"JsonRPCAction\":null,\"Title\":\"iii\",\"SubTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null}],\"DebugMessage\":null}", Description = "Two Result with Pascal Case")] + [TestCase("{\"result\":[{\"jsonrpcAction\":null,\"TItLE\":\"iii\",\"Subtitle\":\"\",\"Actionkeywordassigned\":null,\"icoPath\":null},{\"jsonRPCAction\":null,\"tiTle\":\"iii\",\"subTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null}],\"DebugMessage\":null}", Description = "Two Result with Weird Case")] + public async Task GivenVariousJsonText_WhenVariousNamingCase_ThenExpectNotNullResults_Async(string resultText) + { + var results = await QueryAsync(new Query + { + RawQuery = resultText + }, default); + + Assert.IsNotNull(results); + + foreach (var result in results) + { + Assert.IsNotNull(result); + Assert.IsNotNull(result.Action); + Assert.IsNotNull(result.Title); + } + + } + + public static List ResponseModelsSource = new() + { + new() + { + Result = new() + }, + new() + { + Result = new() + { + new JsonRPCResult + { + Title = "Test1", + SubTitle = "Test2" + } + } + } + }; + + [TestCaseSource(typeof(JsonRPCPluginTest), nameof(ResponseModelsSource))] + public async Task GivenModel_WhenSerializeWithDifferentNamingPolicy_ThenExpectSameResult_Async(JsonRPCQueryResponseModel reference) + { + var camelText = JsonSerializer.Serialize(reference, new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + + var pascalText = JsonSerializer.Serialize(reference); + + var results1 = await QueryAsync(new Query { RawQuery = camelText }, default); + var results2 = await QueryAsync(new Query { RawQuery = pascalText }, default); + + Assert.IsNotNull(results1); + Assert.IsNotNull(results2); + + foreach (var ((result1, result2), referenceResult) in results1.Zip(results2).Zip(reference.Result)) + { + Assert.AreEqual(result1, result2); + Assert.AreEqual(result1, referenceResult); + + Assert.IsNotNull(result1); + Assert.IsNotNull(result1.Action); + } + } + + } +} \ No newline at end of file diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index a2907c927..b3e9052fc 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -61,7 +61,6 @@ namespace Flow.Launcher _settingsVM = new SettingWindowViewModel(_updater, _portable); _settings = _settingsVM.Settings; - _alphabet.Initialize(_settings); _stringMatcher = new StringMatcher(_alphabet); StringMatcher.Instance = _stringMatcher; @@ -69,6 +68,9 @@ namespace Flow.Launcher PluginManager.LoadPlugins(_settings.PluginSettings); _mainVM = new MainViewModel(_settings); + + HotKeyMapper.Initialize(_mainVM); + API = new PublicAPIInstance(_settingsVM, _mainVM, _alphabet); Http.API = API; diff --git a/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs b/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs index d036c008c..b18bbcfad 100644 --- a/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs +++ b/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs @@ -1,8 +1,7 @@ using Flow.Launcher.Core.Resource; +using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.UserSettings; -using NHotkey; -using NHotkey.Wpf; using System; using System.Collections.ObjectModel; using System.Linq; @@ -52,11 +51,7 @@ namespace Flow.Launcher }; _settings.CustomPluginHotkeys.Add(pluginHotkey); - SetHotkey(ctlHotkey.CurrentHotkey, delegate - { - App.API.ChangeQuery(pluginHotkey.ActionKeyword); - Application.Current.MainWindow.Visibility = Visibility.Visible; - }); + HotKeyMapper.SetCustomQueryHotkey(pluginHotkey); } else { @@ -69,12 +64,8 @@ namespace Flow.Launcher updateCustomHotkey.ActionKeyword = tbAction.Text; updateCustomHotkey.Hotkey = ctlHotkey.CurrentHotkey.ToString(); //remove origin hotkey - RemoveHotkey(oldHotkey); - SetHotkey(new HotkeyModel(updateCustomHotkey.Hotkey), delegate - { - App.API.ChangeQuery(updateCustomHotkey.ActionKeyword); - Application.Current.MainWindow.Visibility = Visibility.Visible; - }); + HotKeyMapper.RemoveHotkey(oldHotkey); + HotKeyMapper.SetCustomQueryHotkey(updateCustomHotkey); } Close(); @@ -100,28 +91,8 @@ namespace Flow.Launcher { App.API.ChangeQuery(tbAction.Text); Application.Current.MainWindow.Visibility = Visibility.Visible; - } + Application.Current.MainWindow.Focus(); - private void RemoveHotkey(string hotkeyStr) - { - if (!string.IsNullOrEmpty(hotkeyStr)) - { - HotkeyManager.Current.Remove(hotkeyStr); - } - } - - private void SetHotkey(HotkeyModel hotkey, EventHandler action) - { - string hotkeyStr = hotkey.ToString(); - try - { - HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action); - } - catch (Exception) - { - string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr); - MessageBox.Show(errorMsg); - } } private void cmdEsc_OnPress(object sender, ExecutedRoutedEventArgs e) diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index 20e847f82..f3885ccfd 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -1,8 +1,8 @@ - + WinExe - netcoreapp3.1 + net5.0-windows10.0.19041.0 true true Flow.Launcher.App @@ -17,7 +17,7 @@ AnyCPU true - full + portable false ..\Output\Debug\ DEBUG;TRACE @@ -81,7 +81,7 @@ - + all diff --git a/Flow.Launcher/Helper/ErrorReporting.cs b/Flow.Launcher/Helper/ErrorReporting.cs index 94e2ed2bc..f3f590167 100644 --- a/Flow.Launcher/Helper/ErrorReporting.cs +++ b/Flow.Launcher/Helper/ErrorReporting.cs @@ -3,6 +3,8 @@ using System.Windows.Threading; using NLog; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Exception; +using NLog.Fluent; +using Log = Flow.Launcher.Infrastructure.Logger.Log; namespace Flow.Launcher.Helper { @@ -45,4 +47,4 @@ namespace Flow.Launcher.Helper return info; } } -} +} \ No newline at end of file diff --git a/Flow.Launcher/Helper/HotKeyMapper.cs b/Flow.Launcher/Helper/HotKeyMapper.cs new file mode 100644 index 000000000..d5fcabace --- /dev/null +++ b/Flow.Launcher/Helper/HotKeyMapper.cs @@ -0,0 +1,136 @@ +using Flow.Launcher.Infrastructure.Hotkey; +using Flow.Launcher.Infrastructure.UserSettings; +using System; +using NHotkey; +using NHotkey.Wpf; +using Flow.Launcher.Core.Resource; +using System.Windows; +using Flow.Launcher.ViewModel; + +namespace Flow.Launcher.Helper +{ + internal static class HotKeyMapper + { + private static Settings settings; + private static MainViewModel mainViewModel; + + internal static void Initialize(MainViewModel mainVM) + { + mainViewModel = mainVM; + settings = mainViewModel._settings; + + SetHotkey(settings.Hotkey, OnHotkey); + LoadCustomPluginHotkey(); + } + + private static void SetHotkey(string hotkeyStr, EventHandler action) + { + var hotkey = new HotkeyModel(hotkeyStr); + SetHotkey(hotkey, action); + } + + internal static void SetHotkey(HotkeyModel hotkey, EventHandler action) + { + string hotkeyStr = hotkey.ToString(); + try + { + HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action); + } + catch (Exception) + { + string errorMsg = + string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), + hotkeyStr); + MessageBox.Show(errorMsg); + } + } + + internal static void RemoveHotkey(string hotkeyStr) + { + if (!string.IsNullOrEmpty(hotkeyStr)) + { + HotkeyManager.Current.Remove(hotkeyStr); + } + } + + internal static void OnHotkey(object sender, HotkeyEventArgs e) + { + if (!ShouldIgnoreHotkeys()) + { + UpdateLastQUeryMode(); + + mainViewModel.ToggleFlowLauncher(); + e.Handled = true; + } + } + + /// + /// Checks if Flow Launcher should ignore any hotkeys + /// + private static bool ShouldIgnoreHotkeys() + { + return settings.IgnoreHotkeysOnFullscreen && WindowsInteropHelper.IsWindowFullscreen(); + } + + private static void UpdateLastQUeryMode() + { + switch(settings.LastQueryMode) + { + case LastQueryMode.Empty: + mainViewModel.ChangeQueryText(string.Empty); + break; + case LastQueryMode.Preserved: + mainViewModel.LastQuerySelected = true; + break; + case LastQueryMode.Selected: + mainViewModel.LastQuerySelected = false; + break; + default: + throw new ArgumentException($"wrong LastQueryMode: <{settings.LastQueryMode}>"); + + } + } + + internal static void LoadCustomPluginHotkey() + { + if (settings.CustomPluginHotkeys == null) + return; + + foreach (CustomPluginHotkey hotkey in settings.CustomPluginHotkeys) + { + SetCustomQueryHotkey(hotkey); + } + } + + internal static void SetCustomQueryHotkey(CustomPluginHotkey hotkey) + { + SetHotkey(hotkey.Hotkey, (s, e) => + { + if (ShouldIgnoreHotkeys()) + return; + + mainViewModel.MainWindowVisibility = Visibility.Visible; + mainViewModel.ChangeQueryText(hotkey.ActionKeyword); + }); + } + + internal static bool CheckAvailability(HotkeyModel currentHotkey) + { + try + { + HotkeyManager.Current.AddOrReplace("HotkeyAvailabilityTest", currentHotkey.CharKey, currentHotkey.ModifierKeys, (sender, e) => { }); + + return true; + } + catch + { + } + finally + { + HotkeyManager.Current.Remove("HotkeyAvailabilityTest"); + } + + return false; + } + } +} diff --git a/Flow.Launcher/Helper/SingletonWindowOpener.cs b/Flow.Launcher/Helper/SingletonWindowOpener.cs index 3671b9fd3..fdfaaa4fc 100644 --- a/Flow.Launcher/Helper/SingletonWindowOpener.cs +++ b/Flow.Launcher/Helper/SingletonWindowOpener.cs @@ -11,7 +11,16 @@ namespace Flow.Launcher.Helper var window = Application.Current.Windows.OfType().FirstOrDefault(x => x.GetType() == typeof(T)) ?? (T)Activator.CreateInstance(typeof(T), args); Application.Current.MainWindow.Hide(); + + // Fix UI bug + // Add `window.WindowState = WindowState.Normal` + // If only use `window.Show()`, Settings-window doesn't show when minimized in taskbar + // Not sure why this works tho + // Probably because, when `.Show()` fails, `window.WindowState == Minimized` (not `Normal`) + // https://stackoverflow.com/a/59719760/4230390 + window.WindowState = WindowState.Normal; window.Show(); + window.Focus(); return (T)window; diff --git a/Flow.Launcher/HotkeyControl.xaml b/Flow.Launcher/HotkeyControl.xaml index 0164d7dd7..e732cbe97 100644 --- a/Flow.Launcher/HotkeyControl.xaml +++ b/Flow.Launcher/HotkeyControl.xaml @@ -10,7 +10,7 @@ - + diff --git a/Flow.Launcher/HotkeyControl.xaml.cs b/Flow.Launcher/HotkeyControl.xaml.cs index 5c7141d1c..2b6e275df 100644 --- a/Flow.Launcher/HotkeyControl.xaml.cs +++ b/Flow.Launcher/HotkeyControl.xaml.cs @@ -4,8 +4,8 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; -using NHotkey.Wpf; using Flow.Launcher.Core.Resource; +using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Plugin; @@ -18,11 +18,7 @@ namespace Flow.Launcher public event EventHandler HotkeyChanged; - protected virtual void OnHotkeyChanged() - { - EventHandler handler = HotkeyChanged; - if (handler != null) handler(this, EventArgs.Empty); - } + protected virtual void OnHotkeyChanged() => HotkeyChanged?.Invoke(this, EventArgs.Empty); public HotkeyControl() { @@ -90,24 +86,7 @@ namespace Flow.Launcher SetHotkey(new HotkeyModel(keyStr), triggerValidate); } - private bool CheckHotkeyAvailability() - { - try - { - HotkeyManager.Current.AddOrReplace("HotkeyAvailabilityTest", CurrentHotkey.CharKey, CurrentHotkey.ModifierKeys, (sender, e) => { }); - - return true; - } - catch - { - } - finally - { - HotkeyManager.Current.Remove("HotkeyAvailabilityTest"); - } - - return false; - } + private bool CheckHotkeyAvailability() => HotKeyMapper.CheckAvailability(CurrentHotkey); public new bool IsFocused { diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index b6bf76b7f..90a920e3e 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -31,11 +31,15 @@ Ignore hotkeys in fullscreen mode Python Directory Auto Update + Auto Hide Scroll Bar + Automatically hides the Settings window scroll bar and show when hover the mouse over it Select Hide Flow Launcher on startup Hide tray icon Query Search Precision Should Use Pinyin + Allows using Pinyin to search. Pinyin is the standard system of romanized spelling for transliterating Chinese + Shadow effect is not allowed while current theme has blur effect enabled Plugin @@ -47,6 +51,7 @@ New action keyword: Current Priority: New Priority: + Priority: Plugin Directory Author Init time: @@ -69,6 +74,7 @@ Open Result Modifiers Show Hotkey Custom Query Hotkey + Query Delete Edit Add @@ -107,6 +113,7 @@ or go to https://github.com/Flow-Launcher/Flow.Launcher/releases to download updates manually. Release Notes + Usage Tips: 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 @@ -121,6 +128,7 @@ 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 @@ -131,7 +139,7 @@ Update - Hotkey unavailable + Hotkey Unavailable Version diff --git a/Flow.Launcher/Languages/sk.xaml b/Flow.Launcher/Languages/sk.xaml index 7f1b1739d..ec1b015da 100644 --- a/Flow.Launcher/Languages/sk.xaml +++ b/Flow.Launcher/Languages/sk.xaml @@ -36,6 +36,7 @@ Schovať ikonu z oblasti oznámení Presnosť vyhľadávania Použiť Pinyin + Umožňuje vyhľadávanie pomocou Pinyin. Pinyin je štandardný systém romanizovaného pravopisu pre transliteráciu čínštiny Plugin @@ -107,6 +108,7 @@ alebo prejdite na https://github.com/Flow-Launcher/Flow.Launcher/releases pre manuálne stiahnutie aktualizácie. Poznámky k vydaniu + Tipy na používanie: 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 @@ -121,6 +123,7 @@ Nová skratka pre akciu nemôže byť prázdna Nová skratka pre akciu bola priradená pre iný plugin, prosím, zvoľte inú skratku Úspešné + Úspešne dokončené Použite * ak nechcete určiť skratku pre akciu @@ -158,7 +161,7 @@ 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} + 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 diff --git a/Flow.Launcher/Languages/zh-cn.xaml b/Flow.Launcher/Languages/zh-cn.xaml index 92ce60e7f..efc7f19d9 100644 --- a/Flow.Launcher/Languages/zh-cn.xaml +++ b/Flow.Launcher/Languages/zh-cn.xaml @@ -4,7 +4,7 @@ 注册热键:{0} 失败 启动命令 {0} 失败 - 不是合法的Flow Launcher插件格式 + Flow Launcher插件格式错误 在当前查询中置顶 取消置顶 执行查询:{0} @@ -17,28 +17,37 @@ Flow Launcher设置 通用 - 开机启动 + 便携模式 + 开机自动启动 失去焦点时自动隐藏Flow Launcher 不显示新版本提示 记住上次启动位置 语言 上次搜索关键字模式 保留上次搜索关键字 - 全选上次搜索关键字 + 选择上次搜索关键字 清空上次搜索关键字 最大结果显示个数 全屏模式下忽略热键 Python 路径 自动更新 - Select + 选择 启动时不显示主窗口 隐藏任务栏图标 + 查询搜索精度 + 启动拼音搜索 + 允许使用拼音进行搜索。 插件 浏览更多插件 + 启用 禁用 触发关键字 + 当前操作关键字: + 新动作关键字: + 当前优先级: + 新优先级: 插件目录 作者 加载耗时 @@ -47,6 +56,7 @@ 主题 浏览更多主题 + 在这里输入 查询框字体 结果项字体 窗口模式 @@ -65,6 +75,9 @@ 增加 请选择一项 你确定要删除插件 {0} 的热键吗? + 查询窗口阴影效果 + 阴影效果将占用大量的GPU资源。 + 如果您的计算机性能有限,则不建议使用。 HTTP 代理 @@ -88,8 +101,18 @@ 版本 你已经激活了Flow Launcher {0} 次 检查更新 - 发现新版本 {0} , 请重启 flowlauncher。 + 发现新版本 {0} , 请重启 Flow Launcher。 + 下载更新失败,请检查您与 api.github.com 的连接状态或检查代理设置。 + + 下载更新失败,请检查您与 github-cloud.s3.amazonaws.com 的连接状态或检查代理设置, + 或访问 https://github.com/Flow-Launcher/Flow.Launcher/releases 手动下载更新。 + 更新说明: + 使用技巧: + + + 数字越大,结果排名越高。如果你想要结果比任何其他插件的低,请使用负数 + 请提供有效的整数作为优先级设置值! 旧触发关键字 @@ -98,11 +121,13 @@ 确定 找不到指定的插件 新触发关键字不能为空 - 新触发关键字已经被指派给其他插件了,请重新选择一个关键字 + 新触发关键字已经被指派给其他插件了,请换一个关键字 成功 + 成功完成 如果你不想设置触发关键字,可以使用*代替 + 自定义插件热键 预览 热键不可用,请选择一个新的热键 插件热键不合法 @@ -127,12 +152,24 @@ 发送失败 Flow Launcher出错啦 + + 请稍等... + + 检查新的更新 + 您已经拥有最新的Flow Launcher版本 + 检查到更新 + 更新中... + Flow Launcher无法将您的用户配置文件数据移动到新的更新版本中。 + 请手动将您的用户配置文件数据文件夹从 {0} 到 {1} + 新的更新 发现Flow Launcher新版本 V{0} - 更新Flow Launcher出错 + 尝试安装软件更新时发生错误 更新 取消 - 此更新需要重启Flow Launcher + 更新错误 + 检查网络是否可以连接至github-cloud.s3.amazonaws.com. + 此次更新需要重启Flow Launcher 下列文件会被更新 更新文件 更新日志 diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 4cc0b4428..8b81bb94a 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -34,6 +34,7 @@ + @@ -59,7 +60,7 @@ - + @@ -78,7 +79,6 @@ Style="{DynamicResource QueryBoxStyle}" Text="{Binding QueryText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" PreviewDragOver="OnPreviewDragOver" - TextChanged="OnTextChanged" AllowDrop="True" Visibility="Visible" Background="Transparent" diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 04a1063f8..d97e020dc 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using System.Threading.Tasks; using System.Windows; using System.Windows.Input; using System.Windows.Media.Animation; @@ -10,6 +11,7 @@ using Flow.Launcher.Core.Resource; using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.ViewModel; +using Application = System.Windows.Application; using Screen = System.Windows.Forms.Screen; using ContextMenuStrip = System.Windows.Forms.ContextMenuStrip; using DataFormats = System.Windows.DataFormats; @@ -22,7 +24,6 @@ namespace Flow.Launcher { public partial class MainWindow { - #region Private Fields private readonly Storyboard _progressBarStoryboard = new Storyboard(); @@ -40,20 +41,23 @@ namespace Flow.Launcher _settings = settings; InitializeComponent(); } + public MainWindow() { InitializeComponent(); } - private void OnClosing(object sender, CancelEventArgs e) + private async void OnClosing(object sender, CancelEventArgs e) { _notifyIcon.Visible = false; _viewModel.Save(); + e.Cancel = true; + await PluginManager.DisposePluginsAsync(); + Application.Current.Shutdown(); } private void OnInitialized(object sender, EventArgs e) { - } private void OnLoaded(object sender, RoutedEventArgs _) @@ -61,8 +65,6 @@ namespace Flow.Launcher // show notify icon when flowlauncher is hidden InitializeNotifyIcon(); - // todo is there a way to set blur only once? - ThemeManager.Instance.SetBlurForWindow(); WindowsInteropHelper.DisableControlBox(this); InitProgressbarAnimation(); InitializePosition(); @@ -72,47 +74,64 @@ namespace Flow.Launcher _viewModel.PropertyChanged += (o, e) => { - if (e.PropertyName == nameof(MainViewModel.MainWindowVisibility)) + switch (e.PropertyName) { - if (_viewModel.MainWindowVisibility == Visibility.Visible) + case nameof(MainViewModel.MainWindowVisibility): { - Activate(); - QueryTextBox.Focus(); - UpdatePosition(); - _settings.ActivateTimes++; - if (!_viewModel.LastQuerySelected) + if (_viewModel.MainWindowVisibility == Visibility.Visible) { - QueryTextBox.SelectAll(); - _viewModel.LastQuerySelected = true; + Activate(); + QueryTextBox.Focus(); + UpdatePosition(); + _settings.ActivateTimes++; + if (!_viewModel.LastQuerySelected) + { + QueryTextBox.SelectAll(); + _viewModel.LastQuerySelected = true; + } + + if (_viewModel.ProgressBarVisibility == Visibility.Visible && isProgressBarStoryboardPaused) + { + _progressBarStoryboard.Begin(ProgressBar, true); + isProgressBarStoryboardPaused = false; + } } - if (_viewModel.ProgressBarVisibility == Visibility.Visible && isProgressBarStoryboardPaused) + if (!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(); + _progressBarStoryboard.Stop(ProgressBar); isProgressBarStoryboardPaused = true; } - else if (_viewModel.MainWindowVisibility == Visibility.Visible && isProgressBarStoryboardPaused) + + break; + } + case nameof(MainViewModel.ProgressBarVisibility): + { + Dispatcher.Invoke(async () => { - _progressBarStoryboard.Resume(); - isProgressBarStoryboardPaused = false; + if (_viewModel.ProgressBarVisibility == Visibility.Hidden && !isProgressBarStoryboardPaused) + { + await Task.Delay(50); + _progressBarStoryboard.Stop(ProgressBar); + isProgressBarStoryboardPaused = true; + } + else if (_viewModel.MainWindowVisibility == Visibility.Visible && + isProgressBarStoryboardPaused) + { + _progressBarStoryboard.Begin(ProgressBar, true); + isProgressBarStoryboardPaused = false; + } + }, System.Windows.Threading.DispatcherPriority.Render); + + break; + } + case nameof(MainViewModel.QueryTextCursorMovedToEnd): + if (_viewModel.QueryTextCursorMovedToEnd) + { + MoveQueryTextToEnd(); + _viewModel.QueryTextCursorMovedToEnd = false; } - }, System.Windows.Threading.DispatcherPriority.Render); + break; } }; _settings.PropertyChanged += (o, e) => @@ -189,14 +208,15 @@ namespace Flow.Launcher private void InitProgressbarAnimation() { - var da = new DoubleAnimation(ProgressBar.X2, ActualWidth + 100, new Duration(new TimeSpan(0, 0, 0, 0, 1600))); + var da = new DoubleAnimation(ProgressBar.X2, ActualWidth + 100, + new Duration(new TimeSpan(0, 0, 0, 0, 1600))); var da1 = new DoubleAnimation(ProgressBar.X1, ActualWidth, new Duration(new TimeSpan(0, 0, 0, 0, 1600))); Storyboard.SetTargetProperty(da, new PropertyPath("(Line.X2)")); Storyboard.SetTargetProperty(da1, new PropertyPath("(Line.X1)")); _progressBarStoryboard.Children.Add(da); _progressBarStoryboard.Children.Add(da1); _progressBarStoryboard.RepeatBehavior = RepeatBehavior.Forever; - ProgressBar.BeginStoryboard(_progressBarStoryboard); + _viewModel.ProgressBarVisibility = Visibility.Hidden; isProgressBarStoryboardPaused = true; } @@ -210,10 +230,10 @@ namespace Flow.Launcher { if (sender != null && e.OriginalSource != null) { - var r = (ResultListBox)sender; - var d = (DependencyObject)e.OriginalSource; + var r = (ResultListBox) sender; + var d = (DependencyObject) e.OriginalSource; var item = ItemsControl.ContainerFromElement(r, d) as ListBoxItem; - var result = (ResultViewModel)item?.DataContext; + var result = (ResultViewModel) item?.DataContext; if (result != null) { if (e.ChangedButton == MouseButton.Left) @@ -316,13 +336,9 @@ namespace Flow.Launcher } } - private void OnTextChanged(object sender, TextChangedEventArgs e) + private void MoveQueryTextToEnd() { - if (_viewModel.QueryTextCursorMovedToEnd) - { - QueryTextBox.CaretIndex = QueryTextBox.Text.Length; - _viewModel.QueryTextCursorMovedToEnd = false; - } + QueryTextBox.CaretIndex = QueryTextBox.Text.Length; } } } \ No newline at end of file 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/Properties/PublishProfiles/NetCore3.1-SelfContained.pubxml b/Flow.Launcher/Properties/PublishProfiles/Net5.0-SelfContained.pubxml similarity index 90% rename from Flow.Launcher/Properties/PublishProfiles/NetCore3.1-SelfContained.pubxml rename to Flow.Launcher/Properties/PublishProfiles/Net5.0-SelfContained.pubxml index 2794a0cea..124792e3e 100644 --- a/Flow.Launcher/Properties/PublishProfiles/NetCore3.1-SelfContained.pubxml +++ b/Flow.Launcher/Properties/PublishProfiles/Net5.0-SelfContained.pubxml @@ -7,7 +7,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem Release Any CPU - netcoreapp3.1 + net5.0-windows10.0.19041.0 ..\Output\Release\ win-x64 true diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index 0afa67d1a..f3403696e 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -18,6 +18,10 @@ 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; +using System.Collections.Concurrent; namespace Flow.Launcher { @@ -67,39 +71,31 @@ namespace Flow.Launcher 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() { + SavePluginSettings(); _mainVM.Save(); _settingsVM.Save(); - PluginManager.Save(); ImageLoader.Save(); } - public Task ReloadAllPluginData() - { - return PluginManager.ReloadData(); - } + public Task ReloadAllPluginData() => PluginManager.ReloadData(); - public void ShowMsg(string title, string subTitle = "", string iconPath = "") - { + public void ShowMsgError(string title, string subTitle = "") => + ShowMsg(title, subTitle, Constant.ErrorIcon, 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); }); } @@ -112,54 +108,82 @@ namespace Flow.Launcher }); } - public void StartLoadingBar() + public void StartLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Visible; + + public void StopLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Collapsed; + + public string GetTranslation(string key) => InternationalizationManager.Instance.GetTranslation(key); + + 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); + + private readonly ConcurrentDictionary _pluginJsonStorages = new(); + + public void SavePluginSettings() { - _mainVM.ProgressBarVisibility = Visibility.Visible; + foreach (var value in _pluginJsonStorages.Values) + { + var method = value.GetType().GetMethod("Save"); + method?.Invoke(value, null); + } } - public void StopLoadingBar() + public T LoadSettingJsonStorage() where T : new() { - _mainVM.ProgressBarVisibility = Visibility.Collapsed; + var type = typeof(T); + if (!_pluginJsonStorages.ContainsKey(type)) + _pluginJsonStorages[type] = new PluginJsonStorage(); + + return ((PluginJsonStorage) _pluginJsonStorages[type]).Load(); } - public string GetTranslation(string key) + public void SaveSettingJsonStorage() where T : new() { - return InternationalizationManager.Instance.GetTranslation(key); + var type = typeof(T); + if (!_pluginJsonStorages.ContainsKey(type)) + _pluginJsonStorages[type] = new PluginJsonStorage(); + + ((PluginJsonStorage) _pluginJsonStorages[type]).Save(); } - public List GetAllPlugins() + public void SaveJsonStorage(T settings) where T : new() { - return PluginManager.AllPlugins.ToList(); + var type = typeof(T); + _pluginJsonStorages[type] = new PluginJsonStorage(settings); + + ((PluginJsonStorage) _pluginJsonStorages[type]).Save(); } public event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent; - public MatchResult FuzzySearch(string query, string stringToCompare) => StringMatcher.FuzzySearch(query, stringToCompare); - - public Task HttpGetStringAsync(string url, CancellationToken token = default) - { - return Http.GetAsync(url); - } - - public Task HttpGetStreamAsync(string url, CancellationToken token = default) - { - return Http.GetStreamAsync(url); - } - - public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath) - { - return Http.DownloadAsync(url, filePath); - } - - public void AddActionKeyword(string pluginId, string newActionKeyword) - { - PluginManager.AddActionKeyword(pluginId, newActionKeyword); - } - - public void RemoveActionKeyword(string pluginId, string oldActionKeyword) - { - PluginManager.RemoveActionKeyword(pluginId, oldActionKeyword); - } #endregion #region Private Methods @@ -168,11 +192,12 @@ namespace Flow.Launcher { if (GlobalKeyboardEvent != null) { - return GlobalKeyboardEvent((int)keyevent, vkcode, state); + return GlobalKeyboardEvent((int) keyevent, vkcode, state); } + return true; } #endregion } -} +} \ No newline at end of file diff --git a/Flow.Launcher/SettingWindow.xaml b/Flow.Launcher/SettingWindow.xaml index cb0ef2def..478e114f9 100644 --- a/Flow.Launcher/SettingWindow.xaml +++ b/Flow.Launcher/SettingWindow.xaml @@ -13,9 +13,12 @@ mc:Ignorable="d" Icon="Images\app.ico" Title="{DynamicResource flowlauncher_settings}" - ResizeMode="NoResize" + ResizeMode="CanResizeWithGrip" WindowStartupLocation="CenterScreen" - Height="600" Width="800" + Height="600" Width="900" + MinWidth="850" + MinHeight="500" + Loaded="OnLoaded" Closed="OnClosed" d:DataContext="{d:DesignInstance vm:SettingWindowViewModel}"> @@ -34,8 +37,8 @@ - - + + @@ -61,9 +64,14 @@ - + + + + + Margin="10, 0, 10, 10" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ui:ScrollViewerHelper.AutoHideScrollBars="{Binding AutoHideScrollBar, Mode=OneWay}"> @@ -141,60 +149,58 @@ + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Width="48" Height="48" + HorizontalAlignment="Left" VerticalAlignment="Top" + Grid.RowSpan="2"/> + + + + + + + + + + + + + + + + + + + + + + + @@ -216,8 +222,10 @@ - - Open Theme Folder + @@ -235,7 +243,7 @@ - + @@ -357,7 +365,7 @@ - + @@ -431,6 +439,7 @@ + @@ -448,7 +457,14 @@ -