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-windowstruetrueLibrary
@@ -16,7 +16,7 @@
true
- full
+ portablefalse..\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
-
-
+
+
+
+
+
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}trueLibrary
@@ -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.0Flow.Launcher.PluginFlow-LauncherMIT
@@ -34,7 +34,7 @@
true
- full
+ portablefalse..\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}LibraryProperties
@@ -15,7 +15,7 @@
true
- full
+ portablefalsebin\Debug\DEBUG;TRACE
@@ -49,12 +49,12 @@
-
-
+
+ allruntime; 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.0truetrueFlow.Launcher.App
@@ -17,7 +17,7 @@
AnyCPUtrue
- full
+ portablefalse..\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 modePython DirectoryAuto Update
+ Auto Hide Scroll Bar
+ Automatically hides the Settings window scroll bar and show when hover the mouse over itSelectHide Flow Launcher on startupHide tray iconQuery Search PrecisionShould 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 enabledPlugin
@@ -47,6 +51,7 @@
New action keyword:Current Priority:New Priority:
+ Priority:Plugin DirectoryAuthorInit time:
@@ -69,6 +74,7 @@
Open Result ModifiersShow HotkeyCustom Query Hotkey
+ QueryDeleteEditAdd
@@ -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 emptyThis new Action Keyword is already assigned to another plugin, please choose a different oneSuccess
+ Completed successfullyUse * if you don't want to specify an action keyword
@@ -131,7 +139,7 @@
Update
- Hotkey unavailable
+ Hotkey UnavailableVersion
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ávaniaPoužiť Pinyin
+ Umožňuje vyhľadávanie pomocou Pinyin. Pinyin je štandardný systém romanizovaného pravopisu pre transliteráciu čínštinyPlugin
@@ -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ázdnaNová 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áciaAktualizuje 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áciaJe 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.
FileSystemReleaseAny CPU
- netcoreapp3.1
+ net5.0-windows10.0.19041.0..\Output\Release\win-x64true
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 @@
-
+
+
+
+
+
+
diff --git a/Flow.Launcher/SettingWindow.xaml.cs b/Flow.Launcher/SettingWindow.xaml.cs
index a922b4d67..aa341f431 100644
--- a/Flow.Launcher/SettingWindow.xaml.cs
+++ b/Flow.Launcher/SettingWindow.xaml.cs
@@ -2,18 +2,17 @@ using System;
using System.IO;
using System.Windows;
using System.Windows.Input;
+using System.Windows.Interop;
using System.Windows.Navigation;
using Microsoft.Win32;
-using NHotkey;
-using NHotkey.Wpf;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
-using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.ViewModel;
+using Flow.Launcher.Helper;
namespace Flow.Launcher
{
@@ -35,6 +34,14 @@ namespace Flow.Launcher
}
#region General
+ private void OnLoaded(object sender, RoutedEventArgs e)
+ {
+ // Fix (workaround) for the window freezes after lock screen (Win+L)
+ // https://stackoverflow.com/questions/4951058/software-rendering-mode-wpf
+ HwndSource hwndSource = PresentationSource.FromVisual(this) as HwndSource;
+ HwndTarget hwndTarget = hwndSource.CompositionTarget;
+ hwndTarget.RenderMode = RenderMode.SoftwareOnly;
+ }
private void OnAutoStartupChecked(object sender, RoutedEventArgs e)
{
@@ -118,45 +125,13 @@ namespace Flow.Launcher
{
if (HotkeyControl.CurrentHotkeyAvailable)
{
- SetHotkey(HotkeyControl.CurrentHotkey, (o, args) =>
- {
- if (!Application.Current.MainWindow.IsVisible)
- {
- Application.Current.MainWindow.Visibility = Visibility.Visible;
- }
- else
- {
- Application.Current.MainWindow.Visibility = Visibility.Hidden;
- }
- });
- RemoveHotkey(settings.Hotkey);
+
+ HotKeyMapper.SetHotkey(HotkeyControl.CurrentHotkey, HotKeyMapper.OnHotkey);
+ HotKeyMapper.RemoveHotkey(settings.Hotkey);
settings.Hotkey = HotkeyControl.CurrentHotkey.ToString();
}
}
- 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);
- }
- }
-
- void RemoveHotkey(string hotkeyStr)
- {
- if (!string.IsNullOrEmpty(hotkeyStr))
- {
- HotkeyManager.Current.Remove(hotkeyStr);
- }
- }
-
private void OnDeleteCustomHotkeyClick(object sender, RoutedEventArgs e)
{
var item = viewModel.SelectedCustomPluginHotkey;
@@ -174,7 +149,7 @@ namespace Flow.Launcher
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
settings.CustomPluginHotkeys.Remove(item);
- RemoveHotkey(item.Hotkey);
+ HotKeyMapper.RemoveHotkey(item.Hotkey);
}
}
diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs
index 2b2103605..a5383b179 100644
--- a/Flow.Launcher/Storage/QueryHistory.cs
+++ b/Flow.Launcher/Storage/QueryHistory.cs
@@ -1,13 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using Flow.Launcher.Plugin;
+using System.Text.Json.Serialization;
namespace Flow.Launcher.Storage
{
public class History
{
- public List Items { get; set; } = new List();
+ [JsonInclude]
+ public List Items { get; private set; } = new List();
private int _maxHistory = 300;
diff --git a/Flow.Launcher/Storage/TopMostRecord.cs b/Flow.Launcher/Storage/TopMostRecord.cs
index c92ef4956..052c296cd 100644
--- a/Flow.Launcher/Storage/TopMostRecord.cs
+++ b/Flow.Launcher/Storage/TopMostRecord.cs
@@ -1,6 +1,4 @@
using System.Collections.Generic;
-using System.Linq;
-using System.Text.Json;
using System.Text.Json.Serialization;
using Flow.Launcher.Plugin;
@@ -9,14 +7,8 @@ namespace Flow.Launcher.Storage
// todo this class is not thread safe.... but used from multiple threads.
public class TopMostRecord
{
- ///
- /// You should not directly access this field
- ///
- /// It is public due to System.Text.Json limitation in version 3.1
- ///
- ///
- /// TODO: Set it to private
- public Dictionary records { get; set; } = new Dictionary();
+ [JsonInclude]
+ public Dictionary records { get; private set; } = new Dictionary();
internal bool IsTopMost(Result result)
{
diff --git a/Flow.Launcher/Storage/UserSelectedRecord.cs b/Flow.Launcher/Storage/UserSelectedRecord.cs
index bc7a2da73..23d8c4faf 100644
--- a/Flow.Launcher/Storage/UserSelectedRecord.cs
+++ b/Flow.Launcher/Storage/UserSelectedRecord.cs
@@ -1,48 +1,109 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Text.Json.Serialization;
+using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Plugin;
+using Flow.Launcher.ViewModel;
namespace Flow.Launcher.Storage
{
public class UserSelectedRecord
{
- ///
- /// You should not directly access this field
- ///
- /// It is public due to System.Text.Json limitation in version 3.1
- ///
- ///
- /// TODO: Set it to private
- [JsonPropertyName("records")]
- public Dictionary records { get; set; }
+ private const int HASH_MULTIPLIER = 31;
+ private const int HASH_INITIAL = 23;
+
+ [JsonInclude]
+ public Dictionary recordsWithQuery { get; private set; }
+
+ [JsonInclude, JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public Dictionary records { get; private set; }
+
public UserSelectedRecord()
{
- records = new Dictionary();
+ recordsWithQuery = new Dictionary();
+ }
+
+ private static int GenerateStaticHashCode(string s, int start = HASH_INITIAL)
+ {
+ if (s == null)
+ {
+ return start;
+ }
+
+ unchecked
+ {
+ // skip the empty space
+ // https://stackoverflow.com/a/5155015 31 prime and is 2^5 - 1 which allows fast
+ // optimization without information lost when int overflow
+
+ for (int i = 0; i < s.Length; i++)
+ {
+ start = start * HASH_MULTIPLIER + s[i];
+ }
+
+ return start;
+ }
+ }
+
+ private static int GenerateResultHashCode(Result result)
+ {
+ int hashcode = GenerateStaticHashCode(result.Title);
+ return GenerateStaticHashCode(result.SubTitle, hashcode);
+ }
+
+ private static int GenerateQueryAndResultHashCode(Query query, Result result)
+ {
+ int hashcode = GenerateStaticHashCode(query.ActionKeyword);
+ hashcode = GenerateStaticHashCode(query.Search, hashcode);
+ hashcode = GenerateStaticHashCode(result.Title, hashcode);
+ hashcode = GenerateStaticHashCode(result.SubTitle, hashcode);
+
+ return hashcode;
+ }
+
+ private void TransformOldRecords()
+ {
+ if (records != null)
+ {
+ var localRecords = records;
+ records = null;
+
+ foreach (var pair in localRecords)
+ {
+ recordsWithQuery.TryAdd(GenerateStaticHashCode(pair.Key), pair.Value);
+ }
+ }
}
public void Add(Result result)
{
- var key = result.ToString();
- if (records.ContainsKey(key))
- {
- records[key]++;
- }
- else
- {
- records.Add(key, 1);
+ TransformOldRecords();
- }
+ var keyWithQuery = GenerateQueryAndResultHashCode(result.OriginQuery, result);
+
+ if (!recordsWithQuery.TryAdd(keyWithQuery, 1))
+ recordsWithQuery[keyWithQuery]++;
+
+ var keyWithoutQuery = GenerateResultHashCode(result);
+
+ if (!recordsWithQuery.TryAdd(keyWithoutQuery, 1))
+ recordsWithQuery[keyWithoutQuery]++;
}
public int GetSelectedCount(Result result)
{
- if (records.TryGetValue(result.ToString(), out int value))
- {
- return value;
- }
- return 0;
+ var selectedCount = 0;
+
+ recordsWithQuery.TryGetValue(GenerateQueryAndResultHashCode(result.OriginQuery, result), out int value);
+ selectedCount += value * 5;
+
+ recordsWithQuery.TryGetValue(GenerateResultHashCode(result), out value);
+ selectedCount += value;
+
+ return selectedCount;
}
}
-}
+}
\ No newline at end of file
diff --git a/Flow.Launcher/Themes/Base.xaml b/Flow.Launcher/Themes/Base.xaml
index eb2af490a..df0304bc2 100644
--- a/Flow.Launcher/Themes/Base.xaml
+++ b/Flow.Launcher/Themes/Base.xaml
@@ -46,6 +46,8 @@
+
+
+
+
+
+
+
+
+
+
+
+
@@ -47,23 +65,40 @@
+ BorderThickness="1"
+ GridViewColumnHeader.Click="SortByColumn"
+ MouseDoubleClick="MouseDoubleClickItem">
-
+
-
+
-
+
-
+
+
+
+
+
+
+
+
diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml.cs
index 16ef38cff..30e5a5132 100644
--- a/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml.cs
+++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml.cs
@@ -2,6 +2,8 @@ using Microsoft.Win32;
using System.Windows;
using System.Windows.Controls;
using Flow.Launcher.Core.Plugin;
+using System.ComponentModel;
+using System.Windows.Data;
namespace Flow.Launcher.Plugin.WebSearch
{
@@ -88,5 +90,85 @@ namespace Flow.Launcher.Plugin.WebSearch
{
_settings.BrowserPath = browserPathBox.Text;
}
+
+ GridViewColumnHeader _lastHeaderClicked = null;
+ ListSortDirection _lastDirection = ListSortDirection.Ascending;
+
+ private void SortByColumn(object sender, RoutedEventArgs e)
+ {
+ ListSortDirection direction;
+
+ if (e.OriginalSource is not GridViewColumnHeader headerClicked)
+ {
+ return;
+ }
+
+ if (headerClicked.Role == GridViewColumnHeaderRole.Padding)
+ {
+ return;
+ }
+
+ if (headerClicked != _lastHeaderClicked)
+ {
+ direction = ListSortDirection.Ascending;
+ }
+ else
+ {
+ if (_lastDirection == ListSortDirection.Ascending)
+ {
+ direction = ListSortDirection.Descending;
+ }
+ else
+ {
+ direction = ListSortDirection.Ascending;
+ }
+ }
+
+ var columnBinding = headerClicked.Column.DisplayMemberBinding as Binding;
+ var sortBy = columnBinding?.Path.Path ?? headerClicked.Column.Header as string;
+
+ Sort(sortBy, direction);
+
+ if (direction == ListSortDirection.Ascending)
+ {
+ headerClicked.Column.HeaderTemplate =
+ Resources["HeaderTemplateArrowUp"] as DataTemplate;
+ }
+ else
+ {
+ headerClicked.Column.HeaderTemplate =
+ Resources["HeaderTemplateArrowDown"] as DataTemplate;
+ }
+
+ // Remove arrow from previously sorted header
+ if (_lastHeaderClicked != null && _lastHeaderClicked != headerClicked)
+ {
+ _lastHeaderClicked.Column.HeaderTemplate = null;
+ }
+
+ _lastHeaderClicked = headerClicked;
+ _lastDirection = direction;
+ }
+ private void Sort(string sortBy, ListSortDirection direction)
+ {
+ ICollectionView dataView = CollectionViewSource.GetDefaultView(SearchSourcesListView.ItemsSource);
+ dataView.SortDescriptions.Clear();
+ SortDescription sd = new(sortBy, direction);
+ dataView.SortDescriptions.Add(sd);
+ dataView.Refresh();
+ }
+
+ private void MouseDoubleClickItem(object sender, System.Windows.Input.MouseButtonEventArgs e)
+ {
+ if (((FrameworkElement)e.OriginalSource).DataContext is SearchSource && _settings.SelectedSearchSource != null)
+ {
+ var webSearch = new SearchSourceSettingWindow
+ (
+ _settings.SearchSources, _context, _settings.SelectedSearchSource
+ );
+
+ webSearch.ShowDialog();
+ }
+ }
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsViewModel.cs
index 5943b187e..a2538893b 100644
--- a/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsViewModel.cs
+++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsViewModel.cs
@@ -6,13 +6,12 @@ namespace Flow.Launcher.Plugin.WebSearch
{
private readonly PluginJsonStorage _storage;
- public SettingsViewModel()
+ public SettingsViewModel(Settings settings)
{
- _storage = new PluginJsonStorage();
- Settings = _storage.Load();
+ Settings = settings;
}
- public Settings Settings { get; set; }
+ public Settings Settings { get; }
public void Save()
{
diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Baidu.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Baidu.cs
index b7e2017f9..ccb5b20d7 100644
--- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Baidu.cs
+++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Baidu.cs
@@ -25,14 +25,10 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
const string api = "http://suggestion.baidu.com/su?json=1&wd=";
result = await Http.GetAsync(api + Uri.EscapeUriString(query), token).ConfigureAwait(false);
}
- catch (TaskCanceledException)
- {
- return null;
- }
- catch (HttpRequestException e)
+ catch (Exception e) when (e is HttpRequestException || e.InnerException is TimeoutException)
{
Log.Exception("|Baidu.Suggestions|Can't get suggestion from baidu", e);
- return new List();
+ return null;
}
if (string.IsNullOrEmpty(result)) return new List();
@@ -44,7 +40,7 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
{
json = JsonDocument.Parse(match.Groups[1].Value);
}
- catch(JsonException e)
+ catch (JsonException e)
{
Log.Exception("|Baidu.Suggestions|can't parse suggestions", e);
return new List();
diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs
index ffde2fda2..38c5fb4a0 100644
--- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs
+++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs
@@ -17,45 +17,39 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
{
public override async Task> Suggestions(string query, CancellationToken token)
{
- JsonElement json;
try
{
const string api = "https://api.bing.com/qsonhs.aspx?q=";
using var resultStream = await Http.GetStreamAsync(api + Uri.EscapeUriString(query), token).ConfigureAwait(false);
-
- if (resultStream.Length == 0)
- return new List(); // this handles the cancellation
-
- json = (await JsonDocument.ParseAsync(resultStream, cancellationToken: token)).RootElement.GetProperty("AS");
+
+ using var json = (await JsonDocument.ParseAsync(resultStream, cancellationToken: token));
+ var root = json.RootElement.GetProperty("AS");
+
+ if (root.GetProperty("FullResults").GetInt32() == 0)
+ return new List();
+
+ return root.GetProperty("Results")
+ .EnumerateArray()
+ .SelectMany(r => r.GetProperty("Suggests")
+ .EnumerateArray()
+ .Select(s => s.GetProperty("Txt").GetString()))
+ .ToList();
+
+
}
- catch (TaskCanceledException)
+ catch (Exception e) when (e is HttpRequestException || e.InnerException is TimeoutException)
{
+ Log.Exception("|Baidu.Suggestions|Can't get suggestion from baidu", e);
return null;
}
- catch (HttpRequestException e)
- {
- Log.Exception("|Bing.Suggestions|Can't get suggestion from Bing", e);
- return new List();
- }
catch (JsonException e)
{
Log.Exception("|Bing.Suggestions|can't parse suggestions", e);
return new List();
- }
-
- if (json.GetProperty("FullResults").GetInt32() == 0)
- return new List();
-
- return json.GetProperty("Results")
- .EnumerateArray()
- .SelectMany(r => r.GetProperty("Suggests")
- .EnumerateArray()
- .Select(s => s.GetProperty("Txt").GetString()))
- .ToList();
-
+ }
}
public override string ToString()
diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs
index c33ebd7e1..c5f43d081 100644
--- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs
+++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs
@@ -16,39 +16,32 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
{
public override async Task> Suggestions(string query, CancellationToken token)
{
- JsonDocument json;
-
try
{
const string api = "https://www.google.com/complete/search?output=chrome&q=";
using var resultStream = await Http.GetStreamAsync(api + Uri.EscapeUriString(query)).ConfigureAwait(false);
-
- if (resultStream.Length == 0)
+
+ using var json = await JsonDocument.ParseAsync(resultStream, cancellationToken: token);
+
+ if (json == null)
return new List();
-
- json = await JsonDocument.ParseAsync(resultStream);
+
+ var results = json.RootElement.EnumerateArray().ElementAt(1);
+
+ return results.EnumerateArray().Select(o => o.GetString()).ToList();
}
- catch (TaskCanceledException)
+ catch (Exception e) when (e is HttpRequestException || e.InnerException is TimeoutException)
{
+ Log.Exception("|Baidu.Suggestions|Can't get suggestion from baidu", e);
return null;
}
- catch (HttpRequestException e)
- {
- Log.Exception("|Google.Suggestions|Can't get suggestion from google", e);
- return new List();
- }
catch (JsonException e)
{
Log.Exception("|Google.Suggestions|can't parse suggestions", e);
return new List();
}
-
- var results = json?.RootElement.EnumerateArray().ElementAt(1);
-
- return results?.EnumerateArray().Select(o => o.GetString()).ToList() ?? new List();
-
}
public override string ToString()
diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json b/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json
index 6e9986749..679f976d3 100644
--- a/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json
@@ -1,7 +1,8 @@
{
"ID": "565B73353DBF4806919830B9202EE3BF",
"ActionKeywords": [
- "g",
+ "*",
+ "sc",
"wiki",
"findicon",
"facebook",
@@ -25,7 +26,7 @@
"Name": "Web Searches",
"Description": "Provide the web search ability",
"Author": "qianlifeng",
- "Version": "1.3.2",
+ "Version": "1.4.0",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.WebSearch.dll",
diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/setting.json b/Plugins/Flow.Launcher.Plugin.WebSearch/setting.json
index c31d02ae6..ef9c63d67 100644
--- a/Plugins/Flow.Launcher.Plugin.WebSearch/setting.json
+++ b/Plugins/Flow.Launcher.Plugin.WebSearch/setting.json
@@ -2,7 +2,7 @@
"WebSearches": [
{
"Title": "Google",
- "ActionKeyword": "g",
+ "ActionKeyword": "*",
"IconPath": "Images\\google.png",
"Url": "https://www.google.com/search?q={q}",
"Enabled": true
diff --git a/README.md b/README.md
index 9af47a5ee..e9c7e3360 100644
--- a/README.md
+++ b/README.md
@@ -4,22 +4,37 @@
-
+
[](https://ci.appveyor.com/project/JohnTheGr8/flow-launcher/branch/dev)
[](https://github.com/Flow-Launcher/Flow.Launcher/releases)
-[](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest)

+[](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest)
+[](https://flow-launcher.github.io/docs)
[](https://discord.gg/AvgAQgh)
Flow Launcher. Dedicated to make your workflow flow more seamlessly. Aimed at being more than an app launcher, it searches, integrates and expands on functionalities. Flow will continue to evolve, designed to be open and built with the community at heart.
Remember to star it, flow will love you more :)
+---
+
+
+
+---
+
## Features
+

- Search everything from applications, files, bookmarks, YouTube, Twitter and more. All from the comfort of your keyboard without ever touching the mouse.
- Search for file contents.
+- Do mathematical calculations and copy the result to clipboard.
- Support search using environment variable paths.
- Run batch and PowerShell commands as Administrator or a different user.
- Support languages from Chinese to Italian and more.
@@ -30,58 +45,61 @@ Flow Launcher. Dedicated to make your workflow flow more seamlessly. Aimed at be
[ **SOFTPEDIA EDITOR'S PICK**](https://www.softpedia.com/get/System/Launchers-Shutdown-Tools/Flow-Launcher.shtml)
-## Running Flow Launcher
+## Getting Started
-| [Windows 7 and up](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest) |
-| ---------------------------------------------------------------------------------- |
+### Installation
+
+| [Windows 7 and up installer](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest) | `WinGet install "Flow Launcher"` |
+| -------------------------------------------------------------------------------------------- | -------------------------------- |
Windows may complain about security due to code not being signed, this will be completed at a later stage. If you downloaded from this repo, you are good to continue the set up.
-**Integrations**
- - If you use Python plugins:
- - Install [Python3](https://www.python.org/downloads/), download `.exe` installer.
- - Add Python to `%PATH%` or set it in flow's settings.
- - Use `pip` to install `flowlauncher`, cmd in `pip install flowlauncher`.
- - Start to launch your Python plugins.
- - Flow searches files and contents via Windows Index Search, to use Everything, download the plugin [here](https://github.com/Flow-Launcher/Flow.Launcher.Plugin.Everything/releases/latest).
-
-**Usage**
+### Usage
- Open flow's search window: Alt+Space is the default hotkey.
-- Search programs, bookmarks and control panel items with acronyms eg. `gk` or `gp` for GitKraken Preview; fuzzy search eg. `code` or `vis` for Visual Studio Code.
- Open context menu: on the selected result, press Ctrl+O/Shift+Enter.
- Cancel/Return to previous screen: Esc.
-- Install/Uninstall/Update plugins: in the search window, type `pm install`/`pm uninstall`/`pm update` + the plugin name.
+- Install/Uninstall/Update plugins: in the search window, type `pm` `install`/`uninstall`/`update` + the plugin name.
- Saved user settings are located:
- If using roaming: `%APPDATA%\FlowLauncher`
- If using portable, by default: `%localappdata%\FlowLauncher\app-\UserData`
- Logs are saved along with your user settings folder.
-## Status
+[More tips](https://flow-launcher.github.io/docs/#/usage-tips)
-Flow is under heavy development, but the code base is stable, so contributions are very welcome. If you would like to help maintain it, please do not hesistate to get in touch.
+### Plugins
-## Contributing
+Flow searches files and contents via Windows Index Search, to use **Everything**: `pm install everything`.
-We welcome all contributions. If you are unsure of a change you want to make, simply put an issue in for discussion, otherwise feel free to put in a pull request.
+If you are using Python plugins, flow will prompt to either select the location or allow Python (Embeddable) to be automatic downloaded for use.
-You will find the main goals of flow placed under Projects board, so feel free to contribute on that. If you would like to make small incremental changes, feel free to do so as well.
+Vist [here](https://flow-launcher.github.io/docs/#/plugins) for our plugin portfolio.
-Get in touch if you like to join the Flow-Launcher Team and help build this great tool.
+If you are keen to write your own plugin for flow, please take a look at our plugin development documentation for [C#](https://flow-launcher.github.io/docs/#/develop-csharp-plugins) or [Python](https://flow-launcher.github.io/docs/#/develop-py-plugins)
-**Question/Suggestion**
+## Questions/Suggestions
-Yes please, submit an issue to let us know.
+Yes please, let us know in the [Q&A](https://github.com/Flow-Launcher/Flow.Launcher/discussions/categories/q-a) section.
**Join our community on [Discord](https://discord.gg/AvgAQgh)!**
-## Developing/Debugging
+## Development
-Flow Launcher's target framework is .Net Core 3.1
+### Status
+
+Flow is under heavy development, but the code base is stable, so contributions are very welcome. If you would like to help maintain it, please do not hesistate to get in touch.
+
+### Contributing
+
+We welcome all contributions. If you are unsure of a change you want to make, let us know in the [Discussions](https://github.com/Flow-Launcher/Flow.Launcher/discussions/categories/ideas), otherwise feel free to put in a pull request.
+
+You will find the main goals of flow placed under the [Projects board](https://github.com/Flow-Launcher/Flow.Launcher/projects), so feel free to contribute on that. If you would like to make small incremental changes, feel free to do so as well.
+
+Get in touch if you like to join the Flow-Launcher Team and help build this great tool.
+
+### Developing/Debugging
+
+Flow Launcher's target framework is .Net 5
Install Visual Studio 2019
-Install .Net Core 3.1 SDK via Visual Studio installer or manually from [here](https://dotnet.microsoft.com/download/dotnet-core/thank-you/sdk-3.1.201-windows-x64-installer)
-
-## Documentation
-
-[Wiki](https://github.com/Flow-Launcher/Flow.Launcher/wiki)
+Install .Net 5 SDK via Visual Studio installer or manually from [here](https://dotnet.microsoft.com/download/dotnet/thank-you/sdk-5.0.103-windows-x64-installer)
diff --git a/Scripts/flowlauncher.nuspec b/Scripts/flowlauncher.nuspec
index 6c5deb86b..8877bfbaf 100644
--- a/Scripts/flowlauncher.nuspec
+++ b/Scripts/flowlauncher.nuspec
@@ -11,6 +11,6 @@
Flow Launcher - a launcher for windows
-
+
diff --git a/Scripts/post_build.ps1 b/Scripts/post_build.ps1
index b08fac8f6..093f92768 100644
--- a/Scripts/post_build.ps1
+++ b/Scripts/post_build.ps1
@@ -47,10 +47,20 @@ function Delete-Unused ($path, $config) {
Remove-Item -Path $target -Include "*.xml" -Recurse
}
+function Remove-CreateDumpExe ($path, $config) {
+ $target = "$path\Output\$config"
+
+ $depjson = Get-Content $target\Flow.Launcher.deps.json -raw
+ $depjson -replace '(?s)(.createdump.exe": {.*?}.*?\n)\s*', "" | Out-File $target\Flow.Launcher.deps.json -Encoding UTF8
+ Remove-Item -Path $target -Include "*createdump.exe" -Recurse
+}
+
+
function Validate-Directory ($output) {
New-Item $output -ItemType Directory -Force
}
+
function Pack-Squirrel-Installer ($path, $version, $output) {
# msbuild based installer generation is not working in appveyor, not sure why
Write-Host "Begin pack squirrel installer"
@@ -90,7 +100,7 @@ function Pack-Squirrel-Installer ($path, $version, $output) {
function Publish-Self-Contained ($p) {
$csproj = Join-Path "$p" "Flow.Launcher/Flow.Launcher.csproj" -Resolve
- $profile = Join-Path "$p" "Flow.Launcher/Properties/PublishProfiles/NetCore3.1-SelfContained.pubxml" -Resolve
+ $profile = Join-Path "$p" "Flow.Launcher/Properties/PublishProfiles/Net5.0-SelfContained.pubxml" -Resolve
# we call dotnet publish on the main project.
# The other projects should have been built in Release at this point.
@@ -108,10 +118,12 @@ function Main {
Publish-Self-Contained $p
+ Remove-CreateDumpExe $p $config
+
$o = "$p\Output\Packages"
Validate-Directory $o
Pack-Squirrel-Installer $p $v $o
}
}
-Main
\ No newline at end of file
+Main
diff --git a/SolutionAssemblyInfo.cs b/SolutionAssemblyInfo.cs
index bcddbd1bd..1482bce9e 100644
--- a/SolutionAssemblyInfo.cs
+++ b/SolutionAssemblyInfo.cs
@@ -16,6 +16,6 @@ using System.Runtime.InteropServices;
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
-[assembly: AssemblyVersion("1.7.2")]
-[assembly: AssemblyFileVersion("1.7.2")]
-[assembly: AssemblyInformationalVersion("1.7.2")]
\ No newline at end of file
+[assembly: AssemblyVersion("1.0.0")]
+[assembly: AssemblyFileVersion("1.0.0")]
+[assembly: AssemblyInformationalVersion("1.0.0")]
\ No newline at end of file
diff --git a/appveyor.yml b/appveyor.yml
index 299a829ba..9961a9111 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: '1.7.2.{build}'
+version: '1.8.0.{build}'
init:
- ps: |
@@ -14,6 +14,8 @@ assembly_info:
assembly_file_version: $(flowVersion)
assembly_informational_version: $(flowVersion)
+skip_branch_with_pr: true
+
skip_commits:
files:
- '*.md'
@@ -64,4 +66,4 @@ deploy:
artifact: Squirrel Installer, Squirrel nupkg, Squirrel RELEASES
force_update: true
on:
- APPVEYOR_REPO_TAG: true
\ No newline at end of file
+ APPVEYOR_REPO_TAG: true
diff --git a/global.json b/global.json
index c3efffd40..2ad4d9100 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "3.1.201",
+ "version": "5.0.100",
"rollForward": "latestFeature"
}
}
\ No newline at end of file