Merge pull request #363 from Flow-Launcher/dev

Release 1.8.0 | Plugin 2.0.0
This commit is contained in:
Jeremy Wu 2021-07-19 14:08:51 +10:00 committed by GitHub
commit aeba056df0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
225 changed files with 3538 additions and 1937 deletions

View file

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View file

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View file

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View file

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View file

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View file

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View file

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View file

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View file

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View file

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View file

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View file

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View file

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View file

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View file

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View file

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View file

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View file

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View file

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View file

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View file

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 114 KiB

View file

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

Before

Width:  |  Height:  |  Size: 409 B

After

Width:  |  Height:  |  Size: 409 B

View file

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

Before

Width:  |  Height:  |  Size: 663 B

After

Width:  |  Height:  |  Size: 663 B

View file

Before

Width:  |  Height:  |  Size: 1,019 B

After

Width:  |  Height:  |  Size: 1,019 B

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 114 KiB

View file

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View file

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View file

@ -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" />

View file

@ -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}\"";

View file

@ -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; }
} }
} }

View file

@ -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;
} }
} }
} }

View file

@ -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);
} }
} }
} }

View file

@ -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)

View file

@ -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
} }
} }
} }
} }

View file

@ -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
}); });
} }
} }

View file

@ -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;
}
} }
} }

View file

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Flow.Launcher.Test")]

View 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.");
}
}
}

View file

@ -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);

View file

@ -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"),

View file

@ -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");

View file

@ -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>

View file

@ -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();
} }
} }

View file

@ -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);
} }

View file

@ -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()
{ {

View file

@ -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();
}
}

View file

@ -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() { }
} }

View file

@ -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;
}
} }
} }

View file

@ -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;
} }

View file

@ -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))]

View file

@ -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;
}
}

View 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
{
}
}

View file

@ -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>

View file

@ -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);
}
}

View file

@ -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();
} }
} }

View file

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace Flow.Launcher.Plugin
{
public interface IContextMenu : IFeatures
{
List<Result> LoadContextMenus(Result selectedResult);
}
}

View file

@ -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));
} }
} }

View file

@ -0,0 +1,12 @@
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Represent plugins that support internationalization
/// </summary>
public interface IPluginI18n : IFeatures
{
string GetTranslatedPluginTitle();
string GetTranslatedPluginDescription();
}
}

View 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();
}
}

View file

@ -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();
} }
} }

View 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; }
}
}

View 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();
}
}

View file

@ -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; }

View file

@ -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

View file

@ -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.

View file

@ -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>

View file

@ -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);

View 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);
}
}
}
}

View file

@ -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;

View file

@ -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)

View file

@ -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>

View file

@ -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;
} }
} }
} }

View 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;
}
}
}

View file

@ -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;

View file

@ -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"/>

View file

@ -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
{ {

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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"

View file

@ -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;
}
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show more