Merge branch 'dev' into cacheProgramResult

This commit is contained in:
Jeremy 2021-08-10 21:16:57 +10:00
commit d8bea3cb89
97 changed files with 1776 additions and 1095 deletions

View file

@ -53,15 +53,12 @@
</ItemGroup> </ItemGroup>
<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="FSharp.Core" Version="4.7.1" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.1.3" />
<PackageReference Include="squirrel.windows" Version="1.5.2" /> <PackageReference Include="squirrel.windows" Version="1.5.2" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" /> <ProjectReference Include="..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
<ProjectReference Include="..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" /> <ProjectReference Include="..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />

View file

@ -1,5 +1,4 @@
 /* We basically follow the Json-RPC 2.0 spec (http://www.jsonrpc.org/specification) to invoke methods between Flow Launcher and other plugins,
/* We basically follow the Json-RPC 2.0 spec (http://www.jsonrpc.org/specification) to invoke methods between Flow Launcher and other plugins,
* like python or other self-execute program. But, we added addtional infos (proxy and so on) into rpc request. Also, we didn't use the * like python or other self-execute program. But, we added addtional infos (proxy and so on) into rpc request. Also, we didn't use the
* "id" and "jsonrpc" in the request, since it's not so useful in our request model. * "id" and "jsonrpc" in the request, since it's not so useful in our request model.
* *
@ -13,10 +12,12 @@
* *
*/ */
using Flow.Launcher.Core.Resource;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using System.Text.Json;
namespace Flow.Launcher.Core.Plugin namespace Flow.Launcher.Core.Plugin
{ {
@ -29,12 +30,8 @@ namespace Flow.Launcher.Core.Plugin
public string Data { get; set; } public string Data { get; set; }
} }
public class JsonRPCModelBase
{
public int Id { get; set; }
}
public class JsonRPCResponseModel : JsonRPCModelBase public class JsonRPCResponseModel
{ {
public string Result { get; set; } public string Result { get; set; }
@ -48,45 +45,20 @@ namespace Flow.Launcher.Core.Plugin
public string DebugMessage { get; set; } public string DebugMessage { get; set; }
} }
public class JsonRPCRequestModel : JsonRPCModelBase public class JsonRPCRequestModel
{ {
public string Method { get; set; } public string Method { get; set; }
public object[] Parameters { get; set; } public object[] Parameters { get; set; }
private static readonly JsonSerializerOptions options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public override string ToString() public override string ToString()
{ {
string rpc = string.Empty; return JsonSerializer.Serialize(this, options);
if (Parameters != null && Parameters.Length > 0)
{
string parameters = $"[{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(@"""", @"\\""""");
} }
} }
@ -95,11 +67,7 @@ namespace Flow.Launcher.Core.Plugin
/// </summary> /// </summary>
public class JsonRPCServerRequestModel : JsonRPCRequestModel public class JsonRPCServerRequestModel : JsonRPCRequestModel
{ {
public override string ToString()
{
string rpc = base.ToString();
return rpc + "}";
}
} }
/// <summary> /// <summary>
@ -108,12 +76,6 @@ namespace Flow.Launcher.Core.Plugin
public class JsonRPCClientRequestModel : JsonRPCRequestModel public class JsonRPCClientRequestModel : JsonRPCRequestModel
{ {
public bool DontHideAfterAction { get; set; } public bool DontHideAfterAction { get; set; }
public override string ToString()
{
string rpc = base.ToString();
return rpc + "}";
}
} }
/// <summary> /// <summary>
@ -125,4 +87,4 @@ namespace Flow.Launcher.Core.Plugin
{ {
public JsonRPCClientRequestModel JsonRPCAction { get; set; } public JsonRPCClientRequestModel JsonRPCAction { get; set; }
} }
} }

View file

@ -1,7 +1,9 @@
using System; using Flow.Launcher.Core.Resource;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
@ -9,7 +11,9 @@ using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using ICSharpCode.SharpZipLib.Zip;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.IO;
namespace Flow.Launcher.Core.Plugin namespace Flow.Launcher.Core.Plugin
{ {
@ -31,28 +35,30 @@ namespace Flow.Launcher.Core.Plugin
protected abstract string ExecuteCallback(JsonRPCRequestModel rpcRequest); protected abstract string ExecuteCallback(JsonRPCRequestModel rpcRequest);
protected abstract string ExecuteContextMenu(Result selectedResult); protected abstract string ExecuteContextMenu(Result selectedResult);
private static readonly RecyclableMemoryStreamManager BufferManager = new();
public List<Result> LoadContextMenus(Result selectedResult) public List<Result> LoadContextMenus(Result selectedResult)
{ {
string output = ExecuteContextMenu(selectedResult); var output = ExecuteContextMenu(selectedResult);
try return DeserializedResult(output);
{
return DeserializedResult(output);
}
catch (Exception e)
{
Log.Exception($"|JsonRPCPlugin.LoadContextMenus|Exception on result <{selectedResult}>", e);
return null;
}
} }
private static readonly JsonSerializerOptions options = new()
{
PropertyNameCaseInsensitive = true,
IgnoreNullValues = true,
Converters =
{
new JsonObjectConverter()
}
};
private async Task<List<Result>> DeserializedResultAsync(Stream output) private async Task<List<Result>> DeserializedResultAsync(Stream output)
{ {
if (output == Stream.Null) return null; if (output == Stream.Null) return null;
JsonRPCQueryResponseModel queryResponseModel = await var queryResponseModel =
JsonSerializer.DeserializeAsync<JsonRPCQueryResponseModel>(output); await JsonSerializer.DeserializeAsync<JsonRPCQueryResponseModel>(output, options);
return ParseResults(queryResponseModel); return ParseResults(queryResponseModel);
} }
@ -61,60 +67,71 @@ namespace Flow.Launcher.Core.Plugin
{ {
if (string.IsNullOrEmpty(output)) return null; if (string.IsNullOrEmpty(output)) return null;
JsonRPCQueryResponseModel queryResponseModel = var queryResponseModel =
JsonSerializer.Deserialize<JsonRPCQueryResponseModel>(output); JsonSerializer.Deserialize<JsonRPCQueryResponseModel>(output, options);
return ParseResults(queryResponseModel); return ParseResults(queryResponseModel);
} }
private List<Result> ParseResults(JsonRPCQueryResponseModel queryResponseModel) private List<Result> ParseResults(JsonRPCQueryResponseModel queryResponseModel)
{ {
var results = new List<Result>();
if (queryResponseModel.Result == null) return null; if (queryResponseModel.Result == null) return null;
if(!string.IsNullOrEmpty(queryResponseModel.DebugMessage)) if (!string.IsNullOrEmpty(queryResponseModel.DebugMessage))
{ {
context.API.ShowMsg(queryResponseModel.DebugMessage); context.API.ShowMsg(queryResponseModel.DebugMessage);
} }
foreach (JsonRPCResult result in queryResponseModel.Result) foreach (var result in queryResponseModel.Result)
{ {
result.Action = c => result.Action = c =>
{ {
if (result.JsonRPCAction == null) return false; 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), return !result.JsonRPCAction.DontHideAfterAction;
result.JsonRPCAction.Parameters);
} }
else
var jsonRpcRequestModel =
JsonSerializer.Deserialize<JsonRPCRequestModel>(actionResponse, options);
if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false)
{ {
string actionReponse = ExecuteCallback(result.JsonRPCAction); ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..],
JsonRPCRequestModel jsonRpcRequestModel = jsonRpcRequestModel.Parameters);
JsonSerializer.Deserialize<JsonRPCRequestModel>(actionReponse);
if (jsonRpcRequestModel != null
&& !string.IsNullOrEmpty(jsonRpcRequestModel.Method)
&& jsonRpcRequestModel.Method.StartsWith("Flow.Launcher."))
{
ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method.Substring(4),
jsonRpcRequestModel.Parameters);
}
} }
} }
return !result.JsonRPCAction.DontHideAfterAction; return !result.JsonRPCAction.DontHideAfterAction;
}; };
results.Add(result);
} }
var results = new List<Result>();
results.AddRange(queryResponseModel.Result);
return results; return results;
} }
private void ExecuteFlowLauncherAPI(string method, object[] parameters) private void ExecuteFlowLauncherAPI(string method, object[] parameters)
{ {
MethodInfo methodInfo = PluginManager.API.GetType().GetMethod(method); var parametersTypeArray = parameters.Select(param => param.GetType()).ToArray();
MethodInfo methodInfo = PluginManager.API.GetType().GetMethod(method, parametersTypeArray);
if (methodInfo != null) if (methodInfo != null)
{ {
try try
@ -177,12 +194,14 @@ namespace Flow.Launcher.Core.Plugin
if (result.StartsWith("DEBUG:")) 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 string.Empty;
} }
return result; return result;
} }
catch (Exception e) catch (Exception e)
{ {
@ -195,19 +214,53 @@ namespace Flow.Launcher.Core.Plugin
protected async Task<Stream> ExecuteAsync(ProcessStartInfo startInfo, CancellationToken token = default) protected async Task<Stream> ExecuteAsync(ProcessStartInfo startInfo, CancellationToken token = default)
{ {
Process process = null;
bool disposed = false;
try try
{ {
using var process = Process.Start(startInfo); process = Process.Start(startInfo);
if (process == null) if (process == null)
{ {
Log.Error("|JsonRPCPlugin.ExecuteAsync|Can't start new process"); Log.Error("|JsonRPCPlugin.ExecuteAsync|Can't start new process");
return Stream.Null; 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(); 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) if (!process.StandardError.EndOfStream)
{ {
using var standardError = process.StandardError; using var standardError = process.StandardError;
@ -215,40 +268,26 @@ namespace Flow.Launcher.Core.Plugin
if (!string.IsNullOrEmpty(error)) if (!string.IsNullOrEmpty(error))
{ {
Log.Error($"|JsonRPCPlugin.ExecuteAsync|{error}"); Log.Error($"|{context.CurrentPluginMetadata.Name}.{nameof(ExecuteAsync)}|{error}");
return Stream.Null;
} }
Log.Error("|JsonRPCPlugin.ExecuteAsync|Empty standard output and standard error.");
return Stream.Null;
} }
return result; return buffer;
} }
catch (Exception e) finally
{ {
Log.Exception( process?.Dispose();
$"|JsonRPCPlugin.ExecuteAsync|Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>", disposed = true;
e);
return Stream.Null;
} }
} }
public async Task<List<Result>> QueryAsync(Query query, CancellationToken token) public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
{ {
var output = await ExecuteQueryAsync(query, token); var output = await ExecuteQueryAsync(query, token);
try return await DeserializedResultAsync(output);
{
return await DeserializedResultAsync(output);
}
catch (Exception e)
{
Log.Exception($"|JsonRPCPlugin.Query|Exception when query <{query}>", e);
return null;
}
} }
public Task InitAsync(PluginInitContext context) public virtual Task InitAsync(PluginInitContext context)
{ {
this.context = context; this.context = context;
return Task.CompletedTask; return Task.CompletedTask;

View file

@ -7,9 +7,9 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using ISavable = Flow.Launcher.Plugin.ISavable;
namespace Flow.Launcher.Core.Plugin namespace Flow.Launcher.Core.Plugin
{ {
@ -21,8 +21,8 @@ namespace Flow.Launcher.Core.Plugin
private static IEnumerable<PluginPair> _contextMenuPlugins; private static IEnumerable<PluginPair> _contextMenuPlugins;
public static List<PluginPair> AllPlugins { get; private set; } public static List<PluginPair> AllPlugins { get; private set; }
public static readonly List<PluginPair> GlobalPlugins = new List<PluginPair>(); public static readonly HashSet<PluginPair> GlobalPlugins = new();
public static readonly Dictionary<string, PluginPair> NonGlobalPlugins = new Dictionary<string, PluginPair>(); public static readonly Dictionary<string, PluginPair> NonGlobalPlugins = new();
public static IPublicAPI API { private set; get; } public static IPublicAPI API { private set; get; }
@ -33,7 +33,10 @@ namespace Flow.Launcher.Core.Plugin
/// <summary> /// <summary>
/// Directories that will hold Flow Launcher plugin directory /// Directories that will hold Flow Launcher plugin directory
/// </summary> /// </summary>
private static readonly string[] Directories = { Constant.PreinstalledDirectory, DataLocation.PluginsDirectory }; private static readonly string[] Directories =
{
Constant.PreinstalledDirectory, DataLocation.PluginsDirectory
};
private static void DeletePythonBinding() private static void DeletePythonBinding()
{ {
@ -51,9 +54,26 @@ namespace Flow.Launcher.Core.Plugin
var savable = plugin.Plugin as ISavable; var savable = plugin.Plugin as ISavable;
savable?.Save(); savable?.Save();
} }
API.SavePluginSettings(); API.SavePluginSettings();
} }
public static async ValueTask DisposePluginsAsync()
{
foreach (var pluginPair in AllPlugins)
{
switch (pluginPair.Plugin)
{
case IDisposable disposable:
disposable.Dispose();
break;
case IAsyncDisposable asyncDisposable:
await asyncDisposable.DisposeAsync();
break;
}
}
}
public static async Task ReloadData() public static async Task ReloadData()
{ {
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
@ -99,7 +119,7 @@ namespace Flow.Launcher.Core.Plugin
try try
{ {
var milliseconds = await Stopwatch.DebugAsync($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>", 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; pair.Metadata.InitTime += milliseconds;
Log.Info( Log.Info(
@ -118,7 +138,9 @@ namespace Flow.Launcher.Core.Plugin
_contextMenuPlugins = GetPluginsForInterface<IContextMenu>(); _contextMenuPlugins = GetPluginsForInterface<IContextMenu>();
foreach (var plugin in AllPlugins) foreach (var plugin in AllPlugins)
{ {
foreach (var actionKeyword in plugin.Metadata.ActionKeywords) // set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin
// has multiple global and action keywords because we will only add them here once.
foreach (var actionKeyword in plugin.Metadata.ActionKeywords.Distinct())
{ {
switch (actionKeyword) switch (actionKeyword)
{ {
@ -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)) if (NonGlobalPlugins.ContainsKey(query.ActionKeyword))
{ {
var plugin = NonGlobalPlugins[query.ActionKeyword]; var plugin = NonGlobalPlugins[query.ActionKeyword];
return new List<PluginPair> { plugin }; return new List<PluginPair>
{
plugin
};
} }
else else
{ {
@ -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>(); var results = new List<Result>();
try try
@ -168,7 +193,7 @@ namespace Flow.Launcher.Core.Plugin
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
if (results == null) if (results == null)
return results; return null;
UpdatePluginMetadata(results, metadata, query); UpdatePluginMetadata(results, metadata, query);
metadata.QueryCount += 1; metadata.QueryCount += 1;
@ -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 // null will be fine since the results will only be added into queue if the token hasn't been cancelled
return null; return null;
} }
catch (Exception e)
{
Log.Exception($"|PluginManager.QueryForPlugin|Exception for plugin <{pair.Metadata.Name}> when query <{query}>", e);
}
return results; return results;
} }
@ -229,7 +250,7 @@ namespace Flow.Launcher.Core.Plugin
try try
{ {
results = plugin.LoadContextMenus(result); results = plugin.LoadContextMenus(result) ?? results;
foreach (var r in results) foreach (var r in results)
{ {
r.PluginDirectory = pluginPair.Metadata.PluginDirectory; r.PluginDirectory = pluginPair.Metadata.PluginDirectory;
@ -250,6 +271,8 @@ namespace Flow.Launcher.Core.Plugin
public static bool ActionKeywordRegistered(string actionKeyword) public static bool ActionKeywordRegistered(string actionKeyword)
{ {
// this method is only checking for action keywords (defined as not '*') registration
// hence the actionKeyword != Query.GlobalPluginWildcardSign logic
return actionKeyword != Query.GlobalPluginWildcardSign return actionKeyword != Query.GlobalPluginWildcardSign
&& NonGlobalPlugins.ContainsKey(actionKeyword); && NonGlobalPlugins.ContainsKey(actionKeyword);
} }
@ -274,7 +297,7 @@ namespace Flow.Launcher.Core.Plugin
} }
/// <summary> /// <summary>
/// used to add action keyword for multiple action keyword plugin /// used to remove action keyword for multiple action keyword plugin
/// e.g. web search /// e.g. web search
/// </summary> /// </summary>
public static void RemoveActionKeyword(string id, string oldActionkeyword) public static void RemoveActionKeyword(string id, string oldActionkeyword)
@ -283,9 +306,7 @@ namespace Flow.Launcher.Core.Plugin
if (oldActionkeyword == Query.GlobalPluginWildcardSign if (oldActionkeyword == Query.GlobalPluginWildcardSign
&& // Plugins may have multiple ActionKeywords that are global, eg. WebSearch && // Plugins may have multiple ActionKeywords that are global, eg. WebSearch
plugin.Metadata.ActionKeywords plugin.Metadata.ActionKeywords
.Where(x => x == Query.GlobalPluginWildcardSign) .Count(x => x == Query.GlobalPluginWildcardSign) == 1)
.ToList()
.Count == 1)
{ {
GlobalPlugins.Remove(plugin); GlobalPlugins.Remove(plugin);
} }
@ -306,4 +327,4 @@ namespace Flow.Launcher.Core.Plugin
} }
} }
} }
} }

View file

@ -11,13 +11,13 @@ using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedCommands;
using System.Diagnostics;
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
namespace Flow.Launcher.Core.Plugin namespace Flow.Launcher.Core.Plugin
{ {
public static class PluginsLoader public static class PluginsLoader
{ {
public const string PATH = "PATH";
public const string Python = "python";
public const string PythonExecutable = "pythonw.exe"; public const string PythonExecutable = "pythonw.exe";
public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings) public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings)
@ -85,11 +85,7 @@ namespace Flow.Launcher.Core.Plugin
return; return;
} }
plugins.Add(new PluginPair plugins.Add(new PluginPair {Plugin = plugin, Metadata = metadata});
{
Plugin = plugin,
Metadata = metadata
});
}); });
metadata.InitTime += milliseconds; metadata.InitTime += milliseconds;
} }
@ -119,102 +115,68 @@ namespace Flow.Launcher.Core.Plugin
if (!source.Any(o => o.Language.ToUpper() == AllowedLanguage.Python)) if (!source.Any(o => o.Language.ToUpper() == AllowedLanguage.Python))
return new List<PluginPair>(); return new List<PluginPair>();
// Try setting Constant.PythonPath first, either from if (!string.IsNullOrEmpty(settings.PythonDirectory) && FilesFolders.LocationExists(settings.PythonDirectory))
// PATH or from the given pythonDirectory return SetPythonPathForPluginPairs(source, Path.Combine(settings.PythonDirectory, PythonExecutable));
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 (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 = pythonPath = Path.Combine(pythonDirectory, PythonExecutable);
Path.Combine(paths.Split(';').Where(p => p.ToLower().Contains(Python)).FirstOrDefault(), PythonExecutable); if (File.Exists(pythonPath))
settings.PythonDirectory = FilesFolders.GetPreviousExistingDirectory(FilesFolders.LocationExists, Constant.PythonPath); {
} settings.PythonDirectory = pythonDirectory;
else Constant.PythonPath = pythonPath;
{ }
Log.Error("PluginsLoader", "Failed to set Python path despite the environment variable PATH is found", "PythonPlugins"); else
{
MessageBox.Show("Can't find python in given directory");
}
} }
} }
} }
else else
{ {
var path = Path.Combine(settings.PythonDirectory, PythonExecutable); var installedPythonDirectory = Path.Combine(DataLocation.DataDirectory(), "PythonEmbeddable");
if (File.Exists(path))
// Python 3.8.9 is used for Windows 7 compatibility
DroplexPackage.Drop(App.python_3_8_9_embeddable, installedPythonDirectory).Wait();
pythonPath = Path.Combine(installedPythonDirectory, PythonExecutable);
if (FilesFolders.FileExists(pythonPath))
{ {
Constant.PythonPath = path; settings.PythonDirectory = installedPythonDirectory;
Constant.PythonPath = pythonPath;
} }
else else
{ {
Log.Error("PluginsLoader", $"Tried to automatically set from Settings.PythonDirectory " + Log.Error("PluginsLoader",
$"but can't find python executable in {path}", "PythonPlugins"); $"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, " + MessageBox.Show(
"would you like to install Python to run them? " + "Unable to set Python executable path, please try from Flow's settings (scroll down to the bottom).");
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).");
Log.Error("PluginsLoader", Log.Error("PluginsLoader",
$"Not able to successfully set Python path, the PythonDirectory variable is still an empty string.", $"Not able to successfully set Python path, the PythonDirectory variable is still an empty string.",
"PythonPlugins"); "PythonPlugins");
@ -222,24 +184,26 @@ namespace Flow.Launcher.Core.Plugin
return new List<PluginPair>(); 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) .Where(o => o.Language.ToUpper() == AllowedLanguage.Python)
.Select(metadata => new PluginPair .Select(metadata => new PluginPair
{ {
Plugin = new PythonPlugin(Constant.PythonPath), Plugin = new PythonPlugin(pythonPath),
Metadata = metadata Metadata = metadata
}) })
.ToList(); .ToList();
}
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source) public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
{ {
return source return source
.Where(o => o.Language.ToUpper() == AllowedLanguage.Executable) .Where(o => o.Language.ToUpper() == AllowedLanguage.Executable)
.Select(metadata => new PluginPair .Select(metadata => new PluginPair
{ {
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), Metadata = metadata
Metadata = metadata
}); });
} }
} }

View file

@ -28,17 +28,19 @@ namespace Flow.Launcher.Core.Plugin
var path = Path.Combine(Constant.ProgramDirectory, JsonRPC); var path = Path.Combine(Constant.ProgramDirectory, JsonRPC);
_startInfo.EnvironmentVariables["PYTHONPATH"] = path; _startInfo.EnvironmentVariables["PYTHONPATH"] = path;
//Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable
_startInfo.ArgumentList.Add("-B");
} }
protected override Task<Stream> ExecuteQueryAsync(Query query, CancellationToken token) protected override Task<Stream> ExecuteQueryAsync(Query query, CancellationToken token)
{ {
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel JsonRPCServerRequestModel request = new JsonRPCServerRequestModel
{ {
Method = "query", Method = "query", Parameters = new object[] {query.Search},
Parameters = new object[] { query.Search },
}; };
//Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable
_startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{request}\""; _startInfo.ArgumentList[2] = request.ToString();
// todo happlebao why context can't be used in constructor // todo happlebao why context can't be used in constructor
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
@ -47,22 +49,31 @@ namespace Flow.Launcher.Core.Plugin
protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest) protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest)
{ {
_startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{rpcRequest}\""; _startInfo.ArgumentList[2] = rpcRequest.ToString();
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
// TODO: Async Action // TODO: Async Action
return Execute(_startInfo); return Execute(_startInfo);
} }
protected override string ExecuteContextMenu(Result selectedResult) { protected override string ExecuteContextMenu(Result selectedResult)
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel { {
Method = "context_menu", JsonRPCServerRequestModel request = new JsonRPCServerRequestModel
Parameters = new object[] { selectedResult.ContextData }, {
Method = "context_menu", Parameters = new object[] {selectedResult.ContextData},
}; };
_startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{request}\""; _startInfo.ArgumentList[2] = request.ToString();
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
// TODO: Async Action // TODO: Async Action
return Execute(_startInfo); return Execute(_startInfo);
} }
public override Task InitAsync(PluginInitContext context)
{
this.context = context;
_startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
_startInfo.ArgumentList.Add("");
return Task.CompletedTask;
}
} }
} }

View file

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

View file

@ -0,0 +1,42 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Flow.Launcher.Core.Resource
{
public class JsonObjectConverter : JsonConverter<object>
{
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
switch (reader.TokenType)
{
case JsonTokenType.True:
return true;
case JsonTokenType.False:
return false;
case JsonTokenType.Number when reader.TryGetInt32(out var i):
return i;
case JsonTokenType.Number when reader.TryGetInt64(out var l):
return l;
case JsonTokenType.Number:
return reader.GetDouble();
case JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime):
return datetime;
case JsonTokenType.String:
return reader.GetString();
default:
// Use JsonElement as fallback.
// Newtonsoft uses JArray or JObject.
using (var document = JsonDocument.ParseValue(ref reader))
{
return document.RootElement.Clone();
}
}
}
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{
throw new InvalidOperationException("Should not get here.");
}
}
}

View file

@ -17,6 +17,8 @@ namespace Flow.Launcher.Core.Resource
{ {
public class Theme public class Theme
{ {
private const int ShadowExtraMargin = 12;
private readonly List<string> _themeDirectories = new List<string>(); private readonly List<string> _themeDirectories = new List<string>();
private ResourceDictionary _oldResource; private ResourceDictionary _oldResource;
private string _oldTheme; private string _oldTheme;
@ -26,6 +28,10 @@ namespace Flow.Launcher.Core.Resource
private string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder); private string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder);
private string UserDirectoryPath => Path.Combine(DataLocation.DataDirectory(), Folder); private string UserDirectoryPath => Path.Combine(DataLocation.DataDirectory(), Folder);
public bool BlurEnabled { get; set; }
private double mainWindowWidth;
public Theme() public Theme()
{ {
_themeDirectories.Add(DirectoryPath); _themeDirectories.Add(DirectoryPath);
@ -86,8 +92,12 @@ namespace Flow.Launcher.Core.Resource
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
} }
if (Settings.UseDropShadowEffect) BlurEnabled = IsBlurTheme();
if (Settings.UseDropShadowEffect && !BlurEnabled)
AddDropShadowEffectToCurrentTheme(); AddDropShadowEffectToCurrentTheme();
SetBlurForWindow();
} }
catch (DirectoryNotFoundException e) catch (DirectoryNotFoundException e)
{ {
@ -179,6 +189,21 @@ namespace Flow.Launcher.Core.Resource
Array.ForEach(new[] { resultItemStyle, resultSubItemStyle, resultItemSelectedStyle, resultSubItemSelectedStyle }, o => Array.ForEach(setters, p => o.Setters.Add(p))); Array.ForEach(new[] { resultItemStyle, resultSubItemStyle, resultItemSelectedStyle, resultSubItemSelectedStyle }, o => Array.ForEach(setters, p => o.Setters.Add(p)));
} }
var windowStyle = dict["WindowStyle"] as Style;
var width = windowStyle?.Setters.OfType<Setter>().Where(x => x.Property.Name == "Width")
.Select(x => x.Value).FirstOrDefault();
if (width == null)
{
windowStyle = dict["BaseWindowStyle"] as Style;
width = windowStyle?.Setters.OfType<Setter>().Where(x => x.Property.Name == "Width")
.Select(x => x.Value).FirstOrDefault();
}
mainWindowWidth = (double)width;
return dict; return dict;
} }
@ -224,17 +249,54 @@ namespace Flow.Launcher.Core.Resource
BlurRadius = 15 BlurRadius = 15
}; };
var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter;
if (marginSetter == null)
{
marginSetter = new Setter()
{
Property = Border.MarginProperty,
Value = new Thickness(ShadowExtraMargin),
};
windowBorderStyle.Setters.Add(marginSetter);
}
else
{
var baseMargin = (Thickness) marginSetter.Value;
var newMargin = new Thickness(
baseMargin.Left + ShadowExtraMargin,
baseMargin.Top + ShadowExtraMargin,
baseMargin.Right + ShadowExtraMargin,
baseMargin.Bottom + ShadowExtraMargin);
marginSetter.Value = newMargin;
}
windowBorderStyle.Setters.Add(effectSetter); windowBorderStyle.Setters.Add(effectSetter);
UpdateResourceDictionary(dict); UpdateResourceDictionary(dict);
} }
public void RemoveDropShadowEffectToCurrentTheme() public void RemoveDropShadowEffectFromCurrentTheme()
{ {
var dict = CurrentThemeResourceDictionary(); var dict = CurrentThemeResourceDictionary();
var windowBorderStyle = dict["WindowBorderStyle"] as Style; var windowBorderStyle = dict["WindowBorderStyle"] as Style;
dict.Remove(Border.EffectProperty); var effectSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.EffectProperty) as Setter;
var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter;
if(effectSetter != null)
{
windowBorderStyle.Setters.Remove(effectSetter);
}
if (marginSetter != null)
{
var currentMargin = (Thickness)marginSetter.Value;
var newMargin = new Thickness(
currentMargin.Left - ShadowExtraMargin,
currentMargin.Top - ShadowExtraMargin,
currentMargin.Right - ShadowExtraMargin,
currentMargin.Bottom - ShadowExtraMargin);
marginSetter.Value = newMargin;
}
UpdateResourceDictionary(dict); UpdateResourceDictionary(dict);
} }
@ -281,35 +343,39 @@ namespace Flow.Launcher.Core.Resource
/// </summary> /// </summary>
public void SetBlurForWindow() public void SetBlurForWindow()
{ {
if (BlurEnabled)
{
SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_ENABLE_BLURBEHIND);
}
else
{
SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_DISABLED);
}
}
// Exception of FindResource can't be cathed if global exception handle is set private bool IsBlurTheme()
{
if (Environment.OSVersion.Version >= new Version(6, 2)) if (Environment.OSVersion.Version >= new Version(6, 2))
{ {
var resource = Application.Current.TryFindResource("ThemeBlurEnabled"); var resource = Application.Current.TryFindResource("ThemeBlurEnabled");
bool blur;
if (resource is bool)
{
blur = (bool)resource;
}
else
{
blur = false;
}
if (blur) if (resource is bool)
{ return (bool)resource;
SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_ENABLE_BLURBEHIND);
} return false;
else
{
SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_DISABLED);
}
} }
return false;
} }
private void SetWindowAccent(Window w, AccentState state) private void SetWindowAccent(Window w, AccentState state)
{ {
var windowHelper = new WindowInteropHelper(w); var windowHelper = new WindowInteropHelper(w);
// this determines the width of the main query window
w.Width = mainWindowWidth;
windowHelper.EnsureHandle();
var accent = new AccentPolicy { AccentState = state }; var accent = new AccentPolicy { AccentState = state };
var accentStructSize = Marshal.SizeOf(accent); var accentStructSize = Marshal.SizeOf(accent);

View file

@ -49,8 +49,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="NLog.Schema" Version="4.7.0-rc1" /> <PackageReference Include="Ben.Demystifier" Version="0.4.1" />
<PackageReference Include="NLog.Web.AspNetCore" Version="4.9.0" /> <PackageReference Include="Microsoft.VisualStudio.Threading" Version="16.10.56" />
<PackageReference Include="NLog" Version="4.7.10" />
<PackageReference Include="NLog.Schema" Version="4.7.10" />
<PackageReference Include="NLog.Web.AspNetCore" Version="4.12.0" />
<PackageReference Include="System.Drawing.Common" Version="4.7.0" /> <PackageReference Include="System.Drawing.Common" Version="4.7.0" />
<PackageReference Include="ToolGood.Words.Pinyin" Version="3.0.1.4" /> <PackageReference Include="ToolGood.Words.Pinyin" Version="3.0.1.4" />
</ItemGroup> </ItemGroup>

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Media; using System.Windows.Media;
@ -26,7 +27,8 @@ namespace Flow.Launcher.Infrastructure.Image
private const int MaxCached = 50; private const int MaxCached = 50;
public ConcurrentDictionary<string, ImageUsage> Data { get; private set; } = new ConcurrentDictionary<string, ImageUsage>(); public ConcurrentDictionary<string, ImageUsage> Data { get; private set; } = new ConcurrentDictionary<string, ImageUsage>();
private const int permissibleFactor = 2; private const int permissibleFactor = 2;
private SemaphoreSlim semaphore = new(1, 1);
public void Initialization(Dictionary<string, int> usage) public void Initialization(Dictionary<string, int> usage)
{ {
foreach (var key in usage.Keys) 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 SliceExtra();
// 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) async void SliceExtra()
{ {
// To delete the images from the data dictionary based on the resizing of the Usage Dictionary. // 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
foreach (var key in Data.OrderBy(x => x.Value.usage).Take(Data.Count - MaxCached).Select(x => x.Key)) // This is done so that we don't constantly perform this resizing operation and also maintain the image cache size at the same time
Data.TryRemove(key, out _); 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) 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() public int CacheSize()

View file

@ -5,6 +5,11 @@ using NLog;
using NLog.Config; using NLog.Config;
using NLog.Targets; using NLog.Targets;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using JetBrains.Annotations;
using NLog.Fluent;
using NLog.Targets.Wrappers;
using System.Runtime.ExceptionServices;
using System.Text;
namespace Flow.Launcher.Infrastructure.Logger namespace Flow.Launcher.Infrastructure.Logger
{ {
@ -23,15 +28,37 @@ namespace Flow.Launcher.Infrastructure.Logger
} }
var configuration = new LoggingConfiguration(); var configuration = new LoggingConfiguration();
var target = new FileTarget();
configuration.AddTarget("file", target); const string layout =
target.FileName = CurrentLogDirectory.Replace(@"\", "/") + "/${shortdate}.txt"; @"${date:format=HH\:mm\:ss.ffffK} - " +
@"${level:uppercase=true:padding=-5} - ${logger} - ${message:l}" +
@"${onexception:${newline}" +
@"EXCEPTION OCCURS\: ${exception:format=tostring}${newline}}";
var fileTarget = new FileTarget
{
FileName = CurrentLogDirectory.Replace(@"\", "/") + "/${shortdate}.txt",
Layout = layout
};
var fileTargetASyncWrapper = new AsyncTargetWrapper(fileTarget);
var debugTarget = new OutputDebugStringTarget
{
Layout = layout
};
configuration.AddTarget("file", fileTargetASyncWrapper);
configuration.AddTarget("debug", debugTarget);
#if DEBUG #if DEBUG
var rule = new LoggingRule("*", LogLevel.Debug, target); var fileRule = new LoggingRule("*", LogLevel.Debug, fileTargetASyncWrapper);
var debugRule = new LoggingRule("*", LogLevel.Debug, debugTarget);
configuration.LoggingRules.Add(debugRule);
#else #else
var rule = new LoggingRule("*", LogLevel.Info, target); var fileRule = new LoggingRule("*", LogLevel.Info, fileTargetASyncWrapper);
#endif #endif
configuration.LoggingRules.Add(rule); configuration.LoggingRules.Add(fileRule);
LogManager.Configuration = configuration; LogManager.Configuration = configuration;
} }
@ -39,7 +66,6 @@ namespace Flow.Launcher.Infrastructure.Logger
{ {
var logger = LogManager.GetLogger("FaultyLogger"); var logger = LogManager.GetLogger("FaultyLogger");
message = $"Wrong logger message format <{message}>"; message = $"Wrong logger message format <{message}>";
System.Diagnostics.Debug.WriteLine($"FATAL|{message}");
logger.Fatal(message); logger.Fatal(message);
} }
@ -51,12 +77,11 @@ namespace Flow.Launcher.Infrastructure.Logger
} }
[MethodImpl(MethodImplOptions.Synchronized)]
public static void Exception(string className, string message, System.Exception exception, [CallerMemberName] string methodName = "") public static void Exception(string className, string message, System.Exception exception, [CallerMemberName] string methodName = "")
{ {
exception = exception.Demystify();
#if DEBUG #if DEBUG
throw exception; ExceptionDispatchInfo.Capture(exception).Throw();
#else #else
var classNameWithMethod = CheckClassAndMessageAndReturnFullClassWithMethod(className, message, methodName); var classNameWithMethod = CheckClassAndMessageAndReturnFullClassWithMethod(className, message, methodName);
@ -90,23 +115,9 @@ namespace Flow.Launcher.Infrastructure.Logger
{ {
var logger = LogManager.GetLogger(classAndMethod); var logger = LogManager.GetLogger(classAndMethod);
System.Diagnostics.Debug.WriteLine($"ERROR|{message}"); var messageBuilder = new StringBuilder();
logger.Error("-------------------------- Begin exception --------------------------"); logger.Error(e, message);
logger.Error(message);
do
{
logger.Error($"Exception full name:\n <{e.GetType().FullName}>");
logger.Error($"Exception message:\n <{e.Message}>");
logger.Error($"Exception stack trace:\n <{e.StackTrace}>");
logger.Error($"Exception source:\n <{e.Source}>");
logger.Error($"Exception target site:\n <{e.TargetSite}>");
logger.Error($"Exception HResult:\n <{e.HResult}>");
e = e.InnerException;
} while (e != null);
logger.Error("-------------------------- End exception --------------------------");
} }
private static void LogInternal(string message, LogLevel level) private static void LogInternal(string message, LogLevel level)
@ -117,8 +128,6 @@ namespace Flow.Launcher.Infrastructure.Logger
var prefix = parts[1]; var prefix = parts[1];
var unprefixed = parts[2]; var unprefixed = parts[2];
var logger = LogManager.GetLogger(prefix); var logger = LogManager.GetLogger(prefix);
System.Diagnostics.Debug.WriteLine($"{level.Name}|{message}");
logger.Log(level, unprefixed); logger.Log(level, unprefixed);
} }
else else
@ -128,11 +137,12 @@ namespace Flow.Launcher.Infrastructure.Logger
} }
/// <param name="message">example: "|prefix|unprefixed" </param> /// <param name="message">example: "|prefix|unprefixed" </param>
[MethodImpl(MethodImplOptions.Synchronized)] /// <param name="e">Exception</param>
public static void Exception(string message, System.Exception e) public static void Exception(string message, System.Exception e)
{ {
e = e.Demystify();
#if DEBUG #if DEBUG
throw e; ExceptionDispatchInfo.Capture(e).Throw();
#else #else
if (FormatValid(message)) if (FormatValid(message))
{ {
@ -165,7 +175,6 @@ namespace Flow.Launcher.Infrastructure.Logger
var logger = LogManager.GetLogger(classNameWithMethod); var logger = LogManager.GetLogger(classNameWithMethod);
System.Diagnostics.Debug.WriteLine($"{level.Name}|{message}");
logger.Log(level, message); logger.Log(level, message);
} }

View file

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

View file

@ -87,4 +87,8 @@ namespace Flow.Launcher.Infrastructure.Storage
File.WriteAllText(FilePath, serialized); File.WriteAllText(FilePath, serialized);
} }
} }
[Obsolete("Deprecated as of Flow Launcher v1.8.0, on 2021.06.21. " +
"This is used only for Everything plugin v1.4.9 or below backwards compatibility")]
public class JsonStrorage<T> : JsonStorage<T> where T : new() { }
} }

View file

@ -22,3 +22,4 @@ namespace Flow.Launcher.Infrastructure.Storage
} }
} }
} }

View file

@ -16,11 +16,13 @@ namespace Flow.Launcher.Infrastructure.UserSettings
{ {
var settings = Plugins[metadata.ID]; var settings = Plugins[metadata.ID];
// TODO: Remove. This is one off for 1.2.0 release. // TODO: Remove. This is backwards compatibility for 1.8.0 release.
// Introduced a new action keyword in Explorer, so need to update plugin setting in the UserData folder. // Introduced two new action keywords in Explorer, so need to update plugin setting in the UserData folder.
// This kind of plugin meta update should be handled by a dedicated method trigger by version bump. if (metadata.ID == "572be03c74c642baae319fc283e561a8" && metadata.ActionKeywords.Count > settings.ActionKeywords.Count)
if (metadata.ID == "572be03c74c642baae319fc283e561a8" && metadata.ActionKeywords.Count != settings.ActionKeywords.Count) {
settings.ActionKeywords = metadata.ActionKeywords; settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for index search
settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for path search
}
if (string.IsNullOrEmpty(settings.Version)) if (string.IsNullOrEmpty(settings.Version))
settings.Version = metadata.Version; settings.Version = metadata.Version;
@ -30,6 +32,11 @@ namespace Flow.Launcher.Infrastructure.UserSettings
metadata.ActionKeywords = settings.ActionKeywords; metadata.ActionKeywords = settings.ActionKeywords;
metadata.ActionKeyword = settings.ActionKeywords[0]; metadata.ActionKeyword = settings.ActionKeywords[0];
} }
else
{
metadata.ActionKeywords = new List<string>();
metadata.ActionKeyword = string.Empty;
}
metadata.Disabled = settings.Disabled; metadata.Disabled = settings.Disabled;
metadata.Priority = settings.Priority; metadata.Priority = settings.Priority;
} }

View file

@ -39,7 +39,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
/// </summary> /// </summary>
public bool ShouldUsePinyin { get; set; } = false; 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] [JsonIgnore]
public string QuerySearchPrecisionString public string QuerySearchPrecisionString
@ -97,6 +98,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public bool RememberLastLaunchLocation { get; set; } public bool RememberLastLaunchLocation { get; set; }
public bool IgnoreHotkeysOnFullscreen { get; set; } public bool IgnoreHotkeysOnFullscreen { get; set; }
public bool AutoHideScrollBar { get; set; }
public HttpProxy Proxy { get; set; } = new HttpProxy(); public HttpProxy Proxy { get; set; } = new HttpProxy();
[JsonConverter(typeof(JsonStringEnumConverter))] [JsonConverter(typeof(JsonStringEnumConverter))]

View file

@ -1,36 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
namespace Flow.Launcher.Plugin
{
public interface IFeatures { }
public interface IContextMenu : IFeatures
{
List<Result> LoadContextMenus(Result selectedResult);
}
/// <summary>
/// Represent plugins that support internationalization
/// </summary>
public interface IPluginI18n : IFeatures
{
string GetTranslatedPluginTitle();
string GetTranslatedPluginDescription();
}
public interface IResultUpdated : IFeatures
{
event ResultUpdatedEventHandler ResultsUpdated;
}
public delegate void ResultUpdatedEventHandler(IResultUpdated sender, ResultUpdatedEventArgs e);
public class ResultUpdatedEventArgs : EventArgs
{
public List<Result> Results;
public Query Query;
}
}

View file

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Threading;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Base Interface for Flow's special plugin feature interface
/// </summary>
public interface IFeatures
{
}
}

View file

@ -14,10 +14,10 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<Version>1.4.0</Version> <Version>2.0.0</Version>
<PackageVersion>1.4.0</PackageVersion> <PackageVersion>2.0.0</PackageVersion>
<AssemblyVersion>1.4.0</AssemblyVersion> <AssemblyVersion>2.0.0</AssemblyVersion>
<FileVersion>1.4.0</FileVersion> <FileVersion>2.0.0</FileVersion>
<PackageId>Flow.Launcher.Plugin</PackageId> <PackageId>Flow.Launcher.Plugin</PackageId>
<Authors>Flow-Launcher</Authors> <Authors>Flow-Launcher</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>

View file

@ -13,8 +13,8 @@ namespace Flow.Launcher.Plugin
/// The command that allows user to manual reload is exposed via Plugin.Sys, and /// The command that allows user to manual reload is exposed via Plugin.Sys, and
/// it will call the plugins that have implemented this interface. /// it will call the plugins that have implemented this interface.
/// </summary> /// </summary>
public interface IAsyncReloadable public interface IAsyncReloadable : IFeatures
{ {
Task ReloadDataAsync(); Task ReloadDataAsync();
} }
} }

View file

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

View file

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

View file

@ -15,8 +15,8 @@
/// If requiring reloading data asynchronously, please use the IAsyncReloadable interface /// If requiring reloading data asynchronously, please use the IAsyncReloadable interface
/// </para> /// </para>
/// </summary> /// </summary>
public interface IReloadable public interface IReloadable : IFeatures
{ {
void ReloadData(); void ReloadData();
} }
} }

View file

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Threading;
namespace Flow.Launcher.Plugin
{
public interface IResultUpdated : IFeatures
{
event ResultUpdatedEventHandler ResultsUpdated;
}
public delegate void ResultUpdatedEventHandler(IResultUpdated sender, ResultUpdatedEventArgs e);
public class ResultUpdatedEventArgs : EventArgs
{
public List<Result> Results;
public Query Query;
public CancellationToken Token { get; init; }
}
}

View file

@ -5,8 +5,8 @@
/// Otherwise if LoadSettingJsonStorage or SaveSettingJsonStorage has been callded, /// Otherwise if LoadSettingJsonStorage or SaveSettingJsonStorage has been callded,
/// plugin settings will be automatically saved (see Flow.Launcher/PublicAPIInstance.SavePluginSettings) by Flow /// plugin settings will be automatically saved (see Flow.Launcher/PublicAPIInstance.SavePluginSettings) by Flow
/// </summary> /// </summary>
public interface ISavable public interface ISavable : IFeatures
{ {
void Save(); void Save();
} }
} }

View file

@ -3,4 +3,4 @@
* Defines base objects and interfaces for plugins * Defines base objects and interfaces for plugins
* Plugin authors making C# plugins should reference this DLL via nuget * Plugin authors making C# plugins should reference this DLL via nuget
* Contains base commands used by all plugins * Contains commands and models that can be used by plugins

View file

@ -49,12 +49,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Moq" Version="4.14.1" /> <PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="nunit" Version="3.12.0" /> <PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1"> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -133,6 +133,13 @@ namespace Flow.Launcher.Test.Plugins
{ {
// Given // Given
var queryConstructor = new QueryConstructor(new Settings()); var queryConstructor = new QueryConstructor(new Settings());
var baseQuery = queryConstructor.CreateBaseQuery();
// system running this test could have different locale than the hard-coded 1033 LCID en-US.
var queryKeywordLocale = baseQuery.QueryKeywordLocale;
expectedString = expectedString.Replace("1033", queryKeywordLocale.ToString());
//When //When
var resultString = queryConstructor.QueryForAllFilesAndFolders(userSearchString); var resultString = queryConstructor.QueryForAllFilesAndFolders(userSearchString);

View file

@ -0,0 +1,105 @@
using NUnit;
using NUnit.Framework;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Plugin;
using System.Threading.Tasks;
using System.IO;
using System.Threading;
using System.Text;
using System.Text.Json;
using System.Linq;
using System.Collections.Generic;
namespace Flow.Launcher.Test.Plugins
{
[TestFixture]
// ReSharper disable once InconsistentNaming
internal class JsonRPCPluginTest : JsonRPCPlugin
{
public override string SupportedLanguage { get; set; } = AllowedLanguage.Executable;
protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest)
{
throw new System.NotImplementedException();
}
protected override string ExecuteContextMenu(Result selectedResult)
{
throw new System.NotImplementedException();
}
protected override Task<Stream> ExecuteQueryAsync(Query query, CancellationToken token)
{
var byteInfo = Encoding.UTF8.GetBytes(query.RawQuery);
var resultStream = new MemoryStream(byteInfo);
return Task.FromResult((Stream)resultStream);
}
[TestCase("{\"result\":[],\"DebugMessage\":null}", Description = "Empty Result")]
[TestCase("{\"result\":[{\"JsonRPCAction\":null,\"Title\":\"something\",\"SubTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null}],\"DebugMessage\":null}", Description = "One Result with Pascal Case")]
[TestCase("{\"result\":[{\"jsonRPCAction\":null,\"title\":\"something\",\"subTitle\":\"\",\"actionKeywordAssigned\":null,\"icoPath\":null}],\"debugMessage\":null}", Description = "One Result with camel Case")]
[TestCase("{\"result\":[{\"JsonRPCAction\":null,\"Title\":\"iii\",\"SubTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null},{\"JsonRPCAction\":null,\"Title\":\"iii\",\"SubTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null}],\"DebugMessage\":null}", Description = "Two Result with Pascal Case")]
[TestCase("{\"result\":[{\"jsonrpcAction\":null,\"TItLE\":\"iii\",\"Subtitle\":\"\",\"Actionkeywordassigned\":null,\"icoPath\":null},{\"jsonRPCAction\":null,\"tiTle\":\"iii\",\"subTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null}],\"DebugMessage\":null}", Description = "Two Result with Weird Case")]
public async Task GivenVariousJsonText_WhenVariousNamingCase_ThenExpectNotNullResults_Async(string resultText)
{
var results = await QueryAsync(new Query
{
RawQuery = resultText
}, default);
Assert.IsNotNull(results);
foreach (var result in results)
{
Assert.IsNotNull(result);
Assert.IsNotNull(result.Action);
Assert.IsNotNull(result.Title);
}
}
public static List<JsonRPCQueryResponseModel> ResponseModelsSource = new()
{
new()
{
Result = new()
},
new()
{
Result = new()
{
new JsonRPCResult
{
Title = "Test1",
SubTitle = "Test2"
}
}
}
};
[TestCaseSource(typeof(JsonRPCPluginTest), nameof(ResponseModelsSource))]
public async Task GivenModel_WhenSerializeWithDifferentNamingPolicy_ThenExpectSameResult_Async(JsonRPCQueryResponseModel reference)
{
var camelText = JsonSerializer.Serialize(reference, new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
var pascalText = JsonSerializer.Serialize(reference);
var results1 = await QueryAsync(new Query { RawQuery = camelText }, default);
var results2 = await QueryAsync(new Query { RawQuery = pascalText }, default);
Assert.IsNotNull(results1);
Assert.IsNotNull(results2);
foreach (var ((result1, result2), referenceResult) in results1.Zip(results2).Zip(reference.Result))
{
Assert.AreEqual(result1, result2);
Assert.AreEqual(result1, referenceResult);
Assert.IsNotNull(result1);
Assert.IsNotNull(result1.Action);
}
}
}
}

View file

@ -68,11 +68,14 @@ namespace Flow.Launcher
PluginManager.LoadPlugins(_settings.PluginSettings); PluginManager.LoadPlugins(_settings.PluginSettings);
_mainVM = new MainViewModel(_settings); _mainVM = new MainViewModel(_settings);
HotKeyMapper.Initialize(_mainVM);
API = new PublicAPIInstance(_settingsVM, _mainVM, _alphabet); API = new PublicAPIInstance(_settingsVM, _mainVM, _alphabet);
Http.API = API; Http.API = API;
Http.Proxy = _settings.Proxy; Http.Proxy = _settings.Proxy;
await PluginManager.InitializePlugins(API); await PluginManager.InitializePlugins(API);
var window = new MainWindow(_settings, _mainVM); var window = new MainWindow(_settings, _mainVM);
@ -96,6 +99,8 @@ namespace Flow.Launcher
AutoStartup(); AutoStartup();
AutoUpdates(); AutoUpdates();
API.SaveAppAllSettings();
_mainVM.MainWindowVisibility = _settings.HideOnStartup ? Visibility.Hidden : Visibility.Visible; _mainVM.MainWindowVisibility = _settings.HideOnStartup ? Visibility.Hidden : Visibility.Visible;
Log.Info("|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- "); Log.Info("|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- ");
}); });

View file

@ -1,8 +1,7 @@
using Flow.Launcher.Core.Resource; using Flow.Launcher.Core.Resource;
using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using NHotkey;
using NHotkey.Wpf;
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
@ -52,11 +51,7 @@ namespace Flow.Launcher
}; };
_settings.CustomPluginHotkeys.Add(pluginHotkey); _settings.CustomPluginHotkeys.Add(pluginHotkey);
SetHotkey(ctlHotkey.CurrentHotkey, delegate HotKeyMapper.SetCustomQueryHotkey(pluginHotkey);
{
App.API.ChangeQuery(pluginHotkey.ActionKeyword);
Application.Current.MainWindow.Visibility = Visibility.Visible;
});
} }
else else
{ {
@ -69,12 +64,8 @@ namespace Flow.Launcher
updateCustomHotkey.ActionKeyword = tbAction.Text; updateCustomHotkey.ActionKeyword = tbAction.Text;
updateCustomHotkey.Hotkey = ctlHotkey.CurrentHotkey.ToString(); updateCustomHotkey.Hotkey = ctlHotkey.CurrentHotkey.ToString();
//remove origin hotkey //remove origin hotkey
RemoveHotkey(oldHotkey); HotKeyMapper.RemoveHotkey(oldHotkey);
SetHotkey(new HotkeyModel(updateCustomHotkey.Hotkey), delegate HotKeyMapper.SetCustomQueryHotkey(updateCustomHotkey);
{
App.API.ChangeQuery(updateCustomHotkey.ActionKeyword);
Application.Current.MainWindow.Visibility = Visibility.Visible;
});
} }
Close(); Close();
@ -100,28 +91,8 @@ namespace Flow.Launcher
{ {
App.API.ChangeQuery(tbAction.Text); App.API.ChangeQuery(tbAction.Text);
Application.Current.MainWindow.Visibility = Visibility.Visible; Application.Current.MainWindow.Visibility = Visibility.Visible;
} Application.Current.MainWindow.Focus();
private void RemoveHotkey(string hotkeyStr)
{
if (!string.IsNullOrEmpty(hotkeyStr))
{
HotkeyManager.Current.Remove(hotkeyStr);
}
}
private void SetHotkey(HotkeyModel hotkey, EventHandler<HotkeyEventArgs> action)
{
string hotkeyStr = hotkey.ToString();
try
{
HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action);
}
catch (Exception)
{
string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr);
MessageBox.Show(errorMsg);
}
} }
private void cmdEsc_OnPress(object sender, ExecutedRoutedEventArgs e) private void cmdEsc_OnPress(object sender, ExecutedRoutedEventArgs e)

View file

@ -3,6 +3,8 @@ using System.Windows.Threading;
using NLog; using NLog;
using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Exception; using Flow.Launcher.Infrastructure.Exception;
using NLog.Fluent;
using Log = Flow.Launcher.Infrastructure.Logger.Log;
namespace Flow.Launcher.Helper namespace Flow.Launcher.Helper
{ {
@ -45,4 +47,4 @@ namespace Flow.Launcher.Helper
return info; return info;
} }
} }
} }

View file

@ -0,0 +1,136 @@
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings;
using System;
using NHotkey;
using NHotkey.Wpf;
using Flow.Launcher.Core.Resource;
using System.Windows;
using Flow.Launcher.ViewModel;
namespace Flow.Launcher.Helper
{
internal static class HotKeyMapper
{
private static Settings settings;
private static MainViewModel mainViewModel;
internal static void Initialize(MainViewModel mainVM)
{
mainViewModel = mainVM;
settings = mainViewModel._settings;
SetHotkey(settings.Hotkey, OnHotkey);
LoadCustomPluginHotkey();
}
private static void SetHotkey(string hotkeyStr, EventHandler<HotkeyEventArgs> action)
{
var hotkey = new HotkeyModel(hotkeyStr);
SetHotkey(hotkey, action);
}
internal static void SetHotkey(HotkeyModel hotkey, EventHandler<HotkeyEventArgs> action)
{
string hotkeyStr = hotkey.ToString();
try
{
HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action);
}
catch (Exception)
{
string errorMsg =
string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"),
hotkeyStr);
MessageBox.Show(errorMsg);
}
}
internal static void RemoveHotkey(string hotkeyStr)
{
if (!string.IsNullOrEmpty(hotkeyStr))
{
HotkeyManager.Current.Remove(hotkeyStr);
}
}
internal static void OnHotkey(object sender, HotkeyEventArgs e)
{
if (!ShouldIgnoreHotkeys())
{
UpdateLastQUeryMode();
mainViewModel.ToggleFlowLauncher();
e.Handled = true;
}
}
/// <summary>
/// Checks if Flow Launcher should ignore any hotkeys
/// </summary>
private static bool ShouldIgnoreHotkeys()
{
return settings.IgnoreHotkeysOnFullscreen && WindowsInteropHelper.IsWindowFullscreen();
}
private static void UpdateLastQUeryMode()
{
switch(settings.LastQueryMode)
{
case LastQueryMode.Empty:
mainViewModel.ChangeQueryText(string.Empty);
break;
case LastQueryMode.Preserved:
mainViewModel.LastQuerySelected = true;
break;
case LastQueryMode.Selected:
mainViewModel.LastQuerySelected = false;
break;
default:
throw new ArgumentException($"wrong LastQueryMode: <{settings.LastQueryMode}>");
}
}
internal static void LoadCustomPluginHotkey()
{
if (settings.CustomPluginHotkeys == null)
return;
foreach (CustomPluginHotkey hotkey in settings.CustomPluginHotkeys)
{
SetCustomQueryHotkey(hotkey);
}
}
internal static void SetCustomQueryHotkey(CustomPluginHotkey hotkey)
{
SetHotkey(hotkey.Hotkey, (s, e) =>
{
if (ShouldIgnoreHotkeys())
return;
mainViewModel.MainWindowVisibility = Visibility.Visible;
mainViewModel.ChangeQueryText(hotkey.ActionKeyword);
});
}
internal static bool CheckAvailability(HotkeyModel currentHotkey)
{
try
{
HotkeyManager.Current.AddOrReplace("HotkeyAvailabilityTest", currentHotkey.CharKey, currentHotkey.ModifierKeys, (sender, e) => { });
return true;
}
catch
{
}
finally
{
HotkeyManager.Current.Remove("HotkeyAvailabilityTest");
}
return false;
}
}
}

View file

@ -11,7 +11,16 @@ namespace Flow.Launcher.Helper
var window = Application.Current.Windows.OfType<Window>().FirstOrDefault(x => x.GetType() == typeof(T)) var window = Application.Current.Windows.OfType<Window>().FirstOrDefault(x => x.GetType() == typeof(T))
?? (T)Activator.CreateInstance(typeof(T), args); ?? (T)Activator.CreateInstance(typeof(T), args);
Application.Current.MainWindow.Hide(); Application.Current.MainWindow.Hide();
// Fix UI bug
// Add `window.WindowState = WindowState.Normal`
// If only use `window.Show()`, Settings-window doesn't show when minimized in taskbar
// Not sure why this works tho
// Probably because, when `.Show()` fails, `window.WindowState == Minimized` (not `Normal`)
// https://stackoverflow.com/a/59719760/4230390
window.WindowState = WindowState.Normal;
window.Show(); window.Show();
window.Focus(); window.Focus();
return (T)window; return (T)window;

View file

@ -10,7 +10,7 @@
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="150" /> <ColumnDefinition Width="150" />
<ColumnDefinition Width="120" /> <ColumnDefinition Width="125" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBox x:Name="tbHotkey" TabIndex="100" VerticalContentAlignment="Center" Grid.Column="0" <TextBox x:Name="tbHotkey" TabIndex="100" VerticalContentAlignment="Center" Grid.Column="0"
PreviewKeyDown="TbHotkey_OnPreviewKeyDown" input:InputMethod.IsInputMethodEnabled="False"/> PreviewKeyDown="TbHotkey_OnPreviewKeyDown" input:InputMethod.IsInputMethodEnabled="False"/>

View file

@ -4,8 +4,8 @@ using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using NHotkey.Wpf;
using Flow.Launcher.Core.Resource; using Flow.Launcher.Core.Resource;
using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
@ -18,11 +18,7 @@ namespace Flow.Launcher
public event EventHandler HotkeyChanged; public event EventHandler HotkeyChanged;
protected virtual void OnHotkeyChanged() protected virtual void OnHotkeyChanged() => HotkeyChanged?.Invoke(this, EventArgs.Empty);
{
EventHandler handler = HotkeyChanged;
if (handler != null) handler(this, EventArgs.Empty);
}
public HotkeyControl() public HotkeyControl()
{ {
@ -90,24 +86,7 @@ namespace Flow.Launcher
SetHotkey(new HotkeyModel(keyStr), triggerValidate); SetHotkey(new HotkeyModel(keyStr), triggerValidate);
} }
private bool CheckHotkeyAvailability() private bool CheckHotkeyAvailability() => HotKeyMapper.CheckAvailability(CurrentHotkey);
{
try
{
HotkeyManager.Current.AddOrReplace("HotkeyAvailabilityTest", CurrentHotkey.CharKey, CurrentHotkey.ModifierKeys, (sender, e) => { });
return true;
}
catch
{
}
finally
{
HotkeyManager.Current.Remove("HotkeyAvailabilityTest");
}
return false;
}
public new bool IsFocused public new bool IsFocused
{ {

View file

@ -31,12 +31,15 @@
<system:String x:Key="ignoreHotkeysOnFullscreen">Ignore hotkeys in fullscreen mode</system:String> <system:String x:Key="ignoreHotkeysOnFullscreen">Ignore hotkeys in fullscreen mode</system:String>
<system:String x:Key="pythonDirectory">Python Directory</system:String> <system:String x:Key="pythonDirectory">Python Directory</system:String>
<system:String x:Key="autoUpdates">Auto Update</system:String> <system:String x:Key="autoUpdates">Auto Update</system:String>
<system:String x:Key="autoHideScrollBar">Auto Hide Scroll Bar</system:String>
<system:String x:Key="autoHideScrollBarToolTip">Automatically hides the Settings window scroll bar and show when hover the mouse over it</system:String>
<system:String x:Key="selectPythonDirectory">Select</system:String> <system:String x:Key="selectPythonDirectory">Select</system:String>
<system:String x:Key="hideOnStartup">Hide Flow Launcher on startup</system:String> <system:String x:Key="hideOnStartup">Hide Flow Launcher on startup</system:String>
<system:String x:Key="hideNotifyIcon">Hide tray icon</system:String> <system:String x:Key="hideNotifyIcon">Hide tray icon</system:String>
<system:String x:Key="querySearchPrecision">Query Search Precision</system:String> <system:String x:Key="querySearchPrecision">Query Search Precision</system:String>
<system:String x:Key="ShouldUsePinyin">Should Use Pinyin</system:String> <system:String x:Key="ShouldUsePinyin">Should Use Pinyin</system:String>
<system:String x:Key="ShouldUsePinyinToolTip">Allows using Pinyin to search. Pinyin is the standard system of romanized spelling for transliterating Chinese</system:String> <system:String x:Key="ShouldUsePinyinToolTip">Allows using Pinyin to search. Pinyin is the standard system of romanized spelling for transliterating Chinese</system:String>
<system:String x:Key="shadowEffectNotAllowed">Shadow effect is not allowed while current theme has blur effect enabled</system:String>
<!--Setting Plugin--> <!--Setting Plugin-->
<system:String x:Key="plugin">Plugin</system:String> <system:String x:Key="plugin">Plugin</system:String>
@ -48,6 +51,7 @@
<system:String x:Key="newActionKeyword">New action keyword:</system:String> <system:String x:Key="newActionKeyword">New action keyword:</system:String>
<system:String x:Key="currentPriority">Current Priority:</system:String> <system:String x:Key="currentPriority">Current Priority:</system:String>
<system:String x:Key="newPriority">New Priority:</system:String> <system:String x:Key="newPriority">New Priority:</system:String>
<system:String x:Key="priority">Priority:</system:String>
<system:String x:Key="pluginDirectory">Plugin Directory</system:String> <system:String x:Key="pluginDirectory">Plugin Directory</system:String>
<system:String x:Key="author">Author</system:String> <system:String x:Key="author">Author</system:String>
<system:String x:Key="plugin_init_time">Init time:</system:String> <system:String x:Key="plugin_init_time">Init time:</system:String>
@ -70,6 +74,7 @@
<system:String x:Key="openResultModifiers">Open Result Modifiers</system:String> <system:String x:Key="openResultModifiers">Open Result Modifiers</system:String>
<system:String x:Key="showOpenResultHotkey">Show Hotkey</system:String> <system:String x:Key="showOpenResultHotkey">Show Hotkey</system:String>
<system:String x:Key="customQueryHotkey">Custom Query Hotkey</system:String> <system:String x:Key="customQueryHotkey">Custom Query Hotkey</system:String>
<system:String x:Key="customQuery">Query</system:String>
<system:String x:Key="delete">Delete</system:String> <system:String x:Key="delete">Delete</system:String>
<system:String x:Key="edit">Edit</system:String> <system:String x:Key="edit">Edit</system:String>
<system:String x:Key="add">Add</system:String> <system:String x:Key="add">Add</system:String>
@ -134,7 +139,7 @@
<system:String x:Key="update">Update</system:String> <system:String x:Key="update">Update</system:String>
<!--Hotkey Control--> <!--Hotkey Control-->
<system:String x:Key="hotkeyUnavailable">Hotkey unavailable</system:String> <system:String x:Key="hotkeyUnavailable">Hotkey Unavailable</system:String>
<!--Crash Reporter--> <!--Crash Reporter-->
<system:String x:Key="reportWindow_version">Version</system:String> <system:String x:Key="reportWindow_version">Version</system:String>

View file

@ -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="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="pythonDirectory">Priečinok s Pythonom</system:String>
<system:String x:Key="autoUpdates">Automatická aktualizácia</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="selectPythonDirectory">Vybrať</system:String>
<system:String x:Key="hideOnStartup">Schovať Flow Launcher po spustení</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="hideNotifyIcon">Schovať ikonu z oblasti oznámení</system:String>
<system:String x:Key="querySearchPrecision">Presnosť vyhľadávania</system:String> <system:String x:Key="querySearchPrecision">Presnosť vyhľadávania</system:String>
<system:String x:Key="ShouldUsePinyin">Použiť Pinyin</system:String> <system:String x:Key="ShouldUsePinyin">Použiť Pinyin</system:String>
<system:String x:Key="ShouldUsePinyinToolTip">Umožňuje vyhľadávanie pomocou Pinyin. Pinyin je štandardný systém romanizovaného pravopisu pre transliteráciu čínštiny</system:String> <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--> <!--Setting Plugin-->
<system:String x:Key="plugin">Plugin</system:String> <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="newActionKeyword">Nová akcia skratky:</system:String>
<system:String x:Key="currentPriority">Aktuálna priorita:</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="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="pluginDirectory">Priečinok s pluginmi</system:String>
<system:String x:Key="author">Autor</system:String> <system:String x:Key="author">Autor</system:String>
<system:String x:Key="plugin_init_time">Príprava:</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="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="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="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="delete">Odstrániť</system:String>
<system:String x:Key="edit">Upraviť</system:String> <system:String x:Key="edit">Upraviť</system:String>
<system:String x:Key="add">Pridať</system:String> <system:String x:Key="add">Pridať</system:String>

View file

@ -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="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:flowlauncher="clr-namespace:Flow.Launcher" xmlns:flowlauncher="clr-namespace:Flow.Launcher"
@ -60,7 +60,7 @@
<KeyBinding Key="D8" Modifiers="{Binding OpenResultCommandModifiers}" Command="{Binding OpenResultCommand}" CommandParameter="7"></KeyBinding> <KeyBinding Key="D8" Modifiers="{Binding OpenResultCommandModifiers}" Command="{Binding OpenResultCommand}" CommandParameter="7"></KeyBinding>
<KeyBinding Key="D9" Modifiers="{Binding OpenResultCommandModifiers}" Command="{Binding OpenResultCommand}" CommandParameter="8"></KeyBinding> <KeyBinding Key="D9" Modifiers="{Binding OpenResultCommandModifiers}" Command="{Binding OpenResultCommand}" CommandParameter="8"></KeyBinding>
</Window.InputBindings> </Window.InputBindings>
<Grid Width="750"> <Grid>
<Border Style="{DynamicResource WindowBorderStyle}" MouseDown="OnMouseDown" CornerRadius="5" > <Border Style="{DynamicResource WindowBorderStyle}" MouseDown="OnMouseDown" CornerRadius="5" >
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<Grid> <Grid>
@ -79,7 +79,6 @@
Style="{DynamicResource QueryBoxStyle}" Style="{DynamicResource QueryBoxStyle}"
Text="{Binding QueryText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Text="{Binding QueryText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
PreviewDragOver="OnPreviewDragOver" PreviewDragOver="OnPreviewDragOver"
TextChanged="OnTextChanged"
AllowDrop="True" AllowDrop="True"
Visibility="Visible" Visibility="Visible"
Background="Transparent" Background="Transparent"
@ -94,7 +93,7 @@
</ContextMenu> </ContextMenu>
</TextBox.ContextMenu> </TextBox.ContextMenu>
</TextBox> </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"/> Background="Transparent"/>
</Grid> </Grid>
<Line x:Name="ProgressBar" HorizontalAlignment="Right" <Line x:Name="ProgressBar" HorizontalAlignment="Right"

View file

@ -1,5 +1,6 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media.Animation; using System.Windows.Media.Animation;
@ -10,6 +11,7 @@ using Flow.Launcher.Core.Resource;
using Flow.Launcher.Helper; using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.ViewModel; using Flow.Launcher.ViewModel;
using Application = System.Windows.Application;
using Screen = System.Windows.Forms.Screen; using Screen = System.Windows.Forms.Screen;
using ContextMenuStrip = System.Windows.Forms.ContextMenuStrip; using ContextMenuStrip = System.Windows.Forms.ContextMenuStrip;
using DataFormats = System.Windows.DataFormats; using DataFormats = System.Windows.DataFormats;
@ -22,7 +24,6 @@ namespace Flow.Launcher
{ {
public partial class MainWindow public partial class MainWindow
{ {
#region Private Fields #region Private Fields
private readonly Storyboard _progressBarStoryboard = new Storyboard(); private readonly Storyboard _progressBarStoryboard = new Storyboard();
@ -40,20 +41,23 @@ namespace Flow.Launcher
_settings = settings; _settings = settings;
InitializeComponent(); InitializeComponent();
} }
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
} }
private void OnClosing(object sender, CancelEventArgs e) private async void OnClosing(object sender, CancelEventArgs e)
{ {
_notifyIcon.Visible = false; _notifyIcon.Visible = false;
_viewModel.Save(); _viewModel.Save();
e.Cancel = true;
await PluginManager.DisposePluginsAsync();
Application.Current.Shutdown();
} }
private void OnInitialized(object sender, EventArgs e) private void OnInitialized(object sender, EventArgs e)
{ {
} }
private void OnLoaded(object sender, RoutedEventArgs _) private void OnLoaded(object sender, RoutedEventArgs _)
@ -61,8 +65,6 @@ namespace Flow.Launcher
// show notify icon when flowlauncher is hidden // show notify icon when flowlauncher is hidden
InitializeNotifyIcon(); InitializeNotifyIcon();
// todo is there a way to set blur only once?
ThemeManager.Instance.SetBlurForWindow();
WindowsInteropHelper.DisableControlBox(this); WindowsInteropHelper.DisableControlBox(this);
InitProgressbarAnimation(); InitProgressbarAnimation();
InitializePosition(); InitializePosition();
@ -72,47 +74,63 @@ namespace Flow.Launcher
_viewModel.PropertyChanged += (o, e) => _viewModel.PropertyChanged += (o, e) =>
{ {
if (e.PropertyName == nameof(MainViewModel.MainWindowVisibility)) switch (e.PropertyName)
{ {
if (_viewModel.MainWindowVisibility == Visibility.Visible) case nameof(MainViewModel.MainWindowVisibility):
{
Activate();
QueryTextBox.Focus();
UpdatePosition();
_settings.ActivateTimes++;
if (!_viewModel.LastQuerySelected)
{ {
QueryTextBox.SelectAll(); if (_viewModel.MainWindowVisibility == Visibility.Visible)
_viewModel.LastQuerySelected = true; {
} Activate();
QueryTextBox.Focus();
UpdatePosition();
_settings.ActivateTimes++;
if (!_viewModel.LastQuerySelected)
{
QueryTextBox.SelectAll();
_viewModel.LastQuerySelected = true;
}
if (_viewModel.ProgressBarVisibility == Visibility.Visible && isProgressBarStoryboardPaused) if (_viewModel.ProgressBarVisibility == Visibility.Visible && isProgressBarStoryboardPaused)
{ {
_progressBarStoryboard.Resume(); _progressBarStoryboard.Begin(ProgressBar, true);
isProgressBarStoryboardPaused = false; isProgressBarStoryboardPaused = false;
}
}
else if (!isProgressBarStoryboardPaused)
{
_progressBarStoryboard.Stop(ProgressBar);
isProgressBarStoryboardPaused = true;
}
break;
} }
} case nameof(MainViewModel.ProgressBarVisibility):
else if (!isProgressBarStoryboardPaused)
{
_progressBarStoryboard.Pause();
isProgressBarStoryboardPaused = true;
}
}
else if (e.PropertyName == nameof(MainViewModel.ProgressBarVisibility))
{
Dispatcher.Invoke(() =>
{
if (_viewModel.ProgressBarVisibility == Visibility.Hidden && !isProgressBarStoryboardPaused)
{ {
_progressBarStoryboard.Pause(); Dispatcher.Invoke(async () =>
isProgressBarStoryboardPaused = true; {
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(); MoveQueryTextToEnd();
isProgressBarStoryboardPaused = false; _viewModel.QueryTextCursorMovedToEnd = false;
} }
}, System.Windows.Threading.DispatcherPriority.Render); break;
} }
}; };
_settings.PropertyChanged += (o, e) => _settings.PropertyChanged += (o, e) =>
@ -189,14 +207,15 @@ namespace Flow.Launcher
private void InitProgressbarAnimation() private void InitProgressbarAnimation()
{ {
var da = new DoubleAnimation(ProgressBar.X2, ActualWidth + 100, new Duration(new TimeSpan(0, 0, 0, 0, 1600))); var da = new DoubleAnimation(ProgressBar.X2, ActualWidth + 100,
new Duration(new TimeSpan(0, 0, 0, 0, 1600)));
var da1 = new DoubleAnimation(ProgressBar.X1, ActualWidth, new Duration(new TimeSpan(0, 0, 0, 0, 1600))); var da1 = new DoubleAnimation(ProgressBar.X1, ActualWidth, new Duration(new TimeSpan(0, 0, 0, 0, 1600)));
Storyboard.SetTargetProperty(da, new PropertyPath("(Line.X2)")); Storyboard.SetTargetProperty(da, new PropertyPath("(Line.X2)"));
Storyboard.SetTargetProperty(da1, new PropertyPath("(Line.X1)")); Storyboard.SetTargetProperty(da1, new PropertyPath("(Line.X1)"));
_progressBarStoryboard.Children.Add(da); _progressBarStoryboard.Children.Add(da);
_progressBarStoryboard.Children.Add(da1); _progressBarStoryboard.Children.Add(da1);
_progressBarStoryboard.RepeatBehavior = RepeatBehavior.Forever; _progressBarStoryboard.RepeatBehavior = RepeatBehavior.Forever;
ProgressBar.BeginStoryboard(_progressBarStoryboard);
_viewModel.ProgressBarVisibility = Visibility.Hidden; _viewModel.ProgressBarVisibility = Visibility.Hidden;
isProgressBarStoryboardPaused = true; isProgressBarStoryboardPaused = true;
} }
@ -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;
{
QueryTextBox.CaretIndex = QueryTextBox.Text.Length;
_viewModel.QueryTextCursorMovedToEnd = false;
}
} }
} }
} }

View file

@ -42,7 +42,7 @@
<ColumnDefinition Width="0" /> <ColumnDefinition Width="0" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Image x:Name="ImageIcon" Width="32" Height="32" HorizontalAlignment="Left" <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 Margin="5 0 5 0" Grid.Column="1" HorizontalAlignment="Stretch">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition /> <RowDefinition />

View file

@ -18,6 +18,7 @@
Height="600" Width="900" Height="600" Width="900"
MinWidth="850" MinWidth="850"
MinHeight="500" MinHeight="500"
Loaded="OnLoaded"
Closed="OnClosed" Closed="OnClosed"
d:DataContext="{d:DesignInstance vm:SettingWindowViewModel}"> d:DataContext="{d:DesignInstance vm:SettingWindowViewModel}">
<Window.InputBindings> <Window.InputBindings>
@ -36,8 +37,8 @@
<TabControl Height="auto" SelectedIndex="0"> <TabControl Height="auto" SelectedIndex="0">
<TabItem Header="{DynamicResource general}"> <TabItem Header="{DynamicResource general}">
<ScrollViewer ui:ScrollViewerHelper.AutoHideScrollBars="True" Margin="60,30,0,30"> <ScrollViewer ui:ScrollViewerHelper.AutoHideScrollBars="{Binding AutoHideScrollBar, Mode=OneWay}" Margin="60,0,0,0">
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical" Margin="0,30,0,30">
<ui:ToggleSwitch Margin="10" IsOn="{Binding PortableMode}"> <ui:ToggleSwitch Margin="10" IsOn="{Binding PortableMode}">
<TextBlock Text="{DynamicResource portableMode}" /> <TextBlock Text="{DynamicResource portableMode}" />
</ui:ToggleSwitch> </ui:ToggleSwitch>
@ -63,9 +64,14 @@
<ui:ToggleSwitch Margin="10" IsOn="{Binding AutoUpdates}"> <ui:ToggleSwitch Margin="10" IsOn="{Binding AutoUpdates}">
<TextBlock Text="{DynamicResource autoUpdates}" /> <TextBlock Text="{DynamicResource autoUpdates}" />
</ui:ToggleSwitch> </ui:ToggleSwitch>
<CheckBox Margin="10" IsChecked="{Binding ShouldUsePinyin}" ToolTip="{DynamicResource ShouldUsePinyinToolTip}"> <CheckBox Margin="10" IsChecked="{Binding ShouldUsePinyin}"
ToolTip="{DynamicResource ShouldUsePinyinToolTip}">
<TextBlock Text="{DynamicResource ShouldUsePinyin}" /> <TextBlock Text="{DynamicResource ShouldUsePinyin}" />
</CheckBox> </CheckBox>
<ui:ToggleSwitch Margin="10" IsOn="{Binding AutoHideScrollBar, Mode=TwoWay}"
ToolTip="{DynamicResource autoHideScrollBarToolTip}">
<TextBlock Text="{DynamicResource autoHideScrollBar}" />
</ui:ToggleSwitch>
<StackPanel Margin="10" Orientation="Horizontal"> <StackPanel Margin="10" Orientation="Horizontal">
<TextBlock Text="{DynamicResource querySearchPrecision}" FontSize="14" /> <TextBlock Text="{DynamicResource querySearchPrecision}" FontSize="14" />
<ComboBox Margin="10 0 0 0" MaxWidth="200" <ComboBox Margin="10 0 0 0" MaxWidth="200"
@ -112,7 +118,7 @@
</TextBlock> </TextBlock>
<ListBox SelectedIndex="0" SelectedItem="{Binding SelectedPlugin}" <ListBox SelectedIndex="0" SelectedItem="{Binding SelectedPlugin}"
ItemsSource="{Binding PluginViewModels}" 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> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<StackPanel Orientation="Horizontal" Margin="3"> <StackPanel Orientation="Horizontal" Margin="3">
@ -174,7 +180,7 @@
Text="{Binding PluginPair.Metadata.Description}" Text="{Binding PluginPair.Metadata.Description}"
Grid.Row="1" Opacity="0.5" Grid.Column="2" /> Grid.Row="1" Opacity="0.5" Grid.Column="2" />
<DockPanel Grid.ColumnSpan="2" Grid.Row="2" Margin="0 10 0 8" HorizontalAlignment="Right"> <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}" <TextBlock Text="{Binding Priority}"
ToolTip="Change Plugin Results Priority" ToolTip="Change Plugin Results Priority"
Margin="5 0 0 0" Cursor="Hand" Foreground="Blue" Margin="5 0 0 0" Cursor="Hand" Foreground="Blue"
@ -216,8 +222,10 @@
<Run Text="{DynamicResource browserMoreThemes}" /> <Run Text="{DynamicResource browserMoreThemes}" />
</Hyperlink> </Hyperlink>
</TextBlock> </TextBlock>
<Button DockPanel.Dock="Top" Margin="0,10,0,0" Width="180" HorizontalAlignment="Center" Click="OpenPluginFolder">Open Theme Folder</Button> <Button DockPanel.Dock="Top" Margin="0,10,0,0" Width="180" HorizontalAlignment="Center"
<ListBox DockPanel.Dock="Top" SelectedItem="{Binding SelectedTheme}" ItemsSource="{Binding Themes}" 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" /> Margin="10, 0, 10, 10" Width="180" Height="394" />
</DockPanel> </DockPanel>
@ -235,7 +243,7 @@
<RowDefinition Height="30"/> <RowDefinition Height="30"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{DynamicResource queryWindowShadowEffect}" Margin="0 30 0 0" FontSize="14" /> <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}" <TextBlock Grid.Row="1" Text="{DynamicResource shadowEffectCPUUsage}"
Width="280" Width="280"
FontSize="10" HorizontalAlignment="Left"/> FontSize="10" HorizontalAlignment="Left"/>
@ -357,7 +365,7 @@
</DataTemplate> </DataTemplate>
</GridViewColumn.CellTemplate> </GridViewColumn.CellTemplate>
</GridViewColumn> </GridViewColumn>
<GridViewColumn Header="{DynamicResource actionKeywords}" Width="546"> <GridViewColumn Header="{DynamicResource customQuery}" Width="546">
<GridViewColumn.CellTemplate> <GridViewColumn.CellTemplate>
<DataTemplate DataType="userSettings:CustomPluginHotkey"> <DataTemplate DataType="userSettings:CustomPluginHotkey">
<TextBlock Text="{Binding ActionKeyword}" /> <TextBlock Text="{Binding ActionKeyword}" />

View file

@ -2,18 +2,17 @@ using System;
using System.IO; using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Navigation; using System.Windows.Navigation;
using Microsoft.Win32; using Microsoft.Win32;
using NHotkey;
using NHotkey.Wpf;
using Flow.Launcher.Core.Plugin; using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Core.Resource; using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.ViewModel; using Flow.Launcher.ViewModel;
using Flow.Launcher.Helper;
namespace Flow.Launcher namespace Flow.Launcher
{ {
@ -35,6 +34,14 @@ namespace Flow.Launcher
} }
#region General #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) private void OnAutoStartupChecked(object sender, RoutedEventArgs e)
{ {
@ -118,45 +125,13 @@ namespace Flow.Launcher
{ {
if (HotkeyControl.CurrentHotkeyAvailable) if (HotkeyControl.CurrentHotkeyAvailable)
{ {
SetHotkey(HotkeyControl.CurrentHotkey, (o, args) =>
{ HotKeyMapper.SetHotkey(HotkeyControl.CurrentHotkey, HotKeyMapper.OnHotkey);
if (!Application.Current.MainWindow.IsVisible) HotKeyMapper.RemoveHotkey(settings.Hotkey);
{
Application.Current.MainWindow.Visibility = Visibility.Visible;
}
else
{
Application.Current.MainWindow.Visibility = Visibility.Hidden;
}
});
RemoveHotkey(settings.Hotkey);
settings.Hotkey = HotkeyControl.CurrentHotkey.ToString(); 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) private void OnDeleteCustomHotkeyClick(object sender, RoutedEventArgs e)
{ {
var item = viewModel.SelectedCustomPluginHotkey; var item = viewModel.SelectedCustomPluginHotkey;
@ -174,7 +149,7 @@ namespace Flow.Launcher
MessageBoxButton.YesNo) == MessageBoxResult.Yes) MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{ {
settings.CustomPluginHotkeys.Remove(item); settings.CustomPluginHotkeys.Remove(item);
RemoveHotkey(item.Hotkey); HotKeyMapper.RemoveHotkey(item.Hotkey);
} }
} }

View file

@ -28,6 +28,11 @@ namespace Flow.Launcher.Storage
private static int GenerateStaticHashCode(string s, int start = HASH_INITIAL) private static int GenerateStaticHashCode(string s, int start = HASH_INITIAL)
{ {
if (s == null)
{
return start;
}
unchecked unchecked
{ {
// skip the empty space // skip the empty space
@ -101,4 +106,4 @@ namespace Flow.Launcher.Storage
return selectedCount; return selectedCount;
} }
} }
} }

View file

@ -46,6 +46,8 @@
<Setter Property="Padding" Value="8 10 8 8" /> <Setter Property="Padding" Value="8 10 8 8" />
</Style> </Style>
<Style x:Key="BaseWindowStyle" TargetType="{x:Type Window}"> <Style x:Key="BaseWindowStyle" TargetType="{x:Type Window}">
<Setter Property="Width" Value="750" />
<Setter Property="RenderOptions.ClearTypeHint" Value="Enabled"/>
</Style> </Style>
<Style x:Key="BasePendingLineStyle" TargetType="{x:Type Line}"> <Style x:Key="BasePendingLineStyle" TargetType="{x:Type Line}">

View file

@ -25,7 +25,6 @@
</Style> </Style>
<Style x:Key="WindowStyle" BasedOn="{StaticResource BaseWindowStyle}" TargetType="{x:Type Window}"> <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 Property="Background">
<Setter.Value> <Setter.Value>
<SolidColorBrush Color="Black" Opacity="0.6"/> <SolidColorBrush Color="Black" Opacity="0.6"/>

View file

@ -25,7 +25,6 @@
</Style> </Style>
<Style x:Key="WindowStyle" BasedOn="{StaticResource BaseWindowStyle}" TargetType="{x:Type Window}"> <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 Property="Background">
<Setter.Value> <Setter.Value>
<SolidColorBrush Color="Black" Opacity="0.3"/> <SolidColorBrush Color="Black" Opacity="0.3"/>

View file

@ -23,7 +23,6 @@
</Style> </Style>
<Style x:Key="WindowStyle" BasedOn="{StaticResource BaseWindowStyle}" TargetType="{x:Type Window}"> <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 Property="Background">
<Setter.Value> <Setter.Value>
<SolidColorBrush Color="White" Opacity="0.5"/> <SolidColorBrush Color="White" Opacity="0.5"/>

View file

@ -6,8 +6,6 @@ using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow; using System.Threading.Tasks.Dataflow;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using NHotkey;
using NHotkey.Wpf;
using Flow.Launcher.Core.Plugin; using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Core.Resource; using Flow.Launcher.Core.Resource;
using Flow.Launcher.Helper; using Flow.Launcher.Helper;
@ -19,7 +17,10 @@ using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Storage; using Flow.Launcher.Storage;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
using Microsoft.VisualStudio.Threading;
using System.Threading.Channels; using System.Threading.Channels;
using ISavable = Flow.Launcher.Plugin.ISavable;
using System.Windows.Threading;
namespace Flow.Launcher.ViewModel namespace Flow.Launcher.ViewModel
{ {
@ -36,7 +37,7 @@ namespace Flow.Launcher.ViewModel
private readonly FlowLauncherJsonStorage<History> _historyItemsStorage; private readonly FlowLauncherJsonStorage<History> _historyItemsStorage;
private readonly FlowLauncherJsonStorage<UserSelectedRecord> _userSelectedRecordStorage; private readonly FlowLauncherJsonStorage<UserSelectedRecord> _userSelectedRecordStorage;
private readonly FlowLauncherJsonStorage<TopMostRecord> _topMostRecordStorage; private readonly FlowLauncherJsonStorage<TopMostRecord> _topMostRecordStorage;
private readonly Settings _settings; internal readonly Settings _settings;
private readonly History _history; private readonly History _history;
private readonly UserSelectedRecord _userSelectedRecord; private readonly UserSelectedRecord _userSelectedRecord;
private readonly TopMostRecord _topMostRecord; private readonly TopMostRecord _topMostRecord;
@ -77,8 +78,6 @@ namespace Flow.Launcher.ViewModel
RegisterViewUpdate(); RegisterViewUpdate();
RegisterResultsUpdatedEvent(); RegisterResultsUpdatedEvent();
SetHotkey(_settings.Hotkey, OnHotkey);
SetCustomPluginHotkey();
SetOpenResultModifiers(); SetOpenResultModifiers();
} }
@ -117,7 +116,7 @@ namespace Flow.Launcher.ViewModel
throw t.Exception; throw t.Exception;
#else #else
Log.Error($"Error happen in task dealing with viewupdate for results. {t.Exception}"); Log.Error($"Error happen in task dealing with viewupdate for results. {t.Exception}");
_resultsViewUpdateTask = _resultsViewUpdateTask =
Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted); Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted);
#endif #endif
} }
@ -130,13 +129,17 @@ namespace Flow.Launcher.ViewModel
var plugin = (IResultUpdated)pair.Plugin; var plugin = (IResultUpdated)pair.Plugin;
plugin.ResultsUpdated += (s, e) => 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); return;
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(e.Results, pair.Metadata, e.Query, _updateToken))) }
{
Log.Error("MainViewModel", "Unable to add item to Result Update Queue"); 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()) 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 else
{ {
@ -233,21 +239,24 @@ namespace Flow.Launcher.ViewModel
ReloadPluginDataCommand = new RelayCommand(_ => ReloadPluginDataCommand = new RelayCommand(_ =>
{ {
var msg = new Msg { Owner = Application.Current.MainWindow }; var msg = new Msg
{
Owner = Application.Current.MainWindow
};
MainWindowVisibility = Visibility.Collapsed; MainWindowVisibility = Visibility.Collapsed;
PluginManager PluginManager
.ReloadData() .ReloadData()
.ContinueWith(_ => .ContinueWith(_ =>
Application.Current.Dispatcher.Invoke(() => Application.Current.Dispatcher.Invoke(() =>
{ {
msg.Show( msg.Show(
InternationalizationManager.Instance.GetTranslation("success"), InternationalizationManager.Instance.GetTranslation("success"),
InternationalizationManager.Instance.GetTranslation("completedSuccessfully"), InternationalizationManager.Instance.GetTranslation("completedSuccessfully"),
""); "");
})) }))
.ConfigureAwait(false); .ConfigureAwait(false);
}); });
} }
@ -278,8 +287,8 @@ namespace Flow.Launcher.ViewModel
/// <param name="queryText"></param> /// <param name="queryText"></param>
public void ChangeQueryText(string queryText) public void ChangeQueryText(string queryText)
{ {
QueryTextCursorMovedToEnd = true;
QueryText = queryText; QueryText = queryText;
QueryTextCursorMovedToEnd = true;
} }
public bool LastQuerySelected { get; set; } public bool LastQuerySelected { get; set; }
@ -418,7 +427,10 @@ namespace Flow.Launcher.ViewModel
Title = string.Format(title, h.Query), Title = string.Format(title, h.Query),
SubTitle = string.Format(time, h.ExecutedDateTime), SubTitle = string.Format(time, h.ExecutedDateTime),
IcoPath = "Images\\history.png", IcoPath = "Images\\history.png",
OriginQuery = new Query { RawQuery = h.Query }, OriginQuery = new Query
{
RawQuery = h.Query
},
Action = _ => Action = _ =>
{ {
SelectedResults = Results; 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(); _updateSource?.Cancel();
@ -465,6 +479,12 @@ namespace Flow.Launcher.ViewModel
ProgressBarVisibility = Visibility.Hidden; ProgressBarVisibility = Visibility.Hidden;
_isQueryRunning = true; _isQueryRunning = true;
// Switch to ThreadPool thread
await TaskScheduler.Default;
if (currentCancellationToken.IsCancellationRequested)
return;
var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.NonGlobalPlugins); var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.NonGlobalPlugins);
// handle the exclusiveness of plugin using action keyword // handle the exclusiveness of plugin using action keyword
@ -474,104 +494,80 @@ namespace Flow.Launcher.ViewModel
var plugins = PluginManager.ValidPluginsForQuery(query); 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) ProgressBarVisibility = Visibility.Visible;
{ }
// Wait 45 millisecond for query change in global query }, currentCancellationToken, TaskContinuationOptions.NotOnCanceled, TaskScheduler.Default);
// 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(_ => // plugins is ICollection, meaning LINQ will get the Count and preallocate Array
{
// 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);
Task[] tasks = new Task[plugins.Count]; var tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch
try {
{ false => QueryTask(plugin),
for (var i = 0; i < plugins.Count; i++) true => Task.CompletedTask
{ }).ToArray();
if (!plugins[i].Metadata.Disabled)
{
tasks[i] = QueryTask(plugins[i]);
}
else
{
tasks[i] = Task.CompletedTask; // Avoid Null
}
}
// 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) try
return; {
// 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 if (currentCancellationToken.IsCancellationRequested)
// until the end of all querying return;
_isQueryRunning = false;
if (!currentCancellationToken.IsCancellationRequested)
{
// update to hidden if this is still the current query
ProgressBarVisibility = Visibility.Hidden;
}
// Local function // this should happen once after all queries are done so progress bar should continue
async Task QueryTask(PluginPair plugin) // until the end of all querying
{ _isQueryRunning = false;
// Since it is wrapped within a Task.Run, the synchronous context is null if (!currentCancellationToken.IsCancellationRequested)
// Task.Yield will force it to run in ThreadPool {
await Task.Yield(); // update to hidden if this is still the current query
ProgressBarVisibility = Visibility.Hidden;
}
var results = await PluginManager.QueryForPlugin(plugin, query, currentCancellationToken); // Local function
if (currentCancellationToken.IsCancellationRequested || results == null) return; 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))) IReadOnlyList<Result> results = await PluginManager.QueryForPluginAsync(plugin, query, currentCancellationToken);
{
Log.Error("MainViewModel", "Unable to add item to Result Update Queue"); currentCancellationToken.ThrowIfCancellationRequested();
};
} results ??= _emptyResult;
}, currentCancellationToken)
.ContinueWith(t => Log.Exception("|MainViewModel|Plugins Query Exceptions", t.Exception), if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, plugin.Metadata, query, currentCancellationToken)))
TaskContinuationOptions.OnlyOnFaulted); {
Log.Error("MainViewModel", "Unable to add item to Result Update Queue");
}
}
} }
private void RemoveOldQueryResults(Query query) private void RemoveOldQueryResults(Query query)
{ {
string lastKeyword = _lastQuery.ActionKeyword; if (_lastQuery.ActionKeyword != query.ActionKeyword)
string keyword = query.ActionKeyword;
if (string.IsNullOrEmpty(lastKeyword))
{ {
if (!string.IsNullOrEmpty(keyword)) Results.Clear();
{
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);
}
} }
} }
@ -656,96 +652,12 @@ namespace Flow.Launcher.ViewModel
#region Hotkey #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() private void SetOpenResultModifiers()
{ {
OpenResultCommandModifiers = _settings.OpenResultModifiers ?? DefaultOpenResultModifiers; OpenResultCommandModifiers = _settings.OpenResultModifiers ?? DefaultOpenResultModifiers;
} }
private void OnHotkey(object sender, HotkeyEventArgs e) internal void ToggleFlowLauncher()
{
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()
{ {
if (MainWindowVisibility != Visibility.Visible) if (MainWindowVisibility != Visibility.Visible)
{ {
@ -794,7 +706,6 @@ namespace Flow.Launcher.ViewModel
} }
#endif #endif
foreach (var metaResults in resultsForUpdates) foreach (var metaResults in resultsForUpdates)
{ {
foreach (var result in metaResults.Results) foreach (var result in metaResults.Results)

View file

@ -1,7 +1,6 @@
using System.Windows; using System.Windows;
using System.Windows.Media; using System.Windows.Media;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Infrastructure.Image;
using Flow.Launcher.Core.Plugin; using Flow.Launcher.Core.Plugin;
@ -11,8 +10,6 @@ namespace Flow.Launcher.ViewModel
{ {
public PluginPair PluginPair { get; set; } public PluginPair PluginPair { get; set; }
private readonly Internationalization _translator = InternationalizationManager.Instance;
public ImageSource Image => ImageLoader.Load(PluginPair.Metadata.IcoPath); public ImageSource Image => ImageLoader.Load(PluginPair.Metadata.IcoPath);
public bool PluginState public bool PluginState
{ {
@ -22,7 +19,7 @@ namespace Flow.Launcher.ViewModel
PluginPair.Metadata.Disabled = !value; 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 InitilizaTime => PluginPair.Metadata.InitTime.ToString() + "ms";
public string QueryTime => PluginPair.Metadata.AvgQueryTime + "ms"; public string QueryTime => PluginPair.Metadata.AvgQueryTime + "ms";
public string ActionKeywordsText => string.Join(Query.ActionKeywordSeperater, PluginPair.Metadata.ActionKeywords); public string ActionKeywordsText => string.Join(Query.ActionKeywordSeperater, PluginPair.Metadata.ActionKeywords);

View file

@ -11,62 +11,12 @@ namespace Flow.Launcher.ViewModel
{ {
public class ResultViewModel : BaseModel 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) public ResultViewModel(Result result, Settings settings)
{ {
if (result != null) if (result != null)
{ {
Result = result; Result = result;
}
Image = new LazyAsync<ImageSource>(
SetImage,
ImageLoader.DefaultImage,
() =>
{
OnPropertyChanged(nameof(Image));
});
}
Settings = settings; Settings = settings;
} }
@ -85,44 +35,55 @@ namespace Flow.Launcher.ViewModel
? Result.SubTitle ? Result.SubTitle
: Result.SubTitleToolTip; : 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; var imagePath = Result.IcoPath;
if (string.IsNullOrEmpty(imagePath) && Result.Icon != null) if (string.IsNullOrEmpty(imagePath) && Result.Icon != null)
{ {
try try
{ {
return Result.Icon(); image = Result.Icon();
return;
} }
catch (Exception e) catch (Exception e)
{ {
Log.Exception($"|ResultViewModel.Image|IcoPath is empty and exception when calling Icon() for result <{Result.Title}> of plugin <{Result.PluginDirectory}>", 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)) if (ImageLoader.CacheContainImage(imagePath))
{
// will get here either when icoPath has value\icon delegate is null\when had exception in delegate // 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 Result Result { get; }
public override bool Equals(object obj) public override bool Equals(object obj)
{ {
var r = obj as ResultViewModel; return obj is ResultViewModel r && Result.Equals(r.Result);
if (r != null)
{
return Result.Equals(r.Result);
}
else
{
return false;
}
} }
public override int GetHashCode() public override int GetHashCode()

View file

@ -8,7 +8,7 @@ namespace Flow.Launcher.ViewModel
{ {
public struct ResultsForUpdate public struct ResultsForUpdate
{ {
public List<Result> Results { get; } public IReadOnlyList<Result> Results { get; }
public PluginMetadata Metadata { get; } public PluginMetadata Metadata { get; }
public string ID { get; } public string ID { get; }
@ -16,7 +16,7 @@ namespace Flow.Launcher.ViewModel
public Query Query { get; } public Query Query { get; }
public CancellationToken Token { 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; Results = results;
Metadata = metadata; Metadata = metadata;

View file

@ -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 // This is only required to set at startup. When portable mode enabled/disabled a restart is always required
private bool _portableMode = DataLocation.PortableDataLocationInUse(); private bool _portableMode = DataLocation.PortableDataLocationInUse();
public bool PortableMode public bool PortableMode
@ -138,11 +144,11 @@ namespace Flow.Launcher.ViewModel
public bool ShouldUsePinyin public bool ShouldUsePinyin
{ {
get get
{ {
return Settings.ShouldUsePinyin; return Settings.ShouldUsePinyin;
} }
set set
{ {
Settings.ShouldUsePinyin = value; Settings.ShouldUsePinyin = value;
} }
@ -181,7 +187,7 @@ namespace Flow.Launcher.ViewModel
} }
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_updater.GitHubRepository); HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_updater.GitHubRepository);
if (string.IsNullOrEmpty(proxyUserName) || string.IsNullOrEmpty(Settings.Proxy.Password)) if (string.IsNullOrEmpty(proxyUserName) || string.IsNullOrEmpty(Settings.Proxy.Password))
{ {
request.Proxy = new WebProxy(proxyServer, Settings.Proxy.Port); request.Proxy = new WebProxy(proxyServer, Settings.Proxy.Port);
@ -225,7 +231,7 @@ namespace Flow.Launcher.ViewModel
var metadatas = PluginManager.AllPlugins var metadatas = PluginManager.AllPlugins
.OrderBy(x => x.Metadata.Disabled) .OrderBy(x => x.Metadata.Disabled)
.ThenBy(y => y.Metadata.Name) .ThenBy(y => y.Metadata.Name)
.Select(p => new PluginViewModel { PluginPair = p}) .Select(p => new PluginViewModel { PluginPair = p })
.ToList(); .ToList();
return metadatas; return metadatas;
} }
@ -265,6 +271,9 @@ namespace Flow.Launcher.ViewModel
{ {
Settings.Theme = value; Settings.Theme = value;
ThemeManager.Instance.ChangeTheme(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; } get { return Settings.UseDropShadowEffect; }
set set
{ {
if (ThemeManager.Instance.BlurEnabled && value)
{
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("shadowEffectNotAllowed"));
return;
}
if (value) if (value)
{ {
ThemeManager.Instance.AddDropShadowEffectToCurrentTheme(); ThemeManager.Instance.AddDropShadowEffectToCurrentTheme();
} }
else else
{ {
ThemeManager.Instance.RemoveDropShadowEffectToCurrentTheme(); ThemeManager.Instance.RemoveDropShadowEffectFromCurrentTheme();
} }
Settings.UseDropShadowEffect = value; Settings.UseDropShadowEffect = value;
@ -453,7 +468,7 @@ namespace Flow.Launcher.ViewModel
#region about #region about
public string Website => Constant.Website; 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 string Documentation => Constant.Documentation;
public static string Version => Constant.Version; public static string Version => Constant.Version;
public string ActivatedTimes => string.Format(_translater.GetTranslation("about_activate_times"), Settings.ActivateTimes); public string ActivatedTimes => string.Format(_translater.GetTranslation("about_activate_times"), Settings.ActivateTimes);

View file

@ -10,6 +10,10 @@
<system:String x:Key="plugin_explorer_deletefilefoldersuccess">Deletion successful</system:String> <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_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_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--> <!--Controls-->
<system:String x:Key="plugin_explorer_delete">Delete</system:String> <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_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_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_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_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--> <!--Plugin Infos-->
<system:String x:Key="plugin_explorer_plugin_name">Explorer</system:String> <system:String x:Key="plugin_explorer_plugin_name">Explorer</system:String>

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

View file

@ -20,6 +20,7 @@
<system:String x:Key="plugin_explorer_indexsearchexcludedpaths_header">索引搜索排除的路径</system:String> <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_manageindexoptions">索引选项</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_search">搜索激活:</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> <system:String x:Key="plugin_explorer_actionkeywordview_filecontentsearch">文件内容搜索:</system:String>
<!--Plugin Infos--> <!--Plugin Infos-->

View file

@ -28,7 +28,7 @@ namespace Flow.Launcher.Plugin.Explorer
return new ExplorerSettings(viewModel); return new ExplorerSettings(viewModel);
} }
public async Task InitAsync(PluginInitContext context) public Task InitAsync(PluginInitContext context)
{ {
Context = context; Context = context;
@ -46,7 +46,9 @@ namespace Flow.Launcher.Plugin.Explorer
contextMenu = new ContextMenu(Context, Settings, viewModel); contextMenu = new ContextMenu(Context, Settings, viewModel);
searchManager = new SearchManager(Settings, Context); searchManager = new SearchManager(Settings, Context);
ResultManager.Init(Context); ResultManager.Init(Context, Settings);
return Task.CompletedTask;
} }
public List<Result> LoadContextMenus(Result selectedResult) public List<Result> LoadContextMenus(Result selectedResult)

View file

@ -21,7 +21,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
RecurseSubdirectories = true RecurseSubdirectories = true
}, query, search, criteria, token); }, 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) public static string ConstructSearchCriteria(string search)
@ -57,7 +58,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
{ {
var directoryInfo = new System.IO.DirectoryInfo(path); 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) if (fileSystemInfo is System.IO.DirectoryInfo)
{ {
@ -74,17 +76,10 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
} }
catch (Exception e) catch (Exception e)
{ {
if (!(e is ArgumentException)) Log.Exception("Flow.Plugin.Explorer.", nameof(DirectoryInfoSearch), e);
throw e;
results.Add(new Result {Title = e.Message, Score = 501}); results.Add(new Result {Title = e.Message, Score = 501});
return results; 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. // Initial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection.

View file

@ -10,10 +10,10 @@ namespace Flow.Launcher.Plugin.Explorer.Search
{ {
internal static bool IsEnvironmentVariableSearch(string search) internal static bool IsEnvironmentVariableSearch(string search)
{ {
return LoadEnvironmentStringPaths().Count > 0 return search.StartsWith("%")
&& search.StartsWith("%")
&& search != "%%" && search != "%%"
&& !search.Contains("\\"); && !search.Contains("\\") &&
LoadEnvironmentStringPaths().Count > 0;
} }
internal static Dictionary<string, string> LoadEnvironmentStringPaths() internal static Dictionary<string, string> LoadEnvironmentStringPaths()

View file

@ -10,10 +10,12 @@ namespace Flow.Launcher.Plugin.Explorer.Search
public static class ResultManager public static class ResultManager
{ {
private static PluginInitContext Context; 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; 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) 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, TitleHighlightData = StringMatcher.FuzzySearch(query.Search, title).MatchData,
Action = c => Action = c =>
{ {
if (c.SpecialKeyState.CtrlPressed) if (c.SpecialKeyState.CtrlPressed || (!Settings.PathSearchKeywordEnabled && !Settings.SearchActionKeywordEnabled))
{ {
try try
{ {
@ -39,17 +41,25 @@ namespace Flow.Launcher.Plugin.Explorer.Search
return false; 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; string changeTo = path.EndsWith(Constants.DirectorySeperator) ? path : path + Constants.DirectorySeperator;
Context.API.ChangeQuery(string.IsNullOrEmpty(query.ActionKeyword) ? Context.API.ChangeQuery($"{keyword}{changeTo}");
changeTo :
query.ActionKeyword + " " + changeTo);
return false; return false;
}, },
Score = score, Score = score,
TitleToolTip = Constants.ToolTipOpenDirectory, TitleToolTip = Constants.ToolTipOpenDirectory,
SubTitleToolTip = Constants.ToolTipOpenDirectory, SubTitleToolTip = path,
ContextData = new SearchResult { Type = ResultType.Folder, FullPath = path, ShowIndexState = showIndexState, WindowsIndexed = windowsIndexed } 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 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(":\\")) if (retrievedDirectoryPath.EndsWith(":\\"))
{ {
@ -81,7 +94,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
{ {
Title = title, Title = title,
SubTitle = $"Use > to search within {subtitleFolderName}, " + 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, IcoPath = retrievedDirectoryPath,
Score = 500, Score = 500,
Action = c => Action = c =>
@ -91,7 +104,13 @@ namespace Flow.Launcher.Plugin.Explorer.Search
}, },
TitleToolTip = retrievedDirectoryPath, TitleToolTip = retrievedDirectoryPath,
SubTitleToolTip = 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; return true;
}, },
TitleToolTip = Constants.ToolTipOpenContainingFolder, TitleToolTip = Constants.ToolTipOpenContainingFolder,
SubTitleToolTip = Constants.ToolTipOpenContainingFolder, SubTitleToolTip = filePath,
ContextData = new SearchResult { Type = ResultType.File, FullPath = filePath, ShowIndexState = showIndexState, WindowsIndexed = windowsIndexed } ContextData = new SearchResult
{
Type = ResultType.File,
FullPath = filePath,
ShowIndexState = showIndexState,
WindowsIndexed = windowsIndexed
}
}; };
return result; return result;
} }
@ -148,4 +173,4 @@ namespace Flow.Launcher.Plugin.Explorer.Search
Folder, Folder,
File File
} }
} }

View file

@ -12,20 +12,21 @@ namespace Flow.Launcher.Plugin.Explorer.Search
{ {
public class SearchManager 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) public SearchManager(Settings settings, PluginInitContext context)
{ {
this.context = context; Context = context;
this.settings = settings; Settings = settings;
} }
private class PathEqualityComparator : IEqualityComparer<Result> private class PathEqualityComparator : IEqualityComparer<Result>
{ {
private static PathEqualityComparator instance; private static PathEqualityComparator instance;
public static PathEqualityComparator Instance => instance ??= new PathEqualityComparator(); public static PathEqualityComparator Instance => instance ??= new PathEqualityComparator();
public bool Equals(Result x, Result y) public bool Equals(Result x, Result y)
{ {
return x.SubTitle == y.SubTitle; 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) internal async Task<List<Result>> SearchAsync(Query query, CancellationToken token)
{ {
var results = new HashSet<Result>(PathEqualityComparator.Instance);
var querySearch = query.Search; var querySearch = query.Search;
if (IsFileContentSearch(query.ActionKeyword)) if (IsFileContentSearch(query.ActionKeyword))
return await WindowsIndexFileContentSearchAsync(query, querySearch, token).ConfigureAwait(false); 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 // This allows the user to type the assigned action keyword and only see the list of quick folder links
if (string.IsNullOrEmpty(query.Search)) 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) var quickaccessLinks = QuickAccess.AccessLinkListMatched(query, Settings.QuickAccessLinks);
results.UnionWith(quickaccessLinks);
results.UnionWith(quickaccessLinks);
var isEnvironmentVariable = EnvironmentVariables.IsEnvironmentVariableSearch(querySearch); var isEnvironmentVariable = EnvironmentVariables.IsEnvironmentVariableSearch(querySearch);
if (isEnvironmentVariable) 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\ // Query is a location path with a full environment variable, eg. %appdata%\somefolder\
var isEnvironmentVariablePath = querySearch[1..].Contains("%\\"); var isEnvironmentVariablePath = querySearch[1..].Contains("%\\");
if (!querySearch.IsLocationPathString() && !isEnvironmentVariablePath)
{
results.UnionWith(await WindowsIndexFilesAndFoldersSearchAsync(query, querySearch, token).ConfigureAwait(false));
return results.ToList();
}
var locationPath = querySearch; var locationPath = querySearch;
if (isEnvironmentVariablePath) if (isEnvironmentVariablePath)
@ -99,24 +133,26 @@ namespace Flow.Launcher.Plugin.Explorer.Search
return results.ToList(); 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)) if (string.IsNullOrEmpty(querySearchString))
return new List<Result>(); return new List<Result>();
return await IndexSearch.WindowsIndexSearchAsync(querySearchString, return await IndexSearch.WindowsIndexSearchAsync(
queryConstructor.CreateQueryHelper().ConnectionString, querySearchString,
queryConstructor.QueryForFileContentSearch, queryConstructor.CreateQueryHelper,
settings.IndexSearchExcludedSubdirectoryPaths, queryConstructor.QueryForFileContentSearch,
query, Settings.IndexSearchExcludedSubdirectoryPaths,
token).ConfigureAwait(false); query,
token).ConfigureAwait(false);
} }
public bool IsFileContentSearch(string actionKeyword) public bool IsFileContentSearch(string actionKeyword)
{ {
return actionKeyword == settings.FileContentSearchActionKeyword; return actionKeyword == Settings.FileContentSearchActionKeyword;
} }
private List<Result> DirectoryInfoClassSearch(Query query, string querySearch, CancellationToken token) 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); 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, return await IndexSearch.WindowsIndexSearchAsync(
queryConstructor.CreateQueryHelper().ConnectionString, querySearchString,
queryConstructor.QueryForAllFilesAndFolders, queryConstructor.CreateQueryHelper,
settings.IndexSearchExcludedSubdirectoryPaths, queryConstructor.QueryForAllFilesAndFolders,
query, Settings.IndexSearchExcludedSubdirectoryPaths,
token).ConfigureAwait(false); 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, return await IndexSearch.WindowsIndexSearchAsync(
queryConstructor.CreateQueryHelper().ConnectionString, path,
queryConstructor.QueryForTopLevelDirectorySearch, queryConstructor.CreateQueryHelper,
settings.IndexSearchExcludedSubdirectoryPaths, queryConstructor.QueryForTopLevelDirectorySearch,
query, Settings.IndexSearchExcludedSubdirectoryPaths,
token).ConfigureAwait(false); query,
token).ConfigureAwait(false);
} }
private bool UseWindowsIndexForDirectorySearch(string locationPath) private bool UseWindowsIndexForDirectorySearch(string locationPath)
{ {
var pathToDirectory = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath); var pathToDirectory = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath);
if (!settings.UseWindowsIndexForDirectorySearch) if (!Settings.UseWindowsIndexForDirectorySearch)
return false; return false;
if (settings.IndexSearchExcludedSubdirectoryPaths if (Settings.IndexSearchExcludedSubdirectoryPaths
.Any(x => FilesFolders.ReturnPreviousDirectoryIfIncompleteString(pathToDirectory) .Any(x => FilesFolders.ReturnPreviousDirectoryIfIncompleteString(pathToDirectory)
.StartsWith(x.Path, StringComparison.OrdinalIgnoreCase))) .StartsWith(x.Path, StringComparison.OrdinalIgnoreCase)))
return false; return false;
return IndexSearch.PathIsIndexed(pathToDirectory); return IndexSearch.PathIsIndexed(pathToDirectory);
} }
} }
} }

View file

@ -5,9 +5,11 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.OleDb; using System.Data.OleDb;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows;
namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
{ {
@ -17,20 +19,20 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
// Reserved keywords in oleDB // Reserved keywords in oleDB
private const string reservedStringPattern = @"^[`\@\#\^,\&\/\\\$\%_]+$"; 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 results = new List<Result>();
var fileResults = new List<Result>(); var fileResults = new List<Result>();
try try
{ {
using var conn = new OleDbConnection(connectionString); await using var conn = new OleDbConnection(connectionString);
await conn.OpenAsync(token); await conn.OpenAsync(token);
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
using var command = new OleDbCommand(indexQueryString, conn); await using var command = new OleDbCommand(indexQueryString, conn);
// Results return as an OleDbDataReader. // 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(); token.ThrowIfCancellationRequested();
if (dataReaderResults.HasRows) 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 // # is URI syntax for the fragment component, need to be encoded so LocalPath returns complete path
var encodedFragmentPath = dataReaderResults var encodedFragmentPath = dataReaderResults
.GetString(1) .GetString(1)
.Replace("#", "%23", StringComparison.OrdinalIgnoreCase); .Replace("#", "%23", StringComparison.OrdinalIgnoreCase);
var path = new Uri(encodedFragmentPath).LocalPath; var path = new Uri(encodedFragmentPath).LocalPath;
if (dataReaderResults.GetString(2) == "Directory") if (dataReaderResults.GetString(2) == "Directory")
{ {
results.Add(ResultManager.CreateFolderResult( results.Add(ResultManager.CreateFolderResult(
dataReaderResults.GetString(0), dataReaderResults.GetString(0),
path, path,
path, path,
query, 0, true, true)); query, 0, true, true));
} }
else else
{ {
@ -63,6 +65,11 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
} }
} }
} }
catch (OperationCanceledException)
{
// return empty result when cancelled
return results;
}
catch (InvalidOperationException e) catch (InvalidOperationException e)
{ {
// Internal error from ExecuteReader(): Connection closed. // Internal error from ExecuteReader(): Connection closed.
@ -79,22 +86,36 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
return results; return results;
} }
internal async static Task<List<Result>> WindowsIndexSearchAsync(string searchString, string connectionString, internal async static Task<List<Result>> WindowsIndexSearchAsync(
Func<string, string> constructQuery, string searchString,
List<AccessLink> exclusionList, Func<CSearchQueryHelper> createQueryHelper,
Query query, Func<string, string> constructQuery,
CancellationToken token) List<AccessLink> exclusionList,
Query query,
CancellationToken token)
{ {
var regexMatch = Regex.Match(searchString, reservedStringPattern); var regexMatch = Regex.Match(searchString, reservedStringPattern);
if (regexMatch.Success) if (regexMatch.Success)
return new List<Result>(); return new List<Result>();
try
{
var constructedQuery = constructQuery(searchString);
var constructedQuery = constructQuery(searchString); return RemoveResultsInExclusionList(
return RemoveResultsInExclusionList( await ExecuteWindowsIndexSearchAsync(constructedQuery, createQueryHelper().ConnectionString, query, token).ConfigureAwait(false),
await ExecuteWindowsIndexSearchAsync(constructedQuery, connectionString, query, token).ConfigureAwait(false),
exclusionList, exclusionList,
token); 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) 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) internal static bool PathIsIndexed(string path)
{ {
var csm = new CSearchManager(); try
var indexManager = csm.GetCatalog("SystemIndex").GetCrawlScopeManager(); {
return indexManager.IncludedInCrawlScope(path) > 0; 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) private static void LogException(string message, Exception e)

View file

@ -1,6 +1,9 @@
using Flow.Launcher.Plugin.Explorer.Search; using Flow.Launcher.Plugin.Explorer.Search;
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
namespace Flow.Launcher.Plugin.Explorer namespace Flow.Launcher.Plugin.Explorer
{ {
@ -18,7 +21,59 @@ namespace Flow.Launcher.Plugin.Explorer
public List<AccessLink> IndexSearchExcludedSubdirectoryPaths { get; set; } = new List<AccessLink>(); public List<AccessLink> IndexSearchExcludedSubdirectoryPaths { get; set; } = new List<AccessLink>();
public string SearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign; public string SearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign;
public bool SearchActionKeywordEnabled { get; set; } = true;
public string FileContentSearchActionKeyword { get; set; } = Constants.DefaultContentSearchActionKeyword; 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")
};
} }
} }

View file

@ -41,18 +41,15 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
Process.Start(psi); 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); 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; internal bool IsNewActionKeywordGlobal(string newActionKeyword) => newActionKeyword == Query.GlobalPluginWildcardSign;
} }

View file

@ -7,6 +7,7 @@
mc:Ignorable="d" mc:Ignorable="d"
ResizeMode="NoResize" ResizeMode="NoResize"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="Action Keyword Setting" Height="200" Width="500"> Title="Action Keyword Setting" Height="200" Width="500">
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
@ -16,21 +17,24 @@
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="180" /> <ColumnDefinition Width="180" />
<ColumnDefinition /> <ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Margin="20 10 10 10" FontSize="14" Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" <TextBlock Margin="20 10 10 10" FontSize="14" Grid.Row="0" Grid.Column="0" VerticalAlignment="Center"
HorizontalAlignment="Left" Text="Current Action Keyword:" /> HorizontalAlignment="Left" Text="{DynamicResource plugin_explorer_actionkeyword_current}" />
<TextBox Name="txtCurrentActionKeyword" <TextBox Name="TxtCurrentActionKeyword"
Margin="10" Grid.Row="0" Width="105" Grid.Column="1" Margin="10" Grid.Row="0" Width="105" Grid.Column="1"
VerticalAlignment="Center" VerticalAlignment="Center"
HorizontalAlignment="Left" /> HorizontalAlignment="Left"
Text="{Binding ActionKeyword}"
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row="1" Grid.Column="1"> PreviewKeyDown="TxtCurrentActionKeyword_OnKeyDown"/>
<Button Click="OnConfirmButtonClick" <CheckBox Name="ChkActionKeywordEnabled" ToolTip="{DynamicResource plugin_explorer_actionkeyword_enabled_tooltip}"
Margin="10 0 10 0" Width="80" Height="35" Margin="10" Grid.Row="0" Grid.Column="2" Content="{DynamicResource plugin_explorer_actionkeyword_enabled}"
Content="OK" /> Width="auto"
<Button Click="OnCancelButtonClick" VerticalAlignment="Center" IsChecked="{Binding Enabled}"
Margin="10 0 10 0" Width="80" Height="35" Visibility="{Binding Visible}"/>
Content="Cancel" /> <Button Name="DownButton"
</StackPanel> Click="OnDoneButtonClick" Grid.Row="1" Grid.Column="2"
Margin="10 0 10 0" Width="80" Height="35"
Content="{DynamicResource plugin_explorer_actionkeyword_done}" />
</Grid> </Grid>
</Window> </Window>

View file

@ -1,16 +1,10 @@
using Flow.Launcher.Plugin.Explorer.ViewModels; using Flow.Launcher.Plugin.Explorer.ViewModels;
using ICSharpCode.SharpZipLib.Zip;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using System.Windows; using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace Flow.Launcher.Plugin.Explorer.Views namespace Flow.Launcher.Plugin.Explorer.Views
{ {
@ -21,65 +15,102 @@ namespace Flow.Launcher.Plugin.Explorer.Views
{ {
private SettingsViewModel settingsViewModel; private SettingsViewModel settingsViewModel;
private ActionKeywordView currentActionKeyword; public ActionKeywordView CurrentActionKeyword { get; set; }
private List<ActionKeywordView> actionKeywordListView; public string ActionKeyword
public ActionKeywordSetting(SettingsViewModel settingsViewModel, List<ActionKeywordView> actionKeywordListView, ActionKeywordView selectedActionKeyword)
{ {
InitializeComponent(); get => _actionKeyword;
set
this.settingsViewModel = settingsViewModel; {
// Set Enable to be true if user change ActionKeyword
currentActionKeyword = selectedActionKeyword; if (Enabled is not null)
Enabled = true;
txtCurrentActionKeyword.Text = selectedActionKeyword.Keyword; _actionKeyword = value;
}
this.actionKeywordListView = actionKeywordListView;
} }
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)) CurrentActionKeyword = selectedActionKeyword;
return;
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(); Close();
return;
}
if (CurrentActionKeyword.KeywordProperty == Settings.ActionKeyword.FileContentSearchActionKeyword
&& ActionKeyword == Query.GlobalPluginWildcardSign)
{
MessageBox.Show(
settingsViewModel.Context.API.GetTranslation("plugin_explorer_globalActionKeywordInvalid"));
return; return;
} }
if (settingsViewModel.IsNewActionKeywordGlobal(newActionKeyword) var oldActionKeyword = CurrentActionKeyword.Keyword;
&& currentActionKeyword.Description
== settingsViewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_filecontentsearch"))
{
MessageBox.Show(settingsViewModel.Context.API.GetTranslation("plugin_explorer_globalActionKeywordInvalid"));
return; // == because of nullable
} if (Enabled == false || !settingsViewModel.IsActionKeywordAlreadyAssigned(ActionKeyword))
if (!settingsViewModel.IsActionKeywordAlreadyAssigned(newActionKeyword))
{ {
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(); Close();
return; return;
} }
// The keyword is not valid, so show message
MessageBox.Show(settingsViewModel.Context.API.GetTranslation("newActionKeywordsHasBeenAssigned")); MessageBox.Show(settingsViewModel.Context.API.GetTranslation("newActionKeywordsHasBeenAssigned"));
} }
private void OnCancelButtonClick(object sender, RoutedEventArgs e) private void TxtCurrentActionKeyword_OnKeyDown(object sender, KeyEventArgs e)
{ {
Close(); if (e.Key == Key.Enter)
{
return; DownButton.Focus();
OnDoneButtonClick(sender, e);
e.Handled = true;
}
} }
} }
} }

View file

@ -4,6 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:viewModels="clr-namespace:Flow.Launcher.Plugin.Explorer.ViewModels" xmlns:viewModels="clr-namespace:Flow.Launcher.Plugin.Explorer.ViewModels"
xmlns:views="clr-namespace:Flow.Launcher.Plugin.Explorer.Views"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"> d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources> <UserControl.Resources>
@ -17,14 +18,17 @@
Text="{Binding Path, Mode=OneTime}" Text="{Binding Path, Mode=OneTime}"
Margin="0,5,0,5" /> Margin="0,5,0,5" />
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="ListViewActionKeywords"> <DataTemplate x:Key="ListViewActionKeywords"
DataType="views:ActionKeywordView">
<Grid> <Grid>
<TextBlock <TextBlock
Text="{Binding Description, Mode=OneTime}" Text="{Binding Description, Mode=OneTime}"
Foreground="{Binding Color, Mode=OneWay}"
Margin="0,5,0,0" /> Margin="0,5,0,0" />
<TextBlock <TextBlock
Text="{Binding Keyword, Mode=OneTime}" Text="{Binding Keyword, Mode=OneTime}"
Margin="150,5,0,0" /> Foreground="{Binding Color, Mode=OneWay}"
Margin="250,5,0,0" />
</Grid> </Grid>
</DataTemplate> </DataTemplate>
</UserControl.Resources> </UserControl.Resources>

View file

@ -35,20 +35,20 @@ namespace Flow.Launcher.Plugin.Explorer.Views
actionKeywordsListView = new List<ActionKeywordView> actionKeywordsListView = new List<ActionKeywordView>
{ {
new ActionKeywordView() new(Settings.ActionKeyword.SearchActionKeyword,
{ viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_search")),
Description = viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_search"), new(Settings.ActionKeyword.FileContentSearchActionKeyword,
Keyword = this.viewModel.Settings.SearchActionKeyword viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_filecontentsearch")),
}, new(Settings.ActionKeyword.PathSearchActionKeyword,
new ActionKeywordView() viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_pathsearch")),
{ new(Settings.ActionKeyword.IndexSearchActionKeyword,
Description = viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_filecontentsearch"), viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_indexsearch"))
Keyword = this.viewModel.Settings.FileContentSearchActionKeyword
}
}; };
lbxActionKeywords.ItemsSource = actionKeywordsListView; lbxActionKeywords.ItemsSource = actionKeywordsListView;
ActionKeywordView.Init(viewModel.Settings);
RefreshView(); RefreshView();
} }
@ -71,9 +71,9 @@ namespace Flow.Launcher.Plugin.Explorer.Views
&& btnEdit.Visibility == Visibility.Hidden) && btnEdit.Visibility == Visibility.Hidden)
btnEdit.Visibility = Visibility.Visible; btnEdit.Visibility = Visibility.Visible;
if ((lbxAccessLinks.Items.Count == 0 && lbxExcludedPaths.Items.Count == 0) if (lbxAccessLinks.Items.Count == 0 && lbxExcludedPaths.Items.Count == 0
&& btnDelete.Visibility == Visibility.Visible && btnDelete.Visibility == Visibility.Visible
&& btnEdit.Visibility == Visibility.Visible) && btnEdit.Visibility == Visibility.Visible)
{ {
btnDelete.Visibility = Visibility.Hidden; btnDelete.Visibility = Visibility.Hidden;
btnEdit.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) private void expActionKeywords_Collapsed(object sender, RoutedEventArgs e)
{ {
if (!expActionKeywords.IsExpanded) if (!expActionKeywords.IsExpanded)
expActionKeywords.Height = Double.NaN; expActionKeywords.Height = double.NaN;
} }
private void expAccessLinks_Click(object sender, RoutedEventArgs e) private void expAccessLinks_Click(object sender, RoutedEventArgs e)
@ -135,20 +135,20 @@ namespace Flow.Launcher.Plugin.Explorer.Views
if (expActionKeywords.IsExpanded) if (expActionKeywords.IsExpanded)
expActionKeywords.IsExpanded = false; expActionKeywords.IsExpanded = false;
RefreshView(); RefreshView();
} }
private void expAccessLinks_Collapsed(object sender, RoutedEventArgs e) private void expAccessLinks_Collapsed(object sender, RoutedEventArgs e)
{ {
if (!expAccessLinks.IsExpanded) if (!expAccessLinks.IsExpanded)
expAccessLinks.Height = Double.NaN; expAccessLinks.Height = double.NaN;
} }
private void expExcludedPaths_Click(object sender, RoutedEventArgs e) private void expExcludedPaths_Click(object sender, RoutedEventArgs e)
{ {
if (expExcludedPaths.IsExpanded) if (expExcludedPaths.IsExpanded)
expAccessLinks.Height = Double.NaN; expAccessLinks.Height = double.NaN;
if (expAccessLinks.IsExpanded) if (expAccessLinks.IsExpanded)
expAccessLinks.IsExpanded = false; expAccessLinks.IsExpanded = false;
@ -161,11 +161,12 @@ namespace Flow.Launcher.Plugin.Explorer.Views
private void btnDelete_Click(object sender, RoutedEventArgs e) 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) 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) 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 selectedActionKeyword = lbxActionKeywords.SelectedItem as ActionKeywordView;
var actionKeywordWindow = new ActionKeywordSetting(viewModel, actionKeywordsListView, selectedActionKeyword); var actionKeywordWindow = new ActionKeywordSetting(viewModel,
selectedActionKeyword);
actionKeywordWindow.ShowDialog(); actionKeywordWindow.ShowDialog();
@ -199,7 +201,8 @@ namespace Flow.Launcher.Plugin.Explorer.Views
} }
else else
{ {
var selectedRow = lbxAccessLinks.SelectedItem as AccessLink ?? lbxExcludedPaths.SelectedItem as AccessLink; var selectedRow = lbxAccessLinks.SelectedItem as AccessLink ??
lbxExcludedPaths.SelectedItem as AccessLink;
if (selectedRow != null) if (selectedRow != null)
{ {
@ -215,7 +218,8 @@ namespace Flow.Launcher.Plugin.Explorer.Views
if (expExcludedPaths.IsExpanded) 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; link.Path = folderBrowserDialog.SelectedPath;
} }
} }
@ -235,10 +239,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views
var folderBrowserDialog = new FolderBrowserDialog(); var folderBrowserDialog = new FolderBrowserDialog();
if (folderBrowserDialog.ShowDialog() == DialogResult.OK) if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
{ {
var newAccessLink = new AccessLink var newAccessLink = new AccessLink {Path = folderBrowserDialog.SelectedPath};
{
Path = folderBrowserDialog.SelectedPath
};
AddAccessLink(newAccessLink); AddAccessLink(newAccessLink);
} }
@ -259,10 +260,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views
{ {
if (Directory.Exists(s)) if (Directory.Exists(s))
{ {
var newFolderLink = new AccessLink var newFolderLink = new AccessLink {Path = s};
{
Path = s
};
AddAccessLink(newFolderLink); AddAccessLink(newFolderLink);
} }
@ -275,7 +273,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views
private void AddAccessLink(AccessLink newAccessLink) private void AddAccessLink(AccessLink newAccessLink)
{ {
if (expAccessLinks.IsExpanded 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) if (viewModel.Settings.QuickAccessLinks == null)
viewModel.Settings.QuickAccessLinks = new List<AccessLink>(); viewModel.Settings.QuickAccessLinks = new List<AccessLink>();
@ -313,8 +311,35 @@ namespace Flow.Launcher.Plugin.Explorer.Views
public class ActionKeywordView 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"));
}
} }
} }

View file

@ -1,13 +1,15 @@
{ {
"ID": "572be03c74c642baae319fc283e561a8", "ID": "572be03c74c642baae319fc283e561a8",
"ActionKeywords": [ "ActionKeywords": [
"*", "*",
"doc:" "doc:",
"*",
"*"
], ],
"Name": "Explorer", "Name": "Explorer",
"Description": "Search and manage files and folders. Explorer utilises Windows Index Search", "Description": "Search and manage files and folders. Explorer utilises Windows Index Search",
"Author": "Jeremy Wu", "Author": "Jeremy Wu",
"Version": "1.7.7", "Version": "1.8.4",
"Language": "csharp", "Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher", "Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Explorer.dll", "ExecuteFileName": "Flow.Launcher.Plugin.Explorer.dll",

View file

@ -17,7 +17,8 @@ namespace Flow.Launcher.Plugin.PluginsManager
public List<Result> LoadContextMenus(Result selectedResult) 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> return new List<Result>
{ {
@ -50,7 +51,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
IcoPath = "Images\\request.png", IcoPath = "Images\\request.png",
Action = _ => 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") var link = pluginManifestInfo.UrlSourceCode.StartsWith("https://github.com")
? pluginManifestInfo.UrlSourceCode.Replace("/tree/master", "/issues/new/choose") ? pluginManifestInfo.UrlSourceCode.Replace("/tree/master", "/issues/new/choose")
: pluginManifestInfo.UrlSourceCode; : pluginManifestInfo.UrlSourceCode;

View file

@ -19,6 +19,8 @@
<system:String x:Key="plugin_pluginsmanager_update_title">Plugin Update</system:String> <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_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_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--> <!--Controls-->

View file

@ -19,6 +19,8 @@
<system:String x:Key="plugin_pluginsmanager_update_title">Aktualizácia pluginu</system:String> <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_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_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--> <!--Controls-->

View file

@ -38,7 +38,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
viewModel = new SettingsViewModel(context, Settings); viewModel = new SettingsViewModel(context, Settings);
contextMenu = new ContextMenu(Context); contextMenu = new ContextMenu(Context);
pluginManager = new PluginsManager(Context, Settings); pluginManager = new PluginsManager(Context, Settings);
_ = pluginManager.UpdateManifest().ContinueWith(_ => _manifestUpdateTask = pluginManager.UpdateManifest().ContinueWith(_ =>
{ {
lastUpdateTime = DateTime.Now; lastUpdateTime = DateTime.Now;
}, TaskContinuationOptions.OnlyOnRanToCompletion); }, TaskContinuationOptions.OnlyOnRanToCompletion);
@ -50,6 +50,8 @@ namespace Flow.Launcher.Plugin.PluginsManager
{ {
return contextMenu.LoadContextMenus(selectedResult); return contextMenu.LoadContextMenus(selectedResult);
} }
private Task _manifestUpdateTask = Task.CompletedTask;
public async Task<List<Result>> QueryAsync(Query query, CancellationToken token) public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
{ {
@ -58,9 +60,9 @@ namespace Flow.Launcher.Plugin.PluginsManager
if (string.IsNullOrWhiteSpace(search)) if (string.IsNullOrWhiteSpace(search))
return pluginManager.GetDefaultHotKeys(); 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; lastUpdateTime = DateTime.Now;
}, TaskContinuationOptions.OnlyOnRanToCompletion); }, TaskContinuationOptions.OnlyOnRanToCompletion);

View file

@ -15,7 +15,7 @@ namespace Flow.Launcher.Plugin.PluginsManager.Models
{ {
try 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); .ConfigureAwait(false);
UserPlugins = await JsonSerializer.DeserializeAsync<List<UserPlugin>>(jsonStream).ConfigureAwait(false); UserPlugins = await JsonSerializer.DeserializeAsync<List<UserPlugin>>(jsonStream).ConfigureAwait(false);

View file

@ -59,11 +59,12 @@ namespace Flow.Launcher.Plugin.PluginsManager
} }
else else
{ {
return _downloadManifestTask = pluginsManifest.DownloadManifest().ContinueWith(t => _downloadManifestTask = pluginsManifest.DownloadManifest();
Context.API.ShowMsg("Plugin Manifest Download Fail.", _downloadManifestTask.ContinueWith(_ =>
"Please check if you can connect to github.com. " + Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_update_failed_title"),
"This error means you may not be able to Install and Update Plugin.", icoPath, false), Context.API.GetTranslation("plugin_pluginsmanager_update_failed_subtitle"), icoPath, false),
TaskContinuationOptions.OnlyOnFaulted); TaskContinuationOptions.OnlyOnFaulted);
return _downloadManifestTask;
} }
} }
@ -119,7 +120,10 @@ namespace Flow.Launcher.Plugin.PluginsManager
.ChangeQuery( .ChangeQuery(
$"{Context.CurrentPluginMetadata.ActionKeywords.FirstOrDefault()} {Settings.HotkeyUpdate} {plugin.Name}"); $"{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; shouldHideWindow = false;
return; return;
@ -184,7 +188,6 @@ namespace Flow.Launcher.Plugin.PluginsManager
var uninstallSearch = search.Replace(Settings.HotkeyUpdate, string.Empty).TrimStart(); var uninstallSearch = search.Replace(Settings.HotkeyUpdate, string.Empty).TrimStart();
var resultsForUpdate = var resultsForUpdate =
from existingPlugin in Context.API.GetAllPlugins() from existingPlugin in Context.API.GetAllPlugins()
join pluginFromManifest in pluginsManifest.UserPlugins join pluginFromManifest in pluginsManifest.UserPlugins
@ -268,7 +271,13 @@ namespace Flow.Launcher.Plugin.PluginsManager
} }
return false; return false;
} },
ContextData =
new UserPlugin
{
Website = x.PluginNewUserPlugin.Website,
UrlSourceCode = x.PluginNewUserPlugin.UrlSourceCode
}
}); });
return Search(results, uninstallSearch); return Search(results, uninstallSearch);

View file

@ -6,7 +6,7 @@
"Name": "Plugins Manager", "Name": "Plugins Manager",
"Description": "Management of installing, uninstalling or updating Flow Launcher plugins", "Description": "Management of installing, uninstalling or updating Flow Launcher plugins",
"Author": "Jeremy Wu", "Author": "Jeremy Wu",
"Version": "1.8.1", "Version": "1.8.5",
"Language": "csharp", "Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher", "Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.PluginsManager.dll", "ExecuteFileName": "Flow.Launcher.Plugin.PluginsManager.dll",

View file

@ -13,8 +13,11 @@
<system:String x:Key="flowlauncher_plugin_program_reindex">Reindex</system:String> <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_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">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">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">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_suffixes_header">Suffixes</system:String>
<system:String x:Key="flowlauncher_plugin_program_max_depth_header">Max Depth</system:String> <system:String x:Key="flowlauncher_plugin_program_max_depth_header">Max Depth</system:String>

View file

@ -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_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_reindex">Reindexovať</system:String>
<system:String x:Key="flowlauncher_plugin_program_indexing">Indexovanie</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_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_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_suffixes_header">Prípony</system:String>
<system:String x:Key="flowlauncher_plugin_program_max_depth_header">Max. hĺbka</system:String> <system:String x:Key="flowlauncher_plugin_program_max_depth_header">Max. hĺbka</system:String>

View file

@ -20,27 +20,6 @@ namespace Flow.Launcher.Plugin.Program.Logger
{ {
public const string DirectoryName = "Logs"; 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> /// <summary>
/// Logs an exception /// Logs an exception
/// </summary> /// </summary>
@ -48,8 +27,6 @@ namespace Flow.Launcher.Plugin.Program.Logger
internal static void LogException(string classname, string callingMethodName, string loadingProgramPath, internal static void LogException(string classname, string callingMethodName, string loadingProgramPath,
string interpretationMessage, Exception e) string interpretationMessage, Exception e)
{ {
Debug.WriteLine($"ERROR{classname}|{callingMethodName}|{loadingProgramPath}|{interpretationMessage}");
var logger = LogManager.GetLogger(""); var logger = LogManager.GetLogger("");
var innerExceptionNumber = 1; var innerExceptionNumber = 1;
@ -103,6 +80,7 @@ namespace Flow.Launcher.Plugin.Program.Logger
{ {
var logger = LogManager.GetLogger(""); var logger = LogManager.GetLogger("");
logger.Error(e, $"fail to log exception in program logger, parts length is too small: {parts.Length}, message: {message}"); logger.Error(e, $"fail to log exception in program logger, parts length is too small: {parts.Length}, message: {message}");
return;
} }
var classname = parts[0]; var classname = parts[0];

View file

@ -1,4 +1,5 @@
using System.Windows; using System;
using System.Windows;
namespace Flow.Launcher.Plugin.Program namespace Flow.Launcher.Plugin.Program
{ {
@ -20,18 +21,21 @@ namespace Flow.Launcher.Plugin.Program
private void ButtonBase_OnClick(object sender, RoutedEventArgs e) 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"); string warning = context.API.GetTranslation("flowlauncher_plugin_program_suffixes_cannot_empty");
MessageBox.Show(warning); MessageBox.Show(warning);
return; return;
} }
_settings.ProgramSuffixes = tbSuffixes.Text.Split(Settings.SuffixSeperator); _settings.ProgramSuffixes = suffixes;
string msg = context.API.GetTranslation("flowlauncher_plugin_program_update_file_suffixes"); string msg = context.API.GetTranslation("flowlauncher_plugin_program_update_file_suffixes");
MessageBox.Show(msg); MessageBox.Show(msg);
DialogResult = true; DialogResult = true;
} }
} }
} }

View file

@ -126,7 +126,12 @@ namespace Flow.Launcher.Plugin.Program.Programs
((IShellLinkW)link).GetDescription(buffer, MAX_PATH); ((IShellLinkW)link).GetDescription(buffer, MAX_PATH);
description = buffer.ToString(); description = buffer.ToString();
} }
// To release unmanaged memory
Marshal.ReleaseComObject(link);
return target; return target;
} }
} }
} }

View file

@ -17,6 +17,7 @@ using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.Program.Logger; using Flow.Launcher.Plugin.Program.Logger;
using Rect = System.Windows.Rect; using Rect = System.Windows.Rect;
using Flow.Launcher.Plugin.SharedModels; using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.Infrastructure.Logger;
namespace Flow.Launcher.Plugin.Program.Programs namespace Flow.Launcher.Plugin.Program.Programs
{ {
@ -81,6 +82,11 @@ namespace Flow.Launcher.Plugin.Program.Programs
Apps = new List<Application>().ToArray(); 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 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" + $"|Unable to get logo for {UserModelId} from {path} and" +
$" located in {Package.Location}", new FileNotFoundException()); $" located in {Package.Location}", new FileNotFoundException());
return new BitmapImage(new Uri(Constant.MissingImgIcon)); return new BitmapImage(new Uri(Constant.MissingImgIcon));

View file

@ -16,9 +16,9 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Stretch"> <StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Stretch">
<StackPanel Orientation="Vertical" Width="250"> <StackPanel Orientation="Vertical" Width="250">
<CheckBox Name="StartMenuEnabled" IsChecked="{Binding EnableStartMenuSource}" Margin="5" Content="{DynamicResource flowlauncher_plugin_program_index_start}" /> <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}" /> <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}" /> <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>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal"> <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}" /> <Button Height="31" HorizontalAlignment="Right" Margin="10" Width="100" x:Name="btnLoadAllProgramSource" Click="btnLoadAllProgramSource_OnClick" Content="{DynamicResource flowlauncher_plugin_program_all_programs}" />

View file

@ -4,7 +4,7 @@
"Name": "Program", "Name": "Program",
"Description": "Search programs in Flow.Launcher", "Description": "Search programs in Flow.Launcher",
"Author": "qianlifeng", "Author": "qianlifeng",
"Version": "1.5.0", "Version": "1.5.4",
"Language": "csharp", "Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher", "Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Program.dll", "ExecuteFileName": "Flow.Launcher.Plugin.Program.dll",

View file

@ -271,8 +271,8 @@ namespace Flow.Launcher.Plugin.Shell
public void Init(PluginInitContext context) public void Init(PluginInitContext context)
{ {
this.context = context; this.context = context;
context.API.GlobalKeyboardEvent += API_GlobalKeyboardEvent;
_settings = context.API.LoadSettingJsonStorage<Settings>(); _settings = context.API.LoadSettingJsonStorage<Settings>();
context.API.GlobalKeyboardEvent += API_GlobalKeyboardEvent;
} }
bool API_GlobalKeyboardEvent(int keyevent, int vkcode, SpecialKeyState state) bool API_GlobalKeyboardEvent(int keyevent, int vkcode, SpecialKeyState state)
@ -298,7 +298,11 @@ namespace Flow.Launcher.Plugin.Shell
private void OnWinRPressed() private void OnWinRPressed()
{ {
context.API.ChangeQuery($"{context.CurrentPluginMetadata.ActionKeywords[0]}{Plugin.Query.TermSeperater}"); 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() public Control CreateSettingPanel()

View file

@ -4,7 +4,7 @@
"Name": "Shell", "Name": "Shell",
"Description": "Provide executing commands from Flow Launcher", "Description": "Provide executing commands from Flow Launcher",
"Author": "qianlifeng", "Author": "qianlifeng",
"Version": "1.4.1", "Version": "1.4.3",
"Language": "csharp", "Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher", "Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Shell.dll", "ExecuteFileName": "Flow.Launcher.Plugin.Shell.dll",

View file

@ -1,6 +1,7 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib"> 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_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_window">Nové okno</system:String>
<system:String x:Key="flowlauncher_plugin_websearch_new_tab">Nová karta</system:String> <system:String x:Key="flowlauncher_plugin_websearch_new_tab">Nová karta</system:String>

View file

@ -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> <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 ## Features
![The Flow](https://user-images.githubusercontent.com/26427004/82151677-fa9c7100-989f-11ea-9143-81de60aaf07d.gif) ![The Flow](https://user-images.githubusercontent.com/26427004/82151677-fa9c7100-989f-11ea-9143-81de60aaf07d.gif)
@ -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) [<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. 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>. - 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>. - Cancel/Return to previous screen: <kbd>Esc</kbd>.
- Install/Uninstall/Update plugins: in the search window, type `pm` `install`/`uninstall`/`update` + the plugin name. - Install/Uninstall/Update plugins: in the search window, type `pm` `install`/`uninstall`/`update` + the plugin name.
- Saved user settings are located: - Type `flow user data` to open your saved user settings folder. They are located at:
- If using roaming: `%APPDATA%\FlowLauncher` - If using roaming: `%APPDATA%\FlowLauncher`
- If using portable, by default: `%localappdata%\FlowLauncher\app-<VersionOfYourFlowLauncher>\UserData` - 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) [More tips](https://flow-launcher.github.io/docs/#/usage-tips)
### Integrations ### Plugins
- 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 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. Yes please, let us know in the [Q&A](https://github.com/Flow-Launcher/Flow.Launcher/discussions/categories/q-a) section.
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.
**Join our community on [Discord](https://discord.gg/AvgAQgh)!** **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 Flow Launcher's target framework is .Net 5
Install Visual Studio 2019 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) 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

View file

@ -47,10 +47,20 @@ function Delete-Unused ($path, $config) {
Remove-Item -Path $target -Include "*.xml" -Recurse 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) { function Validate-Directory ($output) {
New-Item $output -ItemType Directory -Force New-Item $output -ItemType Directory -Force
} }
function Pack-Squirrel-Installer ($path, $version, $output) { function Pack-Squirrel-Installer ($path, $version, $output) {
# msbuild based installer generation is not working in appveyor, not sure why # msbuild based installer generation is not working in appveyor, not sure why
Write-Host "Begin pack squirrel installer" Write-Host "Begin pack squirrel installer"
@ -79,7 +89,7 @@ function Pack-Squirrel-Installer ($path, $version, $output) {
Move-Item $temp\* $output -Force Move-Item $temp\* $output -Force
Remove-Item $temp Remove-Item $temp
$file = "$output\Flow-Launcher-v$version.exe" $file = "$output\Flow-Launcher-Setup.exe"
Write-Host "Filename: $file" Write-Host "Filename: $file"
Move-Item "$output\Setup.exe" $file -Force Move-Item "$output\Setup.exe" $file -Force
@ -97,6 +107,13 @@ function Publish-Self-Contained ($p) {
dotnet publish -c Release $csproj /p:PublishProfile=$profile 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 { function Main {
$p = Build-Path $p = Build-Path
$v = Build-Version $v = Build-Version
@ -108,10 +125,14 @@ function Main {
Publish-Self-Contained $p Publish-Self-Contained $p
Remove-CreateDumpExe $p $config
$o = "$p\Output\Packages" $o = "$p\Output\Packages"
Validate-Directory $o Validate-Directory $o
Pack-Squirrel-Installer $p $v $o Pack-Squirrel-Installer $p $v $o
Publish-Portable $o $v
} }
} }
Main Main

View file

@ -16,6 +16,6 @@ using System.Runtime.InteropServices;
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
[assembly: ComVisible(false)] [assembly: ComVisible(false)]
[assembly: AssemblyVersion("1.7.2")] [assembly: AssemblyVersion("1.0.0")]
[assembly: AssemblyFileVersion("1.7.2")] [assembly: AssemblyFileVersion("1.0.0")]
[assembly: AssemblyInformationalVersion("1.7.2")] [assembly: AssemblyInformationalVersion("1.0.0")]

View file

@ -1,4 +1,4 @@
version: '1.7.2.{build}' version: '1.8.1.{build}'
init: init:
- ps: | - ps: |
@ -14,6 +14,8 @@ assembly_info:
assembly_file_version: $(flowVersion) assembly_file_version: $(flowVersion)
assembly_informational_version: $(flowVersion) assembly_informational_version: $(flowVersion)
skip_branch_with_pr: true
skip_commits: skip_commits:
files: files:
- '*.md' - '*.md'
@ -34,6 +36,8 @@ artifacts:
name: Plugin nupkg name: Plugin nupkg
- path: 'Output\Packages\Flow-Launcher-*.exe' - path: 'Output\Packages\Flow-Launcher-*.exe'
name: Squirrel Installer name: Squirrel Installer
- path: Output\Packages\Flow-Launcher-Portable.zip
name: Portable Version
- path: 'Output\Packages\FlowLauncher-*-full.nupkg' - path: 'Output\Packages\FlowLauncher-*-full.nupkg'
name: Squirrel nupkg name: Squirrel nupkg
- path: 'Output\Packages\RELEASES' - path: 'Output\Packages\RELEASES'
@ -45,13 +49,13 @@ deploy:
api_key: api_key:
secure: n80IeWR3pN81p0w4uXq4mO0TdTXoJSHHFL+yTB9YBJ0Wni2DjZGYwOFdaWzW4hRi secure: n80IeWR3pN81p0w4uXq4mO0TdTXoJSHHFL+yTB9YBJ0Wni2DjZGYwOFdaWzW4hRi
on: on:
branch: master APPVEYOR_REPO_TAG: true
- provider: GitHub - provider: GitHub
release: v$(flowVersion) release: v$(flowVersion)
auth_token: auth_token:
secure: ij4UeXUYQBDJxn2YRAAhUOjklOGVKDB87Hn5J8tKIzj13yatoI7sLM666QDQFEgv secure: ij4UeXUYQBDJxn2YRAAhUOjklOGVKDB87Hn5J8tKIzj13yatoI7sLM666QDQFEgv
artifact: Squirrel Installer, Squirrel nupkg, Squirrel RELEASES artifact: Squirrel Installer, Portable Version, Squirrel nupkg, Squirrel RELEASES
draft: true draft: true
force_update: true force_update: true
on: on:
@ -61,7 +65,19 @@ deploy:
release: v$(flowVersion) release: v$(flowVersion)
auth_token: auth_token:
secure: ij4UeXUYQBDJxn2YRAAhUOjklOGVKDB87Hn5J8tKIzj13yatoI7sLM666QDQFEgv secure: ij4UeXUYQBDJxn2YRAAhUOjklOGVKDB87Hn5J8tKIzj13yatoI7sLM666QDQFEgv
artifact: Squirrel Installer, Squirrel nupkg, Squirrel RELEASES artifact: Squirrel Installer, Portable Version, Squirrel nupkg, Squirrel RELEASES
force_update: true force_update: true
on: 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
}