mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into cacheProgramResult
This commit is contained in:
commit
d8bea3cb89
97 changed files with 1776 additions and 1095 deletions
|
|
@ -53,15 +53,12 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Droplex" Version="1.2.0" />
|
||||
<PackageReference Include="Droplex" Version="1.3.1" />
|
||||
<PackageReference Include="FSharp.Core" Version="4.7.1" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.1.3" />
|
||||
<PackageReference Include="squirrel.windows" Version="1.5.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Properties\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
|
||||
/* We basically follow the Json-RPC 2.0 spec (http://www.jsonrpc.org/specification) to invoke methods between Flow Launcher and other plugins,
|
||||
/* We basically follow the Json-RPC 2.0 spec (http://www.jsonrpc.org/specification) to invoke methods between Flow Launcher and other plugins,
|
||||
* like python or other self-execute program. But, we added addtional infos (proxy and so on) into rpc request. Also, we didn't use the
|
||||
* "id" and "jsonrpc" in the request, since it's not so useful in our request model.
|
||||
*
|
||||
|
|
@ -13,10 +12,12 @@
|
|||
*
|
||||
*/
|
||||
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
|
|
@ -29,12 +30,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
public string Data { get; set; }
|
||||
}
|
||||
|
||||
public class JsonRPCModelBase
|
||||
{
|
||||
public int Id { get; set; }
|
||||
}
|
||||
|
||||
public class JsonRPCResponseModel : JsonRPCModelBase
|
||||
public class JsonRPCResponseModel
|
||||
{
|
||||
public string Result { get; set; }
|
||||
|
||||
|
|
@ -48,45 +45,20 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
public string DebugMessage { get; set; }
|
||||
}
|
||||
|
||||
public class JsonRPCRequestModel : JsonRPCModelBase
|
||||
|
||||
public class JsonRPCRequestModel
|
||||
{
|
||||
public string Method { get; set; }
|
||||
|
||||
public object[] Parameters { get; set; }
|
||||
|
||||
private static readonly JsonSerializerOptions options = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
public override string ToString()
|
||||
{
|
||||
string rpc = string.Empty;
|
||||
if (Parameters != null && Parameters.Length > 0)
|
||||
{
|
||||
string parameters = $"[{string.Join(',', Parameters.Select(GetParameterByType))}]";
|
||||
rpc = $@"{{\""method\"":\""{Method}\"",\""parameters\"":{parameters}";
|
||||
}
|
||||
else
|
||||
{
|
||||
rpc = $@"{{\""method\"":\""{Method}\"",\""parameters\"":[]";
|
||||
}
|
||||
|
||||
return rpc;
|
||||
|
||||
}
|
||||
|
||||
private string GetParameterByType(object parameter)
|
||||
=> parameter switch
|
||||
{
|
||||
null => "null",
|
||||
string _ => $@"\""{ReplaceEscapes(parameter.ToString())}\""",
|
||||
bool _ => $@"{parameter.ToString().ToLower()}",
|
||||
_ => parameter.ToString()
|
||||
};
|
||||
|
||||
|
||||
private string ReplaceEscapes(string str)
|
||||
{
|
||||
return str.Replace(@"\", @"\\") //Escapes in ProcessStartInfo
|
||||
.Replace(@"\", @"\\") //Escapes itself when passed to client
|
||||
.Replace(@"""", @"\\""""");
|
||||
return JsonSerializer.Serialize(this, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -95,11 +67,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
/// </summary>
|
||||
public class JsonRPCServerRequestModel : JsonRPCRequestModel
|
||||
{
|
||||
public override string ToString()
|
||||
{
|
||||
string rpc = base.ToString();
|
||||
return rpc + "}";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -108,12 +76,6 @@ namespace Flow.Launcher.Core.Plugin
|
|||
public class JsonRPCClientRequestModel : JsonRPCRequestModel
|
||||
{
|
||||
public bool DontHideAfterAction { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string rpc = base.ToString();
|
||||
return rpc + "}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -125,4 +87,4 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
public JsonRPCClientRequestModel JsonRPCAction { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
using System;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
|
|
@ -9,7 +11,9 @@ using System.Threading.Tasks;
|
|||
using System.Windows.Forms;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Plugin;
|
||||
using ICSharpCode.SharpZipLib.Zip;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.IO;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
|
|
@ -31,28 +35,30 @@ namespace Flow.Launcher.Core.Plugin
|
|||
protected abstract string ExecuteCallback(JsonRPCRequestModel rpcRequest);
|
||||
protected abstract string ExecuteContextMenu(Result selectedResult);
|
||||
|
||||
private static readonly RecyclableMemoryStreamManager BufferManager = new();
|
||||
|
||||
public List<Result> LoadContextMenus(Result selectedResult)
|
||||
{
|
||||
string output = ExecuteContextMenu(selectedResult);
|
||||
try
|
||||
{
|
||||
return DeserializedResult(output);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"|JsonRPCPlugin.LoadContextMenus|Exception on result <{selectedResult}>", e);
|
||||
return null;
|
||||
}
|
||||
var output = ExecuteContextMenu(selectedResult);
|
||||
return DeserializedResult(output);
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
|
||||
JsonRPCQueryResponseModel queryResponseModel = await
|
||||
JsonSerializer.DeserializeAsync<JsonRPCQueryResponseModel>(output);
|
||||
var queryResponseModel =
|
||||
await JsonSerializer.DeserializeAsync<JsonRPCQueryResponseModel>(output, options);
|
||||
|
||||
return ParseResults(queryResponseModel);
|
||||
}
|
||||
|
|
@ -61,60 +67,71 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
if (string.IsNullOrEmpty(output)) return null;
|
||||
|
||||
JsonRPCQueryResponseModel queryResponseModel =
|
||||
JsonSerializer.Deserialize<JsonRPCQueryResponseModel>(output);
|
||||
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))
|
||||
if (!string.IsNullOrEmpty(queryResponseModel.DebugMessage))
|
||||
{
|
||||
context.API.ShowMsg(queryResponseModel.DebugMessage);
|
||||
}
|
||||
|
||||
foreach (JsonRPCResult result in queryResponseModel.Result)
|
||||
foreach (var result in queryResponseModel.Result)
|
||||
{
|
||||
result.Action = c =>
|
||||
{
|
||||
if (result.JsonRPCAction == null) return false;
|
||||
|
||||
if (!string.IsNullOrEmpty(result.JsonRPCAction.Method))
|
||||
if (string.IsNullOrEmpty(result.JsonRPCAction.Method))
|
||||
{
|
||||
if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher."))
|
||||
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))
|
||||
{
|
||||
ExecuteFlowLauncherAPI(result.JsonRPCAction.Method.Substring(4),
|
||||
result.JsonRPCAction.Parameters);
|
||||
return !result.JsonRPCAction.DontHideAfterAction;
|
||||
}
|
||||
else
|
||||
|
||||
var jsonRpcRequestModel =
|
||||
JsonSerializer.Deserialize<JsonRPCRequestModel>(actionResponse, options);
|
||||
|
||||
if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false)
|
||||
{
|
||||
string actionReponse = ExecuteCallback(result.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);
|
||||
}
|
||||
ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..],
|
||||
jsonRpcRequestModel.Parameters);
|
||||
}
|
||||
}
|
||||
|
||||
return !result.JsonRPCAction.DontHideAfterAction;
|
||||
};
|
||||
results.Add(result);
|
||||
}
|
||||
|
||||
var results = new List<Result>();
|
||||
|
||||
results.AddRange(queryResponseModel.Result);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private void ExecuteFlowLauncherAPI(string method, object[] parameters)
|
||||
{
|
||||
MethodInfo methodInfo = PluginManager.API.GetType().GetMethod(method);
|
||||
var parametersTypeArray = parameters.Select(param => param.GetType()).ToArray();
|
||||
MethodInfo methodInfo = PluginManager.API.GetType().GetMethod(method, parametersTypeArray);
|
||||
if (methodInfo != null)
|
||||
{
|
||||
try
|
||||
|
|
@ -177,12 +194,14 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
if (result.StartsWith("DEBUG:"))
|
||||
{
|
||||
MessageBox.Show(new Form { TopMost = true }, result.Substring(6));
|
||||
MessageBox.Show(new Form
|
||||
{
|
||||
TopMost = true
|
||||
}, result.Substring(6));
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
@ -195,19 +214,53 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
protected async Task<Stream> ExecuteAsync(ProcessStartInfo startInfo, CancellationToken token = default)
|
||||
{
|
||||
Process process = null;
|
||||
bool disposed = false;
|
||||
try
|
||||
{
|
||||
using var process = Process.Start(startInfo);
|
||||
process = Process.Start(startInfo);
|
||||
if (process == null)
|
||||
{
|
||||
Log.Error("|JsonRPCPlugin.ExecuteAsync|Can't start new process");
|
||||
return Stream.Null;
|
||||
}
|
||||
|
||||
var result = process.StandardOutput.BaseStream;
|
||||
await using var source = process.StandardOutput.BaseStream;
|
||||
|
||||
var buffer = BufferManager.GetStream();
|
||||
|
||||
token.Register(() =>
|
||||
{
|
||||
// ReSharper disable once AccessToModifiedClosure
|
||||
// Manually Check whether disposed
|
||||
if (!disposed && !process.HasExited)
|
||||
process.Kill();
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
// token expire won't instantly trigger the exception,
|
||||
// manually kill process at before
|
||||
await source.CopyToAsync(buffer, token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
await buffer.DisposeAsync();
|
||||
return Stream.Null;
|
||||
}
|
||||
|
||||
buffer.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
if (buffer.Length == 0)
|
||||
{
|
||||
var errorMessage = process.StandardError.EndOfStream ?
|
||||
"Empty JSONRPC Response" :
|
||||
await process.StandardError.ReadToEndAsync();
|
||||
throw new InvalidDataException($"{context.CurrentPluginMetadata.Name}|{errorMessage}");
|
||||
}
|
||||
|
||||
if (!process.StandardError.EndOfStream)
|
||||
{
|
||||
using var standardError = process.StandardError;
|
||||
|
|
@ -215,40 +268,26 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
Log.Error($"|JsonRPCPlugin.ExecuteAsync|{error}");
|
||||
return Stream.Null;
|
||||
Log.Error($"|{context.CurrentPluginMetadata.Name}.{nameof(ExecuteAsync)}|{error}");
|
||||
}
|
||||
|
||||
Log.Error("|JsonRPCPlugin.ExecuteAsync|Empty standard output and standard error.");
|
||||
return Stream.Null;
|
||||
}
|
||||
|
||||
return result;
|
||||
return buffer;
|
||||
}
|
||||
catch (Exception e)
|
||||
finally
|
||||
{
|
||||
Log.Exception(
|
||||
$"|JsonRPCPlugin.ExecuteAsync|Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>",
|
||||
e);
|
||||
return Stream.Null;
|
||||
process?.Dispose();
|
||||
disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return await DeserializedResultAsync(output);
|
||||
}
|
||||
|
||||
public Task InitAsync(PluginInitContext context)
|
||||
public virtual Task InitAsync(PluginInitContext context)
|
||||
{
|
||||
this.context = context;
|
||||
return Task.CompletedTask;
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.Storage;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using ISavable = Flow.Launcher.Plugin.ISavable;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
|
|
@ -21,8 +21,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
private static IEnumerable<PluginPair> _contextMenuPlugins;
|
||||
|
||||
public static List<PluginPair> AllPlugins { get; private set; }
|
||||
public static readonly List<PluginPair> GlobalPlugins = new List<PluginPair>();
|
||||
public static readonly Dictionary<string, PluginPair> NonGlobalPlugins = new Dictionary<string, PluginPair>();
|
||||
public static readonly HashSet<PluginPair> GlobalPlugins = new();
|
||||
public static readonly Dictionary<string, PluginPair> NonGlobalPlugins = new();
|
||||
|
||||
public static IPublicAPI API { private set; get; }
|
||||
|
||||
|
|
@ -33,7 +33,10 @@ namespace Flow.Launcher.Core.Plugin
|
|||
/// <summary>
|
||||
/// Directories that will hold Flow Launcher plugin directory
|
||||
/// </summary>
|
||||
private static readonly string[] Directories = { Constant.PreinstalledDirectory, DataLocation.PluginsDirectory };
|
||||
private static readonly string[] Directories =
|
||||
{
|
||||
Constant.PreinstalledDirectory, DataLocation.PluginsDirectory
|
||||
};
|
||||
|
||||
private static void DeletePythonBinding()
|
||||
{
|
||||
|
|
@ -51,9 +54,26 @@ namespace Flow.Launcher.Core.Plugin
|
|||
var savable = plugin.Plugin as ISavable;
|
||||
savable?.Save();
|
||||
}
|
||||
|
||||
API.SavePluginSettings();
|
||||
}
|
||||
|
||||
public static async ValueTask DisposePluginsAsync()
|
||||
{
|
||||
foreach (var pluginPair in AllPlugins)
|
||||
{
|
||||
switch (pluginPair.Plugin)
|
||||
{
|
||||
case IDisposable disposable:
|
||||
disposable.Dispose();
|
||||
break;
|
||||
case IAsyncDisposable asyncDisposable:
|
||||
await asyncDisposable.DisposeAsync();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ReloadData()
|
||||
{
|
||||
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
|
||||
|
|
@ -99,7 +119,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
try
|
||||
{
|
||||
var milliseconds = await Stopwatch.DebugAsync($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>",
|
||||
() => pair.Plugin.InitAsync(new PluginInitContext(pair.Metadata, API)));
|
||||
() => pair.Plugin.InitAsync(new PluginInitContext(pair.Metadata, API)));
|
||||
|
||||
pair.Metadata.InitTime += milliseconds;
|
||||
Log.Info(
|
||||
|
|
@ -118,7 +138,9 @@ namespace Flow.Launcher.Core.Plugin
|
|||
_contextMenuPlugins = GetPluginsForInterface<IContextMenu>();
|
||||
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)
|
||||
{
|
||||
|
|
@ -141,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))
|
||||
{
|
||||
var plugin = NonGlobalPlugins[query.ActionKeyword];
|
||||
return new List<PluginPair> { plugin };
|
||||
return new List<PluginPair>
|
||||
{
|
||||
plugin
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -154,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>();
|
||||
try
|
||||
|
|
@ -168,7 +193,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (results == null)
|
||||
return results;
|
||||
return null;
|
||||
UpdatePluginMetadata(results, metadata, query);
|
||||
|
||||
metadata.QueryCount += 1;
|
||||
|
|
@ -181,10 +206,6 @@ namespace Flow.Launcher.Core.Plugin
|
|||
// null will be fine since the results will only be added into queue if the token hasn't been cancelled
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"|PluginManager.QueryForPlugin|Exception for plugin <{pair.Metadata.Name}> when query <{query}>", e);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
|
@ -229,7 +250,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
try
|
||||
{
|
||||
results = plugin.LoadContextMenus(result);
|
||||
results = plugin.LoadContextMenus(result) ?? results;
|
||||
foreach (var r in results)
|
||||
{
|
||||
r.PluginDirectory = pluginPair.Metadata.PluginDirectory;
|
||||
|
|
@ -250,6 +271,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
public static bool ActionKeywordRegistered(string actionKeyword)
|
||||
{
|
||||
// this method is only checking for action keywords (defined as not '*') registration
|
||||
// hence the actionKeyword != Query.GlobalPluginWildcardSign logic
|
||||
return actionKeyword != Query.GlobalPluginWildcardSign
|
||||
&& NonGlobalPlugins.ContainsKey(actionKeyword);
|
||||
}
|
||||
|
|
@ -274,7 +297,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
|
||||
/// <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
|
||||
/// </summary>
|
||||
public static void RemoveActionKeyword(string id, string oldActionkeyword)
|
||||
|
|
@ -283,9 +306,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
if (oldActionkeyword == Query.GlobalPluginWildcardSign
|
||||
&& // Plugins may have multiple ActionKeywords that are global, eg. WebSearch
|
||||
plugin.Metadata.ActionKeywords
|
||||
.Where(x => x == Query.GlobalPluginWildcardSign)
|
||||
.ToList()
|
||||
.Count == 1)
|
||||
.Count(x => x == Query.GlobalPluginWildcardSign) == 1)
|
||||
{
|
||||
GlobalPlugins.Remove(plugin);
|
||||
}
|
||||
|
|
@ -306,4 +327,4 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,13 +11,13 @@ using Flow.Launcher.Infrastructure.Logger;
|
|||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using System.Diagnostics;
|
||||
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
public static class PluginsLoader
|
||||
{
|
||||
public const string PATH = "PATH";
|
||||
public const string Python = "python";
|
||||
public const string PythonExecutable = "pythonw.exe";
|
||||
|
||||
public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings)
|
||||
|
|
@ -85,11 +85,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return;
|
||||
}
|
||||
|
||||
plugins.Add(new PluginPair
|
||||
{
|
||||
Plugin = plugin,
|
||||
Metadata = metadata
|
||||
});
|
||||
plugins.Add(new PluginPair {Plugin = plugin, Metadata = metadata});
|
||||
});
|
||||
metadata.InitTime += milliseconds;
|
||||
}
|
||||
|
|
@ -119,102 +115,68 @@ namespace Flow.Launcher.Core.Plugin
|
|||
if (!source.Any(o => o.Language.ToUpper() == AllowedLanguage.Python))
|
||||
return new List<PluginPair>();
|
||||
|
||||
// Try setting Constant.PythonPath first, either from
|
||||
// PATH or from the given pythonDirectory
|
||||
if (string.IsNullOrEmpty(settings.PythonDirectory))
|
||||
{
|
||||
var paths = Environment.GetEnvironmentVariable(PATH);
|
||||
if (paths != null)
|
||||
{
|
||||
var pythonInPath = paths
|
||||
.Split(';')
|
||||
.Where(p => p.ToLower().Contains(Python))
|
||||
.Any();
|
||||
if (!string.IsNullOrEmpty(settings.PythonDirectory) && FilesFolders.LocationExists(settings.PythonDirectory))
|
||||
return SetPythonPathForPluginPairs(source, Path.Combine(settings.PythonDirectory, PythonExecutable));
|
||||
|
||||
if (pythonInPath)
|
||||
var pythonPath = string.Empty;
|
||||
|
||||
if (MessageBox.Show("Flow detected you have installed Python plugins, " +
|
||||
"would you like to install Python to run them? " +
|
||||
Environment.NewLine + Environment.NewLine +
|
||||
"Click no if it's already installed, " +
|
||||
"and you will be prompted to select the folder that contains the Python executable",
|
||||
string.Empty, MessageBoxButtons.YesNo) == DialogResult.No
|
||||
&& string.IsNullOrEmpty(settings.PythonDirectory))
|
||||
{
|
||||
var dlg = new FolderBrowserDialog
|
||||
{
|
||||
SelectedPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
|
||||
};
|
||||
|
||||
var result = dlg.ShowDialog();
|
||||
if (result == DialogResult.OK)
|
||||
{
|
||||
string pythonDirectory = dlg.SelectedPath;
|
||||
if (!string.IsNullOrEmpty(pythonDirectory))
|
||||
{
|
||||
Constant.PythonPath =
|
||||
Path.Combine(paths.Split(';').Where(p => p.ToLower().Contains(Python)).FirstOrDefault(), PythonExecutable);
|
||||
settings.PythonDirectory = FilesFolders.GetPreviousExistingDirectory(FilesFolders.LocationExists, Constant.PythonPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("PluginsLoader", "Failed to set Python path despite the environment variable PATH is found", "PythonPlugins");
|
||||
pythonPath = Path.Combine(pythonDirectory, PythonExecutable);
|
||||
if (File.Exists(pythonPath))
|
||||
{
|
||||
settings.PythonDirectory = pythonDirectory;
|
||||
Constant.PythonPath = pythonPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageBox.Show("Can't find python in given directory");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var path = Path.Combine(settings.PythonDirectory, PythonExecutable);
|
||||
if (File.Exists(path))
|
||||
var installedPythonDirectory = Path.Combine(DataLocation.DataDirectory(), "PythonEmbeddable");
|
||||
|
||||
// Python 3.8.9 is used for Windows 7 compatibility
|
||||
DroplexPackage.Drop(App.python_3_8_9_embeddable, installedPythonDirectory).Wait();
|
||||
|
||||
pythonPath = Path.Combine(installedPythonDirectory, PythonExecutable);
|
||||
if (FilesFolders.FileExists(pythonPath))
|
||||
{
|
||||
Constant.PythonPath = path;
|
||||
settings.PythonDirectory = installedPythonDirectory;
|
||||
Constant.PythonPath = pythonPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("PluginsLoader", $"Tried to automatically set from Settings.PythonDirectory " +
|
||||
$"but can't find python executable in {path}", "PythonPlugins");
|
||||
Log.Error("PluginsLoader",
|
||||
$"Failed to set Python path after Droplex install, {pythonPath} does not exist",
|
||||
"PythonPlugins");
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(settings.PythonDirectory))
|
||||
if (string.IsNullOrEmpty(settings.PythonDirectory) || string.IsNullOrEmpty(pythonPath))
|
||||
{
|
||||
if (MessageBox.Show("Flow detected you have installed Python plugins, " +
|
||||
"would you like to install Python to run them? " +
|
||||
Environment.NewLine + Environment.NewLine +
|
||||
"Click no if it's already installed, " +
|
||||
"and you will be prompted to select the folder that contains the Python executable",
|
||||
string.Empty, MessageBoxButtons.YesNo) == DialogResult.No
|
||||
&& string.IsNullOrEmpty(settings.PythonDirectory))
|
||||
{
|
||||
var dlg = new FolderBrowserDialog
|
||||
{
|
||||
SelectedPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
|
||||
};
|
||||
|
||||
var result = dlg.ShowDialog();
|
||||
if (result == DialogResult.OK)
|
||||
{
|
||||
string pythonDirectory = dlg.SelectedPath;
|
||||
if (!string.IsNullOrEmpty(pythonDirectory))
|
||||
{
|
||||
var pythonPath = Path.Combine(pythonDirectory, PythonExecutable);
|
||||
if (File.Exists(pythonPath))
|
||||
{
|
||||
settings.PythonDirectory = pythonDirectory;
|
||||
Constant.PythonPath = pythonPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageBox.Show("Can't find python in given directory");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DroplexPackage.Drop(App.python3_9_1).Wait();
|
||||
|
||||
var installedPythonDirectory =
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python39");
|
||||
var pythonPath = Path.Combine(installedPythonDirectory, PythonExecutable);
|
||||
if (FilesFolders.FileExists(pythonPath))
|
||||
{
|
||||
settings.PythonDirectory = installedPythonDirectory;
|
||||
Constant.PythonPath = pythonPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("PluginsLoader",
|
||||
$"Failed to set Python path after Droplex install, {pythonPath} does not exist",
|
||||
"PythonPlugins");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(settings.PythonDirectory))
|
||||
{
|
||||
MessageBox.Show("Unable to set Python executable path, please try from Flow's settings (scroll down to the bottom).");
|
||||
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");
|
||||
|
|
@ -222,24 +184,26 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return new List<PluginPair>();
|
||||
}
|
||||
|
||||
return source
|
||||
return SetPythonPathForPluginPairs(source, pythonPath);
|
||||
}
|
||||
|
||||
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(Constant.PythonPath),
|
||||
Plugin = new PythonPlugin(pythonPath),
|
||||
Metadata = metadata
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
|
||||
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
|
||||
{
|
||||
return source
|
||||
.Where(o => o.Language.ToUpper() == AllowedLanguage.Executable)
|
||||
.Select(metadata => new PluginPair
|
||||
{
|
||||
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath),
|
||||
Metadata = metadata
|
||||
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), Metadata = metadata
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,17 +28,19 @@ namespace Flow.Launcher.Core.Plugin
|
|||
var path = Path.Combine(Constant.ProgramDirectory, JsonRPC);
|
||||
_startInfo.EnvironmentVariables["PYTHONPATH"] = path;
|
||||
|
||||
//Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable
|
||||
_startInfo.ArgumentList.Add("-B");
|
||||
}
|
||||
|
||||
protected override Task<Stream> ExecuteQueryAsync(Query query, CancellationToken token)
|
||||
{
|
||||
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel
|
||||
{
|
||||
Method = "query",
|
||||
Parameters = new object[] { query.Search },
|
||||
Method = "query", Parameters = new object[] {query.Search},
|
||||
};
|
||||
//Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable
|
||||
_startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{request}\"";
|
||||
|
||||
_startInfo.ArgumentList[2] = request.ToString();
|
||||
|
||||
// todo happlebao why context can't be used in constructor
|
||||
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||
|
||||
|
|
@ -47,22 +49,31 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest)
|
||||
{
|
||||
_startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{rpcRequest}\"";
|
||||
_startInfo.ArgumentList[2] = rpcRequest.ToString();
|
||||
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||
// TODO: Async Action
|
||||
return Execute(_startInfo);
|
||||
}
|
||||
|
||||
protected override string ExecuteContextMenu(Result selectedResult) {
|
||||
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel {
|
||||
Method = "context_menu",
|
||||
Parameters = new object[] { selectedResult.ContextData },
|
||||
protected override string ExecuteContextMenu(Result selectedResult)
|
||||
{
|
||||
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel
|
||||
{
|
||||
Method = "context_menu", Parameters = new object[] {selectedResult.ContextData},
|
||||
};
|
||||
_startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{request}\"";
|
||||
_startInfo.ArgumentList[2] = request.ToString();
|
||||
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||
|
||||
// TODO: Async Action
|
||||
return Execute(_startInfo);
|
||||
}
|
||||
|
||||
public override Task InitAsync(PluginInitContext context)
|
||||
{
|
||||
this.context = context;
|
||||
_startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
|
||||
_startInfo.ArgumentList.Add("");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Flow.Launcher.Core/Properties/AssemblyInfo.cs
Normal file
3
Flow.Launcher.Core/Properties/AssemblyInfo.cs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Flow.Launcher.Test")]
|
||||
42
Flow.Launcher.Core/Resource/JsonObjectConverter.cs
Normal file
42
Flow.Launcher.Core/Resource/JsonObjectConverter.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Flow.Launcher.Core.Resource
|
||||
{
|
||||
public class JsonObjectConverter : JsonConverter<object>
|
||||
{
|
||||
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonTokenType.True:
|
||||
return true;
|
||||
case JsonTokenType.False:
|
||||
return false;
|
||||
case JsonTokenType.Number when reader.TryGetInt32(out var i):
|
||||
return i;
|
||||
case JsonTokenType.Number when reader.TryGetInt64(out var l):
|
||||
return l;
|
||||
case JsonTokenType.Number:
|
||||
return reader.GetDouble();
|
||||
case JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime):
|
||||
return datetime;
|
||||
case JsonTokenType.String:
|
||||
return reader.GetString();
|
||||
default:
|
||||
// Use JsonElement as fallback.
|
||||
// Newtonsoft uses JArray or JObject.
|
||||
using (var document = JsonDocument.ParseValue(ref reader))
|
||||
{
|
||||
return document.RootElement.Clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
|
||||
{
|
||||
throw new InvalidOperationException("Should not get here.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,8 @@ namespace Flow.Launcher.Core.Resource
|
|||
{
|
||||
public class Theme
|
||||
{
|
||||
private const int ShadowExtraMargin = 12;
|
||||
|
||||
private readonly List<string> _themeDirectories = new List<string>();
|
||||
private ResourceDictionary _oldResource;
|
||||
private string _oldTheme;
|
||||
|
|
@ -26,6 +28,10 @@ namespace Flow.Launcher.Core.Resource
|
|||
private string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder);
|
||||
private string UserDirectoryPath => Path.Combine(DataLocation.DataDirectory(), Folder);
|
||||
|
||||
public bool BlurEnabled { get; set; }
|
||||
|
||||
private double mainWindowWidth;
|
||||
|
||||
public Theme()
|
||||
{
|
||||
_themeDirectories.Add(DirectoryPath);
|
||||
|
|
@ -86,8 +92,12 @@ namespace Flow.Launcher.Core.Resource
|
|||
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
|
||||
}
|
||||
|
||||
if (Settings.UseDropShadowEffect)
|
||||
BlurEnabled = IsBlurTheme();
|
||||
|
||||
if (Settings.UseDropShadowEffect && !BlurEnabled)
|
||||
AddDropShadowEffectToCurrentTheme();
|
||||
|
||||
SetBlurForWindow();
|
||||
}
|
||||
catch (DirectoryNotFoundException e)
|
||||
{
|
||||
|
|
@ -179,6 +189,21 @@ namespace Flow.Launcher.Core.Resource
|
|||
Array.ForEach(new[] { resultItemStyle, resultSubItemStyle, resultItemSelectedStyle, resultSubItemSelectedStyle }, o => Array.ForEach(setters, p => o.Setters.Add(p)));
|
||||
}
|
||||
|
||||
var windowStyle = dict["WindowStyle"] as Style;
|
||||
|
||||
var width = windowStyle?.Setters.OfType<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;
|
||||
}
|
||||
|
||||
|
|
@ -224,17 +249,54 @@ namespace Flow.Launcher.Core.Resource
|
|||
BlurRadius = 15
|
||||
};
|
||||
|
||||
var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter;
|
||||
if (marginSetter == null)
|
||||
{
|
||||
marginSetter = new Setter()
|
||||
{
|
||||
Property = Border.MarginProperty,
|
||||
Value = new Thickness(ShadowExtraMargin),
|
||||
};
|
||||
windowBorderStyle.Setters.Add(marginSetter);
|
||||
}
|
||||
else
|
||||
{
|
||||
var baseMargin = (Thickness) marginSetter.Value;
|
||||
var newMargin = new Thickness(
|
||||
baseMargin.Left + ShadowExtraMargin,
|
||||
baseMargin.Top + ShadowExtraMargin,
|
||||
baseMargin.Right + ShadowExtraMargin,
|
||||
baseMargin.Bottom + ShadowExtraMargin);
|
||||
marginSetter.Value = newMargin;
|
||||
}
|
||||
|
||||
windowBorderStyle.Setters.Add(effectSetter);
|
||||
|
||||
UpdateResourceDictionary(dict);
|
||||
}
|
||||
|
||||
public void RemoveDropShadowEffectToCurrentTheme()
|
||||
public void RemoveDropShadowEffectFromCurrentTheme()
|
||||
{
|
||||
var dict = CurrentThemeResourceDictionary();
|
||||
var windowBorderStyle = dict["WindowBorderStyle"] as Style;
|
||||
|
||||
dict.Remove(Border.EffectProperty);
|
||||
var effectSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.EffectProperty) as Setter;
|
||||
var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter;
|
||||
|
||||
if(effectSetter != null)
|
||||
{
|
||||
windowBorderStyle.Setters.Remove(effectSetter);
|
||||
}
|
||||
if (marginSetter != null)
|
||||
{
|
||||
var currentMargin = (Thickness)marginSetter.Value;
|
||||
var newMargin = new Thickness(
|
||||
currentMargin.Left - ShadowExtraMargin,
|
||||
currentMargin.Top - ShadowExtraMargin,
|
||||
currentMargin.Right - ShadowExtraMargin,
|
||||
currentMargin.Bottom - ShadowExtraMargin);
|
||||
marginSetter.Value = newMargin;
|
||||
}
|
||||
|
||||
UpdateResourceDictionary(dict);
|
||||
}
|
||||
|
|
@ -281,35 +343,39 @@ namespace Flow.Launcher.Core.Resource
|
|||
/// </summary>
|
||||
public void SetBlurForWindow()
|
||||
{
|
||||
if (BlurEnabled)
|
||||
{
|
||||
SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_ENABLE_BLURBEHIND);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_DISABLED);
|
||||
}
|
||||
}
|
||||
|
||||
// Exception of FindResource can't be cathed if global exception handle is set
|
||||
private bool IsBlurTheme()
|
||||
{
|
||||
if (Environment.OSVersion.Version >= new Version(6, 2))
|
||||
{
|
||||
var resource = Application.Current.TryFindResource("ThemeBlurEnabled");
|
||||
bool blur;
|
||||
if (resource is bool)
|
||||
{
|
||||
blur = (bool)resource;
|
||||
}
|
||||
else
|
||||
{
|
||||
blur = false;
|
||||
}
|
||||
|
||||
if (blur)
|
||||
{
|
||||
SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_ENABLE_BLURBEHIND);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_DISABLED);
|
||||
}
|
||||
if (resource is bool)
|
||||
return (bool)resource;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void SetWindowAccent(Window w, AccentState state)
|
||||
{
|
||||
var windowHelper = new WindowInteropHelper(w);
|
||||
|
||||
// this determines the width of the main query window
|
||||
w.Width = mainWindowWidth;
|
||||
windowHelper.EnsureHandle();
|
||||
|
||||
var accent = new AccentPolicy { AccentState = state };
|
||||
var accentStructSize = Marshal.SizeOf(accent);
|
||||
|
||||
|
|
|
|||
|
|
@ -49,8 +49,11 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NLog.Schema" Version="4.7.0-rc1" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="4.9.0" />
|
||||
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
||||
<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="ToolGood.Words.Pinyin" Version="3.0.1.4" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media;
|
||||
|
||||
|
|
@ -26,7 +27,8 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
private const int MaxCached = 50;
|
||||
public ConcurrentDictionary<string, ImageUsage> Data { get; private set; } = new ConcurrentDictionary<string, ImageUsage>();
|
||||
private const int permissibleFactor = 2;
|
||||
|
||||
private SemaphoreSlim semaphore = new(1, 1);
|
||||
|
||||
public void Initialization(Dictionary<string, int> usage)
|
||||
{
|
||||
foreach (var key in usage.Keys)
|
||||
|
|
@ -60,20 +62,29 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
}
|
||||
);
|
||||
|
||||
// To prevent the dictionary from drastically increasing in size by caching images, the dictionary size is not allowed to grow more than the permissibleFactor * maxCached size
|
||||
// This is done so that we don't constantly perform this resizing operation and also maintain the image cache size at the same time
|
||||
if (Data.Count > permissibleFactor * MaxCached)
|
||||
SliceExtra();
|
||||
|
||||
async void SliceExtra()
|
||||
{
|
||||
// To delete the images from the data dictionary based on the resizing of the Usage Dictionary.
|
||||
foreach (var key in Data.OrderBy(x => x.Value.usage).Take(Data.Count - MaxCached).Select(x => x.Key))
|
||||
Data.TryRemove(key, out _);
|
||||
// To prevent the dictionary from drastically increasing in size by caching images, the dictionary size is not allowed to grow more than the permissibleFactor * maxCached size
|
||||
// This is done so that we don't constantly perform this resizing operation and also maintain the image cache size at the same time
|
||||
if (Data.Count > permissibleFactor * MaxCached)
|
||||
{
|
||||
await semaphore.WaitAsync().ConfigureAwait(false);
|
||||
// To delete the images from the data dictionary based on the resizing of the Usage Dictionary
|
||||
// Double Check to avoid concurrent remove
|
||||
if (Data.Count > permissibleFactor * MaxCached)
|
||||
foreach (var key in Data.OrderBy(x => x.Value.usage).Take(Data.Count - MaxCached).Select(x => x.Key).ToArray())
|
||||
Data.TryRemove(key, out _);
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ContainsKey(string key)
|
||||
{
|
||||
return Data.ContainsKey(key) && Data[key].imageSource != null;
|
||||
return key is not null && Data.ContainsKey(key) && Data[key].imageSource != null;
|
||||
}
|
||||
|
||||
public int CacheSize()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@ using NLog;
|
|||
using NLog.Config;
|
||||
using NLog.Targets;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using JetBrains.Annotations;
|
||||
using NLog.Fluent;
|
||||
using NLog.Targets.Wrappers;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Logger
|
||||
{
|
||||
|
|
@ -23,15 +28,37 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
}
|
||||
|
||||
var configuration = new LoggingConfiguration();
|
||||
var target = new FileTarget();
|
||||
configuration.AddTarget("file", target);
|
||||
target.FileName = CurrentLogDirectory.Replace(@"\", "/") + "/${shortdate}.txt";
|
||||
|
||||
const string layout =
|
||||
@"${date:format=HH\:mm\:ss.ffffK} - " +
|
||||
@"${level:uppercase=true:padding=-5} - ${logger} - ${message:l}" +
|
||||
@"${onexception:${newline}" +
|
||||
@"EXCEPTION OCCURS\: ${exception:format=tostring}${newline}}";
|
||||
|
||||
var fileTarget = new FileTarget
|
||||
{
|
||||
FileName = CurrentLogDirectory.Replace(@"\", "/") + "/${shortdate}.txt",
|
||||
Layout = layout
|
||||
};
|
||||
|
||||
var fileTargetASyncWrapper = new AsyncTargetWrapper(fileTarget);
|
||||
|
||||
var debugTarget = new OutputDebugStringTarget
|
||||
{
|
||||
Layout = layout
|
||||
};
|
||||
|
||||
configuration.AddTarget("file", fileTargetASyncWrapper);
|
||||
configuration.AddTarget("debug", debugTarget);
|
||||
|
||||
#if DEBUG
|
||||
var rule = new LoggingRule("*", LogLevel.Debug, target);
|
||||
var fileRule = new LoggingRule("*", LogLevel.Debug, fileTargetASyncWrapper);
|
||||
var debugRule = new LoggingRule("*", LogLevel.Debug, debugTarget);
|
||||
configuration.LoggingRules.Add(debugRule);
|
||||
#else
|
||||
var rule = new LoggingRule("*", LogLevel.Info, target);
|
||||
var fileRule = new LoggingRule("*", LogLevel.Info, fileTargetASyncWrapper);
|
||||
#endif
|
||||
configuration.LoggingRules.Add(rule);
|
||||
configuration.LoggingRules.Add(fileRule);
|
||||
LogManager.Configuration = configuration;
|
||||
}
|
||||
|
||||
|
|
@ -39,7 +66,6 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
{
|
||||
var logger = LogManager.GetLogger("FaultyLogger");
|
||||
message = $"Wrong logger message format <{message}>";
|
||||
System.Diagnostics.Debug.WriteLine($"FATAL|{message}");
|
||||
logger.Fatal(message);
|
||||
}
|
||||
|
||||
|
|
@ -51,12 +77,11 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
}
|
||||
|
||||
|
||||
|
||||
[MethodImpl(MethodImplOptions.Synchronized)]
|
||||
public static void Exception(string className, string message, System.Exception exception, [CallerMemberName] string methodName = "")
|
||||
{
|
||||
exception = exception.Demystify();
|
||||
#if DEBUG
|
||||
throw exception;
|
||||
ExceptionDispatchInfo.Capture(exception).Throw();
|
||||
#else
|
||||
var classNameWithMethod = CheckClassAndMessageAndReturnFullClassWithMethod(className, message, methodName);
|
||||
|
||||
|
|
@ -90,23 +115,9 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
{
|
||||
var logger = LogManager.GetLogger(classAndMethod);
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"ERROR|{message}");
|
||||
var messageBuilder = new StringBuilder();
|
||||
|
||||
logger.Error("-------------------------- Begin exception --------------------------");
|
||||
logger.Error(message);
|
||||
|
||||
do
|
||||
{
|
||||
logger.Error($"Exception full name:\n <{e.GetType().FullName}>");
|
||||
logger.Error($"Exception message:\n <{e.Message}>");
|
||||
logger.Error($"Exception stack trace:\n <{e.StackTrace}>");
|
||||
logger.Error($"Exception source:\n <{e.Source}>");
|
||||
logger.Error($"Exception target site:\n <{e.TargetSite}>");
|
||||
logger.Error($"Exception HResult:\n <{e.HResult}>");
|
||||
e = e.InnerException;
|
||||
} while (e != null);
|
||||
|
||||
logger.Error("-------------------------- End exception --------------------------");
|
||||
logger.Error(e, message);
|
||||
}
|
||||
|
||||
private static void LogInternal(string message, LogLevel level)
|
||||
|
|
@ -117,8 +128,6 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
var prefix = parts[1];
|
||||
var unprefixed = parts[2];
|
||||
var logger = LogManager.GetLogger(prefix);
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"{level.Name}|{message}");
|
||||
logger.Log(level, unprefixed);
|
||||
}
|
||||
else
|
||||
|
|
@ -128,11 +137,12 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
}
|
||||
|
||||
/// <param name="message">example: "|prefix|unprefixed" </param>
|
||||
[MethodImpl(MethodImplOptions.Synchronized)]
|
||||
/// <param name="e">Exception</param>
|
||||
public static void Exception(string message, System.Exception e)
|
||||
{
|
||||
e = e.Demystify();
|
||||
#if DEBUG
|
||||
throw e;
|
||||
ExceptionDispatchInfo.Capture(e).Throw();
|
||||
#else
|
||||
if (FormatValid(message))
|
||||
{
|
||||
|
|
@ -165,7 +175,6 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
|
||||
var logger = LogManager.GetLogger(classNameWithMethod);
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"{level.Name}|{message}");
|
||||
logger.Log(level, message);
|
||||
}
|
||||
|
||||
|
|
|
|||
8
Flow.Launcher.Infrastructure/Storage/ISavable.cs
Normal file
8
Flow.Launcher.Infrastructure/Storage/ISavable.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
using System;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
{
|
||||
[Obsolete("Deprecated as of Flow Launcher v1.8.0, on 2021.06.21. " +
|
||||
"This is used only for Everything plugin v1.4.9 or below backwards compatibility")]
|
||||
public interface ISavable : Plugin.ISavable { }
|
||||
}
|
||||
|
|
@ -87,4 +87,8 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
File.WriteAllText(FilePath, serialized);
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("Deprecated as of Flow Launcher v1.8.0, on 2021.06.21. " +
|
||||
"This is used only for Everything plugin v1.4.9 or below backwards compatibility")]
|
||||
public class JsonStrorage<T> : JsonStorage<T> where T : new() { }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,3 +22,4 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,11 +16,13 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
{
|
||||
var settings = Plugins[metadata.ID];
|
||||
|
||||
// TODO: Remove. This is one off for 1.2.0 release.
|
||||
// Introduced a new action keyword in Explorer, so need to update plugin setting in the UserData folder.
|
||||
// This kind of plugin meta update should be handled by a dedicated method trigger by version bump.
|
||||
if (metadata.ID == "572be03c74c642baae319fc283e561a8" && metadata.ActionKeywords.Count != settings.ActionKeywords.Count)
|
||||
settings.ActionKeywords = metadata.ActionKeywords;
|
||||
// TODO: Remove. This is backwards compatibility for 1.8.0 release.
|
||||
// Introduced two new action keywords in Explorer, so need to update plugin setting in the UserData folder.
|
||||
if (metadata.ID == "572be03c74c642baae319fc283e561a8" && metadata.ActionKeywords.Count > settings.ActionKeywords.Count)
|
||||
{
|
||||
settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for index search
|
||||
settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for path search
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(settings.Version))
|
||||
settings.Version = metadata.Version;
|
||||
|
|
@ -30,6 +32,11 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
metadata.ActionKeywords = settings.ActionKeywords;
|
||||
metadata.ActionKeyword = settings.ActionKeywords[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
metadata.ActionKeywords = new List<string>();
|
||||
metadata.ActionKeyword = string.Empty;
|
||||
}
|
||||
metadata.Disabled = settings.Disabled;
|
||||
metadata.Priority = settings.Priority;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
/// </summary>
|
||||
public bool ShouldUsePinyin { get; set; } = false;
|
||||
|
||||
internal SearchPrecisionScore QuerySearchPrecision { get; private set; } = SearchPrecisionScore.Regular;
|
||||
[JsonInclude, JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public SearchPrecisionScore QuerySearchPrecision { get; private set; } = SearchPrecisionScore.Regular;
|
||||
|
||||
[JsonIgnore]
|
||||
public string QuerySearchPrecisionString
|
||||
|
|
@ -97,6 +98,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
public bool RememberLastLaunchLocation { get; set; }
|
||||
public bool IgnoreHotkeysOnFullscreen { get; set; }
|
||||
|
||||
public bool AutoHideScrollBar { get; set; }
|
||||
|
||||
public HttpProxy Proxy { get; set; } = new HttpProxy();
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
public interface IFeatures { }
|
||||
|
||||
public interface IContextMenu : IFeatures
|
||||
{
|
||||
List<Result> LoadContextMenus(Result selectedResult);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represent plugins that support internationalization
|
||||
/// </summary>
|
||||
public interface IPluginI18n : IFeatures
|
||||
{
|
||||
string GetTranslatedPluginTitle();
|
||||
|
||||
string GetTranslatedPluginDescription();
|
||||
}
|
||||
|
||||
public interface IResultUpdated : IFeatures
|
||||
{
|
||||
event ResultUpdatedEventHandler ResultsUpdated;
|
||||
}
|
||||
|
||||
public delegate void ResultUpdatedEventHandler(IResultUpdated sender, ResultUpdatedEventArgs e);
|
||||
|
||||
public class ResultUpdatedEventArgs : EventArgs
|
||||
{
|
||||
public List<Result> Results;
|
||||
public Query Query;
|
||||
}
|
||||
}
|
||||
14
Flow.Launcher.Plugin/Features.cs
Normal file
14
Flow.Launcher.Plugin/Features.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Threading;
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Base Interface for Flow's special plugin feature interface
|
||||
/// </summary>
|
||||
public interface IFeatures
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -14,10 +14,10 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>1.4.0</Version>
|
||||
<PackageVersion>1.4.0</PackageVersion>
|
||||
<AssemblyVersion>1.4.0</AssemblyVersion>
|
||||
<FileVersion>1.4.0</FileVersion>
|
||||
<Version>2.0.0</Version>
|
||||
<PackageVersion>2.0.0</PackageVersion>
|
||||
<AssemblyVersion>2.0.0</AssemblyVersion>
|
||||
<FileVersion>2.0.0</FileVersion>
|
||||
<PackageId>Flow.Launcher.Plugin</PackageId>
|
||||
<Authors>Flow-Launcher</Authors>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ namespace Flow.Launcher.Plugin
|
|||
/// The command that allows user to manual reload is exposed via Plugin.Sys, and
|
||||
/// it will call the plugins that have implemented this interface.
|
||||
/// </summary>
|
||||
public interface IAsyncReloadable
|
||||
public interface IAsyncReloadable : IFeatures
|
||||
{
|
||||
Task ReloadDataAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Flow.Launcher.Plugin/Interfaces/IContextMenu.cs
Normal file
9
Flow.Launcher.Plugin/Interfaces/IContextMenu.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
public interface IContextMenu : IFeatures
|
||||
{
|
||||
List<Result> LoadContextMenus(Result selectedResult);
|
||||
}
|
||||
}
|
||||
12
Flow.Launcher.Plugin/Interfaces/IPluginI18n.cs
Normal file
12
Flow.Launcher.Plugin/Interfaces/IPluginI18n.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent plugins that support internationalization
|
||||
/// </summary>
|
||||
public interface IPluginI18n : IFeatures
|
||||
{
|
||||
string GetTranslatedPluginTitle();
|
||||
|
||||
string GetTranslatedPluginDescription();
|
||||
}
|
||||
}
|
||||
|
|
@ -15,8 +15,8 @@
|
|||
/// If requiring reloading data asynchronously, please use the IAsyncReloadable interface
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public interface IReloadable
|
||||
public interface IReloadable : IFeatures
|
||||
{
|
||||
void ReloadData();
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs
Normal file
20
Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
public interface IResultUpdated : IFeatures
|
||||
{
|
||||
event ResultUpdatedEventHandler ResultsUpdated;
|
||||
}
|
||||
|
||||
public delegate void ResultUpdatedEventHandler(IResultUpdated sender, ResultUpdatedEventArgs e);
|
||||
|
||||
public class ResultUpdatedEventArgs : EventArgs
|
||||
{
|
||||
public List<Result> Results;
|
||||
public Query Query;
|
||||
public CancellationToken Token { get; init; }
|
||||
}
|
||||
}
|
||||
|
|
@ -5,8 +5,8 @@
|
|||
/// 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
|
||||
public interface ISavable : IFeatures
|
||||
{
|
||||
void Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,4 +3,4 @@
|
|||
|
||||
* Defines base objects and interfaces for plugins
|
||||
* Plugin authors making C# plugins should reference this DLL via nuget
|
||||
* Contains base commands used by all plugins
|
||||
* Contains commands and models that can be used by plugins
|
||||
|
|
|
|||
|
|
@ -49,12 +49,12 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" Version="4.14.1" />
|
||||
<PackageReference Include="nunit" Version="3.12.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1">
|
||||
<PackageReference Include="nunit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -133,6 +133,13 @@ namespace Flow.Launcher.Test.Plugins
|
|||
{
|
||||
// Given
|
||||
var queryConstructor = new QueryConstructor(new Settings());
|
||||
var baseQuery = queryConstructor.CreateBaseQuery();
|
||||
|
||||
// system running this test could have different locale than the hard-coded 1033 LCID en-US.
|
||||
var queryKeywordLocale = baseQuery.QueryKeywordLocale;
|
||||
expectedString = expectedString.Replace("1033", queryKeywordLocale.ToString());
|
||||
|
||||
|
||||
|
||||
//When
|
||||
var resultString = queryConstructor.QueryForAllFilesAndFolders(userSearchString);
|
||||
|
|
|
|||
105
Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs
Normal file
105
Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
using NUnit;
|
||||
using NUnit.Framework;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Flow.Launcher.Test.Plugins
|
||||
{
|
||||
[TestFixture]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
internal class JsonRPCPluginTest : JsonRPCPlugin
|
||||
{
|
||||
public override string SupportedLanguage { get; set; } = AllowedLanguage.Executable;
|
||||
|
||||
protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
protected override string ExecuteContextMenu(Result selectedResult)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
protected override Task<Stream> ExecuteQueryAsync(Query query, CancellationToken token)
|
||||
{
|
||||
var byteInfo = Encoding.UTF8.GetBytes(query.RawQuery);
|
||||
|
||||
var resultStream = new MemoryStream(byteInfo);
|
||||
return Task.FromResult((Stream)resultStream);
|
||||
}
|
||||
|
||||
[TestCase("{\"result\":[],\"DebugMessage\":null}", Description = "Empty Result")]
|
||||
[TestCase("{\"result\":[{\"JsonRPCAction\":null,\"Title\":\"something\",\"SubTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null}],\"DebugMessage\":null}", Description = "One Result with Pascal Case")]
|
||||
[TestCase("{\"result\":[{\"jsonRPCAction\":null,\"title\":\"something\",\"subTitle\":\"\",\"actionKeywordAssigned\":null,\"icoPath\":null}],\"debugMessage\":null}", Description = "One Result with camel Case")]
|
||||
[TestCase("{\"result\":[{\"JsonRPCAction\":null,\"Title\":\"iii\",\"SubTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null},{\"JsonRPCAction\":null,\"Title\":\"iii\",\"SubTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null}],\"DebugMessage\":null}", Description = "Two Result with Pascal Case")]
|
||||
[TestCase("{\"result\":[{\"jsonrpcAction\":null,\"TItLE\":\"iii\",\"Subtitle\":\"\",\"Actionkeywordassigned\":null,\"icoPath\":null},{\"jsonRPCAction\":null,\"tiTle\":\"iii\",\"subTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null}],\"DebugMessage\":null}", Description = "Two Result with Weird Case")]
|
||||
public async Task GivenVariousJsonText_WhenVariousNamingCase_ThenExpectNotNullResults_Async(string resultText)
|
||||
{
|
||||
var results = await QueryAsync(new Query
|
||||
{
|
||||
RawQuery = resultText
|
||||
}, default);
|
||||
|
||||
Assert.IsNotNull(results);
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
Assert.IsNotNull(result);
|
||||
Assert.IsNotNull(result.Action);
|
||||
Assert.IsNotNull(result.Title);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static List<JsonRPCQueryResponseModel> ResponseModelsSource = new()
|
||||
{
|
||||
new()
|
||||
{
|
||||
Result = new()
|
||||
},
|
||||
new()
|
||||
{
|
||||
Result = new()
|
||||
{
|
||||
new JsonRPCResult
|
||||
{
|
||||
Title = "Test1",
|
||||
SubTitle = "Test2"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
[TestCaseSource(typeof(JsonRPCPluginTest), nameof(ResponseModelsSource))]
|
||||
public async Task GivenModel_WhenSerializeWithDifferentNamingPolicy_ThenExpectSameResult_Async(JsonRPCQueryResponseModel reference)
|
||||
{
|
||||
var camelText = JsonSerializer.Serialize(reference, new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
||||
|
||||
var pascalText = JsonSerializer.Serialize(reference);
|
||||
|
||||
var results1 = await QueryAsync(new Query { RawQuery = camelText }, default);
|
||||
var results2 = await QueryAsync(new Query { RawQuery = pascalText }, default);
|
||||
|
||||
Assert.IsNotNull(results1);
|
||||
Assert.IsNotNull(results2);
|
||||
|
||||
foreach (var ((result1, result2), referenceResult) in results1.Zip(results2).Zip(reference.Result))
|
||||
{
|
||||
Assert.AreEqual(result1, result2);
|
||||
Assert.AreEqual(result1, referenceResult);
|
||||
|
||||
Assert.IsNotNull(result1);
|
||||
Assert.IsNotNull(result1.Action);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -68,11 +68,14 @@ namespace Flow.Launcher
|
|||
|
||||
PluginManager.LoadPlugins(_settings.PluginSettings);
|
||||
_mainVM = new MainViewModel(_settings);
|
||||
|
||||
HotKeyMapper.Initialize(_mainVM);
|
||||
|
||||
API = new PublicAPIInstance(_settingsVM, _mainVM, _alphabet);
|
||||
|
||||
Http.API = API;
|
||||
Http.Proxy = _settings.Proxy;
|
||||
|
||||
|
||||
await PluginManager.InitializePlugins(API);
|
||||
var window = new MainWindow(_settings, _mainVM);
|
||||
|
||||
|
|
@ -96,6 +99,8 @@ namespace Flow.Launcher
|
|||
AutoStartup();
|
||||
AutoUpdates();
|
||||
|
||||
API.SaveAppAllSettings();
|
||||
|
||||
_mainVM.MainWindowVisibility = _settings.HideOnStartup ? Visibility.Hidden : Visibility.Visible;
|
||||
Log.Info("|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- ");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Helper;
|
||||
using Flow.Launcher.Infrastructure.Hotkey;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using NHotkey;
|
||||
using NHotkey.Wpf;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
|
|
@ -52,11 +51,7 @@ namespace Flow.Launcher
|
|||
};
|
||||
_settings.CustomPluginHotkeys.Add(pluginHotkey);
|
||||
|
||||
SetHotkey(ctlHotkey.CurrentHotkey, delegate
|
||||
{
|
||||
App.API.ChangeQuery(pluginHotkey.ActionKeyword);
|
||||
Application.Current.MainWindow.Visibility = Visibility.Visible;
|
||||
});
|
||||
HotKeyMapper.SetCustomQueryHotkey(pluginHotkey);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -69,12 +64,8 @@ namespace Flow.Launcher
|
|||
updateCustomHotkey.ActionKeyword = tbAction.Text;
|
||||
updateCustomHotkey.Hotkey = ctlHotkey.CurrentHotkey.ToString();
|
||||
//remove origin hotkey
|
||||
RemoveHotkey(oldHotkey);
|
||||
SetHotkey(new HotkeyModel(updateCustomHotkey.Hotkey), delegate
|
||||
{
|
||||
App.API.ChangeQuery(updateCustomHotkey.ActionKeyword);
|
||||
Application.Current.MainWindow.Visibility = Visibility.Visible;
|
||||
});
|
||||
HotKeyMapper.RemoveHotkey(oldHotkey);
|
||||
HotKeyMapper.SetCustomQueryHotkey(updateCustomHotkey);
|
||||
}
|
||||
|
||||
Close();
|
||||
|
|
@ -100,28 +91,8 @@ namespace Flow.Launcher
|
|||
{
|
||||
App.API.ChangeQuery(tbAction.Text);
|
||||
Application.Current.MainWindow.Visibility = Visibility.Visible;
|
||||
}
|
||||
Application.Current.MainWindow.Focus();
|
||||
|
||||
private void RemoveHotkey(string hotkeyStr)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(hotkeyStr))
|
||||
{
|
||||
HotkeyManager.Current.Remove(hotkeyStr);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetHotkey(HotkeyModel hotkey, EventHandler<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)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ using System.Windows.Threading;
|
|||
using NLog;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.Exception;
|
||||
using NLog.Fluent;
|
||||
using Log = Flow.Launcher.Infrastructure.Logger.Log;
|
||||
|
||||
namespace Flow.Launcher.Helper
|
||||
{
|
||||
|
|
@ -45,4 +47,4 @@ namespace Flow.Launcher.Helper
|
|||
return info;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
136
Flow.Launcher/Helper/HotKeyMapper.cs
Normal file
136
Flow.Launcher/Helper/HotKeyMapper.cs
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
using Flow.Launcher.Infrastructure.Hotkey;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using System;
|
||||
using NHotkey;
|
||||
using NHotkey.Wpf;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using System.Windows;
|
||||
using Flow.Launcher.ViewModel;
|
||||
|
||||
namespace Flow.Launcher.Helper
|
||||
{
|
||||
internal static class HotKeyMapper
|
||||
{
|
||||
private static Settings settings;
|
||||
private static MainViewModel mainViewModel;
|
||||
|
||||
internal static void Initialize(MainViewModel mainVM)
|
||||
{
|
||||
mainViewModel = mainVM;
|
||||
settings = mainViewModel._settings;
|
||||
|
||||
SetHotkey(settings.Hotkey, OnHotkey);
|
||||
LoadCustomPluginHotkey();
|
||||
}
|
||||
|
||||
private static void SetHotkey(string hotkeyStr, EventHandler<HotkeyEventArgs> action)
|
||||
{
|
||||
var hotkey = new HotkeyModel(hotkeyStr);
|
||||
SetHotkey(hotkey, action);
|
||||
}
|
||||
|
||||
internal static void SetHotkey(HotkeyModel hotkey, EventHandler<HotkeyEventArgs> action)
|
||||
{
|
||||
string hotkeyStr = hotkey.ToString();
|
||||
try
|
||||
{
|
||||
HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
string errorMsg =
|
||||
string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"),
|
||||
hotkeyStr);
|
||||
MessageBox.Show(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void RemoveHotkey(string hotkeyStr)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(hotkeyStr))
|
||||
{
|
||||
HotkeyManager.Current.Remove(hotkeyStr);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void OnHotkey(object sender, HotkeyEventArgs e)
|
||||
{
|
||||
if (!ShouldIgnoreHotkeys())
|
||||
{
|
||||
UpdateLastQUeryMode();
|
||||
|
||||
mainViewModel.ToggleFlowLauncher();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if Flow Launcher should ignore any hotkeys
|
||||
/// </summary>
|
||||
private static bool ShouldIgnoreHotkeys()
|
||||
{
|
||||
return settings.IgnoreHotkeysOnFullscreen && WindowsInteropHelper.IsWindowFullscreen();
|
||||
}
|
||||
|
||||
private static void UpdateLastQUeryMode()
|
||||
{
|
||||
switch(settings.LastQueryMode)
|
||||
{
|
||||
case LastQueryMode.Empty:
|
||||
mainViewModel.ChangeQueryText(string.Empty);
|
||||
break;
|
||||
case LastQueryMode.Preserved:
|
||||
mainViewModel.LastQuerySelected = true;
|
||||
break;
|
||||
case LastQueryMode.Selected:
|
||||
mainViewModel.LastQuerySelected = false;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException($"wrong LastQueryMode: <{settings.LastQueryMode}>");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
internal static void LoadCustomPluginHotkey()
|
||||
{
|
||||
if (settings.CustomPluginHotkeys == null)
|
||||
return;
|
||||
|
||||
foreach (CustomPluginHotkey hotkey in settings.CustomPluginHotkeys)
|
||||
{
|
||||
SetCustomQueryHotkey(hotkey);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SetCustomQueryHotkey(CustomPluginHotkey hotkey)
|
||||
{
|
||||
SetHotkey(hotkey.Hotkey, (s, e) =>
|
||||
{
|
||||
if (ShouldIgnoreHotkeys())
|
||||
return;
|
||||
|
||||
mainViewModel.MainWindowVisibility = Visibility.Visible;
|
||||
mainViewModel.ChangeQueryText(hotkey.ActionKeyword);
|
||||
});
|
||||
}
|
||||
|
||||
internal static bool CheckAvailability(HotkeyModel currentHotkey)
|
||||
{
|
||||
try
|
||||
{
|
||||
HotkeyManager.Current.AddOrReplace("HotkeyAvailabilityTest", currentHotkey.CharKey, currentHotkey.ModifierKeys, (sender, e) => { });
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
HotkeyManager.Current.Remove("HotkeyAvailabilityTest");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,16 @@ namespace Flow.Launcher.Helper
|
|||
var window = Application.Current.Windows.OfType<Window>().FirstOrDefault(x => x.GetType() == typeof(T))
|
||||
?? (T)Activator.CreateInstance(typeof(T), args);
|
||||
Application.Current.MainWindow.Hide();
|
||||
|
||||
// Fix UI bug
|
||||
// Add `window.WindowState = WindowState.Normal`
|
||||
// If only use `window.Show()`, Settings-window doesn't show when minimized in taskbar
|
||||
// Not sure why this works tho
|
||||
// Probably because, when `.Show()` fails, `window.WindowState == Minimized` (not `Normal`)
|
||||
// https://stackoverflow.com/a/59719760/4230390
|
||||
window.WindowState = WindowState.Normal;
|
||||
window.Show();
|
||||
|
||||
window.Focus();
|
||||
|
||||
return (T)window;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="150" />
|
||||
<ColumnDefinition Width="120" />
|
||||
<ColumnDefinition Width="125" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox x:Name="tbHotkey" TabIndex="100" VerticalContentAlignment="Center" Grid.Column="0"
|
||||
PreviewKeyDown="TbHotkey_OnPreviewKeyDown" input:InputMethod.IsInputMethodEnabled="False"/>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ using System.Windows;
|
|||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using NHotkey.Wpf;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Helper;
|
||||
using Flow.Launcher.Infrastructure.Hotkey;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
|
|
@ -18,11 +18,7 @@ namespace Flow.Launcher
|
|||
|
||||
public event EventHandler HotkeyChanged;
|
||||
|
||||
protected virtual void OnHotkeyChanged()
|
||||
{
|
||||
EventHandler handler = HotkeyChanged;
|
||||
if (handler != null) handler(this, EventArgs.Empty);
|
||||
}
|
||||
protected virtual void OnHotkeyChanged() => HotkeyChanged?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
public HotkeyControl()
|
||||
{
|
||||
|
|
@ -90,24 +86,7 @@ namespace Flow.Launcher
|
|||
SetHotkey(new HotkeyModel(keyStr), triggerValidate);
|
||||
}
|
||||
|
||||
private bool CheckHotkeyAvailability()
|
||||
{
|
||||
try
|
||||
{
|
||||
HotkeyManager.Current.AddOrReplace("HotkeyAvailabilityTest", CurrentHotkey.CharKey, CurrentHotkey.ModifierKeys, (sender, e) => { });
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
HotkeyManager.Current.Remove("HotkeyAvailabilityTest");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
private bool CheckHotkeyAvailability() => HotKeyMapper.CheckAvailability(CurrentHotkey);
|
||||
|
||||
public new bool IsFocused
|
||||
{
|
||||
|
|
|
|||
|
|
@ -31,12 +31,15 @@
|
|||
<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="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="hideOnStartup">Hide Flow Launcher on startup</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="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-->
|
||||
<system:String x:Key="plugin">Plugin</system:String>
|
||||
|
|
@ -48,6 +51,7 @@
|
|||
<system:String x:Key="newActionKeyword">New action keyword:</system:String>
|
||||
<system:String x:Key="currentPriority">Current 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="author">Author</system:String>
|
||||
<system:String x:Key="plugin_init_time">Init time:</system:String>
|
||||
|
|
@ -70,6 +74,7 @@
|
|||
<system:String x:Key="openResultModifiers">Open Result Modifiers</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="customQuery">Query</system:String>
|
||||
<system:String x:Key="delete">Delete</system:String>
|
||||
<system:String x:Key="edit">Edit</system:String>
|
||||
<system:String x:Key="add">Add</system:String>
|
||||
|
|
@ -134,7 +139,7 @@
|
|||
<system:String x:Key="update">Update</system:String>
|
||||
|
||||
<!--Hotkey Control-->
|
||||
<system:String x:Key="hotkeyUnavailable">Hotkey unavailable</system:String>
|
||||
<system:String x:Key="hotkeyUnavailable">Hotkey Unavailable</system:String>
|
||||
|
||||
<!--Crash Reporter-->
|
||||
<system:String x:Key="reportWindow_version">Version</system:String>
|
||||
|
|
|
|||
|
|
@ -31,12 +31,15 @@
|
|||
<system:String x:Key="ignoreHotkeysOnFullscreen">Ignorovať klávesové skratky v režime na celú obrazovku</system:String>
|
||||
<system:String x:Key="pythonDirectory">Priečinok s Pythonom</system:String>
|
||||
<system:String x:Key="autoUpdates">Automatická aktualizácia</system:String>
|
||||
<system:String x:Key="autoHideScrollBar">Automaticky skryť posuvník</system:String>
|
||||
<system:String x:Key="autoHideScrollBarToolTip">Automaticky skrývať posuvník v okne nastavení a zobraziť ho, keď naň prejdete myšou</system:String>
|
||||
<system:String x:Key="selectPythonDirectory">Vybrať</system:String>
|
||||
<system:String x:Key="hideOnStartup">Schovať Flow Launcher po spustení</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="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>
|
||||
<system:String x:Key="shadowEffectNotAllowed">Efekt tieňa nie je povolený, kým má aktuálny motív povolený efekt rozostrenia</system:String>
|
||||
|
||||
<!--Setting Plugin-->
|
||||
<system:String x:Key="plugin">Plugin</system:String>
|
||||
|
|
@ -48,6 +51,7 @@
|
|||
<system:String x:Key="newActionKeyword">Nová akcia skratky:</system:String>
|
||||
<system:String x:Key="currentPriority">Aktuálna priorita:</system:String>
|
||||
<system:String x:Key="newPriority">Nová priorita:</system:String>
|
||||
<system:String x:Key="priority">Priorita:</system:String>
|
||||
<system:String x:Key="pluginDirectory">Priečinok s pluginmi</system:String>
|
||||
<system:String x:Key="author">Autor</system:String>
|
||||
<system:String x:Key="plugin_init_time">Príprava:</system:String>
|
||||
|
|
@ -70,6 +74,7 @@
|
|||
<system:String x:Key="openResultModifiers">Modifikáčné klávesy na otvorenie výsledkov</system:String>
|
||||
<system:String x:Key="showOpenResultHotkey">Zobraziť klávesovú skratku</system:String>
|
||||
<system:String x:Key="customQueryHotkey">Vlastná klávesová skratka na vyhľadávanie</system:String>
|
||||
<system:String x:Key="customQuery">Dopyt</system:String>
|
||||
<system:String x:Key="delete">Odstrániť</system:String>
|
||||
<system:String x:Key="edit">Upraviť</system:String>
|
||||
<system:String x:Key="add">Pridať</system:String>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Window x:Class="Flow.Launcher.MainWindow"
|
||||
<Window x:Class="Flow.Launcher.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:flowlauncher="clr-namespace:Flow.Launcher"
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
<KeyBinding Key="D8" Modifiers="{Binding OpenResultCommandModifiers}" Command="{Binding OpenResultCommand}" CommandParameter="7"></KeyBinding>
|
||||
<KeyBinding Key="D9" Modifiers="{Binding OpenResultCommandModifiers}" Command="{Binding OpenResultCommand}" CommandParameter="8"></KeyBinding>
|
||||
</Window.InputBindings>
|
||||
<Grid Width="750">
|
||||
<Grid>
|
||||
<Border Style="{DynamicResource WindowBorderStyle}" MouseDown="OnMouseDown" CornerRadius="5" >
|
||||
<StackPanel Orientation="Vertical">
|
||||
<Grid>
|
||||
|
|
@ -79,7 +79,6 @@
|
|||
Style="{DynamicResource QueryBoxStyle}"
|
||||
Text="{Binding QueryText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
PreviewDragOver="OnPreviewDragOver"
|
||||
TextChanged="OnTextChanged"
|
||||
AllowDrop="True"
|
||||
Visibility="Visible"
|
||||
Background="Transparent"
|
||||
|
|
@ -94,7 +93,7 @@
|
|||
</ContextMenu>
|
||||
</TextBox.ContextMenu>
|
||||
</TextBox>
|
||||
<svgc:SvgControl Source="{Binding Image}" HorizontalAlignment="Right" Width="48" Height="48"
|
||||
<svgc:SvgControl Source="{Binding Image}" HorizontalAlignment="Right" Width="42" Height="42"
|
||||
Background="Transparent"/>
|
||||
</Grid>
|
||||
<Line x:Name="ProgressBar" HorizontalAlignment="Right"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media.Animation;
|
||||
|
|
@ -10,6 +11,7 @@ using Flow.Launcher.Core.Resource;
|
|||
using Flow.Launcher.Helper;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.ViewModel;
|
||||
using Application = System.Windows.Application;
|
||||
using Screen = System.Windows.Forms.Screen;
|
||||
using ContextMenuStrip = System.Windows.Forms.ContextMenuStrip;
|
||||
using DataFormats = System.Windows.DataFormats;
|
||||
|
|
@ -22,7 +24,6 @@ namespace Flow.Launcher
|
|||
{
|
||||
public partial class MainWindow
|
||||
{
|
||||
|
||||
#region Private Fields
|
||||
|
||||
private readonly Storyboard _progressBarStoryboard = new Storyboard();
|
||||
|
|
@ -40,20 +41,23 @@ namespace Flow.Launcher
|
|||
_settings = settings;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void OnClosing(object sender, CancelEventArgs e)
|
||||
private async void OnClosing(object sender, CancelEventArgs e)
|
||||
{
|
||||
_notifyIcon.Visible = false;
|
||||
_viewModel.Save();
|
||||
e.Cancel = true;
|
||||
await PluginManager.DisposePluginsAsync();
|
||||
Application.Current.Shutdown();
|
||||
}
|
||||
|
||||
private void OnInitialized(object sender, EventArgs e)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs _)
|
||||
|
|
@ -61,8 +65,6 @@ namespace Flow.Launcher
|
|||
// show notify icon when flowlauncher is hidden
|
||||
InitializeNotifyIcon();
|
||||
|
||||
// todo is there a way to set blur only once?
|
||||
ThemeManager.Instance.SetBlurForWindow();
|
||||
WindowsInteropHelper.DisableControlBox(this);
|
||||
InitProgressbarAnimation();
|
||||
InitializePosition();
|
||||
|
|
@ -72,47 +74,63 @@ namespace Flow.Launcher
|
|||
|
||||
_viewModel.PropertyChanged += (o, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(MainViewModel.MainWindowVisibility))
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
if (_viewModel.MainWindowVisibility == Visibility.Visible)
|
||||
{
|
||||
Activate();
|
||||
QueryTextBox.Focus();
|
||||
UpdatePosition();
|
||||
_settings.ActivateTimes++;
|
||||
if (!_viewModel.LastQuerySelected)
|
||||
case nameof(MainViewModel.MainWindowVisibility):
|
||||
{
|
||||
QueryTextBox.SelectAll();
|
||||
_viewModel.LastQuerySelected = true;
|
||||
}
|
||||
if (_viewModel.MainWindowVisibility == Visibility.Visible)
|
||||
{
|
||||
Activate();
|
||||
QueryTextBox.Focus();
|
||||
UpdatePosition();
|
||||
_settings.ActivateTimes++;
|
||||
if (!_viewModel.LastQuerySelected)
|
||||
{
|
||||
QueryTextBox.SelectAll();
|
||||
_viewModel.LastQuerySelected = true;
|
||||
}
|
||||
|
||||
if (_viewModel.ProgressBarVisibility == Visibility.Visible && isProgressBarStoryboardPaused)
|
||||
{
|
||||
_progressBarStoryboard.Resume();
|
||||
isProgressBarStoryboardPaused = false;
|
||||
if (_viewModel.ProgressBarVisibility == Visibility.Visible && isProgressBarStoryboardPaused)
|
||||
{
|
||||
_progressBarStoryboard.Begin(ProgressBar, true);
|
||||
isProgressBarStoryboardPaused = false;
|
||||
}
|
||||
}
|
||||
else if (!isProgressBarStoryboardPaused)
|
||||
{
|
||||
_progressBarStoryboard.Stop(ProgressBar);
|
||||
isProgressBarStoryboardPaused = true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (!isProgressBarStoryboardPaused)
|
||||
{
|
||||
_progressBarStoryboard.Pause();
|
||||
isProgressBarStoryboardPaused = true;
|
||||
}
|
||||
}
|
||||
else if (e.PropertyName == nameof(MainViewModel.ProgressBarVisibility))
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
if (_viewModel.ProgressBarVisibility == Visibility.Hidden && !isProgressBarStoryboardPaused)
|
||||
case nameof(MainViewModel.ProgressBarVisibility):
|
||||
{
|
||||
_progressBarStoryboard.Pause();
|
||||
isProgressBarStoryboardPaused = true;
|
||||
Dispatcher.Invoke(async () =>
|
||||
{
|
||||
if (_viewModel.ProgressBarVisibility == Visibility.Hidden && !isProgressBarStoryboardPaused)
|
||||
{
|
||||
await Task.Delay(50);
|
||||
_progressBarStoryboard.Stop(ProgressBar);
|
||||
isProgressBarStoryboardPaused = true;
|
||||
}
|
||||
else if (_viewModel.MainWindowVisibility == Visibility.Visible &&
|
||||
isProgressBarStoryboardPaused)
|
||||
{
|
||||
_progressBarStoryboard.Begin(ProgressBar, true);
|
||||
isProgressBarStoryboardPaused = false;
|
||||
}
|
||||
}, System.Windows.Threading.DispatcherPriority.Render);
|
||||
|
||||
break;
|
||||
}
|
||||
else if (_viewModel.MainWindowVisibility == Visibility.Visible && isProgressBarStoryboardPaused)
|
||||
case nameof(MainViewModel.QueryTextCursorMovedToEnd):
|
||||
if (_viewModel.QueryTextCursorMovedToEnd)
|
||||
{
|
||||
_progressBarStoryboard.Resume();
|
||||
isProgressBarStoryboardPaused = false;
|
||||
MoveQueryTextToEnd();
|
||||
_viewModel.QueryTextCursorMovedToEnd = false;
|
||||
}
|
||||
}, System.Windows.Threading.DispatcherPriority.Render);
|
||||
break;
|
||||
}
|
||||
};
|
||||
_settings.PropertyChanged += (o, e) =>
|
||||
|
|
@ -189,14 +207,15 @@ namespace Flow.Launcher
|
|||
|
||||
private void InitProgressbarAnimation()
|
||||
{
|
||||
var da = new DoubleAnimation(ProgressBar.X2, ActualWidth + 100, new Duration(new TimeSpan(0, 0, 0, 0, 1600)));
|
||||
var da = new DoubleAnimation(ProgressBar.X2, ActualWidth + 100,
|
||||
new Duration(new TimeSpan(0, 0, 0, 0, 1600)));
|
||||
var da1 = new DoubleAnimation(ProgressBar.X1, ActualWidth, new Duration(new TimeSpan(0, 0, 0, 0, 1600)));
|
||||
Storyboard.SetTargetProperty(da, new PropertyPath("(Line.X2)"));
|
||||
Storyboard.SetTargetProperty(da1, new PropertyPath("(Line.X1)"));
|
||||
_progressBarStoryboard.Children.Add(da);
|
||||
_progressBarStoryboard.Children.Add(da1);
|
||||
_progressBarStoryboard.RepeatBehavior = RepeatBehavior.Forever;
|
||||
ProgressBar.BeginStoryboard(_progressBarStoryboard);
|
||||
|
||||
_viewModel.ProgressBarVisibility = Visibility.Hidden;
|
||||
isProgressBarStoryboardPaused = true;
|
||||
}
|
||||
|
|
@ -316,13 +335,9 @@ namespace Flow.Launcher
|
|||
}
|
||||
}
|
||||
|
||||
private void OnTextChanged(object sender, TextChangedEventArgs e)
|
||||
private void MoveQueryTextToEnd()
|
||||
{
|
||||
if (_viewModel.QueryTextCursorMovedToEnd)
|
||||
{
|
||||
QueryTextBox.CaretIndex = QueryTextBox.Text.Length;
|
||||
_viewModel.QueryTextCursorMovedToEnd = false;
|
||||
}
|
||||
QueryTextBox.CaretIndex = QueryTextBox.Text.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
<ColumnDefinition Width="0" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image x:Name="ImageIcon" Width="32" Height="32" HorizontalAlignment="Left"
|
||||
Source="{Binding Image.Value}" />
|
||||
Source="{Binding Image}" />
|
||||
<Grid Margin="5 0 5 0" Grid.Column="1" HorizontalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
Height="600" Width="900"
|
||||
MinWidth="850"
|
||||
MinHeight="500"
|
||||
Loaded="OnLoaded"
|
||||
Closed="OnClosed"
|
||||
d:DataContext="{d:DesignInstance vm:SettingWindowViewModel}">
|
||||
<Window.InputBindings>
|
||||
|
|
@ -36,8 +37,8 @@
|
|||
|
||||
<TabControl Height="auto" SelectedIndex="0">
|
||||
<TabItem Header="{DynamicResource general}">
|
||||
<ScrollViewer ui:ScrollViewerHelper.AutoHideScrollBars="True" Margin="60,30,0,30">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<ScrollViewer ui:ScrollViewerHelper.AutoHideScrollBars="{Binding AutoHideScrollBar, Mode=OneWay}" Margin="60,0,0,0">
|
||||
<StackPanel Orientation="Vertical" Margin="0,30,0,30">
|
||||
<ui:ToggleSwitch Margin="10" IsOn="{Binding PortableMode}">
|
||||
<TextBlock Text="{DynamicResource portableMode}" />
|
||||
</ui:ToggleSwitch>
|
||||
|
|
@ -63,9 +64,14 @@
|
|||
<ui:ToggleSwitch Margin="10" IsOn="{Binding AutoUpdates}">
|
||||
<TextBlock Text="{DynamicResource autoUpdates}" />
|
||||
</ui:ToggleSwitch>
|
||||
<CheckBox Margin="10" IsChecked="{Binding ShouldUsePinyin}" ToolTip="{DynamicResource ShouldUsePinyinToolTip}">
|
||||
<CheckBox Margin="10" IsChecked="{Binding ShouldUsePinyin}"
|
||||
ToolTip="{DynamicResource ShouldUsePinyinToolTip}">
|
||||
<TextBlock Text="{DynamicResource ShouldUsePinyin}" />
|
||||
</CheckBox>
|
||||
<ui:ToggleSwitch Margin="10" IsOn="{Binding AutoHideScrollBar, Mode=TwoWay}"
|
||||
ToolTip="{DynamicResource autoHideScrollBarToolTip}">
|
||||
<TextBlock Text="{DynamicResource autoHideScrollBar}" />
|
||||
</ui:ToggleSwitch>
|
||||
<StackPanel Margin="10" Orientation="Horizontal">
|
||||
<TextBlock Text="{DynamicResource querySearchPrecision}" FontSize="14" />
|
||||
<ComboBox Margin="10 0 0 0" MaxWidth="200"
|
||||
|
|
@ -112,7 +118,7 @@
|
|||
</TextBlock>
|
||||
<ListBox SelectedIndex="0" SelectedItem="{Binding SelectedPlugin}"
|
||||
ItemsSource="{Binding PluginViewModels}"
|
||||
Margin="10, 0, 10, 10" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ui:ScrollViewerHelper.AutoHideScrollBars="True">
|
||||
Margin="10, 0, 10, 10" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ui:ScrollViewerHelper.AutoHideScrollBars="{Binding AutoHideScrollBar, Mode=OneWay}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal" Margin="3">
|
||||
|
|
@ -174,7 +180,7 @@
|
|||
Text="{Binding PluginPair.Metadata.Description}"
|
||||
Grid.Row="1" Opacity="0.5" Grid.Column="2" />
|
||||
<DockPanel Grid.ColumnSpan="2" Grid.Row="2" Margin="0 10 0 8" HorizontalAlignment="Right">
|
||||
<TextBlock Text="Priority" Margin="15,0,0,0" MaxWidth="100"/>
|
||||
<TextBlock Text="{DynamicResource priority}" Margin="15,0,0,0" MaxWidth="100"/>
|
||||
<TextBlock Text="{Binding Priority}"
|
||||
ToolTip="Change Plugin Results Priority"
|
||||
Margin="5 0 0 0" Cursor="Hand" Foreground="Blue"
|
||||
|
|
@ -216,8 +222,10 @@
|
|||
<Run Text="{DynamicResource browserMoreThemes}" />
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
<Button DockPanel.Dock="Top" Margin="0,10,0,0" Width="180" HorizontalAlignment="Center" Click="OpenPluginFolder">Open Theme Folder</Button>
|
||||
<ListBox DockPanel.Dock="Top" SelectedItem="{Binding SelectedTheme}" ItemsSource="{Binding Themes}"
|
||||
<Button DockPanel.Dock="Top" Margin="0,10,0,0" Width="180" HorizontalAlignment="Center"
|
||||
Click="OpenPluginFolder">Open Theme Folder</Button>
|
||||
<ListBox DockPanel.Dock="Top" SelectedItem="{Binding SelectedTheme}" ItemsSource="{Binding Themes}"
|
||||
ui:ScrollViewerHelper.AutoHideScrollBars="{Binding AutoHideScrollBar, Mode=OneWay}"
|
||||
Margin="10, 0, 10, 10" Width="180" Height="394" />
|
||||
|
||||
</DockPanel>
|
||||
|
|
@ -235,7 +243,7 @@
|
|||
<RowDefinition Height="30"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" Text="{DynamicResource queryWindowShadowEffect}" Margin="0 30 0 0" FontSize="14" />
|
||||
<ui:ToggleSwitch Grid.Row="0" IsOn="{Binding DropShadowEffect}" Margin="210 23 0 0" Width="80"/>
|
||||
<ui:ToggleSwitch Grid.Row="0" IsOn="{Binding DropShadowEffect, Mode=TwoWay}" Margin="210 23 0 0" Width="80"/>
|
||||
<TextBlock Grid.Row="1" Text="{DynamicResource shadowEffectCPUUsage}"
|
||||
Width="280"
|
||||
FontSize="10" HorizontalAlignment="Left"/>
|
||||
|
|
@ -357,7 +365,7 @@
|
|||
</DataTemplate>
|
||||
</GridViewColumn.CellTemplate>
|
||||
</GridViewColumn>
|
||||
<GridViewColumn Header="{DynamicResource actionKeywords}" Width="546">
|
||||
<GridViewColumn Header="{DynamicResource customQuery}" Width="546">
|
||||
<GridViewColumn.CellTemplate>
|
||||
<DataTemplate DataType="userSettings:CustomPluginHotkey">
|
||||
<TextBlock Text="{Binding ActionKeyword}" />
|
||||
|
|
|
|||
|
|
@ -2,18 +2,17 @@ using System;
|
|||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Navigation;
|
||||
using Microsoft.Win32;
|
||||
using NHotkey;
|
||||
using NHotkey.Wpf;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.Hotkey;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using Flow.Launcher.ViewModel;
|
||||
using Flow.Launcher.Helper;
|
||||
|
||||
namespace Flow.Launcher
|
||||
{
|
||||
|
|
@ -35,6 +34,14 @@ namespace Flow.Launcher
|
|||
}
|
||||
|
||||
#region General
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Fix (workaround) for the window freezes after lock screen (Win+L)
|
||||
// https://stackoverflow.com/questions/4951058/software-rendering-mode-wpf
|
||||
HwndSource hwndSource = PresentationSource.FromVisual(this) as HwndSource;
|
||||
HwndTarget hwndTarget = hwndSource.CompositionTarget;
|
||||
hwndTarget.RenderMode = RenderMode.SoftwareOnly;
|
||||
}
|
||||
|
||||
private void OnAutoStartupChecked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
|
|
@ -118,45 +125,13 @@ namespace Flow.Launcher
|
|||
{
|
||||
if (HotkeyControl.CurrentHotkeyAvailable)
|
||||
{
|
||||
SetHotkey(HotkeyControl.CurrentHotkey, (o, args) =>
|
||||
{
|
||||
if (!Application.Current.MainWindow.IsVisible)
|
||||
{
|
||||
Application.Current.MainWindow.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
Application.Current.MainWindow.Visibility = Visibility.Hidden;
|
||||
}
|
||||
});
|
||||
RemoveHotkey(settings.Hotkey);
|
||||
|
||||
HotKeyMapper.SetHotkey(HotkeyControl.CurrentHotkey, HotKeyMapper.OnHotkey);
|
||||
HotKeyMapper.RemoveHotkey(settings.Hotkey);
|
||||
settings.Hotkey = HotkeyControl.CurrentHotkey.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
void SetHotkey(HotkeyModel hotkey, EventHandler<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);
|
||||
}
|
||||
}
|
||||
|
||||
void RemoveHotkey(string hotkeyStr)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(hotkeyStr))
|
||||
{
|
||||
HotkeyManager.Current.Remove(hotkeyStr);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDeleteCustomHotkeyClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var item = viewModel.SelectedCustomPluginHotkey;
|
||||
|
|
@ -174,7 +149,7 @@ namespace Flow.Launcher
|
|||
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
||||
{
|
||||
settings.CustomPluginHotkeys.Remove(item);
|
||||
RemoveHotkey(item.Hotkey);
|
||||
HotKeyMapper.RemoveHotkey(item.Hotkey);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,11 @@ namespace Flow.Launcher.Storage
|
|||
|
||||
private static int GenerateStaticHashCode(string s, int start = HASH_INITIAL)
|
||||
{
|
||||
if (s == null)
|
||||
{
|
||||
return start;
|
||||
}
|
||||
|
||||
unchecked
|
||||
{
|
||||
// skip the empty space
|
||||
|
|
@ -101,4 +106,4 @@ namespace Flow.Launcher.Storage
|
|||
return selectedCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -46,6 +46,8 @@
|
|||
<Setter Property="Padding" Value="8 10 8 8" />
|
||||
</Style>
|
||||
<Style x:Key="BaseWindowStyle" TargetType="{x:Type Window}">
|
||||
<Setter Property="Width" Value="750" />
|
||||
<Setter Property="RenderOptions.ClearTypeHint" Value="Enabled"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="BasePendingLineStyle" TargetType="{x:Type Line}">
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@
|
|||
</Style>
|
||||
|
||||
<Style x:Key="WindowStyle" BasedOn="{StaticResource BaseWindowStyle}" TargetType="{x:Type Window}">
|
||||
<Setter Property="Width" Value="750" /> <!-- This is used to set the blur width only -->
|
||||
<Setter Property="Background">
|
||||
<Setter.Value>
|
||||
<SolidColorBrush Color="Black" Opacity="0.6"/>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@
|
|||
</Style>
|
||||
|
||||
<Style x:Key="WindowStyle" BasedOn="{StaticResource BaseWindowStyle}" TargetType="{x:Type Window}">
|
||||
<Setter Property="Width" Value="750" /> <!-- This is used to set the blur width only -->
|
||||
<Setter Property="Background">
|
||||
<Setter.Value>
|
||||
<SolidColorBrush Color="Black" Opacity="0.3"/>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@
|
|||
</Style>
|
||||
|
||||
<Style x:Key="WindowStyle" BasedOn="{StaticResource BaseWindowStyle}" TargetType="{x:Type Window}">
|
||||
<Setter Property="Width" Value="750" /> <!-- This is used to set the blur width only -->
|
||||
<Setter Property="Background">
|
||||
<Setter.Value>
|
||||
<SolidColorBrush Color="White" Opacity="0.5"/>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ using System.Threading.Tasks;
|
|||
using System.Threading.Tasks.Dataflow;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using NHotkey;
|
||||
using NHotkey.Wpf;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Helper;
|
||||
|
|
@ -19,7 +17,10 @@ using Flow.Launcher.Plugin;
|
|||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using Flow.Launcher.Storage;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Microsoft.VisualStudio.Threading;
|
||||
using System.Threading.Channels;
|
||||
using ISavable = Flow.Launcher.Plugin.ISavable;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace Flow.Launcher.ViewModel
|
||||
{
|
||||
|
|
@ -36,7 +37,7 @@ namespace Flow.Launcher.ViewModel
|
|||
private readonly FlowLauncherJsonStorage<History> _historyItemsStorage;
|
||||
private readonly FlowLauncherJsonStorage<UserSelectedRecord> _userSelectedRecordStorage;
|
||||
private readonly FlowLauncherJsonStorage<TopMostRecord> _topMostRecordStorage;
|
||||
private readonly Settings _settings;
|
||||
internal readonly Settings _settings;
|
||||
private readonly History _history;
|
||||
private readonly UserSelectedRecord _userSelectedRecord;
|
||||
private readonly TopMostRecord _topMostRecord;
|
||||
|
|
@ -77,8 +78,6 @@ namespace Flow.Launcher.ViewModel
|
|||
RegisterViewUpdate();
|
||||
RegisterResultsUpdatedEvent();
|
||||
|
||||
SetHotkey(_settings.Hotkey, OnHotkey);
|
||||
SetCustomPluginHotkey();
|
||||
SetOpenResultModifiers();
|
||||
}
|
||||
|
||||
|
|
@ -117,7 +116,7 @@ namespace Flow.Launcher.ViewModel
|
|||
throw t.Exception;
|
||||
#else
|
||||
Log.Error($"Error happen in task dealing with viewupdate for results. {t.Exception}");
|
||||
_resultsViewUpdateTask =
|
||||
_resultsViewUpdateTask =
|
||||
Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted);
|
||||
#endif
|
||||
}
|
||||
|
|
@ -130,13 +129,17 @@ namespace Flow.Launcher.ViewModel
|
|||
var plugin = (IResultUpdated)pair.Plugin;
|
||||
plugin.ResultsUpdated += (s, e) =>
|
||||
{
|
||||
if (e.Query.RawQuery == QueryText) // TODO: allow cancellation
|
||||
if (e.Query.RawQuery != QueryText || e.Token.IsCancellationRequested)
|
||||
{
|
||||
PluginManager.UpdatePluginMetadata(e.Results, pair.Metadata, e.Query);
|
||||
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(e.Results, pair.Metadata, e.Query, _updateToken)))
|
||||
{
|
||||
Log.Error("MainViewModel", "Unable to add item to Result Update Queue");
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
var token = e.Token == default ? _updateToken : e.Token;
|
||||
|
||||
PluginManager.UpdatePluginMetadata(e.Results, pair.Metadata, e.Query);
|
||||
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(e.Results, pair.Metadata, e.Query, token)))
|
||||
{
|
||||
Log.Error("MainViewModel", "Unable to add item to Result Update Queue");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -210,7 +213,10 @@ namespace Flow.Launcher.ViewModel
|
|||
{
|
||||
if (SelectedIsFromQueryResults())
|
||||
{
|
||||
SelectedResults = ContextMenu;
|
||||
// When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing
|
||||
// i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing
|
||||
if (SelectedResults.SelectedItem != null)
|
||||
SelectedResults = ContextMenu;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -233,21 +239,24 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
ReloadPluginDataCommand = new RelayCommand(_ =>
|
||||
{
|
||||
var msg = new Msg { Owner = Application.Current.MainWindow };
|
||||
var msg = new Msg
|
||||
{
|
||||
Owner = Application.Current.MainWindow
|
||||
};
|
||||
|
||||
MainWindowVisibility = Visibility.Collapsed;
|
||||
|
||||
PluginManager
|
||||
.ReloadData()
|
||||
.ContinueWith(_ =>
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
msg.Show(
|
||||
InternationalizationManager.Instance.GetTranslation("success"),
|
||||
InternationalizationManager.Instance.GetTranslation("completedSuccessfully"),
|
||||
"");
|
||||
}))
|
||||
.ConfigureAwait(false);
|
||||
.ReloadData()
|
||||
.ContinueWith(_ =>
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
msg.Show(
|
||||
InternationalizationManager.Instance.GetTranslation("success"),
|
||||
InternationalizationManager.Instance.GetTranslation("completedSuccessfully"),
|
||||
"");
|
||||
}))
|
||||
.ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -278,8 +287,8 @@ namespace Flow.Launcher.ViewModel
|
|||
/// <param name="queryText"></param>
|
||||
public void ChangeQueryText(string queryText)
|
||||
{
|
||||
QueryTextCursorMovedToEnd = true;
|
||||
QueryText = queryText;
|
||||
QueryTextCursorMovedToEnd = true;
|
||||
}
|
||||
|
||||
public bool LastQuerySelected { get; set; }
|
||||
|
|
@ -418,7 +427,10 @@ namespace Flow.Launcher.ViewModel
|
|||
Title = string.Format(title, h.Query),
|
||||
SubTitle = string.Format(time, h.ExecutedDateTime),
|
||||
IcoPath = "Images\\history.png",
|
||||
OriginQuery = new Query { RawQuery = h.Query },
|
||||
OriginQuery = new Query
|
||||
{
|
||||
RawQuery = h.Query
|
||||
},
|
||||
Action = _ =>
|
||||
{
|
||||
SelectedResults = Results;
|
||||
|
|
@ -444,7 +456,9 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
}
|
||||
|
||||
private void QueryResults()
|
||||
private readonly IReadOnlyList<Result> _emptyResult = new List<Result>();
|
||||
|
||||
private async void QueryResults()
|
||||
{
|
||||
_updateSource?.Cancel();
|
||||
|
||||
|
|
@ -465,6 +479,12 @@ namespace Flow.Launcher.ViewModel
|
|||
ProgressBarVisibility = Visibility.Hidden;
|
||||
_isQueryRunning = true;
|
||||
|
||||
// Switch to ThreadPool thread
|
||||
await TaskScheduler.Default;
|
||||
|
||||
if (currentCancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.NonGlobalPlugins);
|
||||
|
||||
// handle the exclusiveness of plugin using action keyword
|
||||
|
|
@ -474,104 +494,80 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
var plugins = PluginManager.ValidPluginsForQuery(query);
|
||||
|
||||
Task.Run(async () =>
|
||||
if (query.ActionKeyword == Plugin.Query.GlobalPluginWildcardSign)
|
||||
{
|
||||
// Wait 45 millisecond for query change in global query
|
||||
// if query changes, return so that it won't be calculated
|
||||
await Task.Delay(45, currentCancellationToken);
|
||||
if (currentCancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
}
|
||||
|
||||
_ = Task.Delay(200, currentCancellationToken).ContinueWith(_ =>
|
||||
{
|
||||
// start the progress bar if query takes more than 200 ms and this is the current running query and it didn't finish yet
|
||||
if (!currentCancellationToken.IsCancellationRequested && _isQueryRunning)
|
||||
{
|
||||
if (query.ActionKeyword == Plugin.Query.GlobalPluginWildcardSign)
|
||||
{
|
||||
// Wait 45 millisecond for query change in global query
|
||||
// if query changes, return so that it won't be calculated
|
||||
await Task.Delay(45, currentCancellationToken);
|
||||
if (currentCancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
}
|
||||
ProgressBarVisibility = Visibility.Visible;
|
||||
}
|
||||
}, currentCancellationToken, TaskContinuationOptions.NotOnCanceled, TaskScheduler.Default);
|
||||
|
||||
_ = Task.Delay(200, currentCancellationToken).ContinueWith(_ =>
|
||||
{
|
||||
// start the progress bar if query takes more than 200 ms and this is the current running query and it didn't finish yet
|
||||
if (!currentCancellationToken.IsCancellationRequested && _isQueryRunning)
|
||||
{
|
||||
ProgressBarVisibility = Visibility.Visible;
|
||||
}
|
||||
}, currentCancellationToken);
|
||||
// plugins is ICollection, meaning LINQ will get the Count and preallocate Array
|
||||
|
||||
Task[] tasks = new Task[plugins.Count];
|
||||
try
|
||||
{
|
||||
for (var i = 0; i < plugins.Count; i++)
|
||||
{
|
||||
if (!plugins[i].Metadata.Disabled)
|
||||
{
|
||||
tasks[i] = QueryTask(plugins[i]);
|
||||
}
|
||||
else
|
||||
{
|
||||
tasks[i] = Task.CompletedTask; // Avoid Null
|
||||
}
|
||||
}
|
||||
var tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch
|
||||
{
|
||||
false => QueryTask(plugin),
|
||||
true => Task.CompletedTask
|
||||
}).ToArray();
|
||||
|
||||
// Check the code, WhenAll will translate all type of IEnumerable or Collection to Array, so make an array at first
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// nothing to do here
|
||||
}
|
||||
|
||||
if (currentCancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
try
|
||||
{
|
||||
// Check the code, WhenAll will translate all type of IEnumerable or Collection to Array, so make an array at first
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// nothing to do here
|
||||
}
|
||||
|
||||
// this should happen once after all queries are done so progress bar should continue
|
||||
// until the end of all querying
|
||||
_isQueryRunning = false;
|
||||
if (!currentCancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// update to hidden if this is still the current query
|
||||
ProgressBarVisibility = Visibility.Hidden;
|
||||
}
|
||||
if (currentCancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
// Local function
|
||||
async Task QueryTask(PluginPair plugin)
|
||||
{
|
||||
// Since it is wrapped within a Task.Run, the synchronous context is null
|
||||
// Task.Yield will force it to run in ThreadPool
|
||||
await Task.Yield();
|
||||
// this should happen once after all queries are done so progress bar should continue
|
||||
// until the end of all querying
|
||||
_isQueryRunning = false;
|
||||
if (!currentCancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// update to hidden if this is still the current query
|
||||
ProgressBarVisibility = Visibility.Hidden;
|
||||
}
|
||||
|
||||
var results = await PluginManager.QueryForPlugin(plugin, query, currentCancellationToken);
|
||||
if (currentCancellationToken.IsCancellationRequested || results == null) return;
|
||||
// Local function
|
||||
async Task QueryTask(PluginPair plugin)
|
||||
{
|
||||
// Since it is wrapped within a ThreadPool Thread, the synchronous context is null
|
||||
// Task.Yield will force it to run in ThreadPool
|
||||
await Task.Yield();
|
||||
|
||||
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, plugin.Metadata, query, currentCancellationToken)))
|
||||
{
|
||||
Log.Error("MainViewModel", "Unable to add item to Result Update Queue");
|
||||
};
|
||||
}
|
||||
}, currentCancellationToken)
|
||||
.ContinueWith(t => Log.Exception("|MainViewModel|Plugins Query Exceptions", t.Exception),
|
||||
TaskContinuationOptions.OnlyOnFaulted);
|
||||
IReadOnlyList<Result> results = await PluginManager.QueryForPluginAsync(plugin, query, currentCancellationToken);
|
||||
|
||||
currentCancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
results ??= _emptyResult;
|
||||
|
||||
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, plugin.Metadata, query, currentCancellationToken)))
|
||||
{
|
||||
Log.Error("MainViewModel", "Unable to add item to Result Update Queue");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void RemoveOldQueryResults(Query query)
|
||||
{
|
||||
string lastKeyword = _lastQuery.ActionKeyword;
|
||||
|
||||
string keyword = query.ActionKeyword;
|
||||
if (string.IsNullOrEmpty(lastKeyword))
|
||||
if (_lastQuery.ActionKeyword != query.ActionKeyword)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(keyword))
|
||||
{
|
||||
Results.KeepResultsFor(PluginManager.NonGlobalPlugins[keyword].Metadata);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrEmpty(keyword))
|
||||
{
|
||||
Results.KeepResultsExcept(PluginManager.NonGlobalPlugins[lastKeyword].Metadata);
|
||||
}
|
||||
else if (lastKeyword != keyword)
|
||||
{
|
||||
Results.KeepResultsFor(PluginManager.NonGlobalPlugins[keyword].Metadata);
|
||||
}
|
||||
Results.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -656,96 +652,12 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
#region Hotkey
|
||||
|
||||
private void SetHotkey(string hotkeyStr, EventHandler<HotkeyEventArgs> action)
|
||||
{
|
||||
var hotkey = new HotkeyModel(hotkeyStr);
|
||||
SetHotkey(hotkey, action);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveHotkey(string hotkeyStr)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(hotkeyStr))
|
||||
{
|
||||
HotkeyManager.Current.Remove(hotkeyStr);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if Flow Launcher should ignore any hotkeys
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private bool ShouldIgnoreHotkeys()
|
||||
{
|
||||
//double if to omit calling win32 function
|
||||
if (_settings.IgnoreHotkeysOnFullscreen)
|
||||
if (WindowsInteropHelper.IsWindowFullscreen())
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void SetCustomPluginHotkey()
|
||||
{
|
||||
if (_settings.CustomPluginHotkeys == null) return;
|
||||
foreach (CustomPluginHotkey hotkey in _settings.CustomPluginHotkeys)
|
||||
{
|
||||
SetHotkey(hotkey.Hotkey, (s, e) =>
|
||||
{
|
||||
if (ShouldIgnoreHotkeys()) return;
|
||||
MainWindowVisibility = Visibility.Visible;
|
||||
ChangeQueryText(hotkey.ActionKeyword);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void SetOpenResultModifiers()
|
||||
{
|
||||
OpenResultCommandModifiers = _settings.OpenResultModifiers ?? DefaultOpenResultModifiers;
|
||||
}
|
||||
|
||||
private void OnHotkey(object sender, HotkeyEventArgs e)
|
||||
{
|
||||
if (!ShouldIgnoreHotkeys())
|
||||
{
|
||||
if (_settings.LastQueryMode == LastQueryMode.Empty)
|
||||
{
|
||||
ChangeQueryText(string.Empty);
|
||||
}
|
||||
else if (_settings.LastQueryMode == LastQueryMode.Preserved)
|
||||
{
|
||||
LastQuerySelected = true;
|
||||
}
|
||||
else if (_settings.LastQueryMode == LastQueryMode.Selected)
|
||||
{
|
||||
LastQuerySelected = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"wrong LastQueryMode: <{_settings.LastQueryMode}>");
|
||||
}
|
||||
|
||||
ToggleFlowLauncher();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleFlowLauncher()
|
||||
internal void ToggleFlowLauncher()
|
||||
{
|
||||
if (MainWindowVisibility != Visibility.Visible)
|
||||
{
|
||||
|
|
@ -794,7 +706,6 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
#endif
|
||||
|
||||
|
||||
foreach (var metaResults in resultsForUpdates)
|
||||
{
|
||||
foreach (var result in metaResults.Results)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Infrastructure.Image;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
|
||||
|
|
@ -11,8 +10,6 @@ namespace Flow.Launcher.ViewModel
|
|||
{
|
||||
public PluginPair PluginPair { get; set; }
|
||||
|
||||
private readonly Internationalization _translator = InternationalizationManager.Instance;
|
||||
|
||||
public ImageSource Image => ImageLoader.Load(PluginPair.Metadata.IcoPath);
|
||||
public bool PluginState
|
||||
{
|
||||
|
|
@ -22,7 +19,7 @@ namespace Flow.Launcher.ViewModel
|
|||
PluginPair.Metadata.Disabled = !value;
|
||||
}
|
||||
}
|
||||
public Visibility ActionKeywordsVisibility => PluginPair.Metadata.ActionKeywords.Count > 1 ? Visibility.Collapsed : Visibility.Visible;
|
||||
public Visibility ActionKeywordsVisibility => PluginPair.Metadata.ActionKeywords.Count == 1 ? Visibility.Visible : Visibility.Collapsed;
|
||||
public string InitilizaTime => PluginPair.Metadata.InitTime.ToString() + "ms";
|
||||
public string QueryTime => PluginPair.Metadata.AvgQueryTime + "ms";
|
||||
public string ActionKeywordsText => string.Join(Query.ActionKeywordSeperater, PluginPair.Metadata.ActionKeywords);
|
||||
|
|
|
|||
|
|
@ -11,62 +11,12 @@ namespace Flow.Launcher.ViewModel
|
|||
{
|
||||
public class ResultViewModel : BaseModel
|
||||
{
|
||||
public class LazyAsync<T> : Lazy<ValueTask<T>>
|
||||
{
|
||||
private readonly T defaultValue;
|
||||
|
||||
private readonly Action _updateCallback;
|
||||
public new T Value
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsValueCreated)
|
||||
{
|
||||
_ = Exercute(); // manually use callback strategy
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (!base.Value.IsCompletedSuccessfully)
|
||||
return defaultValue;
|
||||
|
||||
return base.Value.Result;
|
||||
|
||||
// If none of the variables captured by the local function are captured by other lambdas,
|
||||
// the compiler can avoid heap allocations.
|
||||
async ValueTask Exercute()
|
||||
{
|
||||
await base.Value.ConfigureAwait(false);
|
||||
_updateCallback();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public LazyAsync(Func<ValueTask<T>> factory, T defaultValue, Action updateCallback) : base(factory)
|
||||
{
|
||||
if (defaultValue != null)
|
||||
{
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
_updateCallback = updateCallback;
|
||||
}
|
||||
}
|
||||
|
||||
public ResultViewModel(Result result, Settings settings)
|
||||
{
|
||||
if (result != null)
|
||||
{
|
||||
Result = result;
|
||||
|
||||
Image = new LazyAsync<ImageSource>(
|
||||
SetImage,
|
||||
ImageLoader.DefaultImage,
|
||||
() =>
|
||||
{
|
||||
OnPropertyChanged(nameof(Image));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Settings = settings;
|
||||
}
|
||||
|
|
@ -85,44 +35,55 @@ namespace Flow.Launcher.ViewModel
|
|||
? Result.SubTitle
|
||||
: Result.SubTitleToolTip;
|
||||
|
||||
public LazyAsync<ImageSource> Image { get; set; }
|
||||
private volatile bool ImageLoaded;
|
||||
|
||||
private async ValueTask<ImageSource> SetImage()
|
||||
private ImageSource image = ImageLoader.DefaultImage;
|
||||
|
||||
public ImageSource Image
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!ImageLoaded)
|
||||
{
|
||||
ImageLoaded = true;
|
||||
_ = LoadImageAsync();
|
||||
}
|
||||
return image;
|
||||
}
|
||||
private set => image = value;
|
||||
}
|
||||
private async ValueTask LoadImageAsync()
|
||||
{
|
||||
var imagePath = Result.IcoPath;
|
||||
if (string.IsNullOrEmpty(imagePath) && Result.Icon != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Result.Icon();
|
||||
image = Result.Icon();
|
||||
return;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"|ResultViewModel.Image|IcoPath is empty and exception when calling Icon() for result <{Result.Title}> of plugin <{Result.PluginDirectory}>", e);
|
||||
return ImageLoader.DefaultImage;
|
||||
}
|
||||
}
|
||||
|
||||
if (ImageLoader.CacheContainImage(imagePath))
|
||||
{
|
||||
// will get here either when icoPath has value\icon delegate is null\when had exception in delegate
|
||||
return ImageLoader.Load(imagePath);
|
||||
image = ImageLoader.Load(imagePath);
|
||||
return;
|
||||
}
|
||||
|
||||
return await Task.Run(() => ImageLoader.Load(imagePath));
|
||||
// We need to modify the property not field here to trigger the OnPropertyChanged event
|
||||
Image = await Task.Run(() => ImageLoader.Load(imagePath)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Result Result { get; }
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var r = obj as ResultViewModel;
|
||||
if (r != null)
|
||||
{
|
||||
return Result.Equals(r.Result);
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return obj is ResultViewModel r && Result.Equals(r.Result);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ namespace Flow.Launcher.ViewModel
|
|||
{
|
||||
public struct ResultsForUpdate
|
||||
{
|
||||
public List<Result> Results { get; }
|
||||
public IReadOnlyList<Result> Results { get; }
|
||||
|
||||
public PluginMetadata Metadata { get; }
|
||||
public string ID { get; }
|
||||
|
|
@ -16,7 +16,7 @@ namespace Flow.Launcher.ViewModel
|
|||
public Query Query { get; }
|
||||
public CancellationToken Token { get; }
|
||||
|
||||
public ResultsForUpdate(List<Result> results, PluginMetadata metadata, Query query, CancellationToken token)
|
||||
public ResultsForUpdate(IReadOnlyList<Result> results, PluginMetadata metadata, Query query, CancellationToken token)
|
||||
{
|
||||
Results = results;
|
||||
Metadata = metadata;
|
||||
|
|
|
|||
|
|
@ -61,6 +61,12 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
}
|
||||
|
||||
public bool AutoHideScrollBar
|
||||
{
|
||||
get => Settings.AutoHideScrollBar;
|
||||
set => Settings.AutoHideScrollBar = value;
|
||||
}
|
||||
|
||||
// This is only required to set at startup. When portable mode enabled/disabled a restart is always required
|
||||
private bool _portableMode = DataLocation.PortableDataLocationInUse();
|
||||
public bool PortableMode
|
||||
|
|
@ -138,11 +144,11 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
public bool ShouldUsePinyin
|
||||
{
|
||||
get
|
||||
get
|
||||
{
|
||||
return Settings.ShouldUsePinyin;
|
||||
return Settings.ShouldUsePinyin;
|
||||
}
|
||||
set
|
||||
set
|
||||
{
|
||||
Settings.ShouldUsePinyin = value;
|
||||
}
|
||||
|
|
@ -181,7 +187,7 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
|
||||
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_updater.GitHubRepository);
|
||||
|
||||
|
||||
if (string.IsNullOrEmpty(proxyUserName) || string.IsNullOrEmpty(Settings.Proxy.Password))
|
||||
{
|
||||
request.Proxy = new WebProxy(proxyServer, Settings.Proxy.Port);
|
||||
|
|
@ -225,7 +231,7 @@ namespace Flow.Launcher.ViewModel
|
|||
var metadatas = PluginManager.AllPlugins
|
||||
.OrderBy(x => x.Metadata.Disabled)
|
||||
.ThenBy(y => y.Metadata.Name)
|
||||
.Select(p => new PluginViewModel { PluginPair = p})
|
||||
.Select(p => new PluginViewModel { PluginPair = p })
|
||||
.ToList();
|
||||
return metadatas;
|
||||
}
|
||||
|
|
@ -265,6 +271,9 @@ namespace Flow.Launcher.ViewModel
|
|||
{
|
||||
Settings.Theme = value;
|
||||
ThemeManager.Instance.ChangeTheme(value);
|
||||
|
||||
if (ThemeManager.Instance.BlurEnabled && Settings.UseDropShadowEffect)
|
||||
DropShadowEffect = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -276,13 +285,19 @@ namespace Flow.Launcher.ViewModel
|
|||
get { return Settings.UseDropShadowEffect; }
|
||||
set
|
||||
{
|
||||
if (ThemeManager.Instance.BlurEnabled && value)
|
||||
{
|
||||
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("shadowEffectNotAllowed"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (value)
|
||||
{
|
||||
ThemeManager.Instance.AddDropShadowEffectToCurrentTheme();
|
||||
}
|
||||
else
|
||||
{
|
||||
ThemeManager.Instance.RemoveDropShadowEffectToCurrentTheme();
|
||||
ThemeManager.Instance.RemoveDropShadowEffectFromCurrentTheme();
|
||||
}
|
||||
|
||||
Settings.UseDropShadowEffect = value;
|
||||
|
|
@ -453,7 +468,7 @@ namespace Flow.Launcher.ViewModel
|
|||
#region about
|
||||
|
||||
public string Website => Constant.Website;
|
||||
public string ReleaseNotes => _updater.GitHubRepository + @"/releases/latest";
|
||||
public string ReleaseNotes => _updater.GitHubRepository + @"/releases/latest";
|
||||
public string Documentation => Constant.Documentation;
|
||||
public static string Version => Constant.Version;
|
||||
public string ActivatedTimes => string.Format(_translater.GetTranslation("about_activate_times"), Settings.ActivateTimes);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@
|
|||
<system:String x:Key="plugin_explorer_deletefilefoldersuccess">Deletion successful</system:String>
|
||||
<system:String x:Key="plugin_explorer_deletefilefoldersuccess_detail">Successfully deleted the {0}</system:String>
|
||||
<system:String x:Key="plugin_explorer_globalActionKeywordInvalid">Assigning the global action keyword could bring up too many results during search. Please choose a specific action keyword</system:String>
|
||||
<system:String x:Key="plugin_explorer_windowsSearchServiceNotRunning">The required service for Windows Index Search does not appear to be running</system:String>
|
||||
<system:String x:Key="plugin_explorer_windowsSearchServiceFix">To fix this, start the Windows Search service. Select here to remove this warning</system:String>
|
||||
<system:String x:Key="plugin_explorer_alternative">The warning message has been switched off. As an alternative for searching files and folders, would you like to install Everything plugin?{0}{0}Select 'Yes' to install Everything plugin, or 'No' to return</system:String>
|
||||
<system:String x:Key="plugin_explorer_alternative_title">Explorer Alternative</system:String>
|
||||
|
||||
<!--Controls-->
|
||||
<system:String x:Key="plugin_explorer_delete">Delete</system:String>
|
||||
|
|
@ -19,8 +23,14 @@
|
|||
<system:String x:Key="plugin_explorer_quickaccesslinks_header">Quick Access Links</system:String>
|
||||
<system:String x:Key="plugin_explorer_indexsearchexcludedpaths_header">Index Search Excluded Paths</system:String>
|
||||
<system:String x:Key="plugin_explorer_manageindexoptions">Indexing Options</system:String>
|
||||
<system:String x:Key="plugin_explorer_actionkeywordview_search">Search Activation:</system:String>
|
||||
<system:String x:Key="plugin_explorer_actionkeywordview_search">Search:</system:String>
|
||||
<system:String x:Key="plugin_explorer_actionkeywordview_pathsearch">Path Search:</system:String>
|
||||
<system:String x:Key="plugin_explorer_actionkeywordview_filecontentsearch">File Content Search:</system:String>
|
||||
<system:String x:Key="plugin_explorer_actionkeywordview_indexsearch">Index Search:</system:String>
|
||||
<system:String x:Key="plugin_explorer_actionkeyword_current">Current Action Keyword:</system:String>
|
||||
<system:String x:Key="plugin_explorer_actionkeyword_done">Done</system:String>
|
||||
<system:String x:Key="plugin_explorer_actionkeyword_enabled">Enabled</system:String>
|
||||
<system:String x:Key="plugin_explorer_actionkeyword_enabled_tooltip">When disabled Flow will not execute this search option, and will additionally revert back to '*' to free up the action keyword</system:String>
|
||||
|
||||
<!--Plugin Infos-->
|
||||
<system:String x:Key="plugin_explorer_plugin_name">Explorer</system:String>
|
||||
|
|
|
|||
66
Plugins/Flow.Launcher.Plugin.Explorer/Languages/sk.xaml
Normal file
66
Plugins/Flow.Launcher.Plugin.Explorer/Languages/sk.xaml
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:system="clr-namespace:System;assembly=mscorlib">
|
||||
|
||||
<!--Dialogues-->
|
||||
<system:String x:Key="plugin_explorer_make_selection_warning">Najprv vyberte položku</system:String>
|
||||
<system:String x:Key="plugin_explorer_select_folder_link_warning">Vyberte odkaz na priečinok</system:String>
|
||||
<system:String x:Key="plugin_explorer_delete_folder_link">Naozaj chcete odstrániť {0}?</system:String>
|
||||
<system:String x:Key="plugin_explorer_deletefilefolderconfirm">Naozaj chcete natrvalo odstrániť túto položku {0}?</system:String>
|
||||
<system:String x:Key="plugin_explorer_deletefilefoldersuccess">Odstránenie bolo úspešné</system:String>
|
||||
<system:String x:Key="plugin_explorer_deletefilefoldersuccess_detail">Úspešne odstránené {0}</system:String>
|
||||
<system:String x:Key="plugin_explorer_globalActionKeywordInvalid">Priradenie globálnej skratky akcie by mohlo počas vyhľadávania poskytnúť príliš veľa výsledkov. Vyberte si konkrétne kľúčové slovo akcie</system:String>
|
||||
<system:String x:Key="plugin_explorer_windowsSearchServiceNotRunning">Zdá sa, že požadovaná služba Windows Index Search nie je spustená</system:String>
|
||||
<system:String x:Key="plugin_explorer_windowsSearchServiceFix">Ak to chcete opraviť, spustite službu Windows Search. Ak chcete odstrániť toto upozornenie, kliknite sem</system:String>
|
||||
<system:String x:Key="plugin_explorer_alternative">Upozornenie bolo vypnuté. Chceli by ste ako alternatívu na vyhľadávanie súborov a priečinkov nainštalovať plugin Everything?{0}{0}Ak chcete nainštalovať plugin Everything, zvoľte 'Áno', pre návrat zvoľte 'Nie'</system:String>
|
||||
<system:String x:Key="plugin_explorer_alternative_title">Alternatíva pre Explorer</system:String>
|
||||
|
||||
<!--Controls-->
|
||||
<system:String x:Key="plugin_explorer_delete">Odstrániť</system:String>
|
||||
<system:String x:Key="plugin_explorer_edit">Upraviť</system:String>
|
||||
<system:String x:Key="plugin_explorer_add">Pridať</system:String>
|
||||
<system:String x:Key="plugin_explorer_manageactionkeywords_header">Upraviť skratku akcie</system:String>
|
||||
<system:String x:Key="plugin_explorer_quickaccesslinks_header">Odkazy Rýchleho prístupu</system:String>
|
||||
<system:String x:Key="plugin_explorer_indexsearchexcludedpaths_header">Vylúčené umiestnenia indexovania</system:String>
|
||||
<system:String x:Key="plugin_explorer_manageindexoptions">Možnosti indexovania</system:String>
|
||||
<system:String x:Key="plugin_explorer_actionkeywordview_search">Vyhľadávanie:</system:String>
|
||||
<system:String x:Key="plugin_explorer_actionkeywordview_pathsearch">Vyhľadávanie umiestnenia:</system:String>
|
||||
<system:String x:Key="plugin_explorer_actionkeywordview_filecontentsearch">Vyhľadávanie obsahu súborov:</system:String>
|
||||
<system:String x:Key="plugin_explorer_actionkeywordview_indexsearch">Vyhľadávanie v indexe:</system:String>
|
||||
<system:String x:Key="plugin_explorer_actionkeyword_current">Aktuálna skratka:</system:String>
|
||||
<system:String x:Key="plugin_explorer_actionkeyword_done">Hotovo</system:String>
|
||||
<system:String x:Key="plugin_explorer_actionkeyword_enabled">Povolené</system:String>
|
||||
<system:String x:Key="plugin_explorer_actionkeyword_enabled_tooltip">Ak je vypnuté, Flow túto možnosť vyhľadávania nevykoná a následne sa vráti späť na "*", aby sa uvoľnila skratka akcie.</system:String>
|
||||
|
||||
<!--Plugin Infos-->
|
||||
<system:String x:Key="plugin_explorer_plugin_name">Explorer</system:String>
|
||||
<system:String x:Key="plugin_explorer_plugin_description">Vyhľadáva a spravuje súbory a priečinky. Explorer používa indexovanie vyhľadávania vo Windowse</system:String>
|
||||
|
||||
<!--Context menu items-->
|
||||
<system:String x:Key="plugin_explorer_copypath">Kopírovať cestu</system:String>
|
||||
<system:String x:Key="plugin_explorer_copyfilefolder">Kopírovať</system:String>
|
||||
<system:String x:Key="plugin_explorer_deletefilefolder">Odstrániť</system:String>
|
||||
<system:String x:Key="plugin_explorer_path">Umiestnenie:</system:String>
|
||||
<system:String x:Key="plugin_explorer_deletefilefolder_subtitle">Odstrániť vybrané</system:String>
|
||||
<system:String x:Key="plugin_explorer_runasdifferentuser">Spustiť ako iný používateľ</system:String>
|
||||
<system:String x:Key="plugin_explorer_runasdifferentuser_subtitle">Spustí vybranú položku ako používateľ s iným kontom</system:String>
|
||||
<system:String x:Key="plugin_explorer_opencontainingfolder">Otvoriť umiestnenie priečinka</system:String>
|
||||
<system:String x:Key="plugin_explorer_opencontainingfolder_subtitle">Otvorí umiestnenie, ktoré obsahuje súbor alebo priečinok</system:String>
|
||||
<system:String x:Key="plugin_explorer_openwitheditor">Otvoriť editorom:</system:String>
|
||||
<system:String x:Key="plugin_explorer_excludefromindexsearch">Vylúčiť položku a jej podpriečinky z indexu vyhľadávania</system:String>
|
||||
<system:String x:Key="plugin_explorer_excludedfromindexsearch_msg">Vylúčiť z indexu vyhľadávania</system:String>
|
||||
<system:String x:Key="plugin_explorer_openindexingoptions">Otvoriť možnosti vyhľadávania vo Windowse</system:String>
|
||||
<system:String x:Key="plugin_explorer_openindexingoptions_subtitle">Správa indexovaných súborov a priečinkov</system:String>
|
||||
<system:String x:Key="plugin_explorer_openindexingoptions_errormsg">Nepodarilo sa otvoriť možnosti indexu vyhľadávania</system:String>
|
||||
<system:String x:Key="plugin_explorer_add_to_quickaccess_title">Pridať do Rýchleho prístupu</system:String>
|
||||
<system:String x:Key="plugin_explorer_add_to_quickaccess_subtitle">Pridať tento {0} do Rýchleho prístupu</system:String>
|
||||
<system:String x:Key="plugin_explorer_addfilefoldersuccess">Úspešne pridané</system:String>
|
||||
<system:String x:Key="plugin_explorer_addfilefoldersuccess_detail">Úspešne pridané do Rýchleho prístupu</system:String>
|
||||
<system:String x:Key="plugin_explorer_removefilefoldersuccess">Úspešne odstránené</system:String>
|
||||
<system:String x:Key="plugin_explorer_removefilefoldersuccess_detail">Úspešne odstránené z Rýchleho prístupu</system:String>
|
||||
<system:String x:Key="plugin_explorer_contextmenu_titletooltip">Pridať do Rýchleho prístupu, aby ho bolo možné otvoriť pomocou skratky akcie pluginu Explorer</system:String>
|
||||
<system:String x:Key="plugin_explorer_contextmenu_remove_titletooltip">Odstráni z Rýchleho prístupu</system:String>
|
||||
<system:String x:Key="plugin_explorer_remove_from_quickaccess_title">Odstrániť z Rýchleho prístupu</system:String>
|
||||
<system:String x:Key="plugin_explorer_remove_from_quickaccess_subtitle">Odstráni {0} z Rýchleho prístupu</system:String>
|
||||
|
||||
</ResourceDictionary>
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
<system:String x:Key="plugin_explorer_indexsearchexcludedpaths_header">索引搜索排除的路径</system:String>
|
||||
<system:String x:Key="plugin_explorer_manageindexoptions">索引选项</system:String>
|
||||
<system:String x:Key="plugin_explorer_actionkeywordview_search">搜索激活:</system:String>
|
||||
<system:String x:Key="plugin_explorer_actionkeywordview_pathsearch">路径搜索激活:</system:String>
|
||||
<system:String x:Key="plugin_explorer_actionkeywordview_filecontentsearch">文件内容搜索:</system:String>
|
||||
|
||||
<!--Plugin Infos-->
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ namespace Flow.Launcher.Plugin.Explorer
|
|||
return new ExplorerSettings(viewModel);
|
||||
}
|
||||
|
||||
public async Task InitAsync(PluginInitContext context)
|
||||
public Task InitAsync(PluginInitContext context)
|
||||
{
|
||||
Context = context;
|
||||
|
||||
|
|
@ -46,7 +46,9 @@ namespace Flow.Launcher.Plugin.Explorer
|
|||
|
||||
contextMenu = new ContextMenu(Context, Settings, viewModel);
|
||||
searchManager = new SearchManager(Settings, Context);
|
||||
ResultManager.Init(Context);
|
||||
ResultManager.Init(Context, Settings);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public List<Result> LoadContextMenus(Result selectedResult)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
|
|||
RecurseSubdirectories = true
|
||||
}, query, search, criteria, token);
|
||||
|
||||
return DirectorySearch(new EnumerationOptions(), query, search, criteria, token); // null will be passed as default
|
||||
return DirectorySearch(new EnumerationOptions(), query, search, criteria,
|
||||
token); // null will be passed as default
|
||||
}
|
||||
|
||||
public static string ConstructSearchCriteria(string search)
|
||||
|
|
@ -57,7 +58,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
|
|||
{
|
||||
var directoryInfo = new System.IO.DirectoryInfo(path);
|
||||
|
||||
foreach (var fileSystemInfo in directoryInfo.EnumerateFileSystemInfos(searchCriteria, enumerationOption))
|
||||
foreach (var fileSystemInfo in directoryInfo.EnumerateFileSystemInfos(searchCriteria, enumerationOption)
|
||||
)
|
||||
{
|
||||
if (fileSystemInfo is System.IO.DirectoryInfo)
|
||||
{
|
||||
|
|
@ -74,17 +76,10 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (!(e is ArgumentException))
|
||||
throw e;
|
||||
|
||||
Log.Exception("Flow.Plugin.Explorer.", nameof(DirectoryInfoSearch), e);
|
||||
results.Add(new Result {Title = e.Message, Score = 501});
|
||||
|
||||
return results;
|
||||
|
||||
#if DEBUG // Please investigate and handle error from DirectoryInfo search
|
||||
#else
|
||||
Log.Exception($"|Flow.Launcher.Plugin.Explorer.DirectoryInfoSearch|Error from performing DirectoryInfoSearch", e);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Initial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection.
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
{
|
||||
internal static bool IsEnvironmentVariableSearch(string search)
|
||||
{
|
||||
return LoadEnvironmentStringPaths().Count > 0
|
||||
&& search.StartsWith("%")
|
||||
return search.StartsWith("%")
|
||||
&& search != "%%"
|
||||
&& !search.Contains("\\");
|
||||
&& !search.Contains("\\") &&
|
||||
LoadEnvironmentStringPaths().Count > 0;
|
||||
}
|
||||
|
||||
internal static Dictionary<string, string> LoadEnvironmentStringPaths()
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
public static class ResultManager
|
||||
{
|
||||
private static PluginInitContext Context;
|
||||
private static Settings Settings { get; set; }
|
||||
|
||||
public static void Init(PluginInitContext context)
|
||||
public static void Init(PluginInitContext context, Settings settings)
|
||||
{
|
||||
Context = context;
|
||||
Settings = settings;
|
||||
}
|
||||
|
||||
internal static Result CreateFolderResult(string title, string subtitle, string path, Query query, int score = 0, bool showIndexState = false, bool windowsIndexed = false)
|
||||
|
|
@ -26,7 +28,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
TitleHighlightData = StringMatcher.FuzzySearch(query.Search, title).MatchData,
|
||||
Action = c =>
|
||||
{
|
||||
if (c.SpecialKeyState.CtrlPressed)
|
||||
if (c.SpecialKeyState.CtrlPressed || (!Settings.PathSearchKeywordEnabled && !Settings.SearchActionKeywordEnabled))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -39,17 +41,25 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
return false;
|
||||
}
|
||||
}
|
||||
// one of it is enabled
|
||||
var keyword = Settings.SearchActionKeywordEnabled ? Settings.SearchActionKeyword : Settings.PathSearchActionKeyword;
|
||||
|
||||
keyword = keyword == Query.GlobalPluginWildcardSign ? string.Empty : keyword + " ";
|
||||
|
||||
string changeTo = path.EndsWith(Constants.DirectorySeperator) ? path : path + Constants.DirectorySeperator;
|
||||
Context.API.ChangeQuery(string.IsNullOrEmpty(query.ActionKeyword) ?
|
||||
changeTo :
|
||||
query.ActionKeyword + " " + changeTo);
|
||||
Context.API.ChangeQuery($"{keyword}{changeTo}");
|
||||
return false;
|
||||
},
|
||||
Score = score,
|
||||
TitleToolTip = Constants.ToolTipOpenDirectory,
|
||||
SubTitleToolTip = Constants.ToolTipOpenDirectory,
|
||||
ContextData = new SearchResult { Type = ResultType.Folder, FullPath = path, ShowIndexState = showIndexState, WindowsIndexed = windowsIndexed }
|
||||
SubTitleToolTip = path,
|
||||
ContextData = new SearchResult
|
||||
{
|
||||
Type = ResultType.Folder,
|
||||
FullPath = path,
|
||||
ShowIndexState = showIndexState,
|
||||
WindowsIndexed = windowsIndexed
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +67,10 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
{
|
||||
var retrievedDirectoryPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path);
|
||||
|
||||
var folderName = retrievedDirectoryPath.TrimEnd(Constants.DirectorySeperator).Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None).Last();
|
||||
var folderName = retrievedDirectoryPath.TrimEnd(Constants.DirectorySeperator).Split(new[]
|
||||
{
|
||||
Path.DirectorySeparatorChar
|
||||
}, StringSplitOptions.None).Last();
|
||||
|
||||
if (retrievedDirectoryPath.EndsWith(":\\"))
|
||||
{
|
||||
|
|
@ -81,7 +94,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
{
|
||||
Title = title,
|
||||
SubTitle = $"Use > to search within {subtitleFolderName}, " +
|
||||
$"* to search for file extensions or >* to combine both searches.",
|
||||
$"* to search for file extensions or >* to combine both searches.",
|
||||
IcoPath = retrievedDirectoryPath,
|
||||
Score = 500,
|
||||
Action = c =>
|
||||
|
|
@ -91,7 +104,13 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
},
|
||||
TitleToolTip = retrievedDirectoryPath,
|
||||
SubTitleToolTip = retrievedDirectoryPath,
|
||||
ContextData = new SearchResult { Type = ResultType.Folder, FullPath = retrievedDirectoryPath, ShowIndexState = true, WindowsIndexed = windowsIndexed }
|
||||
ContextData = new SearchResult
|
||||
{
|
||||
Type = ResultType.Folder,
|
||||
FullPath = retrievedDirectoryPath,
|
||||
ShowIndexState = true,
|
||||
WindowsIndexed = windowsIndexed
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -125,8 +144,14 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
return true;
|
||||
},
|
||||
TitleToolTip = Constants.ToolTipOpenContainingFolder,
|
||||
SubTitleToolTip = Constants.ToolTipOpenContainingFolder,
|
||||
ContextData = new SearchResult { Type = ResultType.File, FullPath = filePath, ShowIndexState = showIndexState, WindowsIndexed = windowsIndexed }
|
||||
SubTitleToolTip = filePath,
|
||||
ContextData = new SearchResult
|
||||
{
|
||||
Type = ResultType.File,
|
||||
FullPath = filePath,
|
||||
ShowIndexState = showIndexState,
|
||||
WindowsIndexed = windowsIndexed
|
||||
}
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
|
@ -148,4 +173,4 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
Folder,
|
||||
File
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,20 +12,21 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
{
|
||||
public class SearchManager
|
||||
{
|
||||
private readonly PluginInitContext context;
|
||||
internal static PluginInitContext Context;
|
||||
|
||||
private readonly Settings settings;
|
||||
internal static Settings Settings;
|
||||
|
||||
public SearchManager(Settings settings, PluginInitContext context)
|
||||
{
|
||||
this.context = context;
|
||||
this.settings = settings;
|
||||
Context = context;
|
||||
Settings = settings;
|
||||
}
|
||||
|
||||
private class PathEqualityComparator : IEqualityComparer<Result>
|
||||
{
|
||||
private static PathEqualityComparator instance;
|
||||
public static PathEqualityComparator Instance => instance ??= new PathEqualityComparator();
|
||||
|
||||
public bool Equals(Result x, Result y)
|
||||
{
|
||||
return x.SubTitle == y.SubTitle;
|
||||
|
|
@ -39,37 +40,70 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
|
||||
internal async Task<List<Result>> SearchAsync(Query query, CancellationToken token)
|
||||
{
|
||||
var results = new HashSet<Result>(PathEqualityComparator.Instance);
|
||||
|
||||
var querySearch = query.Search;
|
||||
|
||||
if (IsFileContentSearch(query.ActionKeyword))
|
||||
return await WindowsIndexFileContentSearchAsync(query, querySearch, token).ConfigureAwait(false);
|
||||
|
||||
var result = new HashSet<Result>(PathEqualityComparator.Instance);
|
||||
|
||||
if (ActionKeywordMatch(query, Settings.ActionKeyword.PathSearchActionKeyword) ||
|
||||
ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword))
|
||||
{
|
||||
result.UnionWith(await PathSearchAsync(query, token).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
if ((ActionKeywordMatch(query, Settings.ActionKeyword.IndexSearchActionKeyword) ||
|
||||
ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword)) &&
|
||||
querySearch.Length > 0 &&
|
||||
!querySearch.IsLocationPathString())
|
||||
{
|
||||
result.UnionWith(await WindowsIndexFilesAndFoldersSearchAsync(query, querySearch, token)
|
||||
.ConfigureAwait(false));
|
||||
}
|
||||
|
||||
return result.ToList();
|
||||
}
|
||||
|
||||
private bool ActionKeywordMatch(Query query, Settings.ActionKeyword allowedActionKeyword)
|
||||
{
|
||||
var keyword = query.ActionKeyword.Length == 0 ? Query.GlobalPluginWildcardSign : query.ActionKeyword;
|
||||
|
||||
return allowedActionKeyword switch
|
||||
{
|
||||
Settings.ActionKeyword.SearchActionKeyword => Settings.SearchActionKeywordEnabled &&
|
||||
keyword == Settings.SearchActionKeyword,
|
||||
Settings.ActionKeyword.PathSearchActionKeyword => Settings.PathSearchKeywordEnabled &&
|
||||
keyword == Settings.PathSearchActionKeyword,
|
||||
Settings.ActionKeyword.FileContentSearchActionKeyword => keyword ==
|
||||
Settings.FileContentSearchActionKeyword,
|
||||
Settings.ActionKeyword.IndexSearchActionKeyword => Settings.IndexOnlySearchKeywordEnabled &&
|
||||
keyword == Settings.IndexSearchActionKeyword
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<List<Result>> PathSearchAsync(Query query, CancellationToken token = default)
|
||||
{
|
||||
var querySearch = query.Search;
|
||||
|
||||
// This allows the user to type the assigned action keyword and only see the list of quick folder links
|
||||
if (string.IsNullOrEmpty(query.Search))
|
||||
return QuickAccess.AccessLinkListAll(query, settings.QuickAccessLinks);
|
||||
return QuickAccess.AccessLinkListAll(query, Settings.QuickAccessLinks);
|
||||
|
||||
var quickaccessLinks = QuickAccess.AccessLinkListMatched(query, settings.QuickAccessLinks);
|
||||
var results = new HashSet<Result>(PathEqualityComparator.Instance);
|
||||
|
||||
if (quickaccessLinks.Count > 0)
|
||||
results.UnionWith(quickaccessLinks);
|
||||
var quickaccessLinks = QuickAccess.AccessLinkListMatched(query, Settings.QuickAccessLinks);
|
||||
|
||||
results.UnionWith(quickaccessLinks);
|
||||
|
||||
var isEnvironmentVariable = EnvironmentVariables.IsEnvironmentVariableSearch(querySearch);
|
||||
|
||||
if (isEnvironmentVariable)
|
||||
return EnvironmentVariables.GetEnvironmentStringPathSuggestions(querySearch, query, context);
|
||||
return EnvironmentVariables.GetEnvironmentStringPathSuggestions(querySearch, query, Context);
|
||||
|
||||
// Query is a location path with a full environment variable, eg. %appdata%\somefolder\
|
||||
var isEnvironmentVariablePath = querySearch[1..].Contains("%\\");
|
||||
|
||||
if (!querySearch.IsLocationPathString() && !isEnvironmentVariablePath)
|
||||
{
|
||||
results.UnionWith(await WindowsIndexFilesAndFoldersSearchAsync(query, querySearch, token).ConfigureAwait(false));
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
|
||||
var locationPath = querySearch;
|
||||
|
||||
if (isEnvironmentVariablePath)
|
||||
|
|
@ -99,24 +133,26 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
return results.ToList();
|
||||
}
|
||||
|
||||
private async Task<List<Result>> WindowsIndexFileContentSearchAsync(Query query, string querySearchString, CancellationToken token)
|
||||
private async Task<List<Result>> WindowsIndexFileContentSearchAsync(Query query, string querySearchString,
|
||||
CancellationToken token)
|
||||
{
|
||||
var queryConstructor = new QueryConstructor(settings);
|
||||
var queryConstructor = new QueryConstructor(Settings);
|
||||
|
||||
if (string.IsNullOrEmpty(querySearchString))
|
||||
return new List<Result>();
|
||||
|
||||
return await IndexSearch.WindowsIndexSearchAsync(querySearchString,
|
||||
queryConstructor.CreateQueryHelper().ConnectionString,
|
||||
queryConstructor.QueryForFileContentSearch,
|
||||
settings.IndexSearchExcludedSubdirectoryPaths,
|
||||
query,
|
||||
token).ConfigureAwait(false);
|
||||
return await IndexSearch.WindowsIndexSearchAsync(
|
||||
querySearchString,
|
||||
queryConstructor.CreateQueryHelper,
|
||||
queryConstructor.QueryForFileContentSearch,
|
||||
Settings.IndexSearchExcludedSubdirectoryPaths,
|
||||
query,
|
||||
token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public bool IsFileContentSearch(string actionKeyword)
|
||||
{
|
||||
return actionKeyword == settings.FileContentSearchActionKeyword;
|
||||
return actionKeyword == Settings.FileContentSearchActionKeyword;
|
||||
}
|
||||
|
||||
private List<Result> DirectoryInfoClassSearch(Query query, string querySearch, CancellationToken token)
|
||||
|
|
@ -138,43 +174,47 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
return await windowsIndexSearch(query, querySearchString, token);
|
||||
}
|
||||
|
||||
private async Task<List<Result>> WindowsIndexFilesAndFoldersSearchAsync(Query query, string querySearchString, CancellationToken token)
|
||||
private async Task<List<Result>> WindowsIndexFilesAndFoldersSearchAsync(Query query, string querySearchString,
|
||||
CancellationToken token)
|
||||
{
|
||||
var queryConstructor = new QueryConstructor(settings);
|
||||
var queryConstructor = new QueryConstructor(Settings);
|
||||
|
||||
return await IndexSearch.WindowsIndexSearchAsync(querySearchString,
|
||||
queryConstructor.CreateQueryHelper().ConnectionString,
|
||||
queryConstructor.QueryForAllFilesAndFolders,
|
||||
settings.IndexSearchExcludedSubdirectoryPaths,
|
||||
query,
|
||||
token).ConfigureAwait(false);
|
||||
return await IndexSearch.WindowsIndexSearchAsync(
|
||||
querySearchString,
|
||||
queryConstructor.CreateQueryHelper,
|
||||
queryConstructor.QueryForAllFilesAndFolders,
|
||||
Settings.IndexSearchExcludedSubdirectoryPaths,
|
||||
query,
|
||||
token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<List<Result>> WindowsIndexTopLevelFolderSearchAsync(Query query, string path, CancellationToken token)
|
||||
private async Task<List<Result>> WindowsIndexTopLevelFolderSearchAsync(Query query, string path,
|
||||
CancellationToken token)
|
||||
{
|
||||
var queryConstructor = new QueryConstructor(settings);
|
||||
var queryConstructor = new QueryConstructor(Settings);
|
||||
|
||||
return await IndexSearch.WindowsIndexSearchAsync(path,
|
||||
queryConstructor.CreateQueryHelper().ConnectionString,
|
||||
queryConstructor.QueryForTopLevelDirectorySearch,
|
||||
settings.IndexSearchExcludedSubdirectoryPaths,
|
||||
query,
|
||||
token).ConfigureAwait(false);
|
||||
return await IndexSearch.WindowsIndexSearchAsync(
|
||||
path,
|
||||
queryConstructor.CreateQueryHelper,
|
||||
queryConstructor.QueryForTopLevelDirectorySearch,
|
||||
Settings.IndexSearchExcludedSubdirectoryPaths,
|
||||
query,
|
||||
token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private bool UseWindowsIndexForDirectorySearch(string locationPath)
|
||||
{
|
||||
var pathToDirectory = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath);
|
||||
|
||||
if (!settings.UseWindowsIndexForDirectorySearch)
|
||||
if (!Settings.UseWindowsIndexForDirectorySearch)
|
||||
return false;
|
||||
|
||||
if (settings.IndexSearchExcludedSubdirectoryPaths
|
||||
.Any(x => FilesFolders.ReturnPreviousDirectoryIfIncompleteString(pathToDirectory)
|
||||
.StartsWith(x.Path, StringComparison.OrdinalIgnoreCase)))
|
||||
if (Settings.IndexSearchExcludedSubdirectoryPaths
|
||||
.Any(x => FilesFolders.ReturnPreviousDirectoryIfIncompleteString(pathToDirectory)
|
||||
.StartsWith(x.Path, StringComparison.OrdinalIgnoreCase)))
|
||||
return false;
|
||||
|
||||
return IndexSearch.PathIsIndexed(pathToDirectory);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,9 +5,11 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Data.OleDb;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
||||
{
|
||||
|
|
@ -17,20 +19,20 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
|||
// Reserved keywords in oleDB
|
||||
private const string reservedStringPattern = @"^[`\@\#\^,\&\/\\\$\%_]+$";
|
||||
|
||||
internal async static Task<List<Result>> ExecuteWindowsIndexSearchAsync(string indexQueryString, string connectionString, Query query, CancellationToken token)
|
||||
internal static async Task<List<Result>> ExecuteWindowsIndexSearchAsync(string indexQueryString, string connectionString, Query query, CancellationToken token)
|
||||
{
|
||||
var results = new List<Result>();
|
||||
var fileResults = new List<Result>();
|
||||
|
||||
try
|
||||
{
|
||||
using var conn = new OleDbConnection(connectionString);
|
||||
await using var conn = new OleDbConnection(connectionString);
|
||||
await conn.OpenAsync(token);
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
using var command = new OleDbCommand(indexQueryString, conn);
|
||||
await using var command = new OleDbCommand(indexQueryString, conn);
|
||||
// Results return as an OleDbDataReader.
|
||||
using var dataReaderResults = await command.ExecuteReaderAsync(token) as OleDbDataReader;
|
||||
await using var dataReaderResults = await command.ExecuteReaderAsync(token) as OleDbDataReader;
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
if (dataReaderResults.HasRows)
|
||||
|
|
@ -42,18 +44,18 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
|||
{
|
||||
// # is URI syntax for the fragment component, need to be encoded so LocalPath returns complete path
|
||||
var encodedFragmentPath = dataReaderResults
|
||||
.GetString(1)
|
||||
.Replace("#", "%23", StringComparison.OrdinalIgnoreCase);
|
||||
.GetString(1)
|
||||
.Replace("#", "%23", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var path = new Uri(encodedFragmentPath).LocalPath;
|
||||
|
||||
if (dataReaderResults.GetString(2) == "Directory")
|
||||
{
|
||||
results.Add(ResultManager.CreateFolderResult(
|
||||
dataReaderResults.GetString(0),
|
||||
path,
|
||||
path,
|
||||
query, 0, true, true));
|
||||
dataReaderResults.GetString(0),
|
||||
path,
|
||||
path,
|
||||
query, 0, true, true));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -63,6 +65,11 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
|||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// return empty result when cancelled
|
||||
return results;
|
||||
}
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
// Internal error from ExecuteReader(): Connection closed.
|
||||
|
|
@ -79,22 +86,36 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
|||
return results;
|
||||
}
|
||||
|
||||
internal async static Task<List<Result>> WindowsIndexSearchAsync(string searchString, string connectionString,
|
||||
Func<string, string> constructQuery,
|
||||
List<AccessLink> exclusionList,
|
||||
Query query,
|
||||
CancellationToken token)
|
||||
internal async static Task<List<Result>> WindowsIndexSearchAsync(
|
||||
string searchString,
|
||||
Func<CSearchQueryHelper> createQueryHelper,
|
||||
Func<string, string> constructQuery,
|
||||
List<AccessLink> exclusionList,
|
||||
Query query,
|
||||
CancellationToken token)
|
||||
{
|
||||
var regexMatch = Regex.Match(searchString, reservedStringPattern);
|
||||
|
||||
if (regexMatch.Success)
|
||||
return new List<Result>();
|
||||
|
||||
try
|
||||
{
|
||||
var constructedQuery = constructQuery(searchString);
|
||||
|
||||
var constructedQuery = constructQuery(searchString);
|
||||
return RemoveResultsInExclusionList(
|
||||
await ExecuteWindowsIndexSearchAsync(constructedQuery, connectionString, query, token).ConfigureAwait(false),
|
||||
return RemoveResultsInExclusionList(
|
||||
await ExecuteWindowsIndexSearchAsync(constructedQuery, createQueryHelper().ConnectionString, query, token).ConfigureAwait(false),
|
||||
exclusionList,
|
||||
token);
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
// Occurs because the Windows Indexing (WSearch) is turned off in services and unable to be used by Explorer plugin
|
||||
if (!SearchManager.Settings.WarnWindowsSearchServiceOff)
|
||||
return new List<Result>();
|
||||
|
||||
return ResultForWindexSearchOff(query.RawQuery);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Result> RemoveResultsInExclusionList(List<Result> results, List<AccessLink> exclusionList, CancellationToken token)
|
||||
|
|
@ -132,9 +153,66 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
|||
|
||||
internal static bool PathIsIndexed(string path)
|
||||
{
|
||||
var csm = new CSearchManager();
|
||||
var indexManager = csm.GetCatalog("SystemIndex").GetCrawlScopeManager();
|
||||
return indexManager.IncludedInCrawlScope(path) > 0;
|
||||
try
|
||||
{
|
||||
var csm = new CSearchManager();
|
||||
var indexManager = csm.GetCatalog("SystemIndex").GetCrawlScopeManager();
|
||||
return indexManager.IncludedInCrawlScope(path) > 0;
|
||||
}
|
||||
catch(COMException)
|
||||
{
|
||||
// Occurs because the Windows Indexing (WSearch) is turned off in services and unable to be used by Explorer plugin
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Result> ResultForWindexSearchOff(string rawQuery)
|
||||
{
|
||||
var api = SearchManager.Context.API;
|
||||
|
||||
return new List<Result>
|
||||
{
|
||||
new Result
|
||||
{
|
||||
Title = api.GetTranslation("plugin_explorer_windowsSearchServiceNotRunning"),
|
||||
SubTitle = api.GetTranslation("plugin_explorer_windowsSearchServiceFix"),
|
||||
Action = c =>
|
||||
{
|
||||
SearchManager.Settings.WarnWindowsSearchServiceOff = false;
|
||||
|
||||
var pluginsManagerPlugin= api.GetAllPlugins().FirstOrDefault(x => x.Metadata.ID == "9f8f9b14-2518-4907-b211-35ab6290dee7");
|
||||
|
||||
var actionKeywordCount = pluginsManagerPlugin.Metadata.ActionKeywords.Count;
|
||||
|
||||
if (actionKeywordCount > 1)
|
||||
LogException("PluginsManager's action keyword has increased to more than 1, this does not allow for determining the " +
|
||||
"right action keyword. Explorer's code for managing Windows Search service not running exception needs to be updated",
|
||||
new InvalidOperationException());
|
||||
|
||||
if (MessageBox.Show(string.Format(api.GetTranslation("plugin_explorer_alternative"), Environment.NewLine),
|
||||
api.GetTranslation("plugin_explorer_alternative_title"),
|
||||
MessageBoxButton.YesNo) == MessageBoxResult.Yes
|
||||
&& actionKeywordCount == 1)
|
||||
{
|
||||
api.ChangeQuery(string.Format("{0} install everything", pluginsManagerPlugin.Metadata.ActionKeywords[0]));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clears the warning message because same query string will not alter the displayed result list
|
||||
api.ChangeQuery(string.Empty);
|
||||
|
||||
api.ChangeQuery(rawQuery);
|
||||
}
|
||||
|
||||
var mainWindow = Application.Current.MainWindow;
|
||||
mainWindow.Visibility = Visibility.Visible;
|
||||
mainWindow.Focus();
|
||||
|
||||
return false;
|
||||
},
|
||||
IcoPath = Constants.ExplorerIconImagePath
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static void LogException(string message, Exception e)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
using Flow.Launcher.Plugin.Explorer.Search;
|
||||
using Flow.Launcher.Plugin.Explorer.Search;
|
||||
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
|
||||
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Flow.Launcher.Plugin.Explorer
|
||||
{
|
||||
|
|
@ -18,7 +21,59 @@ namespace Flow.Launcher.Plugin.Explorer
|
|||
public List<AccessLink> IndexSearchExcludedSubdirectoryPaths { get; set; } = new List<AccessLink>();
|
||||
|
||||
public string SearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign;
|
||||
public bool SearchActionKeywordEnabled { get; set; } = true;
|
||||
|
||||
public string FileContentSearchActionKeyword { get; set; } = Constants.DefaultContentSearchActionKeyword;
|
||||
|
||||
public string PathSearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign;
|
||||
|
||||
public bool PathSearchKeywordEnabled { get; set; }
|
||||
|
||||
public string IndexSearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign;
|
||||
|
||||
public bool IndexOnlySearchKeywordEnabled { get; set; }
|
||||
|
||||
public bool WarnWindowsSearchServiceOff { get; set; } = true;
|
||||
|
||||
internal enum ActionKeyword
|
||||
{
|
||||
SearchActionKeyword,
|
||||
PathSearchActionKeyword,
|
||||
FileContentSearchActionKeyword,
|
||||
IndexSearchActionKeyword
|
||||
}
|
||||
|
||||
internal string GetActionKeyword(ActionKeyword actionKeyword) => actionKeyword switch
|
||||
{
|
||||
ActionKeyword.SearchActionKeyword => SearchActionKeyword,
|
||||
ActionKeyword.PathSearchActionKeyword => PathSearchActionKeyword,
|
||||
ActionKeyword.FileContentSearchActionKeyword => FileContentSearchActionKeyword,
|
||||
ActionKeyword.IndexSearchActionKeyword => IndexSearchActionKeyword
|
||||
};
|
||||
|
||||
internal void SetActionKeyword(ActionKeyword actionKeyword, string keyword) => _ = actionKeyword switch
|
||||
{
|
||||
ActionKeyword.SearchActionKeyword => SearchActionKeyword = keyword,
|
||||
ActionKeyword.PathSearchActionKeyword => PathSearchActionKeyword = keyword,
|
||||
ActionKeyword.FileContentSearchActionKeyword => FileContentSearchActionKeyword = keyword,
|
||||
ActionKeyword.IndexSearchActionKeyword => IndexSearchActionKeyword = keyword,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(actionKeyword), actionKeyword, "Unexpected property")
|
||||
};
|
||||
|
||||
internal bool? GetActionKeywordEnabled(ActionKeyword actionKeyword) => actionKeyword switch
|
||||
{
|
||||
ActionKeyword.SearchActionKeyword => SearchActionKeywordEnabled,
|
||||
ActionKeyword.PathSearchActionKeyword => PathSearchKeywordEnabled,
|
||||
ActionKeyword.IndexSearchActionKeyword => IndexOnlySearchKeywordEnabled,
|
||||
_ => null
|
||||
};
|
||||
|
||||
internal void SetActionKeywordEnabled(ActionKeyword actionKeyword, bool enable) => _ = actionKeyword switch
|
||||
{
|
||||
ActionKeyword.SearchActionKeyword => SearchActionKeywordEnabled = enable,
|
||||
ActionKeyword.PathSearchActionKeyword => PathSearchKeywordEnabled = enable,
|
||||
ActionKeyword.IndexSearchActionKeyword => IndexOnlySearchKeywordEnabled = enable,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(actionKeyword), actionKeyword, "Unexpected property")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -41,18 +41,15 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
|
|||
Process.Start(psi);
|
||||
}
|
||||
|
||||
internal void UpdateActionKeyword(string newActionKeyword, string oldActionKeyword)
|
||||
internal void UpdateActionKeyword(Settings.ActionKeyword modifiedActionKeyword, string newActionKeyword, string oldActionKeyword)
|
||||
{
|
||||
PluginManager.ReplaceActionKeyword(Context.CurrentPluginMetadata.ID, oldActionKeyword, newActionKeyword);
|
||||
|
||||
if (Settings.FileContentSearchActionKeyword == oldActionKeyword)
|
||||
Settings.FileContentSearchActionKeyword = newActionKeyword;
|
||||
|
||||
if (Settings.SearchActionKeyword == oldActionKeyword)
|
||||
Settings.SearchActionKeyword = newActionKeyword;
|
||||
}
|
||||
|
||||
internal bool IsActionKeywordAlreadyAssigned(string newActionKeyword) => PluginManager.ActionKeywordRegistered(newActionKeyword);
|
||||
internal bool IsActionKeywordAlreadyAssigned(string newActionKeyword)
|
||||
{
|
||||
return PluginManager.ActionKeywordRegistered(newActionKeyword);
|
||||
}
|
||||
|
||||
internal bool IsNewActionKeywordGlobal(string newActionKeyword) => newActionKeyword == Query.GlobalPluginWildcardSign;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
mc:Ignorable="d"
|
||||
ResizeMode="NoResize"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
DataContext="{Binding RelativeSource={RelativeSource Self}}"
|
||||
Title="Action Keyword Setting" Height="200" Width="500">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
|
|
@ -16,21 +17,24 @@
|
|||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="180" />
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Margin="20 10 10 10" FontSize="14" Grid.Row="0" Grid.Column="0" VerticalAlignment="Center"
|
||||
HorizontalAlignment="Left" Text="Current Action Keyword:" />
|
||||
<TextBox Name="txtCurrentActionKeyword"
|
||||
HorizontalAlignment="Left" Text="{DynamicResource plugin_explorer_actionkeyword_current}" />
|
||||
<TextBox Name="TxtCurrentActionKeyword"
|
||||
Margin="10" Grid.Row="0" Width="105" Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Left" />
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row="1" Grid.Column="1">
|
||||
<Button Click="OnConfirmButtonClick"
|
||||
Margin="10 0 10 0" Width="80" Height="35"
|
||||
Content="OK" />
|
||||
<Button Click="OnCancelButtonClick"
|
||||
Margin="10 0 10 0" Width="80" Height="35"
|
||||
Content="Cancel" />
|
||||
</StackPanel>
|
||||
HorizontalAlignment="Left"
|
||||
Text="{Binding ActionKeyword}"
|
||||
PreviewKeyDown="TxtCurrentActionKeyword_OnKeyDown"/>
|
||||
<CheckBox Name="ChkActionKeywordEnabled" ToolTip="{DynamicResource plugin_explorer_actionkeyword_enabled_tooltip}"
|
||||
Margin="10" Grid.Row="0" Grid.Column="2" Content="{DynamicResource plugin_explorer_actionkeyword_enabled}"
|
||||
Width="auto"
|
||||
VerticalAlignment="Center" IsChecked="{Binding Enabled}"
|
||||
Visibility="{Binding Visible}"/>
|
||||
<Button Name="DownButton"
|
||||
Click="OnDoneButtonClick" Grid.Row="1" Grid.Column="2"
|
||||
Margin="10 0 10 0" Width="80" Height="35"
|
||||
Content="{DynamicResource plugin_explorer_actionkeyword_done}" />
|
||||
</Grid>
|
||||
</Window>
|
||||
</Window>
|
||||
|
|
@ -1,16 +1,10 @@
|
|||
using Flow.Launcher.Plugin.Explorer.ViewModels;
|
||||
using Flow.Launcher.Plugin.Explorer.ViewModels;
|
||||
using ICSharpCode.SharpZipLib.Zip;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace Flow.Launcher.Plugin.Explorer.Views
|
||||
{
|
||||
|
|
@ -21,65 +15,102 @@ namespace Flow.Launcher.Plugin.Explorer.Views
|
|||
{
|
||||
private SettingsViewModel settingsViewModel;
|
||||
|
||||
private ActionKeywordView currentActionKeyword;
|
||||
public ActionKeywordView CurrentActionKeyword { get; set; }
|
||||
|
||||
private List<ActionKeywordView> actionKeywordListView;
|
||||
|
||||
public ActionKeywordSetting(SettingsViewModel settingsViewModel, List<ActionKeywordView> actionKeywordListView, ActionKeywordView selectedActionKeyword)
|
||||
public string ActionKeyword
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
this.settingsViewModel = settingsViewModel;
|
||||
|
||||
currentActionKeyword = selectedActionKeyword;
|
||||
|
||||
txtCurrentActionKeyword.Text = selectedActionKeyword.Keyword;
|
||||
|
||||
this.actionKeywordListView = actionKeywordListView;
|
||||
get => _actionKeyword;
|
||||
set
|
||||
{
|
||||
// Set Enable to be true if user change ActionKeyword
|
||||
if (Enabled is not null)
|
||||
Enabled = true;
|
||||
_actionKeyword = value;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConfirmButtonClick(object sender, RoutedEventArgs e)
|
||||
public bool? Enabled { get; set; }
|
||||
|
||||
private string _actionKeyword;
|
||||
|
||||
public Visibility Visible =>
|
||||
CurrentActionKeyword.Enabled is not null ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
public ActionKeywordSetting(SettingsViewModel settingsViewModel,
|
||||
ActionKeywordView selectedActionKeyword)
|
||||
{
|
||||
var newActionKeyword = txtCurrentActionKeyword.Text;
|
||||
this.settingsViewModel = settingsViewModel;
|
||||
|
||||
if (string.IsNullOrEmpty(newActionKeyword))
|
||||
return;
|
||||
CurrentActionKeyword = selectedActionKeyword;
|
||||
|
||||
if (newActionKeyword == currentActionKeyword.Keyword)
|
||||
ActionKeyword = selectedActionKeyword.Keyword;
|
||||
Enabled = selectedActionKeyword.Enabled;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
TxtCurrentActionKeyword.Focus();
|
||||
}
|
||||
|
||||
private void OnDoneButtonClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ActionKeyword))
|
||||
ActionKeyword = Query.GlobalPluginWildcardSign;
|
||||
|
||||
if (CurrentActionKeyword.Keyword == ActionKeyword && CurrentActionKeyword.Enabled == Enabled)
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (CurrentActionKeyword.KeywordProperty == Settings.ActionKeyword.FileContentSearchActionKeyword
|
||||
&& ActionKeyword == Query.GlobalPluginWildcardSign)
|
||||
{
|
||||
MessageBox.Show(
|
||||
settingsViewModel.Context.API.GetTranslation("plugin_explorer_globalActionKeywordInvalid"));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (settingsViewModel.IsNewActionKeywordGlobal(newActionKeyword)
|
||||
&& currentActionKeyword.Description
|
||||
== settingsViewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_filecontentsearch"))
|
||||
{
|
||||
MessageBox.Show(settingsViewModel.Context.API.GetTranslation("plugin_explorer_globalActionKeywordInvalid"));
|
||||
var oldActionKeyword = CurrentActionKeyword.Keyword;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!settingsViewModel.IsActionKeywordAlreadyAssigned(newActionKeyword))
|
||||
// == because of nullable
|
||||
if (Enabled == false || !settingsViewModel.IsActionKeywordAlreadyAssigned(ActionKeyword))
|
||||
{
|
||||
settingsViewModel.UpdateActionKeyword(newActionKeyword, currentActionKeyword.Keyword);
|
||||
// Update View Data
|
||||
CurrentActionKeyword.Keyword = Enabled == true ? ActionKeyword : Query.GlobalPluginWildcardSign;
|
||||
CurrentActionKeyword.Enabled = Enabled;
|
||||
|
||||
actionKeywordListView.Where(x => x.Description == currentActionKeyword.Description).FirstOrDefault().Keyword = newActionKeyword;
|
||||
switch (Enabled)
|
||||
{
|
||||
// reset to global so it does not take up an action keyword when disabled
|
||||
// not for null Enable plugin
|
||||
case false when oldActionKeyword != Query.GlobalPluginWildcardSign:
|
||||
settingsViewModel.UpdateActionKeyword(CurrentActionKeyword.KeywordProperty,
|
||||
Query.GlobalPluginWildcardSign, oldActionKeyword);
|
||||
break;
|
||||
default:
|
||||
settingsViewModel.UpdateActionKeyword(CurrentActionKeyword.KeywordProperty,
|
||||
CurrentActionKeyword.Keyword, oldActionKeyword);
|
||||
break;
|
||||
}
|
||||
|
||||
Close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// The keyword is not valid, so show message
|
||||
MessageBox.Show(settingsViewModel.Context.API.GetTranslation("newActionKeywordsHasBeenAssigned"));
|
||||
}
|
||||
|
||||
private void OnCancelButtonClick(object sender, RoutedEventArgs e)
|
||||
private void TxtCurrentActionKeyword_OnKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
Close();
|
||||
|
||||
return;
|
||||
if (e.Key == Key.Enter)
|
||||
{
|
||||
DownButton.Focus();
|
||||
OnDoneButtonClick(sender, e);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:viewModels="clr-namespace:Flow.Launcher.Plugin.Explorer.ViewModels"
|
||||
xmlns:views="clr-namespace:Flow.Launcher.Plugin.Explorer.Views"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<UserControl.Resources>
|
||||
|
|
@ -17,14 +18,17 @@
|
|||
Text="{Binding Path, Mode=OneTime}"
|
||||
Margin="0,5,0,5" />
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="ListViewActionKeywords">
|
||||
<DataTemplate x:Key="ListViewActionKeywords"
|
||||
DataType="views:ActionKeywordView">
|
||||
<Grid>
|
||||
<TextBlock
|
||||
Text="{Binding Description, Mode=OneTime}"
|
||||
Foreground="{Binding Color, Mode=OneWay}"
|
||||
Margin="0,5,0,0" />
|
||||
<TextBlock
|
||||
Text="{Binding Keyword, Mode=OneTime}"
|
||||
Margin="150,5,0,0" />
|
||||
Foreground="{Binding Color, Mode=OneWay}"
|
||||
Margin="250,5,0,0" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</UserControl.Resources>
|
||||
|
|
|
|||
|
|
@ -35,20 +35,20 @@ namespace Flow.Launcher.Plugin.Explorer.Views
|
|||
|
||||
actionKeywordsListView = new List<ActionKeywordView>
|
||||
{
|
||||
new ActionKeywordView()
|
||||
{
|
||||
Description = viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_search"),
|
||||
Keyword = this.viewModel.Settings.SearchActionKeyword
|
||||
},
|
||||
new ActionKeywordView()
|
||||
{
|
||||
Description = viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_filecontentsearch"),
|
||||
Keyword = this.viewModel.Settings.FileContentSearchActionKeyword
|
||||
}
|
||||
new(Settings.ActionKeyword.SearchActionKeyword,
|
||||
viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_search")),
|
||||
new(Settings.ActionKeyword.FileContentSearchActionKeyword,
|
||||
viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_filecontentsearch")),
|
||||
new(Settings.ActionKeyword.PathSearchActionKeyword,
|
||||
viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_pathsearch")),
|
||||
new(Settings.ActionKeyword.IndexSearchActionKeyword,
|
||||
viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_indexsearch"))
|
||||
};
|
||||
|
||||
lbxActionKeywords.ItemsSource = actionKeywordsListView;
|
||||
|
||||
ActionKeywordView.Init(viewModel.Settings);
|
||||
|
||||
RefreshView();
|
||||
}
|
||||
|
||||
|
|
@ -71,9 +71,9 @@ namespace Flow.Launcher.Plugin.Explorer.Views
|
|||
&& btnEdit.Visibility == Visibility.Hidden)
|
||||
btnEdit.Visibility = Visibility.Visible;
|
||||
|
||||
if ((lbxAccessLinks.Items.Count == 0 && lbxExcludedPaths.Items.Count == 0)
|
||||
&& btnDelete.Visibility == Visibility.Visible
|
||||
&& btnEdit.Visibility == Visibility.Visible)
|
||||
if (lbxAccessLinks.Items.Count == 0 && lbxExcludedPaths.Items.Count == 0
|
||||
&& btnDelete.Visibility == Visibility.Visible
|
||||
&& btnEdit.Visibility == Visibility.Visible)
|
||||
{
|
||||
btnDelete.Visibility = Visibility.Hidden;
|
||||
btnEdit.Visibility = Visibility.Hidden;
|
||||
|
|
@ -122,7 +122,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views
|
|||
private void expActionKeywords_Collapsed(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!expActionKeywords.IsExpanded)
|
||||
expActionKeywords.Height = Double.NaN;
|
||||
expActionKeywords.Height = double.NaN;
|
||||
}
|
||||
|
||||
private void expAccessLinks_Click(object sender, RoutedEventArgs e)
|
||||
|
|
@ -135,20 +135,20 @@ namespace Flow.Launcher.Plugin.Explorer.Views
|
|||
|
||||
if (expActionKeywords.IsExpanded)
|
||||
expActionKeywords.IsExpanded = false;
|
||||
|
||||
|
||||
RefreshView();
|
||||
}
|
||||
|
||||
private void expAccessLinks_Collapsed(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!expAccessLinks.IsExpanded)
|
||||
expAccessLinks.Height = Double.NaN;
|
||||
expAccessLinks.Height = double.NaN;
|
||||
}
|
||||
|
||||
private void expExcludedPaths_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (expExcludedPaths.IsExpanded)
|
||||
expAccessLinks.Height = Double.NaN;
|
||||
expAccessLinks.Height = double.NaN;
|
||||
|
||||
if (expAccessLinks.IsExpanded)
|
||||
expAccessLinks.IsExpanded = false;
|
||||
|
|
@ -161,11 +161,12 @@ namespace Flow.Launcher.Plugin.Explorer.Views
|
|||
|
||||
private void btnDelete_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var selectedRow = lbxAccessLinks.SelectedItem as AccessLink?? lbxExcludedPaths.SelectedItem as AccessLink;
|
||||
var selectedRow = lbxAccessLinks.SelectedItem as AccessLink ?? lbxExcludedPaths.SelectedItem as AccessLink;
|
||||
|
||||
if (selectedRow != null)
|
||||
{
|
||||
string msg = string.Format(viewModel.Context.API.GetTranslation("plugin_explorer_delete_folder_link"), selectedRow.Path);
|
||||
string msg = string.Format(viewModel.Context.API.GetTranslation("plugin_explorer_delete_folder_link"),
|
||||
selectedRow.Path);
|
||||
|
||||
if (MessageBox.Show(msg, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
||||
{
|
||||
|
|
@ -191,7 +192,8 @@ namespace Flow.Launcher.Plugin.Explorer.Views
|
|||
{
|
||||
var selectedActionKeyword = lbxActionKeywords.SelectedItem as ActionKeywordView;
|
||||
|
||||
var actionKeywordWindow = new ActionKeywordSetting(viewModel, actionKeywordsListView, selectedActionKeyword);
|
||||
var actionKeywordWindow = new ActionKeywordSetting(viewModel,
|
||||
selectedActionKeyword);
|
||||
|
||||
actionKeywordWindow.ShowDialog();
|
||||
|
||||
|
|
@ -199,7 +201,8 @@ namespace Flow.Launcher.Plugin.Explorer.Views
|
|||
}
|
||||
else
|
||||
{
|
||||
var selectedRow = lbxAccessLinks.SelectedItem as AccessLink ?? lbxExcludedPaths.SelectedItem as AccessLink;
|
||||
var selectedRow = lbxAccessLinks.SelectedItem as AccessLink ??
|
||||
lbxExcludedPaths.SelectedItem as AccessLink;
|
||||
|
||||
if (selectedRow != null)
|
||||
{
|
||||
|
|
@ -215,7 +218,8 @@ namespace Flow.Launcher.Plugin.Explorer.Views
|
|||
|
||||
if (expExcludedPaths.IsExpanded)
|
||||
{
|
||||
var link = viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.First(x => x.Path == selectedRow.Path);
|
||||
var link = viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.First(x =>
|
||||
x.Path == selectedRow.Path);
|
||||
link.Path = folderBrowserDialog.SelectedPath;
|
||||
}
|
||||
}
|
||||
|
|
@ -235,10 +239,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views
|
|||
var folderBrowserDialog = new FolderBrowserDialog();
|
||||
if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
|
||||
{
|
||||
var newAccessLink = new AccessLink
|
||||
{
|
||||
Path = folderBrowserDialog.SelectedPath
|
||||
};
|
||||
var newAccessLink = new AccessLink {Path = folderBrowserDialog.SelectedPath};
|
||||
|
||||
AddAccessLink(newAccessLink);
|
||||
}
|
||||
|
|
@ -259,10 +260,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views
|
|||
{
|
||||
if (Directory.Exists(s))
|
||||
{
|
||||
var newFolderLink = new AccessLink
|
||||
{
|
||||
Path = s
|
||||
};
|
||||
var newFolderLink = new AccessLink {Path = s};
|
||||
|
||||
AddAccessLink(newFolderLink);
|
||||
}
|
||||
|
|
@ -275,7 +273,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views
|
|||
private void AddAccessLink(AccessLink newAccessLink)
|
||||
{
|
||||
if (expAccessLinks.IsExpanded
|
||||
&& !viewModel.Settings.QuickAccessLinks.Any(x => x.Path == newAccessLink.Path))
|
||||
&& !viewModel.Settings.QuickAccessLinks.Any(x => x.Path == newAccessLink.Path))
|
||||
{
|
||||
if (viewModel.Settings.QuickAccessLinks == null)
|
||||
viewModel.Settings.QuickAccessLinks = new List<AccessLink>();
|
||||
|
|
@ -313,8 +311,35 @@ namespace Flow.Launcher.Plugin.Explorer.Views
|
|||
|
||||
public class ActionKeywordView
|
||||
{
|
||||
public string Description { get; set; }
|
||||
private static Settings _settings;
|
||||
|
||||
public string Keyword { get; set; }
|
||||
public static void Init(Settings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
internal ActionKeywordView(Settings.ActionKeyword actionKeyword, string description)
|
||||
{
|
||||
KeywordProperty = actionKeyword;
|
||||
Description = description;
|
||||
}
|
||||
|
||||
public string Description { get; private init; }
|
||||
public string Color => Enabled ?? true ? "Black" : "Gray";
|
||||
|
||||
internal Settings.ActionKeyword KeywordProperty { get; }
|
||||
|
||||
public string Keyword
|
||||
{
|
||||
get => _settings.GetActionKeyword(KeywordProperty);
|
||||
set => _settings.SetActionKeyword(KeywordProperty, value);
|
||||
}
|
||||
|
||||
public bool? Enabled
|
||||
{
|
||||
get => _settings.GetActionKeywordEnabled(KeywordProperty);
|
||||
set => _settings.SetActionKeywordEnabled(KeywordProperty,
|
||||
value ?? throw new ArgumentException("Unexpected null value"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
{
|
||||
"ID": "572be03c74c642baae319fc283e561a8",
|
||||
"ActionKeywords": [
|
||||
"*",
|
||||
"doc:"
|
||||
"*",
|
||||
"doc:",
|
||||
"*",
|
||||
"*"
|
||||
],
|
||||
"Name": "Explorer",
|
||||
"Description": "Search and manage files and folders. Explorer utilises Windows Index Search",
|
||||
"Author": "Jeremy Wu",
|
||||
"Version": "1.7.7",
|
||||
"Version": "1.8.4",
|
||||
"Language": "csharp",
|
||||
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
|
||||
"ExecuteFileName": "Flow.Launcher.Plugin.Explorer.dll",
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
|
||||
public List<Result> LoadContextMenus(Result selectedResult)
|
||||
{
|
||||
var pluginManifestInfo = selectedResult.ContextData as UserPlugin;
|
||||
if(selectedResult.ContextData is not UserPlugin pluginManifestInfo)
|
||||
return new List<Result>();
|
||||
|
||||
return new List<Result>
|
||||
{
|
||||
|
|
@ -50,7 +51,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
IcoPath = "Images\\request.png",
|
||||
Action = _ =>
|
||||
{
|
||||
// standard UrlSourceCode format in PluginsManifest's plugins.json file: https://github.com/jjw24/WoxDictionary/tree/master
|
||||
// standard UrlSourceCode format in PluginsManifest's plugins.json file: https://github.com/jjw24/Flow.Launcher.Plugin.Putty/tree/master
|
||||
var link = pluginManifestInfo.UrlSourceCode.StartsWith("https://github.com")
|
||||
? pluginManifestInfo.UrlSourceCode.Replace("/tree/master", "/issues/new/choose")
|
||||
: pluginManifestInfo.UrlSourceCode;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
<system:String x:Key="plugin_pluginsmanager_update_title">Plugin Update</system:String>
|
||||
<system:String x:Key="plugin_pluginsmanager_update_exists">This plugin has an update, would you like to see it?</system:String>
|
||||
<system:String x:Key="plugin_pluginsmanager_update_alreadyexists">This plugin is already installed</system:String>
|
||||
<system:String x:Key="plugin_pluginsmanager_update_failed_title">Plugin Manifest Download Failed</system:String>
|
||||
<system:String x:Key="plugin_pluginsmanager_update_failed_subtitle">Please check if you can connect to github.com. This error means you may not be able to install or update plugins.</system:String>
|
||||
|
||||
<!--Controls-->
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
<system:String x:Key="plugin_pluginsmanager_update_title">Aktualizácia pluginu</system:String>
|
||||
<system:String x:Key="plugin_pluginsmanager_update_exists">Tento plugin má dostupnú aktualizáciu, chcete ju zobraziť?</system:String>
|
||||
<system:String x:Key="plugin_pluginsmanager_update_alreadyexists">Tento plugin je už nainštalovaný</system:String>
|
||||
<system:String x:Key="plugin_pluginsmanager_update_failed_title">Stiahnutie manifestu pluginu zlyhalo</system:String>
|
||||
<system:String x:Key="plugin_pluginsmanager_update_failed_subtitle">Skontrolujte, či sa môžete pripojiť na github.com. Táto chyba znamená, že pravdepodobne nemôžete pluginy inštalovať alebo aktualizovať.</system:String>
|
||||
|
||||
<!--Controls-->
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
viewModel = new SettingsViewModel(context, Settings);
|
||||
contextMenu = new ContextMenu(Context);
|
||||
pluginManager = new PluginsManager(Context, Settings);
|
||||
_ = pluginManager.UpdateManifest().ContinueWith(_ =>
|
||||
_manifestUpdateTask = pluginManager.UpdateManifest().ContinueWith(_ =>
|
||||
{
|
||||
lastUpdateTime = DateTime.Now;
|
||||
}, TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||
|
|
@ -50,6 +50,8 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
{
|
||||
return contextMenu.LoadContextMenus(selectedResult);
|
||||
}
|
||||
|
||||
private Task _manifestUpdateTask = Task.CompletedTask;
|
||||
|
||||
public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
|
||||
{
|
||||
|
|
@ -58,9 +60,9 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
if (string.IsNullOrWhiteSpace(search))
|
||||
return pluginManager.GetDefaultHotKeys();
|
||||
|
||||
if ((DateTime.Now - lastUpdateTime).TotalHours > 12) // 12 hours
|
||||
if ((DateTime.Now - lastUpdateTime).TotalHours > 12 && _manifestUpdateTask.IsCompleted) // 12 hours
|
||||
{
|
||||
_ = pluginManager.UpdateManifest().ContinueWith(t =>
|
||||
_manifestUpdateTask = pluginManager.UpdateManifest().ContinueWith(t =>
|
||||
{
|
||||
lastUpdateTime = DateTime.Now;
|
||||
}, TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace Flow.Launcher.Plugin.PluginsManager.Models
|
|||
{
|
||||
try
|
||||
{
|
||||
await using var jsonStream = await Http.GetStreamAsync("https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/main/plugins.json")
|
||||
await using var jsonStream = await Http.GetStreamAsync("https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/plugin_api_v2/plugins.json")
|
||||
.ConfigureAwait(false);
|
||||
|
||||
UserPlugins = await JsonSerializer.DeserializeAsync<List<UserPlugin>>(jsonStream).ConfigureAwait(false);
|
||||
|
|
|
|||
|
|
@ -59,11 +59,12 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
}
|
||||
else
|
||||
{
|
||||
return _downloadManifestTask = pluginsManifest.DownloadManifest().ContinueWith(t =>
|
||||
Context.API.ShowMsg("Plugin Manifest Download Fail.",
|
||||
"Please check if you can connect to github.com. " +
|
||||
"This error means you may not be able to Install and Update Plugin.", icoPath, false),
|
||||
_downloadManifestTask = pluginsManifest.DownloadManifest();
|
||||
_downloadManifestTask.ContinueWith(_ =>
|
||||
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_update_failed_title"),
|
||||
Context.API.GetTranslation("plugin_pluginsmanager_update_failed_subtitle"), icoPath, false),
|
||||
TaskContinuationOptions.OnlyOnFaulted);
|
||||
return _downloadManifestTask;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -119,7 +120,10 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
.ChangeQuery(
|
||||
$"{Context.CurrentPluginMetadata.ActionKeywords.FirstOrDefault()} {Settings.HotkeyUpdate} {plugin.Name}");
|
||||
|
||||
Application.Current.MainWindow.Show();
|
||||
var mainWindow = Application.Current.MainWindow;
|
||||
mainWindow.Visibility = Visibility.Visible;
|
||||
mainWindow.Focus();
|
||||
|
||||
shouldHideWindow = false;
|
||||
|
||||
return;
|
||||
|
|
@ -184,7 +188,6 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
|
||||
var uninstallSearch = search.Replace(Settings.HotkeyUpdate, string.Empty).TrimStart();
|
||||
|
||||
|
||||
var resultsForUpdate =
|
||||
from existingPlugin in Context.API.GetAllPlugins()
|
||||
join pluginFromManifest in pluginsManifest.UserPlugins
|
||||
|
|
@ -268,7 +271,13 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
ContextData =
|
||||
new UserPlugin
|
||||
{
|
||||
Website = x.PluginNewUserPlugin.Website,
|
||||
UrlSourceCode = x.PluginNewUserPlugin.UrlSourceCode
|
||||
}
|
||||
});
|
||||
|
||||
return Search(results, uninstallSearch);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
"Name": "Plugins Manager",
|
||||
"Description": "Management of installing, uninstalling or updating Flow Launcher plugins",
|
||||
"Author": "Jeremy Wu",
|
||||
"Version": "1.8.1",
|
||||
"Version": "1.8.5",
|
||||
"Language": "csharp",
|
||||
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
|
||||
"ExecuteFileName": "Flow.Launcher.Plugin.PluginsManager.dll",
|
||||
|
|
|
|||
|
|
@ -13,8 +13,11 @@
|
|||
<system:String x:Key="flowlauncher_plugin_program_reindex">Reindex</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_program_indexing">Indexing</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_program_index_start">Index Start Menu</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_program_index_start_tooltip">When enabled, Flow will load programs from the start menu</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_program_index_registry">Index Registry</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_program_index_registry_tooltip">When enabled, Flow will load programs from the registry</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_program_enable_description">Enable Program Description</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_program_enable_description_tooltip">Disabling this will also stop Flow from searching via the program desciption</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_program_suffixes_header">Suffixes</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_program_max_depth_header">Max Depth</system:String>
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,12 @@
|
|||
<system:String x:Key="flowlauncher_plugin_program_suffixes">Prípony súborov</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_program_reindex">Reindexovať</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_program_indexing">Indexovanie</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_program_index_start">Indexovať Ponuku Štart</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_program_index_registry">Indexovať Registry</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_program_index_start">Indexovať ponuku Štart</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_program_index_start_tooltip">Ak je povolené, Flow načíta programy z ponuky Štart</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_program_index_registry">Indexovať databázu Registry</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_program_index_registry_tooltip">Ak je povolené, Flow načíta programy z databázy Registry</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_program_enable_description">Povoliť popis programu</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_program_enable_description_tooltip">Zakázaním tejto funkcie sa tiež zastaví vyhľadávanie popisu programu cez Flow</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_program_suffixes_header">Prípony</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_program_max_depth_header">Max. hĺbka</system:String>
|
||||
|
||||
|
|
|
|||
|
|
@ -20,27 +20,6 @@ namespace Flow.Launcher.Plugin.Program.Logger
|
|||
{
|
||||
public const string DirectoryName = "Logs";
|
||||
|
||||
static ProgramLogger()
|
||||
{
|
||||
var path = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Version);
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
|
||||
var configuration = new LoggingConfiguration();
|
||||
var target = new FileTarget();
|
||||
configuration.AddTarget("file", target);
|
||||
target.FileName = path.Replace(@"\", "/") + "/${shortdate}.txt";
|
||||
#if DEBUG
|
||||
var rule = new LoggingRule("*", LogLevel.Debug, target);
|
||||
#else
|
||||
var rule = new LoggingRule("*", LogLevel.Error, target);
|
||||
#endif
|
||||
configuration.LoggingRules.Add(rule);
|
||||
LogManager.Configuration = configuration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs an exception
|
||||
/// </summary>
|
||||
|
|
@ -48,8 +27,6 @@ namespace Flow.Launcher.Plugin.Program.Logger
|
|||
internal static void LogException(string classname, string callingMethodName, string loadingProgramPath,
|
||||
string interpretationMessage, Exception e)
|
||||
{
|
||||
Debug.WriteLine($"ERROR{classname}|{callingMethodName}|{loadingProgramPath}|{interpretationMessage}");
|
||||
|
||||
var logger = LogManager.GetLogger("");
|
||||
|
||||
var innerExceptionNumber = 1;
|
||||
|
|
@ -103,6 +80,7 @@ namespace Flow.Launcher.Plugin.Program.Logger
|
|||
{
|
||||
var logger = LogManager.GetLogger("");
|
||||
logger.Error(e, $"fail to log exception in program logger, parts length is too small: {parts.Length}, message: {message}");
|
||||
return;
|
||||
}
|
||||
|
||||
var classname = parts[0];
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Windows;
|
||||
using System;
|
||||
using System.Windows;
|
||||
|
||||
namespace Flow.Launcher.Plugin.Program
|
||||
{
|
||||
|
|
@ -20,18 +21,21 @@ namespace Flow.Launcher.Plugin.Program
|
|||
|
||||
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (string.IsNullOrEmpty(tbSuffixes.Text))
|
||||
var suffixes = tbSuffixes.Text.Split(Settings.SuffixSeperator, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (suffixes.Length == 0)
|
||||
{
|
||||
string warning = context.API.GetTranslation("flowlauncher_plugin_program_suffixes_cannot_empty");
|
||||
MessageBox.Show(warning);
|
||||
return;
|
||||
}
|
||||
|
||||
_settings.ProgramSuffixes = tbSuffixes.Text.Split(Settings.SuffixSeperator);
|
||||
_settings.ProgramSuffixes = suffixes;
|
||||
|
||||
string msg = context.API.GetTranslation("flowlauncher_plugin_program_update_file_suffixes");
|
||||
MessageBox.Show(msg);
|
||||
|
||||
DialogResult = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -126,7 +126,12 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
((IShellLinkW)link).GetDescription(buffer, MAX_PATH);
|
||||
description = buffer.ToString();
|
||||
}
|
||||
|
||||
// To release unmanaged memory
|
||||
Marshal.ReleaseComObject(link);
|
||||
|
||||
return target;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ using Flow.Launcher.Infrastructure;
|
|||
using Flow.Launcher.Plugin.Program.Logger;
|
||||
using Rect = System.Windows.Rect;
|
||||
using Flow.Launcher.Plugin.SharedModels;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
|
||||
namespace Flow.Launcher.Plugin.Program.Programs
|
||||
{
|
||||
|
|
@ -81,6 +82,11 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
|
||||
Apps = new List<Application>().ToArray();
|
||||
}
|
||||
|
||||
if (Marshal.ReleaseComObject(stream) > 0)
|
||||
{
|
||||
Log.Error("Flow.Launcher.Plugin.Program.Programs.UWP", "AppxManifest.xml was leaked");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -559,7 +565,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
}
|
||||
else
|
||||
{
|
||||
ProgramLogger.LogException($"|UWP|ImageFromPath|{path}" +
|
||||
ProgramLogger.LogException($"|UWP|ImageFromPath|{(string.IsNullOrEmpty(path) ? "Not Avaliable" : path)}" +
|
||||
$"|Unable to get logo for {UserModelId} from {path} and" +
|
||||
$" located in {Package.Location}", new FileNotFoundException());
|
||||
return new BitmapImage(new Uri(Constant.MissingImgIcon));
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@
|
|||
</Grid.RowDefinitions>
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Stretch">
|
||||
<StackPanel Orientation="Vertical" Width="250">
|
||||
<CheckBox Name="StartMenuEnabled" IsChecked="{Binding EnableStartMenuSource}" Margin="5" Content="{DynamicResource flowlauncher_plugin_program_index_start}" />
|
||||
<CheckBox Name="RegistryEnabled" IsChecked="{Binding EnableRegistrySource}" Margin="5" Content="{DynamicResource flowlauncher_plugin_program_index_registry}" />
|
||||
<CheckBox Name="DescriptionEnabled" IsChecked="{Binding EnableDescription}" Margin="5" Content="{DynamicResource flowlauncher_plugin_program_enable_description}" />
|
||||
<CheckBox Name="StartMenuEnabled" IsChecked="{Binding EnableStartMenuSource}" Margin="5" Content="{DynamicResource flowlauncher_plugin_program_index_start}" ToolTip="{DynamicResource flowlauncher_plugin_program_index_start_tooltip}" />
|
||||
<CheckBox Name="RegistryEnabled" IsChecked="{Binding EnableRegistrySource}" Margin="5" Content="{DynamicResource flowlauncher_plugin_program_index_registry}" ToolTip="{DynamicResource flowlauncher_plugin_program_index_registry_tooltip}" />
|
||||
<CheckBox Name="DescriptionEnabled" IsChecked="{Binding EnableDescription}" Margin="5" Content="{DynamicResource flowlauncher_plugin_program_enable_description}" ToolTip="{DynamicResource flowlauncher_plugin_program_enable_description_tooltip}" />
|
||||
</StackPanel>
|
||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
|
||||
<Button Height="31" HorizontalAlignment="Right" Margin="10" Width="100" x:Name="btnLoadAllProgramSource" Click="btnLoadAllProgramSource_OnClick" Content="{DynamicResource flowlauncher_plugin_program_all_programs}" />
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"Name": "Program",
|
||||
"Description": "Search programs in Flow.Launcher",
|
||||
"Author": "qianlifeng",
|
||||
"Version": "1.5.0",
|
||||
"Version": "1.5.4",
|
||||
"Language": "csharp",
|
||||
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
|
||||
"ExecuteFileName": "Flow.Launcher.Plugin.Program.dll",
|
||||
|
|
|
|||
|
|
@ -271,8 +271,8 @@ namespace Flow.Launcher.Plugin.Shell
|
|||
public void Init(PluginInitContext context)
|
||||
{
|
||||
this.context = context;
|
||||
context.API.GlobalKeyboardEvent += API_GlobalKeyboardEvent;
|
||||
_settings = context.API.LoadSettingJsonStorage<Settings>();
|
||||
context.API.GlobalKeyboardEvent += API_GlobalKeyboardEvent;
|
||||
}
|
||||
|
||||
bool API_GlobalKeyboardEvent(int keyevent, int vkcode, SpecialKeyState state)
|
||||
|
|
@ -298,7 +298,11 @@ namespace Flow.Launcher.Plugin.Shell
|
|||
private void OnWinRPressed()
|
||||
{
|
||||
context.API.ChangeQuery($"{context.CurrentPluginMetadata.ActionKeywords[0]}{Plugin.Query.TermSeperater}");
|
||||
Application.Current.MainWindow.Visibility = Visibility.Visible;
|
||||
|
||||
// show the main window and set focus to the query box
|
||||
Window mainWindow = Application.Current.MainWindow;
|
||||
mainWindow.Visibility = Visibility.Visible;
|
||||
mainWindow.Focus();
|
||||
}
|
||||
|
||||
public Control CreateSettingPanel()
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"Name": "Shell",
|
||||
"Description": "Provide executing commands from Flow Launcher",
|
||||
"Author": "qianlifeng",
|
||||
"Version": "1.4.1",
|
||||
"Version": "1.4.3",
|
||||
"Language": "csharp",
|
||||
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
|
||||
"ExecuteFileName": "Flow.Launcher.Plugin.Shell.dll",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:system="clr-namespace:System;assembly=mscorlib">
|
||||
|
||||
<system:String x:Key="flowlauncher_plugin_websearch_open_search_in">Otvoriť vyhľadávanie v:</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_websearch_new_window">Nové okno</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_websearch_new_tab">Nová karta</system:String>
|
||||
|
|
|
|||
73
README.md
73
README.md
|
|
@ -16,6 +16,18 @@ Flow Launcher. Dedicated to make your workflow flow more seamlessly. Aimed at be
|
|||
|
||||
<sub>Remember to star it, flow will love you more :)</sub>
|
||||
|
||||
---
|
||||
|
||||
<h4 align="center">
|
||||
<a href="#Features">Features</a> •
|
||||
<a href="#Getting-Started">Getting Started</a> •
|
||||
<a href="#QuestionsSuggestions">Questions/Suggestions</a> •
|
||||
<a href="#Development">Development</a> •
|
||||
<a href="https://flow-launcher.github.io/docs">Documentation</a>
|
||||
</h4>
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||

|
||||
|
|
@ -33,10 +45,12 @@ Flow Launcher. Dedicated to make your workflow flow more seamlessly. Aimed at be
|
|||
|
||||
[<img width="12px" src="https://user-images.githubusercontent.com/26427004/104119722-9033c600-5385-11eb-9d57-4c376862fd36.png"> **SOFTPEDIA EDITOR'S PICK**](https://www.softpedia.com/get/System/Launchers-Shutdown-Tools/Flow-Launcher.shtml)
|
||||
|
||||
## Running Flow Launcher
|
||||
## Getting Started
|
||||
|
||||
| [Windows 7 and up](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest) |
|
||||
| ---------------------------------------------------------------------------------- |
|
||||
### Installation
|
||||
|
||||
| [Windows 7+ installer](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest/download/Flow-Launcher-Setup.exe) | [Portable](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest/download/Flow-Launcher-Portable.zip) | `WinGet install "Flow Launcher"` |
|
||||
| --------------------------------- | --------------------------------- | --------------------------------- |
|
||||
|
||||
Windows may complain about security due to code not being signed, this will be completed at a later stage. If you downloaded from this repo, you are good to continue the set up.
|
||||
|
||||
|
|
@ -45,52 +59,47 @@ Windows may complain about security due to code not being signed, this will be c
|
|||
- Open context menu: on the selected result, press <kbd>Ctrl</kbd>+<kbd>O</kbd>/<kbd>Shift</kbd>+<kbd>Enter</kbd>.
|
||||
- Cancel/Return to previous screen: <kbd>Esc</kbd>.
|
||||
- Install/Uninstall/Update plugins: in the search window, type `pm` `install`/`uninstall`/`update` + the plugin name.
|
||||
- Saved user settings are located:
|
||||
- Type `flow user data` to open your saved user settings folder. They are located at:
|
||||
- If using roaming: `%APPDATA%\FlowLauncher`
|
||||
- If using portable, by default: `%localappdata%\FlowLauncher\app-<VersionOfYourFlowLauncher>\UserData`
|
||||
- Logs are saved along with your user settings folder.
|
||||
- Type `open log location` to open your logs folder, they are saved along with your user settings folder.
|
||||
|
||||
[More tips](https://flow-launcher.github.io/docs/#/usage-tips)
|
||||
|
||||
### Integrations
|
||||
- If you use Python plugins:
|
||||
- Install [Python3](https://www.python.org/downloads/), download `.exe` installer.
|
||||
- Add Python to `%PATH%` or set it in flow's settings.
|
||||
- Use `pip` to install `flowlauncher`, open cmd and type `pip install flowlauncher`.
|
||||
- The Python plugin may require additional modules to be installed, please ensure you check by visiting the plugin's website via `pm install` + plugin name, go to context menu and select `Open website`.
|
||||
- Start to launch your Python plugins.
|
||||
- Flow searches files and contents via Windows Index Search, to use Everything: `pm install everything`.
|
||||
### Plugins
|
||||
|
||||
## Plugins
|
||||
Flow searches files and contents via Windows Index Search, to use **Everything**: `pm install everything`.
|
||||
|
||||
There is a [list of actively maintained plugins](https://github.com/Flow-Launcher/docs/blob/main/Plugins.md) in the documentation section, some of which are integrated from other launchers.
|
||||
If you are using Python plugins, flow will prompt to either select the location or allow Python (Embeddable) to be automatic downloaded for use.
|
||||
|
||||
## Status
|
||||
Vist [here](https://flow-launcher.github.io/docs/#/plugins) for our plugin portfolio.
|
||||
|
||||
Flow is under heavy development, but the code base is stable, so contributions are very welcome. If you would like to help maintain it, please do not hesistate to get in touch.
|
||||
If you are keen to write your own plugin for flow, please take a look at our plugin development documentation for [C#](https://flow-launcher.github.io/docs/#/develop-dotnet-plugins) or [Python](https://flow-launcher.github.io/docs/#/develop-py-plugins)
|
||||
|
||||
## Contributing
|
||||
## Questions/Suggestions
|
||||
|
||||
We welcome all contributions. If you are unsure of a change you want to make, simply put an issue in for discussion, otherwise feel free to put in a pull request.
|
||||
|
||||
You will find the main goals of flow placed under Projects board, so feel free to contribute on that. If you would like to make small incremental changes, feel free to do so as well.
|
||||
|
||||
Get in touch if you like to join the Flow-Launcher Team and help build this great tool.
|
||||
|
||||
## Question/Suggestion
|
||||
|
||||
Yes please, submit an issue to let us know.
|
||||
Yes please, let us know in the [Q&A](https://github.com/Flow-Launcher/Flow.Launcher/discussions/categories/q-a) section.
|
||||
|
||||
**Join our community on [Discord](https://discord.gg/AvgAQgh)!**
|
||||
|
||||
## Developing/Debugging
|
||||
## Development
|
||||
|
||||
### Status
|
||||
|
||||
Flow is under heavy development, but the code base is stable, so contributions are very welcome. If you would like to help maintain it, please do not hesistate to get in touch.
|
||||
|
||||
### Contributing
|
||||
|
||||
We welcome all contributions. If you are unsure of a change you want to make, let us know in the [Discussions](https://github.com/Flow-Launcher/Flow.Launcher/discussions/categories/ideas), otherwise feel free to put in a pull request.
|
||||
|
||||
You will find the main goals of flow placed under the [Projects board](https://github.com/Flow-Launcher/Flow.Launcher/projects), so feel free to contribute on that. If you would like to make small incremental changes, feel free to do so as well.
|
||||
|
||||
Get in touch if you like to join the Flow-Launcher Team and help build this great tool.
|
||||
|
||||
### Developing/Debugging
|
||||
|
||||
Flow Launcher's target framework is .Net 5
|
||||
|
||||
Install Visual Studio 2019
|
||||
|
||||
Install .Net 5 SDK via Visual Studio installer or manually from [here](https://dotnet.microsoft.com/download/dotnet/thank-you/sdk-5.0.103-windows-x64-installer)
|
||||
|
||||
## Documentation
|
||||
|
||||
Visit [here](https://flow-launcher.github.io/docs) for more information on usage, development and design documentations
|
||||
|
|
|
|||
|
|
@ -47,10 +47,20 @@ function Delete-Unused ($path, $config) {
|
|||
Remove-Item -Path $target -Include "*.xml" -Recurse
|
||||
}
|
||||
|
||||
function Remove-CreateDumpExe ($path, $config) {
|
||||
$target = "$path\Output\$config"
|
||||
|
||||
$depjson = Get-Content $target\Flow.Launcher.deps.json -raw
|
||||
$depjson -replace '(?s)(.createdump.exe": {.*?}.*?\n)\s*', "" | Out-File $target\Flow.Launcher.deps.json -Encoding UTF8
|
||||
Remove-Item -Path $target -Include "*createdump.exe" -Recurse
|
||||
}
|
||||
|
||||
|
||||
function Validate-Directory ($output) {
|
||||
New-Item $output -ItemType Directory -Force
|
||||
}
|
||||
|
||||
|
||||
function Pack-Squirrel-Installer ($path, $version, $output) {
|
||||
# msbuild based installer generation is not working in appveyor, not sure why
|
||||
Write-Host "Begin pack squirrel installer"
|
||||
|
|
@ -79,7 +89,7 @@ function Pack-Squirrel-Installer ($path, $version, $output) {
|
|||
Move-Item $temp\* $output -Force
|
||||
Remove-Item $temp
|
||||
|
||||
$file = "$output\Flow-Launcher-v$version.exe"
|
||||
$file = "$output\Flow-Launcher-Setup.exe"
|
||||
Write-Host "Filename: $file"
|
||||
|
||||
Move-Item "$output\Setup.exe" $file -Force
|
||||
|
|
@ -97,6 +107,13 @@ function Publish-Self-Contained ($p) {
|
|||
dotnet publish -c Release $csproj /p:PublishProfile=$profile
|
||||
}
|
||||
|
||||
function Publish-Portable ($outputLocation, $version) {
|
||||
|
||||
& $outputLocation\Flow-Launcher-Setup.exe --silent | Out-Null
|
||||
mkdir "$env:LocalAppData\FlowLauncher\app-$version\UserData"
|
||||
Compress-Archive -Path $env:LocalAppData\FlowLauncher -DestinationPath $outputLocation\Flow-Launcher-Portable.zip
|
||||
}
|
||||
|
||||
function Main {
|
||||
$p = Build-Path
|
||||
$v = Build-Version
|
||||
|
|
@ -108,10 +125,14 @@ function Main {
|
|||
|
||||
Publish-Self-Contained $p
|
||||
|
||||
Remove-CreateDumpExe $p $config
|
||||
|
||||
$o = "$p\Output\Packages"
|
||||
Validate-Directory $o
|
||||
Pack-Squirrel-Installer $p $v $o
|
||||
|
||||
Publish-Portable $o $v
|
||||
}
|
||||
}
|
||||
|
||||
Main
|
||||
Main
|
||||
|
|
|
|||
|
|
@ -16,6 +16,6 @@ using System.Runtime.InteropServices;
|
|||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: AssemblyVersion("1.7.2")]
|
||||
[assembly: AssemblyFileVersion("1.7.2")]
|
||||
[assembly: AssemblyInformationalVersion("1.7.2")]
|
||||
[assembly: AssemblyVersion("1.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0")]
|
||||
[assembly: AssemblyInformationalVersion("1.0.0")]
|
||||
26
appveyor.yml
26
appveyor.yml
|
|
@ -1,4 +1,4 @@
|
|||
version: '1.7.2.{build}'
|
||||
version: '1.8.1.{build}'
|
||||
|
||||
init:
|
||||
- ps: |
|
||||
|
|
@ -14,6 +14,8 @@ assembly_info:
|
|||
assembly_file_version: $(flowVersion)
|
||||
assembly_informational_version: $(flowVersion)
|
||||
|
||||
skip_branch_with_pr: true
|
||||
|
||||
skip_commits:
|
||||
files:
|
||||
- '*.md'
|
||||
|
|
@ -34,6 +36,8 @@ artifacts:
|
|||
name: Plugin nupkg
|
||||
- path: 'Output\Packages\Flow-Launcher-*.exe'
|
||||
name: Squirrel Installer
|
||||
- path: Output\Packages\Flow-Launcher-Portable.zip
|
||||
name: Portable Version
|
||||
- path: 'Output\Packages\FlowLauncher-*-full.nupkg'
|
||||
name: Squirrel nupkg
|
||||
- path: 'Output\Packages\RELEASES'
|
||||
|
|
@ -45,13 +49,13 @@ deploy:
|
|||
api_key:
|
||||
secure: n80IeWR3pN81p0w4uXq4mO0TdTXoJSHHFL+yTB9YBJ0Wni2DjZGYwOFdaWzW4hRi
|
||||
on:
|
||||
branch: master
|
||||
APPVEYOR_REPO_TAG: true
|
||||
|
||||
- provider: GitHub
|
||||
release: v$(flowVersion)
|
||||
auth_token:
|
||||
secure: ij4UeXUYQBDJxn2YRAAhUOjklOGVKDB87Hn5J8tKIzj13yatoI7sLM666QDQFEgv
|
||||
artifact: Squirrel Installer, Squirrel nupkg, Squirrel RELEASES
|
||||
artifact: Squirrel Installer, Portable Version, Squirrel nupkg, Squirrel RELEASES
|
||||
draft: true
|
||||
force_update: true
|
||||
on:
|
||||
|
|
@ -61,7 +65,19 @@ deploy:
|
|||
release: v$(flowVersion)
|
||||
auth_token:
|
||||
secure: ij4UeXUYQBDJxn2YRAAhUOjklOGVKDB87Hn5J8tKIzj13yatoI7sLM666QDQFEgv
|
||||
artifact: Squirrel Installer, Squirrel nupkg, Squirrel RELEASES
|
||||
artifact: Squirrel Installer, Portable Version, Squirrel nupkg, Squirrel RELEASES
|
||||
force_update: true
|
||||
on:
|
||||
APPVEYOR_REPO_TAG: true
|
||||
APPVEYOR_REPO_TAG: true
|
||||
|
||||
environment:
|
||||
winget_token:
|
||||
secure: HKfVT2FYZITAG0qqMCePYhIem5a/gzvAgYDSlr6RlXfGmeBUOANUtgJ9X6fNroxN
|
||||
|
||||
on_success:
|
||||
- ps: |
|
||||
if ($env:APPVEYOR_REPO_BRANCH -eq "master" -and $env:APPVEYOR_REPO_TAG -eq "true")
|
||||
{
|
||||
iwr https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe
|
||||
.\wingetcreate.exe update Flow-Launcher.Flow-Launcher -s true -u https://github.com/Flow-Launcher/Flow.Launcher/releases/download/v$env:flowVersion/Flow-Launcher-v$env:flowVersion.exe -v $env:flowVersion -t $env:winget_token
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue