Merge pull request #363 from Flow-Launcher/dev
Release 1.8.0 | Plugin 2.0.0
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 409 B After Width: | Height: | Size: 409 B |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 663 B After Width: | Height: | Size: 663 B |
|
Before Width: | Height: | Size: 1,019 B After Width: | Height: | Size: 1,019 B |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>net5.0-windows</TargetFramework>
|
||||||
<UseWpf>true</UseWpf>
|
<UseWpf>true</UseWpf>
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
<DebugSymbols>true</DebugSymbols>
|
<DebugSymbols>true</DebugSymbols>
|
||||||
<DebugType>full</DebugType>
|
<DebugType>portable</DebugType>
|
||||||
<Optimize>false</Optimize>
|
<Optimize>false</Optimize>
|
||||||
<OutputPath>..\Output\Debug\</OutputPath>
|
<OutputPath>..\Output\Debug\</OutputPath>
|
||||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
|
@ -53,14 +53,11 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Droplex" Version="1.3.1" />
|
||||||
<PackageReference Include="FSharp.Core" Version="4.7.1" />
|
<PackageReference Include="FSharp.Core" Version="4.7.1" />
|
||||||
<PackageReference Include="squirrel.windows" Version="1.5.2" />
|
<PackageReference Include="squirrel.windows" Version="1.5.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Properties\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
|
<ProjectReference Include="..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
|
||||||
<ProjectReference Include="..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
|
<ProjectReference Include="..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.Plugin
|
namespace Flow.Launcher.Core.Plugin
|
||||||
|
|
@ -21,17 +24,17 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string ExecuteQuery(Query query)
|
protected override Task<Stream> ExecuteQueryAsync(Query query, CancellationToken token)
|
||||||
{
|
{
|
||||||
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel
|
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel
|
||||||
{
|
{
|
||||||
Method = "query",
|
Method = "query",
|
||||||
Parameters = new object[] { query.Search },
|
Parameters = new object[] {query.Search},
|
||||||
};
|
};
|
||||||
|
|
||||||
_startInfo.Arguments = $"\"{request}\"";
|
_startInfo.Arguments = $"\"{request}\"";
|
||||||
|
|
||||||
return Execute(_startInfo);
|
return ExecuteAsync(_startInfo, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest)
|
protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest)
|
||||||
|
|
@ -40,10 +43,12 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
return Execute(_startInfo);
|
return Execute(_startInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string ExecuteContextMenu(Result selectedResult) {
|
protected override string ExecuteContextMenu(Result selectedResult)
|
||||||
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel {
|
{
|
||||||
|
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel
|
||||||
|
{
|
||||||
Method = "contextmenu",
|
Method = "contextmenu",
|
||||||
Parameters = new object[] { selectedResult.ContextData },
|
Parameters = new object[] {selectedResult.ContextData},
|
||||||
};
|
};
|
||||||
|
|
||||||
_startInfo.Arguments = $"\"{request}\"";
|
_startInfo.Arguments = $"\"{request}\"";
|
||||||
|
|
|
||||||
|
|
@ -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
|
* 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.
|
* "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.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.Plugin
|
namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
|
|
@ -29,12 +30,8 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
public string Data { get; set; }
|
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; }
|
public string Result { get; set; }
|
||||||
|
|
||||||
|
|
@ -45,57 +42,23 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
[JsonPropertyName("result")]
|
[JsonPropertyName("result")]
|
||||||
public new List<JsonRPCResult> Result { get; set; }
|
public new List<JsonRPCResult> Result { get; set; }
|
||||||
}
|
|
||||||
|
|
||||||
public class JsonRPCRequestModel : JsonRPCModelBase
|
public string DebugMessage { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class JsonRPCRequestModel
|
||||||
{
|
{
|
||||||
public string Method { get; set; }
|
public string Method { get; set; }
|
||||||
|
|
||||||
public object[] Parameters { get; set; }
|
public object[] Parameters { get; set; }
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions options = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
};
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
string rpc = string.Empty;
|
return JsonSerializer.Serialize(this, options);
|
||||||
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(@"""", @"\\""""");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,11 +67,7 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class JsonRPCServerRequestModel : JsonRPCRequestModel
|
public class JsonRPCServerRequestModel : JsonRPCRequestModel
|
||||||
{
|
{
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
string rpc = base.ToString();
|
|
||||||
return rpc + "}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -117,12 +76,6 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
public class JsonRPCClientRequestModel : JsonRPCRequestModel
|
public class JsonRPCClientRequestModel : JsonRPCRequestModel
|
||||||
{
|
{
|
||||||
public bool DontHideAfterAction { get; set; }
|
public bool DontHideAfterAction { get; set; }
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
string rpc = base.ToString();
|
|
||||||
return rpc + "}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -134,4 +87,4 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
public JsonRPCClientRequestModel JsonRPCAction { get; set; }
|
public JsonRPCClientRequestModel JsonRPCAction { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
using System;
|
using Flow.Launcher.Core.Resource;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using Flow.Launcher.Infrastructure.Exception;
|
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.Plugin
|
namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
|
|
@ -17,7 +19,7 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
/// Represent the plugin that using JsonPRC
|
/// Represent the plugin that using JsonPRC
|
||||||
/// every JsonRPC plugin should has its own plugin instance
|
/// every JsonRPC plugin should has its own plugin instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal abstract class JsonRPCPlugin : IPlugin, IContextMenu
|
internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu
|
||||||
{
|
{
|
||||||
protected PluginInitContext context;
|
protected PluginInitContext context;
|
||||||
public const string JsonRPC = "JsonRPC";
|
public const string JsonRPC = "JsonRPC";
|
||||||
|
|
@ -27,24 +29,10 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract string SupportedLanguage { get; set; }
|
public abstract string SupportedLanguage { get; set; }
|
||||||
|
|
||||||
protected abstract string ExecuteQuery(Query query);
|
protected abstract Task<Stream> ExecuteQueryAsync(Query query, CancellationToken token);
|
||||||
protected abstract string ExecuteCallback(JsonRPCRequestModel rpcRequest);
|
protected abstract string ExecuteCallback(JsonRPCRequestModel rpcRequest);
|
||||||
protected abstract string ExecuteContextMenu(Result selectedResult);
|
protected abstract string ExecuteContextMenu(Result selectedResult);
|
||||||
|
|
||||||
public List<Result> 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<Result> LoadContextMenus(Result selectedResult)
|
public List<Result> LoadContextMenus(Result selectedResult)
|
||||||
{
|
{
|
||||||
string output = ExecuteContextMenu(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<List<Result>> DeserializedResultAsync(Stream output)
|
||||||
|
{
|
||||||
|
if (output == Stream.Null) return null;
|
||||||
|
|
||||||
|
var queryResponseModel = await
|
||||||
|
JsonSerializer.DeserializeAsync<JsonRPCQueryResponseModel>(output, options);
|
||||||
|
|
||||||
|
await output.DisposeAsync();
|
||||||
|
|
||||||
|
return ParseResults(queryResponseModel);
|
||||||
|
}
|
||||||
|
|
||||||
private List<Result> DeserializedResult(string output)
|
private List<Result> DeserializedResult(string output)
|
||||||
{
|
{
|
||||||
if (!String.IsNullOrEmpty(output))
|
if (string.IsNullOrEmpty(output)) return null;
|
||||||
|
|
||||||
|
var queryResponseModel =
|
||||||
|
JsonSerializer.Deserialize<JsonRPCQueryResponseModel>(output, options);
|
||||||
|
return ParseResults(queryResponseModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private List<Result> ParseResults(JsonRPCQueryResponseModel queryResponseModel)
|
||||||
|
{
|
||||||
|
var results = new List<Result>();
|
||||||
|
if (queryResponseModel.Result == null) return null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(queryResponseModel.DebugMessage))
|
||||||
{
|
{
|
||||||
List<Result> results = new List<Result>();
|
context.API.ShowMsg(queryResponseModel.DebugMessage);
|
||||||
|
}
|
||||||
|
|
||||||
JsonRPCQueryResponseModel queryResponseModel = JsonSerializer.Deserialize<JsonRPCQueryResponseModel>(output);
|
foreach (JsonRPCResult result in queryResponseModel.Result)
|
||||||
if (queryResponseModel.Result == null) return null;
|
{
|
||||||
|
result.Action = c =>
|
||||||
foreach (JsonRPCResult result in queryResponseModel.Result)
|
|
||||||
{
|
{
|
||||||
JsonRPCResult result1 = result;
|
if (result.JsonRPCAction == null) return false;
|
||||||
result.Action = c =>
|
|
||||||
{
|
|
||||||
if (result1.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."))
|
return !result.JsonRPCAction.DontHideAfterAction;
|
||||||
{
|
|
||||||
ExecuteFlowLauncherAPI(result1.JsonRPCAction.Method.Substring(4), result1.JsonRPCAction.Parameters);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
string actionReponse = ExecuteCallback(result1.JsonRPCAction);
|
|
||||||
JsonRPCRequestModel jsonRpcRequestModel = JsonSerializer.Deserialize<JsonRPCRequestModel>(actionReponse);
|
|
||||||
if (jsonRpcRequestModel != null
|
|
||||||
&& !String.IsNullOrEmpty(jsonRpcRequestModel.Method)
|
|
||||||
&& jsonRpcRequestModel.Method.StartsWith("Flow.Launcher."))
|
|
||||||
{
|
|
||||||
ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method.Substring(4), jsonRpcRequestModel.Parameters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return !result1.JsonRPCAction.DontHideAfterAction;
|
|
||||||
};
|
var jsonRpcRequestModel = JsonSerializer.Deserialize<JsonRPCRequestModel>(actionResponse, options);
|
||||||
results.Add(result);
|
|
||||||
}
|
if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false)
|
||||||
return results;
|
{
|
||||||
}
|
ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..],
|
||||||
else
|
jsonRpcRequestModel.Parameters);
|
||||||
{
|
}
|
||||||
return null;
|
}
|
||||||
|
|
||||||
|
return !result.JsonRPCAction.DontHideAfterAction;
|
||||||
|
};
|
||||||
|
results.Add(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteFlowLauncherAPI(string method, object[] parameters)
|
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)
|
if (methodInfo != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -117,9 +144,7 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
#if (DEBUG)
|
#if (DEBUG)
|
||||||
{
|
throw;
|
||||||
throw;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -130,17 +155,20 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="fileName"></param>
|
/// <param name="fileName"></param>
|
||||||
/// <param name="arguments"></param>
|
/// <param name="arguments"></param>
|
||||||
|
/// <param name="token">Cancellation Token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected string Execute(string fileName, string arguments)
|
protected Task<Stream> ExecuteAsync(string fileName, string arguments, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
ProcessStartInfo start = new ProcessStartInfo();
|
ProcessStartInfo start = new ProcessStartInfo
|
||||||
start.FileName = fileName;
|
{
|
||||||
start.Arguments = arguments;
|
FileName = fileName,
|
||||||
start.UseShellExecute = false;
|
Arguments = arguments,
|
||||||
start.CreateNoWindow = true;
|
UseShellExecute = false,
|
||||||
start.RedirectStandardOutput = true;
|
CreateNoWindow = true,
|
||||||
start.RedirectStandardError = true;
|
RedirectStandardOutput = true,
|
||||||
return Execute(start);
|
RedirectStandardError = true
|
||||||
|
};
|
||||||
|
return ExecuteAsync(start, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected string Execute(ProcessStartInfo startInfo)
|
protected string Execute(ProcessStartInfo startInfo)
|
||||||
|
|
@ -148,52 +176,104 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var process = Process.Start(startInfo);
|
using var process = Process.Start(startInfo);
|
||||||
if (process == null)
|
if (process == null) return string.Empty;
|
||||||
{
|
|
||||||
Log.Error("|JsonRPCPlugin.Execute|Can't start new process");
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var standardOutput = process.StandardOutput;
|
using var standardOutput = process.StandardOutput;
|
||||||
var result = standardOutput.ReadToEnd();
|
var result = standardOutput.ReadToEnd();
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(result))
|
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();
|
Log.Error($"|JsonRPCPlugin.Execute|{error}");
|
||||||
if (!string.IsNullOrEmpty(error))
|
return string.Empty;
|
||||||
{
|
|
||||||
Log.Error($"|JsonRPCPlugin.Execute|{error}");
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Log.Error("|JsonRPCPlugin.Execute|Empty standard output and standard error.");
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else if (result.StartsWith("DEBUG:"))
|
Log.Error("|JsonRPCPlugin.Execute|Empty standard output and standard error.");
|
||||||
{
|
|
||||||
MessageBox.Show(new Form { TopMost = true }, result.Substring(6));
|
|
||||||
return string.Empty;
|
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)
|
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;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Init(PluginInitContext ctx)
|
protected async Task<Stream> 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<List<Result>> 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
using Flow.Launcher.Infrastructure;
|
using Flow.Launcher.Infrastructure;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
@ -11,17 +13,26 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
private readonly AssemblyDependencyResolver dependencyResolver;
|
private readonly AssemblyDependencyResolver dependencyResolver;
|
||||||
|
|
||||||
private readonly AssemblyDependencyResolver referencedPluginPackageDependencyResolver;
|
|
||||||
|
|
||||||
private readonly AssemblyName assemblyName;
|
private readonly AssemblyName assemblyName;
|
||||||
|
|
||||||
|
private static readonly ConcurrentDictionary<string, byte> loadedAssembly;
|
||||||
|
|
||||||
|
static PluginAssemblyLoader()
|
||||||
|
{
|
||||||
|
var currentAssemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||||
|
loadedAssembly = new ConcurrentDictionary<string, byte>(
|
||||||
|
currentAssemblies.Select(x => new KeyValuePair<string, byte>(x.FullName, default)));
|
||||||
|
|
||||||
|
AppDomain.CurrentDomain.AssemblyLoad += (sender, args) =>
|
||||||
|
{
|
||||||
|
loadedAssembly[args.LoadedAssembly.FullName] = default;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
internal PluginAssemblyLoader(string assemblyFilePath)
|
internal PluginAssemblyLoader(string assemblyFilePath)
|
||||||
{
|
{
|
||||||
dependencyResolver = new AssemblyDependencyResolver(assemblyFilePath);
|
dependencyResolver = new AssemblyDependencyResolver(assemblyFilePath);
|
||||||
assemblyName = new AssemblyName(Path.GetFileNameWithoutExtension(assemblyFilePath));
|
assemblyName = new AssemblyName(Path.GetFileNameWithoutExtension(assemblyFilePath));
|
||||||
|
|
||||||
referencedPluginPackageDependencyResolver =
|
|
||||||
new AssemblyDependencyResolver(Path.Combine(Constant.ProgramDirectory, "Flow.Launcher.Plugin.dll"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal Assembly LoadAssemblyAndDependencies()
|
internal Assembly LoadAssemblyAndDependencies()
|
||||||
|
|
@ -33,25 +44,24 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
string assemblyPath = dependencyResolver.ResolveAssemblyToPath(assemblyName);
|
string assemblyPath = dependencyResolver.ResolveAssemblyToPath(assemblyName);
|
||||||
|
|
||||||
// When resolving dependencies, ignore assembly depenedencies that already exits with Flow.Launcher.Plugin
|
// When resolving dependencies, ignore assembly depenedencies that already exits with Flow.Launcher
|
||||||
// Otherwise will get unexpected behaviour with plugins, e.g. JsonIgnore attribute not honored in WebSearch or other plugins
|
// Otherwise duplicate assembly will be loaded and some weird behavior will occur, such as WinRT.Runtime.dll
|
||||||
// that use Newtonsoft.Json
|
// will fail due to loading multiple versions in process, each with their own static instance of registration state
|
||||||
if (assemblyPath == null || ExistsInReferencedPluginPackage(assemblyName))
|
if (assemblyPath == null || ExistsInReferencedPackage(assemblyName))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return LoadFromAssemblyPath(assemblyPath);
|
return LoadFromAssemblyPath(assemblyPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal Type FromAssemblyGetTypeOfInterface(Assembly assembly, params Type[] types)
|
internal Type FromAssemblyGetTypeOfInterface(Assembly assembly, Type type)
|
||||||
{
|
{
|
||||||
var allTypes = assembly.ExportedTypes;
|
var allTypes = assembly.ExportedTypes;
|
||||||
|
return allTypes.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Any(t => t == type));
|
||||||
return allTypes.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Intersect(types).Any());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool ExistsInReferencedPluginPackage(AssemblyName assemblyName)
|
internal bool ExistsInReferencedPackage(AssemblyName assemblyName)
|
||||||
{
|
{
|
||||||
return referencedPluginPackageDependencyResolver.ResolveAssemblyToPath(assemblyName) != null;
|
return loadedAssembly.ContainsKey(assemblyName.FullName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -20,7 +20,7 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
public static List<PluginMetadata> Parse(string[] pluginDirectories)
|
public static List<PluginMetadata> Parse(string[] pluginDirectories)
|
||||||
{
|
{
|
||||||
var allPluginMetadata = new List<PluginMetadata>();
|
var allPluginMetadata = new List<PluginMetadata>();
|
||||||
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
|
// todo use linq when diable plugin is implmented since parallel.foreach + list is not thread saft
|
||||||
foreach (var directory in directories)
|
foreach (var directory in directories)
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Flow.Launcher.Infrastructure;
|
using Flow.Launcher.Infrastructure;
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
using Flow.Launcher.Infrastructure.Storage;
|
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
|
using ISavable = Flow.Launcher.Plugin.ISavable;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.Plugin
|
namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
|
|
@ -21,8 +21,8 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
private static IEnumerable<PluginPair> _contextMenuPlugins;
|
private static IEnumerable<PluginPair> _contextMenuPlugins;
|
||||||
|
|
||||||
public static List<PluginPair> AllPlugins { get; private set; }
|
public static List<PluginPair> AllPlugins { get; private set; }
|
||||||
public static readonly List<PluginPair> GlobalPlugins = new List<PluginPair>();
|
public static readonly HashSet<PluginPair> GlobalPlugins = new();
|
||||||
public static readonly Dictionary<string, PluginPair> NonGlobalPlugins = new Dictionary<string, PluginPair>();
|
public static readonly Dictionary<string, PluginPair> NonGlobalPlugins = new();
|
||||||
|
|
||||||
public static IPublicAPI API { private set; get; }
|
public static IPublicAPI API { private set; get; }
|
||||||
|
|
||||||
|
|
@ -33,7 +33,10 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Directories that will hold Flow Launcher plugin directory
|
/// Directories that will hold Flow Launcher plugin directory
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly string[] Directories = { Constant.PreinstalledDirectory, DataLocation.PluginsDirectory };
|
private static readonly string[] Directories =
|
||||||
|
{
|
||||||
|
Constant.PreinstalledDirectory, DataLocation.PluginsDirectory
|
||||||
|
};
|
||||||
|
|
||||||
private static void DeletePythonBinding()
|
private static void DeletePythonBinding()
|
||||||
{
|
{
|
||||||
|
|
@ -51,6 +54,24 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
var savable = plugin.Plugin as ISavable;
|
var savable = plugin.Plugin as ISavable;
|
||||||
savable?.Save();
|
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()
|
public static async Task ReloadData()
|
||||||
|
|
@ -97,16 +118,9 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var milliseconds = pair.Plugin switch
|
var milliseconds = await Stopwatch.DebugAsync($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>",
|
||||||
{
|
() => pair.Plugin.InitAsync(new PluginInitContext(pair.Metadata, API)));
|
||||||
IAsyncPlugin plugin
|
|
||||||
=> await Stopwatch.DebugAsync($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>",
|
|
||||||
() => plugin.InitAsync(new PluginInitContext(pair.Metadata, API))),
|
|
||||||
IPlugin plugin
|
|
||||||
=> Stopwatch.Debug($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>",
|
|
||||||
() => plugin.Init(new PluginInitContext(pair.Metadata, API))),
|
|
||||||
_ => throw new ArgumentException(),
|
|
||||||
};
|
|
||||||
pair.Metadata.InitTime += milliseconds;
|
pair.Metadata.InitTime += milliseconds;
|
||||||
Log.Info(
|
Log.Info(
|
||||||
$"|PluginManager.InitializePlugins|Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>");
|
$"|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<IContextMenu>();
|
_contextMenuPlugins = GetPluginsForInterface<IContextMenu>();
|
||||||
foreach (var plugin in AllPlugins)
|
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)
|
switch (actionKeyword)
|
||||||
{
|
{
|
||||||
|
|
@ -147,12 +163,15 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<PluginPair> ValidPluginsForQuery(Query query)
|
public static ICollection<PluginPair> ValidPluginsForQuery(Query query)
|
||||||
{
|
{
|
||||||
if (NonGlobalPlugins.ContainsKey(query.ActionKeyword))
|
if (NonGlobalPlugins.ContainsKey(query.ActionKeyword))
|
||||||
{
|
{
|
||||||
var plugin = NonGlobalPlugins[query.ActionKeyword];
|
var plugin = NonGlobalPlugins[query.ActionKeyword];
|
||||||
return new List<PluginPair> { plugin };
|
return new List<PluginPair>
|
||||||
|
{
|
||||||
|
plugin
|
||||||
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -160,7 +179,7 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<List<Result>> QueryForPlugin(PluginPair pair, Query query, CancellationToken token)
|
public static async Task<List<Result>> QueryForPluginAsync(PluginPair pair, Query query, CancellationToken token)
|
||||||
{
|
{
|
||||||
var results = new List<Result>();
|
var results = new List<Result>();
|
||||||
try
|
try
|
||||||
|
|
@ -169,22 +188,12 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
|
|
||||||
long milliseconds = -1L;
|
long milliseconds = -1L;
|
||||||
|
|
||||||
switch (pair.Plugin)
|
milliseconds = await Stopwatch.DebugAsync($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}",
|
||||||
{
|
async () => results = await pair.Plugin.QueryAsync(query, token).ConfigureAwait(false));
|
||||||
case IAsyncPlugin plugin:
|
|
||||||
milliseconds = await Stopwatch.DebugAsync($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}",
|
|
||||||
async () => results = await plugin.QueryAsync(query, token).ConfigureAwait(false));
|
|
||||||
break;
|
|
||||||
case IPlugin plugin:
|
|
||||||
await Task.Run(() => milliseconds = Stopwatch.Debug($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}",
|
|
||||||
() => results = plugin.Query(query)), token).ConfigureAwait(false);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new ArgumentOutOfRangeException();
|
|
||||||
}
|
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
if (results == null)
|
if (results == null)
|
||||||
return results;
|
return null;
|
||||||
UpdatePluginMetadata(results, metadata, query);
|
UpdatePluginMetadata(results, metadata, query);
|
||||||
|
|
||||||
metadata.QueryCount += 1;
|
metadata.QueryCount += 1;
|
||||||
|
|
@ -195,11 +204,7 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
// null will be fine since the results will only be added into queue if the token hasn't been cancelled
|
// null will be fine since the results will only be added into queue if the token hasn't been cancelled
|
||||||
return results = null;
|
return null;
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Log.Exception($"|PluginManager.QueryForPlugin|Exception for plugin <{pair.Metadata.Name}> when query <{query}>", e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
|
|
@ -245,7 +250,7 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
results = plugin.LoadContextMenus(result);
|
results = plugin.LoadContextMenus(result) ?? results;
|
||||||
foreach (var r in results)
|
foreach (var r in results)
|
||||||
{
|
{
|
||||||
r.PluginDirectory = pluginPair.Metadata.PluginDirectory;
|
r.PluginDirectory = pluginPair.Metadata.PluginDirectory;
|
||||||
|
|
@ -266,6 +271,8 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
|
|
||||||
public static bool ActionKeywordRegistered(string actionKeyword)
|
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
|
return actionKeyword != Query.GlobalPluginWildcardSign
|
||||||
&& NonGlobalPlugins.ContainsKey(actionKeyword);
|
&& NonGlobalPlugins.ContainsKey(actionKeyword);
|
||||||
}
|
}
|
||||||
|
|
@ -290,7 +297,7 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// used to add action keyword for multiple action keyword plugin
|
/// used to remove action keyword for multiple action keyword plugin
|
||||||
/// e.g. web search
|
/// e.g. web search
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void RemoveActionKeyword(string id, string oldActionkeyword)
|
public static void RemoveActionKeyword(string id, string oldActionkeyword)
|
||||||
|
|
@ -299,9 +306,7 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
if (oldActionkeyword == Query.GlobalPluginWildcardSign
|
if (oldActionkeyword == Query.GlobalPluginWildcardSign
|
||||||
&& // Plugins may have multiple ActionKeywords that are global, eg. WebSearch
|
&& // Plugins may have multiple ActionKeywords that are global, eg. WebSearch
|
||||||
plugin.Metadata.ActionKeywords
|
plugin.Metadata.ActionKeywords
|
||||||
.Where(x => x == Query.GlobalPluginWildcardSign)
|
.Count(x => x == Query.GlobalPluginWildcardSign) == 1)
|
||||||
.ToList()
|
|
||||||
.Count == 1)
|
|
||||||
{
|
{
|
||||||
GlobalPlugins.Remove(plugin);
|
GlobalPlugins.Remove(plugin);
|
||||||
}
|
}
|
||||||
|
|
@ -322,4 +327,4 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,26 +3,27 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.Loader;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
using Droplex;
|
||||||
using Flow.Launcher.Infrastructure;
|
using Flow.Launcher.Infrastructure;
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
|
using Flow.Launcher.Plugin.SharedCommands;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.Plugin
|
namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
public static class PluginsLoader
|
public static class PluginsLoader
|
||||||
{
|
{
|
||||||
public const string PATH = "PATH";
|
|
||||||
public const string Python = "python";
|
|
||||||
public const string PythonExecutable = "pythonw.exe";
|
public const string PythonExecutable = "pythonw.exe";
|
||||||
|
|
||||||
public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings)
|
public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings)
|
||||||
{
|
{
|
||||||
var dotnetPlugins = DotNetPlugins(metadatas);
|
var dotnetPlugins = DotNetPlugins(metadatas);
|
||||||
var pythonPlugins = PythonPlugins(metadatas, settings.PythonDirectory);
|
var pythonPlugins = PythonPlugins(metadatas, settings);
|
||||||
var executablePlugins = ExecutablePlugins(metadatas);
|
var executablePlugins = ExecutablePlugins(metadatas);
|
||||||
var plugins = dotnetPlugins.Concat(pythonPlugins).Concat(executablePlugins).ToList();
|
var plugins = dotnetPlugins.Concat(pythonPlugins).Concat(executablePlugins).ToList();
|
||||||
return plugins;
|
return plugins;
|
||||||
|
|
@ -43,23 +44,23 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
|
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
|
||||||
var assembly = assemblyLoader.LoadAssemblyAndDependencies();
|
var assembly = assemblyLoader.LoadAssemblyAndDependencies();
|
||||||
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, typeof(IPlugin),
|
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly,
|
||||||
typeof(IAsyncPlugin));
|
typeof(IAsyncPlugin));
|
||||||
|
|
||||||
var plugin = Activator.CreateInstance(type);
|
var plugin = Activator.CreateInstance(type) as IAsyncPlugin;
|
||||||
#else
|
#else
|
||||||
Assembly assembly = null;
|
Assembly assembly = null;
|
||||||
object plugin = null;
|
IAsyncPlugin plugin = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
|
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
|
||||||
assembly = assemblyLoader.LoadAssemblyAndDependencies();
|
assembly = assemblyLoader.LoadAssemblyAndDependencies();
|
||||||
|
|
||||||
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, typeof(IPlugin),
|
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly,
|
||||||
typeof(IAsyncPlugin));
|
typeof(IAsyncPlugin));
|
||||||
|
|
||||||
plugin = Activator.CreateInstance(type);
|
plugin = Activator.CreateInstance(type) as IAsyncPlugin;
|
||||||
}
|
}
|
||||||
catch (Exception e) when (assembly == null)
|
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);
|
Log.Exception($"|PluginsLoader.DotNetPlugins|The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
if (plugin == null)
|
if (plugin == null)
|
||||||
{
|
{
|
||||||
erroredPlugins.Add(metadata.Name);
|
erroredPlugins.Add(metadata.Name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
plugins.Add(new PluginPair
|
plugins.Add(new PluginPair {Plugin = plugin, Metadata = metadata});
|
||||||
{
|
|
||||||
Plugin = plugin,
|
|
||||||
Metadata = metadata
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
metadata.InitTime += milliseconds;
|
metadata.InitTime += milliseconds;
|
||||||
}
|
}
|
||||||
|
|
@ -113,73 +110,100 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
return plugins;
|
return plugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<PluginPair> PythonPlugins(List<PluginMetadata> source, string pythonDirectory)
|
public static IEnumerable<PluginPair> PythonPlugins(List<PluginMetadata> source, PluginsSettings settings)
|
||||||
{
|
{
|
||||||
// try to set Constant.PythonPath, either from
|
if (!source.Any(o => o.Language.ToUpper() == AllowedLanguage.Python))
|
||||||
// PATH or from the given pythonDirectory
|
return new List<PluginPair>();
|
||||||
if (string.IsNullOrEmpty(pythonDirectory))
|
|
||||||
{
|
|
||||||
var paths = Environment.GetEnvironmentVariable(PATH);
|
|
||||||
if (paths != null)
|
|
||||||
{
|
|
||||||
var pythonInPath = paths
|
|
||||||
.Split(';')
|
|
||||||
.Where(p => p.ToLower().Contains(Python))
|
|
||||||
.Any();
|
|
||||||
|
|
||||||
if (pythonInPath)
|
if (!string.IsNullOrEmpty(settings.PythonDirectory) && FilesFolders.LocationExists(settings.PythonDirectory))
|
||||||
{
|
return SetPythonPathForPluginPairs(source, Path.Combine(settings.PythonDirectory, PythonExecutable));
|
||||||
Constant.PythonPath = PythonExecutable;
|
|
||||||
}
|
var pythonPath = string.Empty;
|
||||||
else
|
|
||||||
{
|
if (MessageBox.Show("Flow detected you have installed Python plugins, " +
|
||||||
Log.Error("|PluginsLoader.PythonPlugins|Python can't be found in PATH.");
|
"would you like to install Python to run them? " +
|
||||||
}
|
Environment.NewLine + Environment.NewLine +
|
||||||
}
|
"Click no if it's already installed, " +
|
||||||
else
|
"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
|
else
|
||||||
{
|
{
|
||||||
var path = Path.Combine(pythonDirectory, PythonExecutable);
|
var installedPythonDirectory = Path.Combine(DataLocation.DataDirectory(), "PythonEmbeddable");
|
||||||
if (File.Exists(path))
|
|
||||||
|
// 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
|
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,
|
if (string.IsNullOrEmpty(settings.PythonDirectory) || string.IsNullOrEmpty(pythonPath))
|
||||||
// load every python plugin pair.
|
|
||||||
if (String.IsNullOrEmpty(Constant.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<PluginPair>();
|
return new List<PluginPair>();
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
return SetPythonPathForPluginPairs(source, pythonPath);
|
||||||
return source
|
|
||||||
.Where(o => o.Language.ToUpper() == AllowedLanguage.Python)
|
|
||||||
.Select(metadata => new PluginPair
|
|
||||||
{
|
|
||||||
Plugin = new PythonPlugin(Constant.PythonPath),
|
|
||||||
Metadata = metadata
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
|
private static IEnumerable<PluginPair> SetPythonPathForPluginPairs(List<PluginMetadata> 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<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
|
||||||
{
|
{
|
||||||
return source
|
return source
|
||||||
.Where(o => o.Language.ToUpper() == AllowedLanguage.Executable)
|
.Where(o => o.Language.ToUpper() == AllowedLanguage.Executable)
|
||||||
.Select(metadata => new PluginPair
|
.Select(metadata => new PluginPair
|
||||||
{
|
{
|
||||||
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath),
|
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), Metadata = metadata
|
||||||
Metadata = metadata
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Flow.Launcher.Infrastructure;
|
using Flow.Launcher.Infrastructure;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
|
|
||||||
|
|
@ -26,39 +28,52 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
var path = Path.Combine(Constant.ProgramDirectory, JsonRPC);
|
var path = Path.Combine(Constant.ProgramDirectory, JsonRPC);
|
||||||
_startInfo.EnvironmentVariables["PYTHONPATH"] = path;
|
_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<Stream> ExecuteQueryAsync(Query query, CancellationToken token)
|
||||||
{
|
{
|
||||||
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel
|
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel
|
||||||
{
|
{
|
||||||
Method = "query",
|
Method = "query", Parameters = new object[] {query.Search},
|
||||||
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
|
// todo happlebao why context can't be used in constructor
|
||||||
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||||
|
|
||||||
return Execute(_startInfo);
|
return ExecuteAsync(_startInfo, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest)
|
protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest)
|
||||||
{
|
{
|
||||||
_startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{rpcRequest}\"";
|
_startInfo.ArgumentList[2] = rpcRequest.ToString();
|
||||||
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||||
|
// TODO: Async Action
|
||||||
return Execute(_startInfo);
|
return Execute(_startInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string ExecuteContextMenu(Result selectedResult) {
|
protected override string ExecuteContextMenu(Result selectedResult)
|
||||||
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel {
|
{
|
||||||
Method = "context_menu",
|
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel
|
||||||
Parameters = new object[] { selectedResult.ContextData },
|
{
|
||||||
|
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;
|
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||||
|
|
||||||
|
// TODO: Async Action
|
||||||
return Execute(_startInfo);
|
return Execute(_startInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override Task InitAsync(PluginInitContext context)
|
||||||
|
{
|
||||||
|
this.context = context;
|
||||||
|
_startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
|
||||||
|
_startInfo.ArgumentList.Add("");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
3
Flow.Launcher.Core/Properties/AssemblyInfo.cs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("Flow.Launcher.Test")]
|
||||||
42
Flow.Launcher.Core/Resource/JsonObjectConverter.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Core.Resource
|
||||||
|
{
|
||||||
|
public class JsonObjectConverter : JsonConverter<object>
|
||||||
|
{
|
||||||
|
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
switch (reader.TokenType)
|
||||||
|
{
|
||||||
|
case JsonTokenType.True:
|
||||||
|
return true;
|
||||||
|
case JsonTokenType.False:
|
||||||
|
return false;
|
||||||
|
case JsonTokenType.Number when reader.TryGetInt32(out var i):
|
||||||
|
return i;
|
||||||
|
case JsonTokenType.Number when reader.TryGetInt64(out var l):
|
||||||
|
return l;
|
||||||
|
case JsonTokenType.Number:
|
||||||
|
return reader.GetDouble();
|
||||||
|
case JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime):
|
||||||
|
return datetime;
|
||||||
|
case JsonTokenType.String:
|
||||||
|
return reader.GetString();
|
||||||
|
default:
|
||||||
|
// Use JsonElement as fallback.
|
||||||
|
// Newtonsoft uses JArray or JObject.
|
||||||
|
using (var document = JsonDocument.ParseValue(ref reader))
|
||||||
|
{
|
||||||
|
return document.RootElement.Clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Should not get here.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,8 @@ namespace Flow.Launcher.Core.Resource
|
||||||
{
|
{
|
||||||
public class Theme
|
public class Theme
|
||||||
{
|
{
|
||||||
|
private const int ShadowExtraMargin = 12;
|
||||||
|
|
||||||
private readonly List<string> _themeDirectories = new List<string>();
|
private readonly List<string> _themeDirectories = new List<string>();
|
||||||
private ResourceDictionary _oldResource;
|
private ResourceDictionary _oldResource;
|
||||||
private string _oldTheme;
|
private string _oldTheme;
|
||||||
|
|
@ -26,6 +28,10 @@ namespace Flow.Launcher.Core.Resource
|
||||||
private string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder);
|
private string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder);
|
||||||
private string UserDirectoryPath => Path.Combine(DataLocation.DataDirectory(), Folder);
|
private string UserDirectoryPath => Path.Combine(DataLocation.DataDirectory(), Folder);
|
||||||
|
|
||||||
|
public bool BlurEnabled { get; set; }
|
||||||
|
|
||||||
|
private double mainWindowWidth;
|
||||||
|
|
||||||
public Theme()
|
public Theme()
|
||||||
{
|
{
|
||||||
_themeDirectories.Add(DirectoryPath);
|
_themeDirectories.Add(DirectoryPath);
|
||||||
|
|
@ -86,8 +92,12 @@ namespace Flow.Launcher.Core.Resource
|
||||||
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
|
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Settings.UseDropShadowEffect)
|
BlurEnabled = IsBlurTheme();
|
||||||
|
|
||||||
|
if (Settings.UseDropShadowEffect && !BlurEnabled)
|
||||||
AddDropShadowEffectToCurrentTheme();
|
AddDropShadowEffectToCurrentTheme();
|
||||||
|
|
||||||
|
SetBlurForWindow();
|
||||||
}
|
}
|
||||||
catch (DirectoryNotFoundException e)
|
catch (DirectoryNotFoundException e)
|
||||||
{
|
{
|
||||||
|
|
@ -179,6 +189,21 @@ namespace Flow.Launcher.Core.Resource
|
||||||
Array.ForEach(new[] { resultItemStyle, resultSubItemStyle, resultItemSelectedStyle, resultSubItemSelectedStyle }, o => Array.ForEach(setters, p => o.Setters.Add(p)));
|
Array.ForEach(new[] { resultItemStyle, resultSubItemStyle, resultItemSelectedStyle, resultSubItemSelectedStyle }, o => Array.ForEach(setters, p => o.Setters.Add(p)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var windowStyle = dict["WindowStyle"] as Style;
|
||||||
|
|
||||||
|
var width = windowStyle?.Setters.OfType<Setter>().Where(x => x.Property.Name == "Width")
|
||||||
|
.Select(x => x.Value).FirstOrDefault();
|
||||||
|
|
||||||
|
if (width == null)
|
||||||
|
{
|
||||||
|
windowStyle = dict["BaseWindowStyle"] as Style;
|
||||||
|
|
||||||
|
width = windowStyle?.Setters.OfType<Setter>().Where(x => x.Property.Name == "Width")
|
||||||
|
.Select(x => x.Value).FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWindowWidth = (double)width;
|
||||||
|
|
||||||
return dict;
|
return dict;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -224,17 +249,54 @@ namespace Flow.Launcher.Core.Resource
|
||||||
BlurRadius = 15
|
BlurRadius = 15
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter;
|
||||||
|
if (marginSetter == null)
|
||||||
|
{
|
||||||
|
marginSetter = new Setter()
|
||||||
|
{
|
||||||
|
Property = Border.MarginProperty,
|
||||||
|
Value = new Thickness(ShadowExtraMargin),
|
||||||
|
};
|
||||||
|
windowBorderStyle.Setters.Add(marginSetter);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var baseMargin = (Thickness) marginSetter.Value;
|
||||||
|
var newMargin = new Thickness(
|
||||||
|
baseMargin.Left + ShadowExtraMargin,
|
||||||
|
baseMargin.Top + ShadowExtraMargin,
|
||||||
|
baseMargin.Right + ShadowExtraMargin,
|
||||||
|
baseMargin.Bottom + ShadowExtraMargin);
|
||||||
|
marginSetter.Value = newMargin;
|
||||||
|
}
|
||||||
|
|
||||||
windowBorderStyle.Setters.Add(effectSetter);
|
windowBorderStyle.Setters.Add(effectSetter);
|
||||||
|
|
||||||
UpdateResourceDictionary(dict);
|
UpdateResourceDictionary(dict);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveDropShadowEffectToCurrentTheme()
|
public void RemoveDropShadowEffectFromCurrentTheme()
|
||||||
{
|
{
|
||||||
var dict = CurrentThemeResourceDictionary();
|
var dict = CurrentThemeResourceDictionary();
|
||||||
var windowBorderStyle = dict["WindowBorderStyle"] as Style;
|
var windowBorderStyle = dict["WindowBorderStyle"] as Style;
|
||||||
|
|
||||||
dict.Remove(Border.EffectProperty);
|
var effectSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.EffectProperty) as Setter;
|
||||||
|
var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter;
|
||||||
|
|
||||||
|
if(effectSetter != null)
|
||||||
|
{
|
||||||
|
windowBorderStyle.Setters.Remove(effectSetter);
|
||||||
|
}
|
||||||
|
if (marginSetter != null)
|
||||||
|
{
|
||||||
|
var currentMargin = (Thickness)marginSetter.Value;
|
||||||
|
var newMargin = new Thickness(
|
||||||
|
currentMargin.Left - ShadowExtraMargin,
|
||||||
|
currentMargin.Top - ShadowExtraMargin,
|
||||||
|
currentMargin.Right - ShadowExtraMargin,
|
||||||
|
currentMargin.Bottom - ShadowExtraMargin);
|
||||||
|
marginSetter.Value = newMargin;
|
||||||
|
}
|
||||||
|
|
||||||
UpdateResourceDictionary(dict);
|
UpdateResourceDictionary(dict);
|
||||||
}
|
}
|
||||||
|
|
@ -281,35 +343,39 @@ namespace Flow.Launcher.Core.Resource
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SetBlurForWindow()
|
public void SetBlurForWindow()
|
||||||
{
|
{
|
||||||
|
if (BlurEnabled)
|
||||||
|
{
|
||||||
|
SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_ENABLE_BLURBEHIND);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_DISABLED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Exception of FindResource can't be cathed if global exception handle is set
|
private bool IsBlurTheme()
|
||||||
|
{
|
||||||
if (Environment.OSVersion.Version >= new Version(6, 2))
|
if (Environment.OSVersion.Version >= new Version(6, 2))
|
||||||
{
|
{
|
||||||
var resource = Application.Current.TryFindResource("ThemeBlurEnabled");
|
var resource = Application.Current.TryFindResource("ThemeBlurEnabled");
|
||||||
bool blur;
|
|
||||||
if (resource is bool)
|
|
||||||
{
|
|
||||||
blur = (bool)resource;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
blur = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (blur)
|
if (resource is bool)
|
||||||
{
|
return (bool)resource;
|
||||||
SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_ENABLE_BLURBEHIND);
|
|
||||||
}
|
return false;
|
||||||
else
|
|
||||||
{
|
|
||||||
SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_DISABLED);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetWindowAccent(Window w, AccentState state)
|
private void SetWindowAccent(Window w, AccentState state)
|
||||||
{
|
{
|
||||||
var windowHelper = new WindowInteropHelper(w);
|
var windowHelper = new WindowInteropHelper(w);
|
||||||
|
|
||||||
|
// this determines the width of the main query window
|
||||||
|
w.Width = mainWindowWidth;
|
||||||
|
windowHelper.EnsureHandle();
|
||||||
|
|
||||||
var accent = new AccentPolicy { AccentState = state };
|
var accent = new AccentPolicy { AccentState = state };
|
||||||
var accentStructSize = Marshal.SizeOf(accent);
|
var accentStructSize = Marshal.SizeOf(accent);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ namespace Flow.Launcher.Core
|
||||||
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e) when (e is HttpRequestException || e is WebException || e is SocketException || e is TaskCanceledException)
|
catch (Exception e) when (e is HttpRequestException || e is WebException || e is SocketException || e.InnerException is TimeoutException)
|
||||||
{
|
{
|
||||||
Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e);
|
Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e);
|
||||||
api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"),
|
api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"),
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ namespace Flow.Launcher.Infrastructure
|
||||||
public static readonly string PreinstalledDirectory = Path.Combine(ProgramDirectory, Plugins);
|
public static readonly string PreinstalledDirectory = Path.Combine(ProgramDirectory, Plugins);
|
||||||
public const string Issue = "https://github.com/Flow-Launcher/Flow.Launcher/issues/new";
|
public const string Issue = "https://github.com/Flow-Launcher/Flow.Launcher/issues/new";
|
||||||
public static readonly string Version = FileVersionInfo.GetVersionInfo(Assembly.Location.NonNull()).ProductVersion;
|
public static readonly string Version = FileVersionInfo.GetVersionInfo(Assembly.Location.NonNull()).ProductVersion;
|
||||||
|
public const string Documentation = "https://flow-launcher.github.io/docs/#/usage-tips";
|
||||||
|
|
||||||
public static readonly int ThumbnailSize = 64;
|
public static readonly int ThumbnailSize = 64;
|
||||||
private static readonly string ImagesDirectory = Path.Combine(ProgramDirectory, "Images");
|
private static readonly string ImagesDirectory = Path.Combine(ProgramDirectory, "Images");
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>net5.0-windows</TargetFramework>
|
||||||
<ProjectGuid>{4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}</ProjectGuid>
|
<ProjectGuid>{4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}</ProjectGuid>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<UseWpf>true</UseWpf>
|
<UseWpf>true</UseWpf>
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
<DebugSymbols>true</DebugSymbols>
|
<DebugSymbols>true</DebugSymbols>
|
||||||
<DebugType>full</DebugType>
|
<DebugType>portable</DebugType>
|
||||||
<Optimize>false</Optimize>
|
<Optimize>false</Optimize>
|
||||||
<OutputPath>..\Output\Debug\</OutputPath>
|
<OutputPath>..\Output\Debug\</OutputPath>
|
||||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
|
@ -49,8 +49,11 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="NLog.Schema" Version="4.7.0-rc1" />
|
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
||||||
<PackageReference Include="NLog.Web.AspNetCore" Version="4.9.0" />
|
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="16.10.56" />
|
||||||
|
<PackageReference Include="NLog" Version="4.7.10" />
|
||||||
|
<PackageReference Include="NLog.Schema" Version="4.7.10" />
|
||||||
|
<PackageReference Include="NLog.Web.AspNetCore" Version="4.12.0" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="4.7.0" />
|
<PackageReference Include="System.Drawing.Common" Version="4.7.0" />
|
||||||
<PackageReference Include="ToolGood.Words.Pinyin" Version="3.0.1.4" />
|
<PackageReference Include="ToolGood.Words.Pinyin" Version="3.0.1.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ namespace Flow.Launcher.Infrastructure.Http
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var response = await client.GetAsync(url, token);
|
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
|
||||||
if (response.StatusCode == HttpStatusCode.OK)
|
if (response.StatusCode == HttpStatusCode.OK)
|
||||||
{
|
{
|
||||||
await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
|
await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
|
||||||
|
|
@ -150,7 +150,7 @@ namespace Flow.Launcher.Infrastructure.Http
|
||||||
public static async Task<Stream> GetStreamAsync([NotNull] string url, CancellationToken token = default)
|
public static async Task<Stream> GetStreamAsync([NotNull] string url, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
Log.Debug($"|Http.Get|Url <{url}>");
|
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();
|
return await response.Content.ReadAsStreamAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,11 @@ using NLog;
|
||||||
using NLog.Config;
|
using NLog.Config;
|
||||||
using NLog.Targets;
|
using NLog.Targets;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
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
|
namespace Flow.Launcher.Infrastructure.Logger
|
||||||
{
|
{
|
||||||
|
|
@ -23,15 +28,37 @@ namespace Flow.Launcher.Infrastructure.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
var configuration = new LoggingConfiguration();
|
var configuration = new LoggingConfiguration();
|
||||||
var target = new FileTarget();
|
|
||||||
configuration.AddTarget("file", target);
|
const string layout =
|
||||||
target.FileName = CurrentLogDirectory.Replace(@"\", "/") + "/${shortdate}.txt";
|
@"${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
|
#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
|
#else
|
||||||
var rule = new LoggingRule("*", LogLevel.Info, target);
|
var fileRule = new LoggingRule("*", LogLevel.Info, fileTargetASyncWrapper);
|
||||||
#endif
|
#endif
|
||||||
configuration.LoggingRules.Add(rule);
|
configuration.LoggingRules.Add(fileRule);
|
||||||
LogManager.Configuration = configuration;
|
LogManager.Configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,7 +66,6 @@ namespace Flow.Launcher.Infrastructure.Logger
|
||||||
{
|
{
|
||||||
var logger = LogManager.GetLogger("FaultyLogger");
|
var logger = LogManager.GetLogger("FaultyLogger");
|
||||||
message = $"Wrong logger message format <{message}>";
|
message = $"Wrong logger message format <{message}>";
|
||||||
System.Diagnostics.Debug.WriteLine($"FATAL|{message}");
|
|
||||||
logger.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 = "")
|
public static void Exception(string className, string message, System.Exception exception, [CallerMemberName] string methodName = "")
|
||||||
{
|
{
|
||||||
|
exception = exception.Demystify();
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
throw exception;
|
ExceptionDispatchInfo.Capture(exception).Throw();
|
||||||
#else
|
#else
|
||||||
var classNameWithMethod = CheckClassAndMessageAndReturnFullClassWithMethod(className, message, methodName);
|
var classNameWithMethod = CheckClassAndMessageAndReturnFullClassWithMethod(className, message, methodName);
|
||||||
|
|
||||||
|
|
@ -90,23 +115,9 @@ namespace Flow.Launcher.Infrastructure.Logger
|
||||||
{
|
{
|
||||||
var logger = LogManager.GetLogger(classAndMethod);
|
var logger = LogManager.GetLogger(classAndMethod);
|
||||||
|
|
||||||
System.Diagnostics.Debug.WriteLine($"ERROR|{message}");
|
var messageBuilder = new StringBuilder();
|
||||||
|
|
||||||
logger.Error("-------------------------- Begin exception --------------------------");
|
logger.Error(e, message);
|
||||||
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 --------------------------");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void LogInternal(string message, LogLevel level)
|
private static void LogInternal(string message, LogLevel level)
|
||||||
|
|
@ -117,8 +128,6 @@ namespace Flow.Launcher.Infrastructure.Logger
|
||||||
var prefix = parts[1];
|
var prefix = parts[1];
|
||||||
var unprefixed = parts[2];
|
var unprefixed = parts[2];
|
||||||
var logger = LogManager.GetLogger(prefix);
|
var logger = LogManager.GetLogger(prefix);
|
||||||
|
|
||||||
System.Diagnostics.Debug.WriteLine($"{level.Name}|{message}");
|
|
||||||
logger.Log(level, unprefixed);
|
logger.Log(level, unprefixed);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -128,11 +137,12 @@ namespace Flow.Launcher.Infrastructure.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <param name="message">example: "|prefix|unprefixed" </param>
|
/// <param name="message">example: "|prefix|unprefixed" </param>
|
||||||
[MethodImpl(MethodImplOptions.Synchronized)]
|
/// <param name="e">Exception</param>
|
||||||
public static void Exception(string message, System.Exception e)
|
public static void Exception(string message, System.Exception e)
|
||||||
{
|
{
|
||||||
|
e = e.Demystify();
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
throw e;
|
ExceptionDispatchInfo.Capture(e).Throw();
|
||||||
#else
|
#else
|
||||||
if (FormatValid(message))
|
if (FormatValid(message))
|
||||||
{
|
{
|
||||||
|
|
@ -165,7 +175,6 @@ namespace Flow.Launcher.Infrastructure.Logger
|
||||||
|
|
||||||
var logger = LogManager.GetLogger(classNameWithMethod);
|
var logger = LogManager.GetLogger(classNameWithMethod);
|
||||||
|
|
||||||
System.Diagnostics.Debug.WriteLine($"{level.Name}|{message}");
|
|
||||||
logger.Log(level, message);
|
logger.Log(level, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Storage
|
namespace Flow.Launcher.Infrastructure.Storage
|
||||||
{
|
{
|
||||||
public class FlowLauncherJsonStorage<T> : JsonStrorage<T> where T : new()
|
public class FlowLauncherJsonStorage<T> : JsonStorage<T> where T : new()
|
||||||
{
|
{
|
||||||
public FlowLauncherJsonStorage()
|
public FlowLauncherJsonStorage()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
namespace Flow.Launcher.Infrastructure.Storage
|
using System;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Infrastructure.Storage
|
||||||
{
|
{
|
||||||
/// <summary>
|
[Obsolete("Deprecated as of Flow Launcher v1.8.0, on 2021.06.21. " +
|
||||||
/// Save plugin settings/cache,
|
"This is used only for Everything plugin v1.4.9 or below backwards compatibility")]
|
||||||
/// todo should be merged into a abstract class intead of seperate interface
|
public interface ISavable : Plugin.ISavable { }
|
||||||
/// </summary>
|
}
|
||||||
public interface ISavable
|
|
||||||
{
|
|
||||||
void Save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -9,10 +9,9 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Serialize object using json format.
|
/// Serialize object using json format.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class JsonStrorage<T> where T : new()
|
public class JsonStorage<T> where T : new()
|
||||||
{
|
{
|
||||||
private readonly JsonSerializerOptions _serializerSettings;
|
protected T _data;
|
||||||
private T _data;
|
|
||||||
// need a new directory name
|
// need a new directory name
|
||||||
public const string DirectoryName = "Settings";
|
public const string DirectoryName = "Settings";
|
||||||
public const string FileSuffix = ".json";
|
public const string FileSuffix = ".json";
|
||||||
|
|
@ -20,24 +19,14 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
public string DirectoryPath { get; set; }
|
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()
|
public T Load()
|
||||||
{
|
{
|
||||||
if (File.Exists(FilePath))
|
if (File.Exists(FilePath))
|
||||||
{
|
{
|
||||||
var searlized = File.ReadAllText(FilePath);
|
var serialized = File.ReadAllText(FilePath);
|
||||||
if (!string.IsNullOrWhiteSpace(searlized))
|
if (!string.IsNullOrWhiteSpace(serialized))
|
||||||
{
|
{
|
||||||
Deserialize(searlized);
|
Deserialize(serialized);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -51,16 +40,16 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
return _data.NonNull();
|
return _data.NonNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Deserialize(string searlized)
|
private void Deserialize(string serialized)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_data = JsonSerializer.Deserialize<T>(searlized, _serializerSettings);
|
_data = JsonSerializer.Deserialize<T>(serialized);
|
||||||
}
|
}
|
||||||
catch (JsonException e)
|
catch (JsonException e)
|
||||||
{
|
{
|
||||||
LoadDefault();
|
LoadDefault();
|
||||||
Log.Exception($"|JsonStrorage.Deserialize|Deserialize error for json <{FilePath}>", e);
|
Log.Exception($"|JsonStorage.Deserialize|Deserialize error for json <{FilePath}>", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_data == null)
|
if (_data == null)
|
||||||
|
|
@ -98,4 +87,8 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
File.WriteAllText(FilePath, serialized);
|
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<T> : JsonStorage<T> where T : new() { }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,23 @@ using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Storage
|
namespace Flow.Launcher.Infrastructure.Storage
|
||||||
{
|
{
|
||||||
public class PluginJsonStorage<T> :JsonStrorage<T> where T : new()
|
public class PluginJsonStorage<T> :JsonStorage<T> where T : new()
|
||||||
{
|
{
|
||||||
public PluginJsonStorage()
|
public PluginJsonStorage()
|
||||||
{
|
{
|
||||||
// C# releated, add python releated below
|
// C# related, add python related below
|
||||||
var dataType = typeof(T);
|
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);
|
DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Plugins, assemblyName);
|
||||||
Helper.ValidateDirectory(DirectoryPath);
|
Helper.ValidateDirectory(DirectoryPath);
|
||||||
|
|
||||||
FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}");
|
FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PluginJsonStorage(T data) : this()
|
||||||
|
{
|
||||||
|
_data = data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,13 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
{
|
{
|
||||||
var settings = Plugins[metadata.ID];
|
var settings = Plugins[metadata.ID];
|
||||||
|
|
||||||
// TODO: Remove. This is one off for 1.2.0 release.
|
// TODO: Remove. This is backwards compatibility for 1.8.0 release.
|
||||||
// Introduced a new action keyword in Explorer, so need to update plugin setting in the UserData folder.
|
// Introduced two new action keywords 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.
|
|
||||||
if (metadata.ID == "572be03c74c642baae319fc283e561a8" && metadata.ActionKeywords.Count != settings.ActionKeywords.Count)
|
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))
|
if (string.IsNullOrEmpty(settings.Version))
|
||||||
settings.Version = metadata.Version;
|
settings.Version = metadata.Version;
|
||||||
|
|
@ -30,6 +32,11 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
metadata.ActionKeywords = settings.ActionKeywords;
|
metadata.ActionKeywords = settings.ActionKeywords;
|
||||||
metadata.ActionKeyword = settings.ActionKeywords[0];
|
metadata.ActionKeyword = settings.ActionKeywords[0];
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
metadata.ActionKeywords = new List<string>();
|
||||||
|
metadata.ActionKeyword = string.Empty;
|
||||||
|
}
|
||||||
metadata.Disabled = settings.Disabled;
|
metadata.Disabled = settings.Disabled;
|
||||||
metadata.Priority = settings.Priority;
|
metadata.Priority = settings.Priority;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
public bool RememberLastLaunchLocation { get; set; }
|
public bool RememberLastLaunchLocation { get; set; }
|
||||||
public bool IgnoreHotkeysOnFullscreen { get; set; }
|
public bool IgnoreHotkeysOnFullscreen { get; set; }
|
||||||
|
|
||||||
|
public bool AutoHideScrollBar { get; set; }
|
||||||
|
|
||||||
public HttpProxy Proxy { get; set; } = new HttpProxy();
|
public HttpProxy Proxy { get; set; } = new HttpProxy();
|
||||||
|
|
||||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
|
|
||||||
|
|
@ -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<Result> LoadContextMenus(Result selectedResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represent plugins that support internationalization
|
|
||||||
/// </summary>
|
|
||||||
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<Result> Results;
|
|
||||||
public Query Query;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
14
Flow.Launcher.Plugin/Features.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Plugin
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base Interface for Flow's special plugin feature interface
|
||||||
|
/// </summary>
|
||||||
|
public interface IFeatures
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>net5.0-windows</TargetFramework>
|
||||||
<ProjectGuid>{8451ECDD-2EA4-4966-BB0A-7BBC40138E80}</ProjectGuid>
|
<ProjectGuid>{8451ECDD-2EA4-4966-BB0A-7BBC40138E80}</ProjectGuid>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
|
|
@ -14,10 +14,10 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>1.4.0</Version>
|
<Version>2.0.0</Version>
|
||||||
<PackageVersion>1.4.0</PackageVersion>
|
<PackageVersion>2.0.0</PackageVersion>
|
||||||
<AssemblyVersion>1.4.0</AssemblyVersion>
|
<AssemblyVersion>2.0.0</AssemblyVersion>
|
||||||
<FileVersion>1.4.0</FileVersion>
|
<FileVersion>2.0.0</FileVersion>
|
||||||
<PackageId>Flow.Launcher.Plugin</PackageId>
|
<PackageId>Flow.Launcher.Plugin</PackageId>
|
||||||
<Authors>Flow-Launcher</Authors>
|
<Authors>Flow-Launcher</Authors>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
<DebugSymbols>true</DebugSymbols>
|
<DebugSymbols>true</DebugSymbols>
|
||||||
<DebugType>full</DebugType>
|
<DebugType>portable</DebugType>
|
||||||
<Optimize>false</Optimize>
|
<Optimize>false</Optimize>
|
||||||
<OutputPath>..\Output\Debug\</OutputPath>
|
<OutputPath>..\Output\Debug\</OutputPath>
|
||||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Public APIs that plugin can use
|
|
||||||
/// </summary>
|
|
||||||
public interface IPublicAPI
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Change Flow.Launcher query
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="query">query text</param>
|
|
||||||
/// <param name="requery">
|
|
||||||
/// 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
|
|
||||||
/// </param>
|
|
||||||
void ChangeQuery(string query, bool requery = false);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Restart Flow Launcher
|
|
||||||
/// </summary>
|
|
||||||
void RestartApp();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Save all Flow Launcher settings
|
|
||||||
/// </summary>
|
|
||||||
void SaveAppAllSettings();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reloads any Plugins that have the
|
|
||||||
/// IReloadable implemented. It refeshes
|
|
||||||
/// Plugin's in memory data with new content
|
|
||||||
/// added by user.
|
|
||||||
/// </summary>
|
|
||||||
Task ReloadAllPluginData();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Check for new Flow Launcher update
|
|
||||||
/// </summary>
|
|
||||||
void CheckForNewUpdate();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Show message box
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="title">Message title</param>
|
|
||||||
/// <param name="subTitle">Message subtitle</param>
|
|
||||||
/// <param name="iconPath">Message icon path (relative path to your plugin folder)</param>
|
|
||||||
void ShowMsg(string title, string subTitle = "", string iconPath = "");
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Show message box
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="title">Message title</param>
|
|
||||||
/// <param name="subTitle">Message subtitle</param>
|
|
||||||
/// <param name="iconPath">Message icon path (relative path to your plugin folder)</param>
|
|
||||||
/// <param name="useMainWindowAsOwner">when true will use main windows as the owner</param>
|
|
||||||
void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Open setting dialog
|
|
||||||
/// </summary>
|
|
||||||
void OpenSettingDialog();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get translation of current language
|
|
||||||
/// You need to implement IPluginI18n if you want to support multiple languages for your plugin
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="key"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
string GetTranslation(string key);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get all loaded plugins
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
List<PluginPair> GetAllPlugins();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fired after global keyboard events
|
|
||||||
/// if you want to hook something like Ctrl+R, you should use this event
|
|
||||||
/// </summary>
|
|
||||||
event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent;
|
|
||||||
|
|
||||||
MatchResult FuzzySearch(string query, string stringToCompare);
|
|
||||||
|
|
||||||
Task<string> HttpGetStringAsync(string url, CancellationToken token = default);
|
|
||||||
|
|
||||||
Task<Stream> 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);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -13,8 +13,8 @@ namespace Flow.Launcher.Plugin
|
||||||
/// The command that allows user to manual reload is exposed via Plugin.Sys, and
|
/// The command that allows user to manual reload is exposed via Plugin.Sys, and
|
||||||
/// it will call the plugins that have implemented this interface.
|
/// it will call the plugins that have implemented this interface.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IAsyncReloadable
|
public interface IAsyncReloadable : IFeatures
|
||||||
{
|
{
|
||||||
Task ReloadDataAsync();
|
Task ReloadDataAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
9
Flow.Launcher.Plugin/Interfaces/IContextMenu.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Plugin
|
||||||
|
{
|
||||||
|
public interface IContextMenu : IFeatures
|
||||||
|
{
|
||||||
|
List<Result> LoadContextMenus(Result selectedResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Flow.Launcher.Plugin
|
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
|
/// or performaing CPU intense jobs (performing better with cancellation), please try the IAsyncPlugin interface
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IPlugin
|
public interface IPlugin : IAsyncPlugin
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Querying when user's search changes
|
/// Querying when user's search changes
|
||||||
|
|
@ -27,5 +29,9 @@ namespace Flow.Launcher.Plugin
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context"></param>
|
/// <param name="context"></param>
|
||||||
void Init(PluginInitContext context);
|
void Init(PluginInitContext context);
|
||||||
|
|
||||||
|
Task IAsyncPlugin.InitAsync(PluginInitContext context) => Task.Run(() => Init(context));
|
||||||
|
|
||||||
|
Task<List<Result>> IAsyncPlugin.QueryAsync(Query query, CancellationToken token) => Task.Run(() => Query(query));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
12
Flow.Launcher.Plugin/Interfaces/IPluginI18n.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
namespace Flow.Launcher.Plugin
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represent plugins that support internationalization
|
||||||
|
/// </summary>
|
||||||
|
public interface IPluginI18n : IFeatures
|
||||||
|
{
|
||||||
|
string GetTranslatedPluginTitle();
|
||||||
|
|
||||||
|
string GetTranslatedPluginDescription();
|
||||||
|
}
|
||||||
|
}
|
||||||
189
Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
Normal file
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Public APIs that plugin can use
|
||||||
|
/// </summary>
|
||||||
|
public interface IPublicAPI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Change Flow.Launcher query
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">query text</param>
|
||||||
|
/// <param name="requery">
|
||||||
|
/// 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
|
||||||
|
/// </param>
|
||||||
|
void ChangeQuery(string query, bool requery = false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restart Flow Launcher
|
||||||
|
/// </summary>
|
||||||
|
void RestartApp();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save everything, all of Flow Launcher and plugins' data and settings
|
||||||
|
/// </summary>
|
||||||
|
void SaveAppAllSettings();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save all Flow's plugins settings
|
||||||
|
/// </summary>
|
||||||
|
void SavePluginSettings();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reloads any Plugins that have the
|
||||||
|
/// IReloadable implemented. It refeshes
|
||||||
|
/// Plugin's in memory data with new content
|
||||||
|
/// added by user.
|
||||||
|
/// </summary>
|
||||||
|
Task ReloadAllPluginData();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check for new Flow Launcher update
|
||||||
|
/// </summary>
|
||||||
|
void CheckForNewUpdate();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Show the error message using Flow's standard error icon.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="title">Message title</param>
|
||||||
|
/// <param name="subTitle">Optional message subtitle</param>
|
||||||
|
void ShowMsgError(string title, string subTitle = "");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Show message box
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="title">Message title</param>
|
||||||
|
/// <param name="subTitle">Message subtitle</param>
|
||||||
|
/// <param name="iconPath">Message icon path (relative path to your plugin folder)</param>
|
||||||
|
void ShowMsg(string title, string subTitle = "", string iconPath = "");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Show message box
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="title">Message title</param>
|
||||||
|
/// <param name="subTitle">Message subtitle</param>
|
||||||
|
/// <param name="iconPath">Message icon path (relative path to your plugin folder)</param>
|
||||||
|
/// <param name="useMainWindowAsOwner">when true will use main windows as the owner</param>
|
||||||
|
void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Open setting dialog
|
||||||
|
/// </summary>
|
||||||
|
void OpenSettingDialog();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get translation of current language
|
||||||
|
/// You need to implement IPluginI18n if you want to support multiple languages for your plugin
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
string GetTranslation(string key);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all loaded plugins
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
List<PluginPair> GetAllPlugins();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired after global keyboard events
|
||||||
|
/// if you want to hook something like Ctrl+R, you should use this event
|
||||||
|
/// </summary>
|
||||||
|
event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fuzzy Search the string with the given query. This is the core search mechanism Flow uses
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">Query string</param>
|
||||||
|
/// <param name="stringToCompare">The string that will be compared against the query</param>
|
||||||
|
/// <returns>Match results</returns>
|
||||||
|
MatchResult FuzzySearch(string query, string stringToCompare);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Http download the spefic url and return as string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">URL to call Http Get</param>
|
||||||
|
/// <param name="token">Cancellation Token</param>
|
||||||
|
/// <returns>Task to get string result</returns>
|
||||||
|
Task<string> HttpGetStringAsync(string url, CancellationToken token = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Http download the spefic url and return as stream
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">URL to call Http Get</param>
|
||||||
|
/// <param name="token">Cancellation Token</param>
|
||||||
|
/// <returns>Task to get stream result</returns>
|
||||||
|
Task<Stream> HttpGetStreamAsync(string url, CancellationToken token = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Download the specific url to a cretain file path
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">URL to download file</param>
|
||||||
|
/// <param name="token">place to store file</param>
|
||||||
|
/// <returns>Task showing the progress</returns>
|
||||||
|
Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add ActionKeyword for specific plugin
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pluginId">ID for plugin that needs to add action keyword</param>
|
||||||
|
/// <param name="newActionKeyword">The actionkeyword that is supposed to be added</param>
|
||||||
|
void AddActionKeyword(string pluginId, string newActionKeyword);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove ActionKeyword for specific plugin
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pluginId">ID for plugin that needs to remove action keyword</param>
|
||||||
|
/// <param name="newActionKeyword">The actionkeyword that is supposed to be removed</param>
|
||||||
|
void RemoveActionKeyword(string pluginId, string oldActionKeyword);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Log debug message
|
||||||
|
/// Message will only be logged in Debug mode
|
||||||
|
/// </summary>
|
||||||
|
void LogDebug(string className, string message, [CallerMemberName] string methodName = "");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Log info message
|
||||||
|
/// </summary>
|
||||||
|
void LogInfo(string className, string message, [CallerMemberName] string methodName = "");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Log warning message
|
||||||
|
/// </summary>
|
||||||
|
void LogWarn(string className, string message, [CallerMemberName] string methodName = "");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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
|
||||||
|
/// </summary>
|
||||||
|
void LogException(string className, string message, Exception e, [CallerMemberName] string methodName = "");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type for deserialization</typeparam>
|
||||||
|
/// <returns></returns>
|
||||||
|
T LoadSettingJsonStorage<T>() where T : new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type for Serialization</typeparam>
|
||||||
|
/// <returns></returns>
|
||||||
|
void SaveSettingJsonStorage<T>() where T : new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,8 +15,8 @@
|
||||||
/// If requiring reloading data asynchronously, please use the IAsyncReloadable interface
|
/// If requiring reloading data asynchronously, please use the IAsyncReloadable interface
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IReloadable
|
public interface IReloadable : IFeatures
|
||||||
{
|
{
|
||||||
void ReloadData();
|
void ReloadData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
20
Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs
Normal file
|
|
@ -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<Result> Results;
|
||||||
|
public Query Query;
|
||||||
|
public CancellationToken Token { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
12
Flow.Launcher.Plugin/Interfaces/ISavable.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
namespace Flow.Launcher.Plugin
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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
|
||||||
|
/// </summary>
|
||||||
|
public interface ISavable : IFeatures
|
||||||
|
{
|
||||||
|
void Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
{
|
{
|
||||||
public class PluginPair
|
public class PluginPair
|
||||||
{
|
{
|
||||||
public object Plugin { get; internal set; }
|
public IAsyncPlugin Plugin { get; internal set; }
|
||||||
public PluginMetadata Metadata { get; internal set; }
|
public PluginMetadata Metadata { get; internal set; }
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,4 @@
|
||||||
|
|
||||||
* Defines base objects and interfaces for plugins
|
* Defines base objects and interfaces for plugins
|
||||||
* Plugin authors making C# plugins should reference this DLL via nuget
|
* 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
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,18 @@ namespace Flow.Launcher.Plugin
|
||||||
{
|
{
|
||||||
|
|
||||||
private string _pluginDirectory;
|
private string _pluginDirectory;
|
||||||
|
|
||||||
private string _icoPath;
|
private string _icoPath;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides the title of the result. This is always required.
|
||||||
|
/// </summary>
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
public string SubTitle { get; set; }
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides additional details for the result. This is optional
|
||||||
|
/// </summary>
|
||||||
|
public string SubTitle { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This holds the action keyword that triggered the result.
|
/// This holds the action keyword that triggered the result.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>net5.0-windows10.0.19041.0</TargetFramework>
|
||||||
<ProjectGuid>{FF742965-9A80-41A5-B042-D6C7D3A21708}</ProjectGuid>
|
<ProjectGuid>{FF742965-9A80-41A5-B042-D6C7D3A21708}</ProjectGuid>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
<DebugSymbols>true</DebugSymbols>
|
<DebugSymbols>true</DebugSymbols>
|
||||||
<DebugType>full</DebugType>
|
<DebugType>portable</DebugType>
|
||||||
<Optimize>false</Optimize>
|
<Optimize>false</Optimize>
|
||||||
<OutputPath>bin\Debug\</OutputPath>
|
<OutputPath>bin\Debug\</OutputPath>
|
||||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
|
@ -49,12 +49,12 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Moq" Version="4.14.1" />
|
<PackageReference Include="Moq" Version="4.14.1" />
|
||||||
<PackageReference Include="nunit" Version="3.12.0" />
|
<PackageReference Include="nunit" Version="3.13.2" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1">
|
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -133,6 +133,13 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
{
|
{
|
||||||
// Given
|
// Given
|
||||||
var queryConstructor = new QueryConstructor(new Settings());
|
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
|
//When
|
||||||
var resultString = queryConstructor.QueryForAllFilesAndFolders(userSearchString);
|
var resultString = queryConstructor.QueryForAllFilesAndFolders(userSearchString);
|
||||||
|
|
|
||||||
105
Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs
Normal file
|
|
@ -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<Stream> 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<JsonRPCQueryResponseModel> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -61,7 +61,6 @@ namespace Flow.Launcher
|
||||||
_settingsVM = new SettingWindowViewModel(_updater, _portable);
|
_settingsVM = new SettingWindowViewModel(_updater, _portable);
|
||||||
_settings = _settingsVM.Settings;
|
_settings = _settingsVM.Settings;
|
||||||
|
|
||||||
|
|
||||||
_alphabet.Initialize(_settings);
|
_alphabet.Initialize(_settings);
|
||||||
_stringMatcher = new StringMatcher(_alphabet);
|
_stringMatcher = new StringMatcher(_alphabet);
|
||||||
StringMatcher.Instance = _stringMatcher;
|
StringMatcher.Instance = _stringMatcher;
|
||||||
|
|
@ -69,6 +68,9 @@ namespace Flow.Launcher
|
||||||
|
|
||||||
PluginManager.LoadPlugins(_settings.PluginSettings);
|
PluginManager.LoadPlugins(_settings.PluginSettings);
|
||||||
_mainVM = new MainViewModel(_settings);
|
_mainVM = new MainViewModel(_settings);
|
||||||
|
|
||||||
|
HotKeyMapper.Initialize(_mainVM);
|
||||||
|
|
||||||
API = new PublicAPIInstance(_settingsVM, _mainVM, _alphabet);
|
API = new PublicAPIInstance(_settingsVM, _mainVM, _alphabet);
|
||||||
|
|
||||||
Http.API = API;
|
Http.API = API;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
using Flow.Launcher.Core.Resource;
|
using Flow.Launcher.Core.Resource;
|
||||||
|
using Flow.Launcher.Helper;
|
||||||
using Flow.Launcher.Infrastructure.Hotkey;
|
using Flow.Launcher.Infrastructure.Hotkey;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
using NHotkey;
|
|
||||||
using NHotkey.Wpf;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
@ -52,11 +51,7 @@ namespace Flow.Launcher
|
||||||
};
|
};
|
||||||
_settings.CustomPluginHotkeys.Add(pluginHotkey);
|
_settings.CustomPluginHotkeys.Add(pluginHotkey);
|
||||||
|
|
||||||
SetHotkey(ctlHotkey.CurrentHotkey, delegate
|
HotKeyMapper.SetCustomQueryHotkey(pluginHotkey);
|
||||||
{
|
|
||||||
App.API.ChangeQuery(pluginHotkey.ActionKeyword);
|
|
||||||
Application.Current.MainWindow.Visibility = Visibility.Visible;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -69,12 +64,8 @@ namespace Flow.Launcher
|
||||||
updateCustomHotkey.ActionKeyword = tbAction.Text;
|
updateCustomHotkey.ActionKeyword = tbAction.Text;
|
||||||
updateCustomHotkey.Hotkey = ctlHotkey.CurrentHotkey.ToString();
|
updateCustomHotkey.Hotkey = ctlHotkey.CurrentHotkey.ToString();
|
||||||
//remove origin hotkey
|
//remove origin hotkey
|
||||||
RemoveHotkey(oldHotkey);
|
HotKeyMapper.RemoveHotkey(oldHotkey);
|
||||||
SetHotkey(new HotkeyModel(updateCustomHotkey.Hotkey), delegate
|
HotKeyMapper.SetCustomQueryHotkey(updateCustomHotkey);
|
||||||
{
|
|
||||||
App.API.ChangeQuery(updateCustomHotkey.ActionKeyword);
|
|
||||||
Application.Current.MainWindow.Visibility = Visibility.Visible;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Close();
|
Close();
|
||||||
|
|
@ -100,28 +91,8 @@ namespace Flow.Launcher
|
||||||
{
|
{
|
||||||
App.API.ChangeQuery(tbAction.Text);
|
App.API.ChangeQuery(tbAction.Text);
|
||||||
Application.Current.MainWindow.Visibility = Visibility.Visible;
|
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<HotkeyEventArgs> 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)
|
private void cmdEsc_OnPress(object sender, ExecutedRoutedEventArgs e)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>net5.0-windows10.0.19041.0</TargetFramework>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
<StartupObject>Flow.Launcher.App</StartupObject>
|
<StartupObject>Flow.Launcher.App</StartupObject>
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
<DebugSymbols>true</DebugSymbols>
|
<DebugSymbols>true</DebugSymbols>
|
||||||
<DebugType>full</DebugType>
|
<DebugType>portable</DebugType>
|
||||||
<Optimize>false</Optimize>
|
<Optimize>false</Optimize>
|
||||||
<OutputPath>..\Output\Debug\</OutputPath>
|
<OutputPath>..\Output\Debug\</OutputPath>
|
||||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="InputSimulator" Version="1.0.4" />
|
<PackageReference Include="InputSimulator" Version="1.0.4" />
|
||||||
<PackageReference Include="ModernWpfUI" Version="0.8.3" />
|
<PackageReference Include="ModernWpfUI" Version="0.9.3" />
|
||||||
<PackageReference Include="NHotkey.Wpf" Version="1.2.1" />
|
<PackageReference Include="NHotkey.Wpf" Version="1.2.1" />
|
||||||
<PackageReference Include="NuGet.CommandLine" Version="5.4.0">
|
<PackageReference Include="NuGet.CommandLine" Version="5.4.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ using System.Windows.Threading;
|
||||||
using NLog;
|
using NLog;
|
||||||
using Flow.Launcher.Infrastructure;
|
using Flow.Launcher.Infrastructure;
|
||||||
using Flow.Launcher.Infrastructure.Exception;
|
using Flow.Launcher.Infrastructure.Exception;
|
||||||
|
using NLog.Fluent;
|
||||||
|
using Log = Flow.Launcher.Infrastructure.Logger.Log;
|
||||||
|
|
||||||
namespace Flow.Launcher.Helper
|
namespace Flow.Launcher.Helper
|
||||||
{
|
{
|
||||||
|
|
@ -45,4 +47,4 @@ namespace Flow.Launcher.Helper
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
136
Flow.Launcher/Helper/HotKeyMapper.cs
Normal file
|
|
@ -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<HotkeyEventArgs> action)
|
||||||
|
{
|
||||||
|
var hotkey = new HotkeyModel(hotkeyStr);
|
||||||
|
SetHotkey(hotkey, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void SetHotkey(HotkeyModel hotkey, EventHandler<HotkeyEventArgs> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if Flow Launcher should ignore any hotkeys
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,16 @@ namespace Flow.Launcher.Helper
|
||||||
var window = Application.Current.Windows.OfType<Window>().FirstOrDefault(x => x.GetType() == typeof(T))
|
var window = Application.Current.Windows.OfType<Window>().FirstOrDefault(x => x.GetType() == typeof(T))
|
||||||
?? (T)Activator.CreateInstance(typeof(T), args);
|
?? (T)Activator.CreateInstance(typeof(T), args);
|
||||||
Application.Current.MainWindow.Hide();
|
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.Show();
|
||||||
|
|
||||||
window.Focus();
|
window.Focus();
|
||||||
|
|
||||||
return (T)window;
|
return (T)window;
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="150" />
|
<ColumnDefinition Width="150" />
|
||||||
<ColumnDefinition Width="120" />
|
<ColumnDefinition Width="125" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<TextBox x:Name="tbHotkey" TabIndex="100" VerticalContentAlignment="Center" Grid.Column="0"
|
<TextBox x:Name="tbHotkey" TabIndex="100" VerticalContentAlignment="Center" Grid.Column="0"
|
||||||
PreviewKeyDown="TbHotkey_OnPreviewKeyDown" input:InputMethod.IsInputMethodEnabled="False"/>
|
PreviewKeyDown="TbHotkey_OnPreviewKeyDown" input:InputMethod.IsInputMethodEnabled="False"/>
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using NHotkey.Wpf;
|
|
||||||
using Flow.Launcher.Core.Resource;
|
using Flow.Launcher.Core.Resource;
|
||||||
|
using Flow.Launcher.Helper;
|
||||||
using Flow.Launcher.Infrastructure.Hotkey;
|
using Flow.Launcher.Infrastructure.Hotkey;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
|
|
||||||
|
|
@ -18,11 +18,7 @@ namespace Flow.Launcher
|
||||||
|
|
||||||
public event EventHandler HotkeyChanged;
|
public event EventHandler HotkeyChanged;
|
||||||
|
|
||||||
protected virtual void OnHotkeyChanged()
|
protected virtual void OnHotkeyChanged() => HotkeyChanged?.Invoke(this, EventArgs.Empty);
|
||||||
{
|
|
||||||
EventHandler handler = HotkeyChanged;
|
|
||||||
if (handler != null) handler(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
public HotkeyControl()
|
public HotkeyControl()
|
||||||
{
|
{
|
||||||
|
|
@ -90,24 +86,7 @@ namespace Flow.Launcher
|
||||||
SetHotkey(new HotkeyModel(keyStr), triggerValidate);
|
SetHotkey(new HotkeyModel(keyStr), triggerValidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CheckHotkeyAvailability()
|
private bool CheckHotkeyAvailability() => HotKeyMapper.CheckAvailability(CurrentHotkey);
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
HotkeyManager.Current.AddOrReplace("HotkeyAvailabilityTest", CurrentHotkey.CharKey, CurrentHotkey.ModifierKeys, (sender, e) => { });
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
HotkeyManager.Current.Remove("HotkeyAvailabilityTest");
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public new bool IsFocused
|
public new bool IsFocused
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -31,11 +31,15 @@
|
||||||
<system:String x:Key="ignoreHotkeysOnFullscreen">Ignore hotkeys in fullscreen mode</system:String>
|
<system:String x:Key="ignoreHotkeysOnFullscreen">Ignore hotkeys in fullscreen mode</system:String>
|
||||||
<system:String x:Key="pythonDirectory">Python Directory</system:String>
|
<system:String x:Key="pythonDirectory">Python Directory</system:String>
|
||||||
<system:String x:Key="autoUpdates">Auto Update</system:String>
|
<system:String x:Key="autoUpdates">Auto Update</system:String>
|
||||||
|
<system:String x:Key="autoHideScrollBar">Auto Hide Scroll Bar</system:String>
|
||||||
|
<system:String x:Key="autoHideScrollBarToolTip">Automatically hides the Settings window scroll bar and show when hover the mouse over it</system:String>
|
||||||
<system:String x:Key="selectPythonDirectory">Select</system:String>
|
<system:String x:Key="selectPythonDirectory">Select</system:String>
|
||||||
<system:String x:Key="hideOnStartup">Hide Flow Launcher on startup</system:String>
|
<system:String x:Key="hideOnStartup">Hide Flow Launcher on startup</system:String>
|
||||||
<system:String x:Key="hideNotifyIcon">Hide tray icon</system:String>
|
<system:String x:Key="hideNotifyIcon">Hide tray icon</system:String>
|
||||||
<system:String x:Key="querySearchPrecision">Query Search Precision</system:String>
|
<system:String x:Key="querySearchPrecision">Query Search Precision</system:String>
|
||||||
<system:String x:Key="ShouldUsePinyin">Should Use Pinyin</system:String>
|
<system:String x:Key="ShouldUsePinyin">Should Use Pinyin</system:String>
|
||||||
|
<system:String x:Key="ShouldUsePinyinToolTip">Allows using Pinyin to search. Pinyin is the standard system of romanized spelling for transliterating Chinese</system:String>
|
||||||
|
<system:String x:Key="shadowEffectNotAllowed">Shadow effect is not allowed while current theme has blur effect enabled</system:String>
|
||||||
|
|
||||||
<!--Setting Plugin-->
|
<!--Setting Plugin-->
|
||||||
<system:String x:Key="plugin">Plugin</system:String>
|
<system:String x:Key="plugin">Plugin</system:String>
|
||||||
|
|
@ -47,6 +51,7 @@
|
||||||
<system:String x:Key="newActionKeyword">New action keyword:</system:String>
|
<system:String x:Key="newActionKeyword">New action keyword:</system:String>
|
||||||
<system:String x:Key="currentPriority">Current Priority:</system:String>
|
<system:String x:Key="currentPriority">Current Priority:</system:String>
|
||||||
<system:String x:Key="newPriority">New Priority:</system:String>
|
<system:String x:Key="newPriority">New Priority:</system:String>
|
||||||
|
<system:String x:Key="priority">Priority:</system:String>
|
||||||
<system:String x:Key="pluginDirectory">Plugin Directory</system:String>
|
<system:String x:Key="pluginDirectory">Plugin Directory</system:String>
|
||||||
<system:String x:Key="author">Author</system:String>
|
<system:String x:Key="author">Author</system:String>
|
||||||
<system:String x:Key="plugin_init_time">Init time:</system:String>
|
<system:String x:Key="plugin_init_time">Init time:</system:String>
|
||||||
|
|
@ -69,6 +74,7 @@
|
||||||
<system:String x:Key="openResultModifiers">Open Result Modifiers</system:String>
|
<system:String x:Key="openResultModifiers">Open Result Modifiers</system:String>
|
||||||
<system:String x:Key="showOpenResultHotkey">Show Hotkey</system:String>
|
<system:String x:Key="showOpenResultHotkey">Show Hotkey</system:String>
|
||||||
<system:String x:Key="customQueryHotkey">Custom Query Hotkey</system:String>
|
<system:String x:Key="customQueryHotkey">Custom Query Hotkey</system:String>
|
||||||
|
<system:String x:Key="customQuery">Query</system:String>
|
||||||
<system:String x:Key="delete">Delete</system:String>
|
<system:String x:Key="delete">Delete</system:String>
|
||||||
<system:String x:Key="edit">Edit</system:String>
|
<system:String x:Key="edit">Edit</system:String>
|
||||||
<system:String x:Key="add">Add</system:String>
|
<system:String x:Key="add">Add</system:String>
|
||||||
|
|
@ -107,6 +113,7 @@
|
||||||
or go to https://github.com/Flow-Launcher/Flow.Launcher/releases to download updates manually.
|
or go to https://github.com/Flow-Launcher/Flow.Launcher/releases to download updates manually.
|
||||||
</system:String>
|
</system:String>
|
||||||
<system:String x:Key="releaseNotes">Release Notes</system:String>
|
<system:String x:Key="releaseNotes">Release Notes</system:String>
|
||||||
|
<system:String x:Key="documentation">Usage Tips:</system:String>
|
||||||
|
|
||||||
<!--Priority Setting Dialog-->
|
<!--Priority Setting Dialog-->
|
||||||
<system:String x:Key="priority_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</system:String>
|
<system:String x:Key="priority_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</system:String>
|
||||||
|
|
@ -121,6 +128,7 @@
|
||||||
<system:String x:Key="newActionKeywordsCannotBeEmpty">New Action Keyword can't be empty</system:String>
|
<system:String x:Key="newActionKeywordsCannotBeEmpty">New Action Keyword can't be empty</system:String>
|
||||||
<system:String x:Key="newActionKeywordsHasBeenAssigned">This new Action Keyword is already assigned to another plugin, please choose a different one</system:String>
|
<system:String x:Key="newActionKeywordsHasBeenAssigned">This new Action Keyword is already assigned to another plugin, please choose a different one</system:String>
|
||||||
<system:String x:Key="success">Success</system:String>
|
<system:String x:Key="success">Success</system:String>
|
||||||
|
<system:String x:Key="completedSuccessfully">Completed successfully</system:String>
|
||||||
<system:String x:Key="actionkeyword_tips">Use * if you don't want to specify an action keyword</system:String>
|
<system:String x:Key="actionkeyword_tips">Use * if you don't want to specify an action keyword</system:String>
|
||||||
|
|
||||||
<!--Custom Query Hotkey Dialog-->
|
<!--Custom Query Hotkey Dialog-->
|
||||||
|
|
@ -131,7 +139,7 @@
|
||||||
<system:String x:Key="update">Update</system:String>
|
<system:String x:Key="update">Update</system:String>
|
||||||
|
|
||||||
<!--Hotkey Control-->
|
<!--Hotkey Control-->
|
||||||
<system:String x:Key="hotkeyUnavailable">Hotkey unavailable</system:String>
|
<system:String x:Key="hotkeyUnavailable">Hotkey Unavailable</system:String>
|
||||||
|
|
||||||
<!--Crash Reporter-->
|
<!--Crash Reporter-->
|
||||||
<system:String x:Key="reportWindow_version">Version</system:String>
|
<system:String x:Key="reportWindow_version">Version</system:String>
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
<system:String x:Key="hideNotifyIcon">Schovať ikonu z oblasti oznámení</system:String>
|
<system:String x:Key="hideNotifyIcon">Schovať ikonu z oblasti oznámení</system:String>
|
||||||
<system:String x:Key="querySearchPrecision">Presnosť vyhľadávania</system:String>
|
<system:String x:Key="querySearchPrecision">Presnosť vyhľadávania</system:String>
|
||||||
<system:String x:Key="ShouldUsePinyin">Použiť Pinyin</system:String>
|
<system:String x:Key="ShouldUsePinyin">Použiť Pinyin</system:String>
|
||||||
|
<system:String x:Key="ShouldUsePinyinToolTip">Umožňuje vyhľadávanie pomocou Pinyin. Pinyin je štandardný systém romanizovaného pravopisu pre transliteráciu čínštiny</system:String>
|
||||||
|
|
||||||
<!--Setting Plugin-->
|
<!--Setting Plugin-->
|
||||||
<system:String x:Key="plugin">Plugin</system:String>
|
<system:String x:Key="plugin">Plugin</system:String>
|
||||||
|
|
@ -107,6 +108,7 @@
|
||||||
alebo prejdite na https://github.com/Flow-Launcher/Flow.Launcher/releases pre manuálne stiahnutie aktualizácie.
|
alebo prejdite na https://github.com/Flow-Launcher/Flow.Launcher/releases pre manuálne stiahnutie aktualizácie.
|
||||||
</system:String>
|
</system:String>
|
||||||
<system:String x:Key="releaseNotes">Poznámky k vydaniu</system:String>
|
<system:String x:Key="releaseNotes">Poznámky k vydaniu</system:String>
|
||||||
|
<system:String x:Key="documentation">Tipy na používanie:</system:String>
|
||||||
|
|
||||||
<!--Priority Setting Dialog-->
|
<!--Priority Setting Dialog-->
|
||||||
<system:String x:Key="priority_tips">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</system:String>
|
<system:String x:Key="priority_tips">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</system:String>
|
||||||
|
|
@ -121,6 +123,7 @@
|
||||||
<system:String x:Key="newActionKeywordsCannotBeEmpty">Nová skratka pre akciu nemôže byť prázdna</system:String>
|
<system:String x:Key="newActionKeywordsCannotBeEmpty">Nová skratka pre akciu nemôže byť prázdna</system:String>
|
||||||
<system:String x:Key="newActionKeywordsHasBeenAssigned">Nová skratka pre akciu bola priradená pre iný plugin, prosím, zvoľte inú skratku</system:String>
|
<system:String x:Key="newActionKeywordsHasBeenAssigned">Nová skratka pre akciu bola priradená pre iný plugin, prosím, zvoľte inú skratku</system:String>
|
||||||
<system:String x:Key="success">Úspešné</system:String>
|
<system:String x:Key="success">Úspešné</system:String>
|
||||||
|
<system:String x:Key="completedSuccessfully">Úspešne dokončené</system:String>
|
||||||
<system:String x:Key="actionkeyword_tips">Použite * ak nechcete určiť skratku pre akciu</system:String>
|
<system:String x:Key="actionkeyword_tips">Použite * ak nechcete určiť skratku pre akciu</system:String>
|
||||||
|
|
||||||
<!--Custom Query Hotkey Dialog-->
|
<!--Custom Query Hotkey Dialog-->
|
||||||
|
|
@ -158,7 +161,7 @@
|
||||||
<system:String x:Key="update_flowlauncher_update_found">Bola nájdená aktualizácia</system:String>
|
<system:String x:Key="update_flowlauncher_update_found">Bola nájdená aktualizácia</system:String>
|
||||||
<system:String x:Key="update_flowlauncher_updating">Aktualizuje sa…</system:String>
|
<system:String x:Key="update_flowlauncher_updating">Aktualizuje sa…</system:String>
|
||||||
<system:String x:Key="update_flowlauncher_fail_moving_portable_user_profile_data">Flow Launcher nedokázal presunúť používateľské údaje do aktualizovanej verzie.
|
<system:String x:Key="update_flowlauncher_fail_moving_portable_user_profile_data">Flow Launcher nedokázal presunúť používateľské údaje do aktualizovanej verzie.
|
||||||
Prosím, presuňte profilový priečinok „data“ z {0} do {1}</system:String>
|
Prosím, presuňte profilový priečinok data z {0} do {1}</system:String>
|
||||||
<system:String x:Key="update_flowlauncher_new_update">Nová aktualizácia</system:String>
|
<system:String x:Key="update_flowlauncher_new_update">Nová aktualizácia</system:String>
|
||||||
<system:String x:Key="update_flowlauncher_update_new_version_available">Je dostupná nová verzia Flow Launchera {0}</system:String>
|
<system:String x:Key="update_flowlauncher_update_new_version_available">Je dostupná nová verzia Flow Launchera {0}</system:String>
|
||||||
<system:String x:Key="update_flowlauncher_update_error">Počas inštalácie aktualizácií došlo k chybe</system:String>
|
<system:String x:Key="update_flowlauncher_update_error">Počas inštalácie aktualizácií došlo k chybe</system:String>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<!--主窗体-->
|
<!--主窗体-->
|
||||||
<system:String x:Key="registerHotkeyFailed">注册热键:{0} 失败</system:String>
|
<system:String x:Key="registerHotkeyFailed">注册热键:{0} 失败</system:String>
|
||||||
<system:String x:Key="couldnotStartCmd">启动命令 {0} 失败</system:String>
|
<system:String x:Key="couldnotStartCmd">启动命令 {0} 失败</system:String>
|
||||||
<system:String x:Key="invalidFlowLauncherPluginFileFormat">不是合法的Flow Launcher插件格式</system:String>
|
<system:String x:Key="invalidFlowLauncherPluginFileFormat">Flow Launcher插件格式错误</system:String>
|
||||||
<system:String x:Key="setAsTopMostInThisQuery">在当前查询中置顶</system:String>
|
<system:String x:Key="setAsTopMostInThisQuery">在当前查询中置顶</system:String>
|
||||||
<system:String x:Key="cancelTopMostInThisQuery">取消置顶</system:String>
|
<system:String x:Key="cancelTopMostInThisQuery">取消置顶</system:String>
|
||||||
<system:String x:Key="executeQuery">执行查询:{0}</system:String>
|
<system:String x:Key="executeQuery">执行查询:{0}</system:String>
|
||||||
|
|
@ -17,28 +17,37 @@
|
||||||
<!--设置,通用-->
|
<!--设置,通用-->
|
||||||
<system:String x:Key="flowlauncher_settings">Flow Launcher设置</system:String>
|
<system:String x:Key="flowlauncher_settings">Flow Launcher设置</system:String>
|
||||||
<system:String x:Key="general">通用</system:String>
|
<system:String x:Key="general">通用</system:String>
|
||||||
<system:String x:Key="startFlowLauncherOnSystemStartup">开机启动</system:String>
|
<system:String x:Key="portableMode">便携模式</system:String>
|
||||||
|
<system:String x:Key="startFlowLauncherOnSystemStartup">开机自动启动</system:String>
|
||||||
<system:String x:Key="hideFlowLauncherWhenLoseFocus">失去焦点时自动隐藏Flow Launcher</system:String>
|
<system:String x:Key="hideFlowLauncherWhenLoseFocus">失去焦点时自动隐藏Flow Launcher</system:String>
|
||||||
<system:String x:Key="dontPromptUpdateMsg">不显示新版本提示</system:String>
|
<system:String x:Key="dontPromptUpdateMsg">不显示新版本提示</system:String>
|
||||||
<system:String x:Key="rememberLastLocation">记住上次启动位置</system:String>
|
<system:String x:Key="rememberLastLocation">记住上次启动位置</system:String>
|
||||||
<system:String x:Key="language">语言</system:String>
|
<system:String x:Key="language">语言</system:String>
|
||||||
<system:String x:Key="lastQueryMode">上次搜索关键字模式</system:String>
|
<system:String x:Key="lastQueryMode">上次搜索关键字模式</system:String>
|
||||||
<system:String x:Key="LastQueryPreserved">保留上次搜索关键字</system:String>
|
<system:String x:Key="LastQueryPreserved">保留上次搜索关键字</system:String>
|
||||||
<system:String x:Key="LastQuerySelected">全选上次搜索关键字</system:String>
|
<system:String x:Key="LastQuerySelected">选择上次搜索关键字</system:String>
|
||||||
<system:String x:Key="LastQueryEmpty">清空上次搜索关键字</system:String>
|
<system:String x:Key="LastQueryEmpty">清空上次搜索关键字</system:String>
|
||||||
<system:String x:Key="maxShowResults">最大结果显示个数</system:String>
|
<system:String x:Key="maxShowResults">最大结果显示个数</system:String>
|
||||||
<system:String x:Key="ignoreHotkeysOnFullscreen">全屏模式下忽略热键</system:String>
|
<system:String x:Key="ignoreHotkeysOnFullscreen">全屏模式下忽略热键</system:String>
|
||||||
<system:String x:Key="pythonDirectory">Python 路径</system:String>
|
<system:String x:Key="pythonDirectory">Python 路径</system:String>
|
||||||
<system:String x:Key="autoUpdates">自动更新</system:String>
|
<system:String x:Key="autoUpdates">自动更新</system:String>
|
||||||
<system:String x:Key="selectPythonDirectory">Select</system:String>
|
<system:String x:Key="selectPythonDirectory">选择</system:String>
|
||||||
<system:String x:Key="hideOnStartup">启动时不显示主窗口</system:String>
|
<system:String x:Key="hideOnStartup">启动时不显示主窗口</system:String>
|
||||||
<system:String x:Key="hideNotifyIcon">隐藏任务栏图标</system:String>
|
<system:String x:Key="hideNotifyIcon">隐藏任务栏图标</system:String>
|
||||||
|
<system:String x:Key="querySearchPrecision">查询搜索精度</system:String>
|
||||||
|
<system:String x:Key="ShouldUsePinyin">启动拼音搜索</system:String>
|
||||||
|
<system:String x:Key="ShouldUsePinyinToolTip">允许使用拼音进行搜索。</system:String>
|
||||||
|
|
||||||
<!--设置,插件-->
|
<!--设置,插件-->
|
||||||
<system:String x:Key="plugin">插件</system:String>
|
<system:String x:Key="plugin">插件</system:String>
|
||||||
<system:String x:Key="browserMorePlugins">浏览更多插件</system:String>
|
<system:String x:Key="browserMorePlugins">浏览更多插件</system:String>
|
||||||
|
<system:String x:Key="enable">启用</system:String>
|
||||||
<system:String x:Key="disable">禁用</system:String>
|
<system:String x:Key="disable">禁用</system:String>
|
||||||
<system:String x:Key="actionKeywords">触发关键字</system:String>
|
<system:String x:Key="actionKeywords">触发关键字</system:String>
|
||||||
|
<system:String x:Key="currentActionKeywords">当前操作关键字:</system:String>
|
||||||
|
<system:String x:Key="newActionKeyword">新动作关键字:</system:String>
|
||||||
|
<system:String x:Key="currentPriority">当前优先级:</system:String>
|
||||||
|
<system:String x:Key="newPriority">新优先级:</system:String>
|
||||||
<system:String x:Key="pluginDirectory">插件目录</system:String>
|
<system:String x:Key="pluginDirectory">插件目录</system:String>
|
||||||
<system:String x:Key="author">作者</system:String>
|
<system:String x:Key="author">作者</system:String>
|
||||||
<system:String x:Key="plugin_init_time">加载耗时</system:String>
|
<system:String x:Key="plugin_init_time">加载耗时</system:String>
|
||||||
|
|
@ -47,6 +56,7 @@
|
||||||
<!--设置,主题-->
|
<!--设置,主题-->
|
||||||
<system:String x:Key="theme">主题</system:String>
|
<system:String x:Key="theme">主题</system:String>
|
||||||
<system:String x:Key="browserMoreThemes">浏览更多主题</system:String>
|
<system:String x:Key="browserMoreThemes">浏览更多主题</system:String>
|
||||||
|
<system:String x:Key="hiThere">在这里输入</system:String>
|
||||||
<system:String x:Key="queryBoxFont">查询框字体</system:String>
|
<system:String x:Key="queryBoxFont">查询框字体</system:String>
|
||||||
<system:String x:Key="resultItemFont">结果项字体</system:String>
|
<system:String x:Key="resultItemFont">结果项字体</system:String>
|
||||||
<system:String x:Key="windowMode">窗口模式</system:String>
|
<system:String x:Key="windowMode">窗口模式</system:String>
|
||||||
|
|
@ -65,6 +75,9 @@
|
||||||
<system:String x:Key="add">增加</system:String>
|
<system:String x:Key="add">增加</system:String>
|
||||||
<system:String x:Key="pleaseSelectAnItem">请选择一项</system:String>
|
<system:String x:Key="pleaseSelectAnItem">请选择一项</system:String>
|
||||||
<system:String x:Key="deleteCustomHotkeyWarning">你确定要删除插件 {0} 的热键吗?</system:String>
|
<system:String x:Key="deleteCustomHotkeyWarning">你确定要删除插件 {0} 的热键吗?</system:String>
|
||||||
|
<system:String x:Key="queryWindowShadowEffect">查询窗口阴影效果</system:String>
|
||||||
|
<system:String x:Key="shadowEffectCPUUsage">阴影效果将占用大量的GPU资源。</system:String>
|
||||||
|
<system:String x:Key="shadowEffectPerformance">如果您的计算机性能有限,则不建议使用。</system:String>
|
||||||
|
|
||||||
<!--设置,代理-->
|
<!--设置,代理-->
|
||||||
<system:String x:Key="proxy">HTTP 代理</system:String>
|
<system:String x:Key="proxy">HTTP 代理</system:String>
|
||||||
|
|
@ -88,8 +101,18 @@
|
||||||
<system:String x:Key="version">版本</system:String>
|
<system:String x:Key="version">版本</system:String>
|
||||||
<system:String x:Key="about_activate_times">你已经激活了Flow Launcher {0} 次</system:String>
|
<system:String x:Key="about_activate_times">你已经激活了Flow Launcher {0} 次</system:String>
|
||||||
<system:String x:Key="checkUpdates">检查更新</system:String>
|
<system:String x:Key="checkUpdates">检查更新</system:String>
|
||||||
<system:String x:Key="newVersionTips">发现新版本 {0} , 请重启 flowlauncher。</system:String>
|
<system:String x:Key="newVersionTips">发现新版本 {0} , 请重启 Flow Launcher。</system:String>
|
||||||
|
<system:String x:Key="checkUpdatesFailed">下载更新失败,请检查您与 api.github.com 的连接状态或检查代理设置。</system:String>
|
||||||
|
<system:String x:Key="downloadUpdatesFailed">
|
||||||
|
下载更新失败,请检查您与 github-cloud.s3.amazonaws.com 的连接状态或检查代理设置,
|
||||||
|
或访问 https://github.com/Flow-Launcher/Flow.Launcher/releases 手动下载更新。
|
||||||
|
</system:String>
|
||||||
<system:String x:Key="releaseNotes">更新说明:</system:String>
|
<system:String x:Key="releaseNotes">更新说明:</system:String>
|
||||||
|
<system:String x:Key="documentation">使用技巧:</system:String>
|
||||||
|
|
||||||
|
<!--优先级设置对话框-->
|
||||||
|
<system:String x:Key="priority_tips">数字越大,结果排名越高。如果你想要结果比任何其他插件的低,请使用负数</system:String>
|
||||||
|
<system:String x:Key="invalidPriority">请提供有效的整数作为优先级设置值!</system:String>
|
||||||
|
|
||||||
<!--Action Keyword 设置对话框-->
|
<!--Action Keyword 设置对话框-->
|
||||||
<system:String x:Key="oldActionKeywords">旧触发关键字</system:String>
|
<system:String x:Key="oldActionKeywords">旧触发关键字</system:String>
|
||||||
|
|
@ -98,11 +121,13 @@
|
||||||
<system:String x:Key="done">确定</system:String>
|
<system:String x:Key="done">确定</system:String>
|
||||||
<system:String x:Key="cannotFindSpecifiedPlugin">找不到指定的插件</system:String>
|
<system:String x:Key="cannotFindSpecifiedPlugin">找不到指定的插件</system:String>
|
||||||
<system:String x:Key="newActionKeywordsCannotBeEmpty">新触发关键字不能为空</system:String>
|
<system:String x:Key="newActionKeywordsCannotBeEmpty">新触发关键字不能为空</system:String>
|
||||||
<system:String x:Key="newActionKeywordsHasBeenAssigned">新触发关键字已经被指派给其他插件了,请重新选择一个关键字</system:String>
|
<system:String x:Key="newActionKeywordsHasBeenAssigned">新触发关键字已经被指派给其他插件了,请换一个关键字</system:String>
|
||||||
<system:String x:Key="success">成功</system:String>
|
<system:String x:Key="success">成功</system:String>
|
||||||
|
<system:String x:Key="completedSuccessfully">成功完成</system:String>
|
||||||
<system:String x:Key="actionkeyword_tips">如果你不想设置触发关键字,可以使用*代替</system:String>
|
<system:String x:Key="actionkeyword_tips">如果你不想设置触发关键字,可以使用*代替</system:String>
|
||||||
|
|
||||||
<!--Custom Query Hotkey 对话框-->
|
<!--Custom Query Hotkey 对话框-->
|
||||||
|
<system:String x:Key="customeQueryHotkeyTitle">自定义插件热键</system:String>
|
||||||
<system:String x:Key="preview">预览</system:String>
|
<system:String x:Key="preview">预览</system:String>
|
||||||
<system:String x:Key="hotkeyIsNotUnavailable">热键不可用,请选择一个新的热键</system:String>
|
<system:String x:Key="hotkeyIsNotUnavailable">热键不可用,请选择一个新的热键</system:String>
|
||||||
<system:String x:Key="invalidPluginHotkey">插件热键不合法</system:String>
|
<system:String x:Key="invalidPluginHotkey">插件热键不合法</system:String>
|
||||||
|
|
@ -127,12 +152,24 @@
|
||||||
<system:String x:Key="reportWindow_report_failed">发送失败</system:String>
|
<system:String x:Key="reportWindow_report_failed">发送失败</system:String>
|
||||||
<system:String x:Key="reportWindow_flowlauncher_got_an_error">Flow Launcher出错啦</system:String>
|
<system:String x:Key="reportWindow_flowlauncher_got_an_error">Flow Launcher出错啦</system:String>
|
||||||
|
|
||||||
|
<!--General Notice-->
|
||||||
|
<system:String x:Key="pleaseWait">请稍等...</system:String>
|
||||||
|
|
||||||
<!--更新-->
|
<!--更新-->
|
||||||
|
<system:String x:Key="update_flowlauncher_update_check">检查新的更新</system:String>
|
||||||
|
<system:String x:Key="update_flowlauncher_already_on_latest">您已经拥有最新的Flow Launcher版本</system:String>
|
||||||
|
<system:String x:Key="update_flowlauncher_update_found">检查到更新</system:String>
|
||||||
|
<system:String x:Key="update_flowlauncher_updating">更新中...</system:String>
|
||||||
|
<system:String x:Key="update_flowlauncher_fail_moving_portable_user_profile_data">Flow Launcher无法将您的用户配置文件数据移动到新的更新版本中。
|
||||||
|
请手动将您的用户配置文件数据文件夹从 {0} 到 {1}</system:String>
|
||||||
|
<system:String x:Key="update_flowlauncher_new_update">新的更新</system:String>
|
||||||
<system:String x:Key="update_flowlauncher_update_new_version_available">发现Flow Launcher新版本 V{0}</system:String>
|
<system:String x:Key="update_flowlauncher_update_new_version_available">发现Flow Launcher新版本 V{0}</system:String>
|
||||||
<system:String x:Key="update_flowlauncher_update_error">更新Flow Launcher出错</system:String>
|
<system:String x:Key="update_flowlauncher_update_error">尝试安装软件更新时发生错误</system:String>
|
||||||
<system:String x:Key="update_flowlauncher_update">更新</system:String>
|
<system:String x:Key="update_flowlauncher_update">更新</system:String>
|
||||||
<system:String x:Key="update_flowlauncher_update_cancel">取消</system:String>
|
<system:String x:Key="update_flowlauncher_update_cancel">取消</system:String>
|
||||||
<system:String x:Key="update_flowlauncher_update_restart_flowlauncher_tip">此更新需要重启Flow Launcher</system:String>
|
<system:String x:Key="update_flowlauncher_fail">更新错误</system:String>
|
||||||
|
<system:String x:Key="update_flowlauncher_check_connection">检查网络是否可以连接至github-cloud.s3.amazonaws.com.</system:String>
|
||||||
|
<system:String x:Key="update_flowlauncher_update_restart_flowlauncher_tip">此次更新需要重启Flow Launcher</system:String>
|
||||||
<system:String x:Key="update_flowlauncher_update_upadte_files">下列文件会被更新</system:String>
|
<system:String x:Key="update_flowlauncher_update_upadte_files">下列文件会被更新</system:String>
|
||||||
<system:String x:Key="update_flowlauncher_update_files">更新文件</system:String>
|
<system:String x:Key="update_flowlauncher_update_files">更新文件</system:String>
|
||||||
<system:String x:Key="update_flowlauncher_update_upadte_description">更新日志</system:String>
|
<system:String x:Key="update_flowlauncher_update_upadte_description">更新日志</system:String>
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
<Window.InputBindings>
|
<Window.InputBindings>
|
||||||
<KeyBinding Key="Escape" Command="{Binding EscCommand}"></KeyBinding>
|
<KeyBinding Key="Escape" Command="{Binding EscCommand}"></KeyBinding>
|
||||||
<KeyBinding Key="F1" Command="{Binding StartHelpCommand}"></KeyBinding>
|
<KeyBinding Key="F1" Command="{Binding StartHelpCommand}"></KeyBinding>
|
||||||
|
<KeyBinding Key="F5" Command="{Binding ReloadPluginDataCommand}"></KeyBinding>
|
||||||
<KeyBinding Key="Tab" Command="{Binding SelectNextItemCommand}"></KeyBinding>
|
<KeyBinding Key="Tab" Command="{Binding SelectNextItemCommand}"></KeyBinding>
|
||||||
<KeyBinding Key="Tab" Modifiers="Shift" Command="{Binding SelectPrevItemCommand}"></KeyBinding>
|
<KeyBinding Key="Tab" Modifiers="Shift" Command="{Binding SelectPrevItemCommand}"></KeyBinding>
|
||||||
<KeyBinding Key="N" Modifiers="Ctrl" Command="{Binding SelectNextItemCommand}"></KeyBinding>
|
<KeyBinding Key="N" Modifiers="Ctrl" Command="{Binding SelectNextItemCommand}"></KeyBinding>
|
||||||
|
|
@ -59,7 +60,7 @@
|
||||||
<KeyBinding Key="D8" Modifiers="{Binding OpenResultCommandModifiers}" Command="{Binding OpenResultCommand}" CommandParameter="7"></KeyBinding>
|
<KeyBinding Key="D8" Modifiers="{Binding OpenResultCommandModifiers}" Command="{Binding OpenResultCommand}" CommandParameter="7"></KeyBinding>
|
||||||
<KeyBinding Key="D9" Modifiers="{Binding OpenResultCommandModifiers}" Command="{Binding OpenResultCommand}" CommandParameter="8"></KeyBinding>
|
<KeyBinding Key="D9" Modifiers="{Binding OpenResultCommandModifiers}" Command="{Binding OpenResultCommand}" CommandParameter="8"></KeyBinding>
|
||||||
</Window.InputBindings>
|
</Window.InputBindings>
|
||||||
<Grid Width="750">
|
<Grid>
|
||||||
<Border Style="{DynamicResource WindowBorderStyle}" MouseDown="OnMouseDown" CornerRadius="5" >
|
<Border Style="{DynamicResource WindowBorderStyle}" MouseDown="OnMouseDown" CornerRadius="5" >
|
||||||
<StackPanel Orientation="Vertical">
|
<StackPanel Orientation="Vertical">
|
||||||
<Grid>
|
<Grid>
|
||||||
|
|
@ -78,7 +79,6 @@
|
||||||
Style="{DynamicResource QueryBoxStyle}"
|
Style="{DynamicResource QueryBoxStyle}"
|
||||||
Text="{Binding QueryText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
Text="{Binding QueryText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||||
PreviewDragOver="OnPreviewDragOver"
|
PreviewDragOver="OnPreviewDragOver"
|
||||||
TextChanged="OnTextChanged"
|
|
||||||
AllowDrop="True"
|
AllowDrop="True"
|
||||||
Visibility="Visible"
|
Visibility="Visible"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Media.Animation;
|
using System.Windows.Media.Animation;
|
||||||
|
|
@ -10,6 +11,7 @@ using Flow.Launcher.Core.Resource;
|
||||||
using Flow.Launcher.Helper;
|
using Flow.Launcher.Helper;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
using Flow.Launcher.ViewModel;
|
using Flow.Launcher.ViewModel;
|
||||||
|
using Application = System.Windows.Application;
|
||||||
using Screen = System.Windows.Forms.Screen;
|
using Screen = System.Windows.Forms.Screen;
|
||||||
using ContextMenuStrip = System.Windows.Forms.ContextMenuStrip;
|
using ContextMenuStrip = System.Windows.Forms.ContextMenuStrip;
|
||||||
using DataFormats = System.Windows.DataFormats;
|
using DataFormats = System.Windows.DataFormats;
|
||||||
|
|
@ -22,7 +24,6 @@ namespace Flow.Launcher
|
||||||
{
|
{
|
||||||
public partial class MainWindow
|
public partial class MainWindow
|
||||||
{
|
{
|
||||||
|
|
||||||
#region Private Fields
|
#region Private Fields
|
||||||
|
|
||||||
private readonly Storyboard _progressBarStoryboard = new Storyboard();
|
private readonly Storyboard _progressBarStoryboard = new Storyboard();
|
||||||
|
|
@ -40,20 +41,23 @@ namespace Flow.Launcher
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnClosing(object sender, CancelEventArgs e)
|
private async void OnClosing(object sender, CancelEventArgs e)
|
||||||
{
|
{
|
||||||
_notifyIcon.Visible = false;
|
_notifyIcon.Visible = false;
|
||||||
_viewModel.Save();
|
_viewModel.Save();
|
||||||
|
e.Cancel = true;
|
||||||
|
await PluginManager.DisposePluginsAsync();
|
||||||
|
Application.Current.Shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnInitialized(object sender, EventArgs e)
|
private void OnInitialized(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnLoaded(object sender, RoutedEventArgs _)
|
private void OnLoaded(object sender, RoutedEventArgs _)
|
||||||
|
|
@ -61,8 +65,6 @@ namespace Flow.Launcher
|
||||||
// show notify icon when flowlauncher is hidden
|
// show notify icon when flowlauncher is hidden
|
||||||
InitializeNotifyIcon();
|
InitializeNotifyIcon();
|
||||||
|
|
||||||
// todo is there a way to set blur only once?
|
|
||||||
ThemeManager.Instance.SetBlurForWindow();
|
|
||||||
WindowsInteropHelper.DisableControlBox(this);
|
WindowsInteropHelper.DisableControlBox(this);
|
||||||
InitProgressbarAnimation();
|
InitProgressbarAnimation();
|
||||||
InitializePosition();
|
InitializePosition();
|
||||||
|
|
@ -72,47 +74,64 @@ namespace Flow.Launcher
|
||||||
|
|
||||||
_viewModel.PropertyChanged += (o, e) =>
|
_viewModel.PropertyChanged += (o, e) =>
|
||||||
{
|
{
|
||||||
if (e.PropertyName == nameof(MainViewModel.MainWindowVisibility))
|
switch (e.PropertyName)
|
||||||
{
|
{
|
||||||
if (_viewModel.MainWindowVisibility == Visibility.Visible)
|
case nameof(MainViewModel.MainWindowVisibility):
|
||||||
{
|
{
|
||||||
Activate();
|
if (_viewModel.MainWindowVisibility == Visibility.Visible)
|
||||||
QueryTextBox.Focus();
|
|
||||||
UpdatePosition();
|
|
||||||
_settings.ActivateTimes++;
|
|
||||||
if (!_viewModel.LastQuerySelected)
|
|
||||||
{
|
{
|
||||||
QueryTextBox.SelectAll();
|
Activate();
|
||||||
_viewModel.LastQuerySelected = true;
|
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();
|
_progressBarStoryboard.Stop(ProgressBar);
|
||||||
isProgressBarStoryboardPaused = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (!isProgressBarStoryboardPaused)
|
|
||||||
{
|
|
||||||
_progressBarStoryboard.Pause();
|
|
||||||
isProgressBarStoryboardPaused = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (e.PropertyName == nameof(MainViewModel.ProgressBarVisibility))
|
|
||||||
{
|
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
if (_viewModel.ProgressBarVisibility == Visibility.Hidden && !isProgressBarStoryboardPaused)
|
|
||||||
{
|
|
||||||
_progressBarStoryboard.Pause();
|
|
||||||
isProgressBarStoryboardPaused = true;
|
isProgressBarStoryboardPaused = true;
|
||||||
}
|
}
|
||||||
else if (_viewModel.MainWindowVisibility == Visibility.Visible && isProgressBarStoryboardPaused)
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case nameof(MainViewModel.ProgressBarVisibility):
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(async () =>
|
||||||
{
|
{
|
||||||
_progressBarStoryboard.Resume();
|
if (_viewModel.ProgressBarVisibility == Visibility.Hidden && !isProgressBarStoryboardPaused)
|
||||||
isProgressBarStoryboardPaused = false;
|
{
|
||||||
|
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) =>
|
_settings.PropertyChanged += (o, e) =>
|
||||||
|
|
@ -189,14 +208,15 @@ namespace Flow.Launcher
|
||||||
|
|
||||||
private void InitProgressbarAnimation()
|
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)));
|
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(da, new PropertyPath("(Line.X2)"));
|
||||||
Storyboard.SetTargetProperty(da1, new PropertyPath("(Line.X1)"));
|
Storyboard.SetTargetProperty(da1, new PropertyPath("(Line.X1)"));
|
||||||
_progressBarStoryboard.Children.Add(da);
|
_progressBarStoryboard.Children.Add(da);
|
||||||
_progressBarStoryboard.Children.Add(da1);
|
_progressBarStoryboard.Children.Add(da1);
|
||||||
_progressBarStoryboard.RepeatBehavior = RepeatBehavior.Forever;
|
_progressBarStoryboard.RepeatBehavior = RepeatBehavior.Forever;
|
||||||
ProgressBar.BeginStoryboard(_progressBarStoryboard);
|
|
||||||
_viewModel.ProgressBarVisibility = Visibility.Hidden;
|
_viewModel.ProgressBarVisibility = Visibility.Hidden;
|
||||||
isProgressBarStoryboardPaused = true;
|
isProgressBarStoryboardPaused = true;
|
||||||
}
|
}
|
||||||
|
|
@ -210,10 +230,10 @@ namespace Flow.Launcher
|
||||||
{
|
{
|
||||||
if (sender != null && e.OriginalSource != null)
|
if (sender != null && e.OriginalSource != null)
|
||||||
{
|
{
|
||||||
var r = (ResultListBox)sender;
|
var r = (ResultListBox) sender;
|
||||||
var d = (DependencyObject)e.OriginalSource;
|
var d = (DependencyObject) e.OriginalSource;
|
||||||
var item = ItemsControl.ContainerFromElement(r, d) as ListBoxItem;
|
var item = ItemsControl.ContainerFromElement(r, d) as ListBoxItem;
|
||||||
var result = (ResultViewModel)item?.DataContext;
|
var result = (ResultViewModel) item?.DataContext;
|
||||||
if (result != null)
|
if (result != null)
|
||||||
{
|
{
|
||||||
if (e.ChangedButton == MouseButton.Left)
|
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;
|
||||||
{
|
|
||||||
QueryTextBox.CaretIndex = QueryTextBox.Text.Length;
|
|
||||||
_viewModel.QueryTextCursorMovedToEnd = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||