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>
<PackageReference Include="Droplex" Version="1.2.0" />
<PackageReference Include="Droplex" Version="1.3.1" />
<PackageReference Include="FSharp.Core" Version="4.7.1" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.1.3" />
<PackageReference Include="squirrel.windows" Version="1.5.2" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
<ProjectReference Include="..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />

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
* "id" and "jsonrpc" in the request, since it's not so useful in our request model.
*
@ -13,10 +12,12 @@
*
*/
using Flow.Launcher.Core.Resource;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using Flow.Launcher.Plugin;
using System.Text.Json;
namespace Flow.Launcher.Core.Plugin
{
@ -29,12 +30,8 @@ namespace Flow.Launcher.Core.Plugin
public string Data { get; set; }
}
public class JsonRPCModelBase
{
public int Id { get; set; }
}
public class JsonRPCResponseModel : JsonRPCModelBase
public class JsonRPCResponseModel
{
public string Result { get; set; }
@ -48,45 +45,20 @@ namespace Flow.Launcher.Core.Plugin
public string DebugMessage { get; set; }
}
public class JsonRPCRequestModel : JsonRPCModelBase
public class JsonRPCRequestModel
{
public string Method { get; set; }
public object[] Parameters { get; set; }
private static readonly JsonSerializerOptions options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public override string ToString()
{
string rpc = string.Empty;
if (Parameters != null && Parameters.Length > 0)
{
string parameters = $"[{string.Join(',', Parameters.Select(GetParameterByType))}]";
rpc = $@"{{\""method\"":\""{Method}\"",\""parameters\"":{parameters}";
}
else
{
rpc = $@"{{\""method\"":\""{Method}\"",\""parameters\"":[]";
}
return rpc;
}
private string GetParameterByType(object parameter)
=> parameter switch
{
null => "null",
string _ => $@"\""{ReplaceEscapes(parameter.ToString())}\""",
bool _ => $@"{parameter.ToString().ToLower()}",
_ => parameter.ToString()
};
private string ReplaceEscapes(string str)
{
return str.Replace(@"\", @"\\") //Escapes in ProcessStartInfo
.Replace(@"\", @"\\") //Escapes itself when passed to client
.Replace(@"""", @"\\""""");
return JsonSerializer.Serialize(this, options);
}
}
@ -95,11 +67,7 @@ namespace Flow.Launcher.Core.Plugin
/// </summary>
public class JsonRPCServerRequestModel : JsonRPCRequestModel
{
public override string ToString()
{
string rpc = base.ToString();
return rpc + "}";
}
}
/// <summary>
@ -108,12 +76,6 @@ namespace Flow.Launcher.Core.Plugin
public class JsonRPCClientRequestModel : JsonRPCRequestModel
{
public bool DontHideAfterAction { get; set; }
public override string ToString()
{
string rpc = base.ToString();
return rpc + "}";
}
}
/// <summary>
@ -125,4 +87,4 @@ namespace Flow.Launcher.Core.Plugin
{
public JsonRPCClientRequestModel JsonRPCAction { get; set; }
}
}
}

View file

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

View file

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

View file

@ -11,13 +11,13 @@ using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using System.Diagnostics;
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
namespace Flow.Launcher.Core.Plugin
{
public static class PluginsLoader
{
public const string PATH = "PATH";
public const string Python = "python";
public const string PythonExecutable = "pythonw.exe";
public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings)
@ -85,11 +85,7 @@ namespace Flow.Launcher.Core.Plugin
return;
}
plugins.Add(new PluginPair
{
Plugin = plugin,
Metadata = metadata
});
plugins.Add(new PluginPair {Plugin = plugin, Metadata = metadata});
});
metadata.InitTime += milliseconds;
}
@ -119,102 +115,68 @@ namespace Flow.Launcher.Core.Plugin
if (!source.Any(o => o.Language.ToUpper() == AllowedLanguage.Python))
return new List<PluginPair>();
// Try setting Constant.PythonPath first, either from
// PATH or from the given pythonDirectory
if (string.IsNullOrEmpty(settings.PythonDirectory))
{
var paths = Environment.GetEnvironmentVariable(PATH);
if (paths != null)
{
var pythonInPath = paths
.Split(';')
.Where(p => p.ToLower().Contains(Python))
.Any();
if (!string.IsNullOrEmpty(settings.PythonDirectory) && FilesFolders.LocationExists(settings.PythonDirectory))
return SetPythonPathForPluginPairs(source, Path.Combine(settings.PythonDirectory, PythonExecutable));
if (pythonInPath)
var pythonPath = string.Empty;
if (MessageBox.Show("Flow detected you have installed Python plugins, " +
"would you like to install Python to run them? " +
Environment.NewLine + Environment.NewLine +
"Click no if it's already installed, " +
"and you will be prompted to select the folder that contains the Python executable",
string.Empty, MessageBoxButtons.YesNo) == DialogResult.No
&& string.IsNullOrEmpty(settings.PythonDirectory))
{
var dlg = new FolderBrowserDialog
{
SelectedPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
};
var result = dlg.ShowDialog();
if (result == DialogResult.OK)
{
string pythonDirectory = dlg.SelectedPath;
if (!string.IsNullOrEmpty(pythonDirectory))
{
Constant.PythonPath =
Path.Combine(paths.Split(';').Where(p => p.ToLower().Contains(Python)).FirstOrDefault(), PythonExecutable);
settings.PythonDirectory = FilesFolders.GetPreviousExistingDirectory(FilesFolders.LocationExists, Constant.PythonPath);
}
else
{
Log.Error("PluginsLoader", "Failed to set Python path despite the environment variable PATH is found", "PythonPlugins");
pythonPath = Path.Combine(pythonDirectory, PythonExecutable);
if (File.Exists(pythonPath))
{
settings.PythonDirectory = pythonDirectory;
Constant.PythonPath = pythonPath;
}
else
{
MessageBox.Show("Can't find python in given directory");
}
}
}
}
else
{
var path = Path.Combine(settings.PythonDirectory, PythonExecutable);
if (File.Exists(path))
var installedPythonDirectory = Path.Combine(DataLocation.DataDirectory(), "PythonEmbeddable");
// Python 3.8.9 is used for Windows 7 compatibility
DroplexPackage.Drop(App.python_3_8_9_embeddable, installedPythonDirectory).Wait();
pythonPath = Path.Combine(installedPythonDirectory, PythonExecutable);
if (FilesFolders.FileExists(pythonPath))
{
Constant.PythonPath = path;
settings.PythonDirectory = installedPythonDirectory;
Constant.PythonPath = pythonPath;
}
else
{
Log.Error("PluginsLoader", $"Tried to automatically set from Settings.PythonDirectory " +
$"but can't find python executable in {path}", "PythonPlugins");
Log.Error("PluginsLoader",
$"Failed to set Python path after Droplex install, {pythonPath} does not exist",
"PythonPlugins");
}
}
if (string.IsNullOrEmpty(settings.PythonDirectory))
if (string.IsNullOrEmpty(settings.PythonDirectory) || string.IsNullOrEmpty(pythonPath))
{
if (MessageBox.Show("Flow detected you have installed Python plugins, " +
"would you like to install Python to run them? " +
Environment.NewLine + Environment.NewLine +
"Click no if it's already installed, " +
"and you will be prompted to select the folder that contains the Python executable",
string.Empty, MessageBoxButtons.YesNo) == DialogResult.No
&& string.IsNullOrEmpty(settings.PythonDirectory))
{
var dlg = new FolderBrowserDialog
{
SelectedPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
};
var result = dlg.ShowDialog();
if (result == DialogResult.OK)
{
string pythonDirectory = dlg.SelectedPath;
if (!string.IsNullOrEmpty(pythonDirectory))
{
var pythonPath = Path.Combine(pythonDirectory, PythonExecutable);
if (File.Exists(pythonPath))
{
settings.PythonDirectory = pythonDirectory;
Constant.PythonPath = pythonPath;
}
else
{
MessageBox.Show("Can't find python in given directory");
}
}
}
}
else
{
DroplexPackage.Drop(App.python3_9_1).Wait();
var installedPythonDirectory =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python39");
var pythonPath = Path.Combine(installedPythonDirectory, PythonExecutable);
if (FilesFolders.FileExists(pythonPath))
{
settings.PythonDirectory = installedPythonDirectory;
Constant.PythonPath = pythonPath;
}
else
{
Log.Error("PluginsLoader",
$"Failed to set Python path after Droplex install, {pythonPath} does not exist",
"PythonPlugins");
}
}
}
if (string.IsNullOrEmpty(settings.PythonDirectory))
{
MessageBox.Show("Unable to set Python executable path, please try from Flow's settings (scroll down to the bottom).");
MessageBox.Show(
"Unable to set Python executable path, please try from Flow's settings (scroll down to the bottom).");
Log.Error("PluginsLoader",
$"Not able to successfully set Python path, the PythonDirectory variable is still an empty string.",
"PythonPlugins");
@ -222,24 +184,26 @@ namespace Flow.Launcher.Core.Plugin
return new List<PluginPair>();
}
return source
return SetPythonPathForPluginPairs(source, pythonPath);
}
private static IEnumerable<PluginPair> SetPythonPathForPluginPairs(List<PluginMetadata> source, string pythonPath)
=> source
.Where(o => o.Language.ToUpper() == AllowedLanguage.Python)
.Select(metadata => new PluginPair
{
Plugin = new PythonPlugin(Constant.PythonPath),
Plugin = new PythonPlugin(pythonPath),
Metadata = metadata
})
.ToList();
}
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
{
return source
.Where(o => o.Language.ToUpper() == AllowedLanguage.Executable)
.Select(metadata => new PluginPair
{
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath),
Metadata = metadata
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), Metadata = metadata
});
}
}

View file

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

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

View file

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

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media;
@ -26,7 +27,8 @@ namespace Flow.Launcher.Infrastructure.Image
private const int MaxCached = 50;
public ConcurrentDictionary<string, ImageUsage> Data { get; private set; } = new ConcurrentDictionary<string, ImageUsage>();
private const int permissibleFactor = 2;
private SemaphoreSlim semaphore = new(1, 1);
public void Initialization(Dictionary<string, int> usage)
{
foreach (var key in usage.Keys)
@ -60,20 +62,29 @@ namespace Flow.Launcher.Infrastructure.Image
}
);
// To prevent the dictionary from drastically increasing in size by caching images, the dictionary size is not allowed to grow more than the permissibleFactor * maxCached size
// This is done so that we don't constantly perform this resizing operation and also maintain the image cache size at the same time
if (Data.Count > permissibleFactor * MaxCached)
SliceExtra();
async void SliceExtra()
{
// To delete the images from the data dictionary based on the resizing of the Usage Dictionary.
foreach (var key in Data.OrderBy(x => x.Value.usage).Take(Data.Count - MaxCached).Select(x => x.Key))
Data.TryRemove(key, out _);
// To prevent the dictionary from drastically increasing in size by caching images, the dictionary size is not allowed to grow more than the permissibleFactor * maxCached size
// This is done so that we don't constantly perform this resizing operation and also maintain the image cache size at the same time
if (Data.Count > permissibleFactor * MaxCached)
{
await semaphore.WaitAsync().ConfigureAwait(false);
// To delete the images from the data dictionary based on the resizing of the Usage Dictionary
// Double Check to avoid concurrent remove
if (Data.Count > permissibleFactor * MaxCached)
foreach (var key in Data.OrderBy(x => x.Value.usage).Take(Data.Count - MaxCached).Select(x => x.Key).ToArray())
Data.TryRemove(key, out _);
semaphore.Release();
}
}
}
}
public bool ContainsKey(string key)
{
return Data.ContainsKey(key) && Data[key].imageSource != null;
return key is not null && Data.ContainsKey(key) && Data[key].imageSource != null;
}
public int CacheSize()

View file

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

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

View file

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

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>
<Version>1.4.0</Version>
<PackageVersion>1.4.0</PackageVersion>
<AssemblyVersion>1.4.0</AssemblyVersion>
<FileVersion>1.4.0</FileVersion>
<Version>2.0.0</Version>
<PackageVersion>2.0.0</PackageVersion>
<AssemblyVersion>2.0.0</AssemblyVersion>
<FileVersion>2.0.0</FileVersion>
<PackageId>Flow.Launcher.Plugin</PackageId>
<Authors>Flow-Launcher</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>

View file

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

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
/// </para>
/// </summary>
public interface IReloadable
public interface IReloadable : IFeatures
{
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,
/// plugin settings will be automatically saved (see Flow.Launcher/PublicAPIInstance.SavePluginSettings) by Flow
/// </summary>
public interface ISavable
public interface ISavable : IFeatures
{
void Save();
}
}
}

View file

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

View file

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

View file

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

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

View file

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

View file

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

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))
?? (T)Activator.CreateInstance(typeof(T), args);
Application.Current.MainWindow.Hide();
// Fix UI bug
// Add `window.WindowState = WindowState.Normal`
// If only use `window.Show()`, Settings-window doesn't show when minimized in taskbar
// Not sure why this works tho
// Probably because, when `.Show()` fails, `window.WindowState == Minimized` (not `Normal`)
// https://stackoverflow.com/a/59719760/4230390
window.WindowState = WindowState.Normal;
window.Show();
window.Focus();
return (T)window;

View file

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

View file

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

View file

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

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="pythonDirectory">Priečinok s Pythonom</system:String>
<system:String x:Key="autoUpdates">Automatická aktualizácia</system:String>
<system:String x:Key="autoHideScrollBar">Automaticky skryť posuvník</system:String>
<system:String x:Key="autoHideScrollBarToolTip">Automaticky skrývať posuvník v okne nastavení a zobraziť ho, keď naň prejdete myšou</system:String>
<system:String x:Key="selectPythonDirectory">Vybrať</system:String>
<system:String x:Key="hideOnStartup">Schovať Flow Launcher po spustení</system:String>
<system:String x:Key="hideNotifyIcon">Schovať ikonu z oblasti oznámení</system:String>
<system:String x:Key="querySearchPrecision">Presnosť vyhľadávania</system:String>
<system:String x:Key="ShouldUsePinyin">Použiť Pinyin</system:String>
<system:String x:Key="ShouldUsePinyinToolTip">Umožňuje vyhľadávanie pomocou Pinyin. Pinyin je štandardný systém romanizovaného pravopisu pre transliteráciu čínštiny</system:String>
<system:String x:Key="shadowEffectNotAllowed">Efekt tieňa nie je povolený, kým má aktuálny motív povolený efekt rozostrenia</system:String>
<!--Setting Plugin-->
<system:String x:Key="plugin">Plugin</system:String>
@ -48,6 +51,7 @@
<system:String x:Key="newActionKeyword">Nová akcia skratky:</system:String>
<system:String x:Key="currentPriority">Aktuálna priorita:</system:String>
<system:String x:Key="newPriority">Nová priorita:</system:String>
<system:String x:Key="priority">Priorita:</system:String>
<system:String x:Key="pluginDirectory">Priečinok s pluginmi</system:String>
<system:String x:Key="author">Autor</system:String>
<system:String x:Key="plugin_init_time">Príprava:</system:String>
@ -70,6 +74,7 @@
<system:String x:Key="openResultModifiers">Modifikáčné klávesy na otvorenie výsledkov</system:String>
<system:String x:Key="showOpenResultHotkey">Zobraziť klávesovú skratku</system:String>
<system:String x:Key="customQueryHotkey">Vlastná klávesová skratka na vyhľadávanie</system:String>
<system:String x:Key="customQuery">Dopyt</system:String>
<system:String x:Key="delete">Odstrániť</system:String>
<system:String x:Key="edit">Upraviť</system:String>
<system:String x:Key="add">Pridať</system:String>

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

View file

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

View file

@ -42,7 +42,7 @@
<ColumnDefinition Width="0" />
</Grid.ColumnDefinitions>
<Image x:Name="ImageIcon" Width="32" Height="32" HorizontalAlignment="Left"
Source="{Binding Image.Value}" />
Source="{Binding Image}" />
<Grid Margin="5 0 5 0" Grid.Column="1" HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition />

View file

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

View file

@ -2,18 +2,17 @@ using System;
using System.IO;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Navigation;
using Microsoft.Win32;
using NHotkey;
using NHotkey.Wpf;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.ViewModel;
using Flow.Launcher.Helper;
namespace Flow.Launcher
{
@ -35,6 +34,14 @@ namespace Flow.Launcher
}
#region General
private void OnLoaded(object sender, RoutedEventArgs e)
{
// Fix (workaround) for the window freezes after lock screen (Win+L)
// https://stackoverflow.com/questions/4951058/software-rendering-mode-wpf
HwndSource hwndSource = PresentationSource.FromVisual(this) as HwndSource;
HwndTarget hwndTarget = hwndSource.CompositionTarget;
hwndTarget.RenderMode = RenderMode.SoftwareOnly;
}
private void OnAutoStartupChecked(object sender, RoutedEventArgs e)
{
@ -118,45 +125,13 @@ namespace Flow.Launcher
{
if (HotkeyControl.CurrentHotkeyAvailable)
{
SetHotkey(HotkeyControl.CurrentHotkey, (o, args) =>
{
if (!Application.Current.MainWindow.IsVisible)
{
Application.Current.MainWindow.Visibility = Visibility.Visible;
}
else
{
Application.Current.MainWindow.Visibility = Visibility.Hidden;
}
});
RemoveHotkey(settings.Hotkey);
HotKeyMapper.SetHotkey(HotkeyControl.CurrentHotkey, HotKeyMapper.OnHotkey);
HotKeyMapper.RemoveHotkey(settings.Hotkey);
settings.Hotkey = HotkeyControl.CurrentHotkey.ToString();
}
}
void SetHotkey(HotkeyModel hotkey, EventHandler<HotkeyEventArgs> action)
{
string hotkeyStr = hotkey.ToString();
try
{
HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action);
}
catch (Exception)
{
string errorMsg =
string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr);
MessageBox.Show(errorMsg);
}
}
void RemoveHotkey(string hotkeyStr)
{
if (!string.IsNullOrEmpty(hotkeyStr))
{
HotkeyManager.Current.Remove(hotkeyStr);
}
}
private void OnDeleteCustomHotkeyClick(object sender, RoutedEventArgs e)
{
var item = viewModel.SelectedCustomPluginHotkey;
@ -174,7 +149,7 @@ namespace Flow.Launcher
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
settings.CustomPluginHotkeys.Remove(item);
RemoveHotkey(item.Hotkey);
HotKeyMapper.RemoveHotkey(item.Hotkey);
}
}

View file

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

View file

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

View file

@ -25,7 +25,6 @@
</Style>
<Style x:Key="WindowStyle" BasedOn="{StaticResource BaseWindowStyle}" TargetType="{x:Type Window}">
<Setter Property="Width" Value="750" /> <!-- This is used to set the blur width only -->
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="Black" Opacity="0.6"/>

View file

@ -25,7 +25,6 @@
</Style>
<Style x:Key="WindowStyle" BasedOn="{StaticResource BaseWindowStyle}" TargetType="{x:Type Window}">
<Setter Property="Width" Value="750" /> <!-- This is used to set the blur width only -->
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="Black" Opacity="0.3"/>

View file

@ -23,7 +23,6 @@
</Style>
<Style x:Key="WindowStyle" BasedOn="{StaticResource BaseWindowStyle}" TargetType="{x:Type Window}">
<Setter Property="Width" Value="750" /> <!-- This is used to set the blur width only -->
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="White" Opacity="0.5"/>

View file

@ -6,8 +6,6 @@ using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Windows;
using System.Windows.Input;
using NHotkey;
using NHotkey.Wpf;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Helper;
@ -19,7 +17,10 @@ using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Storage;
using Flow.Launcher.Infrastructure.Logger;
using Microsoft.VisualStudio.Threading;
using System.Threading.Channels;
using ISavable = Flow.Launcher.Plugin.ISavable;
using System.Windows.Threading;
namespace Flow.Launcher.ViewModel
{
@ -36,7 +37,7 @@ namespace Flow.Launcher.ViewModel
private readonly FlowLauncherJsonStorage<History> _historyItemsStorage;
private readonly FlowLauncherJsonStorage<UserSelectedRecord> _userSelectedRecordStorage;
private readonly FlowLauncherJsonStorage<TopMostRecord> _topMostRecordStorage;
private readonly Settings _settings;
internal readonly Settings _settings;
private readonly History _history;
private readonly UserSelectedRecord _userSelectedRecord;
private readonly TopMostRecord _topMostRecord;
@ -77,8 +78,6 @@ namespace Flow.Launcher.ViewModel
RegisterViewUpdate();
RegisterResultsUpdatedEvent();
SetHotkey(_settings.Hotkey, OnHotkey);
SetCustomPluginHotkey();
SetOpenResultModifiers();
}
@ -117,7 +116,7 @@ namespace Flow.Launcher.ViewModel
throw t.Exception;
#else
Log.Error($"Error happen in task dealing with viewupdate for results. {t.Exception}");
_resultsViewUpdateTask =
_resultsViewUpdateTask =
Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted);
#endif
}
@ -130,13 +129,17 @@ namespace Flow.Launcher.ViewModel
var plugin = (IResultUpdated)pair.Plugin;
plugin.ResultsUpdated += (s, e) =>
{
if (e.Query.RawQuery == QueryText) // TODO: allow cancellation
if (e.Query.RawQuery != QueryText || e.Token.IsCancellationRequested)
{
PluginManager.UpdatePluginMetadata(e.Results, pair.Metadata, e.Query);
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(e.Results, pair.Metadata, e.Query, _updateToken)))
{
Log.Error("MainViewModel", "Unable to add item to Result Update Queue");
};
return;
}
var token = e.Token == default ? _updateToken : e.Token;
PluginManager.UpdatePluginMetadata(e.Results, pair.Metadata, e.Query);
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(e.Results, pair.Metadata, e.Query, token)))
{
Log.Error("MainViewModel", "Unable to add item to Result Update Queue");
}
};
}
@ -210,7 +213,10 @@ namespace Flow.Launcher.ViewModel
{
if (SelectedIsFromQueryResults())
{
SelectedResults = ContextMenu;
// When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing
// i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing
if (SelectedResults.SelectedItem != null)
SelectedResults = ContextMenu;
}
else
{
@ -233,21 +239,24 @@ namespace Flow.Launcher.ViewModel
ReloadPluginDataCommand = new RelayCommand(_ =>
{
var msg = new Msg { Owner = Application.Current.MainWindow };
var msg = new Msg
{
Owner = Application.Current.MainWindow
};
MainWindowVisibility = Visibility.Collapsed;
PluginManager
.ReloadData()
.ContinueWith(_ =>
Application.Current.Dispatcher.Invoke(() =>
{
msg.Show(
InternationalizationManager.Instance.GetTranslation("success"),
InternationalizationManager.Instance.GetTranslation("completedSuccessfully"),
"");
}))
.ConfigureAwait(false);
.ReloadData()
.ContinueWith(_ =>
Application.Current.Dispatcher.Invoke(() =>
{
msg.Show(
InternationalizationManager.Instance.GetTranslation("success"),
InternationalizationManager.Instance.GetTranslation("completedSuccessfully"),
"");
}))
.ConfigureAwait(false);
});
}
@ -278,8 +287,8 @@ namespace Flow.Launcher.ViewModel
/// <param name="queryText"></param>
public void ChangeQueryText(string queryText)
{
QueryTextCursorMovedToEnd = true;
QueryText = queryText;
QueryTextCursorMovedToEnd = true;
}
public bool LastQuerySelected { get; set; }
@ -418,7 +427,10 @@ namespace Flow.Launcher.ViewModel
Title = string.Format(title, h.Query),
SubTitle = string.Format(time, h.ExecutedDateTime),
IcoPath = "Images\\history.png",
OriginQuery = new Query { RawQuery = h.Query },
OriginQuery = new Query
{
RawQuery = h.Query
},
Action = _ =>
{
SelectedResults = Results;
@ -444,7 +456,9 @@ namespace Flow.Launcher.ViewModel
}
}
private void QueryResults()
private readonly IReadOnlyList<Result> _emptyResult = new List<Result>();
private async void QueryResults()
{
_updateSource?.Cancel();
@ -465,6 +479,12 @@ namespace Flow.Launcher.ViewModel
ProgressBarVisibility = Visibility.Hidden;
_isQueryRunning = true;
// Switch to ThreadPool thread
await TaskScheduler.Default;
if (currentCancellationToken.IsCancellationRequested)
return;
var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.NonGlobalPlugins);
// handle the exclusiveness of plugin using action keyword
@ -474,104 +494,80 @@ namespace Flow.Launcher.ViewModel
var plugins = PluginManager.ValidPluginsForQuery(query);
Task.Run(async () =>
if (query.ActionKeyword == Plugin.Query.GlobalPluginWildcardSign)
{
// Wait 45 millisecond for query change in global query
// if query changes, return so that it won't be calculated
await Task.Delay(45, currentCancellationToken);
if (currentCancellationToken.IsCancellationRequested)
return;
}
_ = Task.Delay(200, currentCancellationToken).ContinueWith(_ =>
{
// start the progress bar if query takes more than 200 ms and this is the current running query and it didn't finish yet
if (!currentCancellationToken.IsCancellationRequested && _isQueryRunning)
{
if (query.ActionKeyword == Plugin.Query.GlobalPluginWildcardSign)
{
// Wait 45 millisecond for query change in global query
// if query changes, return so that it won't be calculated
await Task.Delay(45, currentCancellationToken);
if (currentCancellationToken.IsCancellationRequested)
return;
}
ProgressBarVisibility = Visibility.Visible;
}
}, currentCancellationToken, TaskContinuationOptions.NotOnCanceled, TaskScheduler.Default);
_ = Task.Delay(200, currentCancellationToken).ContinueWith(_ =>
{
// start the progress bar if query takes more than 200 ms and this is the current running query and it didn't finish yet
if (!currentCancellationToken.IsCancellationRequested && _isQueryRunning)
{
ProgressBarVisibility = Visibility.Visible;
}
}, currentCancellationToken);
// plugins is ICollection, meaning LINQ will get the Count and preallocate Array
Task[] tasks = new Task[plugins.Count];
try
{
for (var i = 0; i < plugins.Count; i++)
{
if (!plugins[i].Metadata.Disabled)
{
tasks[i] = QueryTask(plugins[i]);
}
else
{
tasks[i] = Task.CompletedTask; // Avoid Null
}
}
var tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch
{
false => QueryTask(plugin),
true => Task.CompletedTask
}).ToArray();
// Check the code, WhenAll will translate all type of IEnumerable or Collection to Array, so make an array at first
await Task.WhenAll(tasks);
}
catch (OperationCanceledException)
{
// nothing to do here
}
if (currentCancellationToken.IsCancellationRequested)
return;
try
{
// Check the code, WhenAll will translate all type of IEnumerable or Collection to Array, so make an array at first
await Task.WhenAll(tasks);
}
catch (OperationCanceledException)
{
// nothing to do here
}
// this should happen once after all queries are done so progress bar should continue
// until the end of all querying
_isQueryRunning = false;
if (!currentCancellationToken.IsCancellationRequested)
{
// update to hidden if this is still the current query
ProgressBarVisibility = Visibility.Hidden;
}
if (currentCancellationToken.IsCancellationRequested)
return;
// Local function
async Task QueryTask(PluginPair plugin)
{
// Since it is wrapped within a Task.Run, the synchronous context is null
// Task.Yield will force it to run in ThreadPool
await Task.Yield();
// this should happen once after all queries are done so progress bar should continue
// until the end of all querying
_isQueryRunning = false;
if (!currentCancellationToken.IsCancellationRequested)
{
// update to hidden if this is still the current query
ProgressBarVisibility = Visibility.Hidden;
}
var results = await PluginManager.QueryForPlugin(plugin, query, currentCancellationToken);
if (currentCancellationToken.IsCancellationRequested || results == null) return;
// Local function
async Task QueryTask(PluginPair plugin)
{
// Since it is wrapped within a ThreadPool Thread, the synchronous context is null
// Task.Yield will force it to run in ThreadPool
await Task.Yield();
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, plugin.Metadata, query, currentCancellationToken)))
{
Log.Error("MainViewModel", "Unable to add item to Result Update Queue");
};
}
}, currentCancellationToken)
.ContinueWith(t => Log.Exception("|MainViewModel|Plugins Query Exceptions", t.Exception),
TaskContinuationOptions.OnlyOnFaulted);
IReadOnlyList<Result> results = await PluginManager.QueryForPluginAsync(plugin, query, currentCancellationToken);
currentCancellationToken.ThrowIfCancellationRequested();
results ??= _emptyResult;
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, plugin.Metadata, query, currentCancellationToken)))
{
Log.Error("MainViewModel", "Unable to add item to Result Update Queue");
}
}
}
private void RemoveOldQueryResults(Query query)
{
string lastKeyword = _lastQuery.ActionKeyword;
string keyword = query.ActionKeyword;
if (string.IsNullOrEmpty(lastKeyword))
if (_lastQuery.ActionKeyword != query.ActionKeyword)
{
if (!string.IsNullOrEmpty(keyword))
{
Results.KeepResultsFor(PluginManager.NonGlobalPlugins[keyword].Metadata);
}
}
else
{
if (string.IsNullOrEmpty(keyword))
{
Results.KeepResultsExcept(PluginManager.NonGlobalPlugins[lastKeyword].Metadata);
}
else if (lastKeyword != keyword)
{
Results.KeepResultsFor(PluginManager.NonGlobalPlugins[keyword].Metadata);
}
Results.Clear();
}
}
@ -656,96 +652,12 @@ namespace Flow.Launcher.ViewModel
#region Hotkey
private void SetHotkey(string hotkeyStr, EventHandler<HotkeyEventArgs> action)
{
var hotkey = new HotkeyModel(hotkeyStr);
SetHotkey(hotkey, action);
}
private void SetHotkey(HotkeyModel hotkey, EventHandler<HotkeyEventArgs> action)
{
string hotkeyStr = hotkey.ToString();
try
{
HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action);
}
catch (Exception)
{
string errorMsg =
string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"),
hotkeyStr);
MessageBox.Show(errorMsg);
}
}
public void RemoveHotkey(string hotkeyStr)
{
if (!string.IsNullOrEmpty(hotkeyStr))
{
HotkeyManager.Current.Remove(hotkeyStr);
}
}
/// <summary>
/// Checks if Flow Launcher should ignore any hotkeys
/// </summary>
/// <returns></returns>
private bool ShouldIgnoreHotkeys()
{
//double if to omit calling win32 function
if (_settings.IgnoreHotkeysOnFullscreen)
if (WindowsInteropHelper.IsWindowFullscreen())
return true;
return false;
}
private void SetCustomPluginHotkey()
{
if (_settings.CustomPluginHotkeys == null) return;
foreach (CustomPluginHotkey hotkey in _settings.CustomPluginHotkeys)
{
SetHotkey(hotkey.Hotkey, (s, e) =>
{
if (ShouldIgnoreHotkeys()) return;
MainWindowVisibility = Visibility.Visible;
ChangeQueryText(hotkey.ActionKeyword);
});
}
}
private void SetOpenResultModifiers()
{
OpenResultCommandModifiers = _settings.OpenResultModifiers ?? DefaultOpenResultModifiers;
}
private void OnHotkey(object sender, HotkeyEventArgs e)
{
if (!ShouldIgnoreHotkeys())
{
if (_settings.LastQueryMode == LastQueryMode.Empty)
{
ChangeQueryText(string.Empty);
}
else if (_settings.LastQueryMode == LastQueryMode.Preserved)
{
LastQuerySelected = true;
}
else if (_settings.LastQueryMode == LastQueryMode.Selected)
{
LastQuerySelected = false;
}
else
{
throw new ArgumentException($"wrong LastQueryMode: <{_settings.LastQueryMode}>");
}
ToggleFlowLauncher();
e.Handled = true;
}
}
private void ToggleFlowLauncher()
internal void ToggleFlowLauncher()
{
if (MainWindowVisibility != Visibility.Visible)
{
@ -794,7 +706,6 @@ namespace Flow.Launcher.ViewModel
}
#endif
foreach (var metaResults in resultsForUpdates)
{
foreach (var result in metaResults.Results)

View file

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

View file

@ -11,62 +11,12 @@ namespace Flow.Launcher.ViewModel
{
public class ResultViewModel : BaseModel
{
public class LazyAsync<T> : Lazy<ValueTask<T>>
{
private readonly T defaultValue;
private readonly Action _updateCallback;
public new T Value
{
get
{
if (!IsValueCreated)
{
_ = Exercute(); // manually use callback strategy
return defaultValue;
}
if (!base.Value.IsCompletedSuccessfully)
return defaultValue;
return base.Value.Result;
// If none of the variables captured by the local function are captured by other lambdas,
// the compiler can avoid heap allocations.
async ValueTask Exercute()
{
await base.Value.ConfigureAwait(false);
_updateCallback();
}
}
}
public LazyAsync(Func<ValueTask<T>> factory, T defaultValue, Action updateCallback) : base(factory)
{
if (defaultValue != null)
{
this.defaultValue = defaultValue;
}
_updateCallback = updateCallback;
}
}
public ResultViewModel(Result result, Settings settings)
{
if (result != null)
{
Result = result;
Image = new LazyAsync<ImageSource>(
SetImage,
ImageLoader.DefaultImage,
() =>
{
OnPropertyChanged(nameof(Image));
});
}
}
Settings = settings;
}
@ -85,44 +35,55 @@ namespace Flow.Launcher.ViewModel
? Result.SubTitle
: Result.SubTitleToolTip;
public LazyAsync<ImageSource> Image { get; set; }
private volatile bool ImageLoaded;
private async ValueTask<ImageSource> SetImage()
private ImageSource image = ImageLoader.DefaultImage;
public ImageSource Image
{
get
{
if (!ImageLoaded)
{
ImageLoaded = true;
_ = LoadImageAsync();
}
return image;
}
private set => image = value;
}
private async ValueTask LoadImageAsync()
{
var imagePath = Result.IcoPath;
if (string.IsNullOrEmpty(imagePath) && Result.Icon != null)
{
try
{
return Result.Icon();
image = Result.Icon();
return;
}
catch (Exception e)
{
Log.Exception($"|ResultViewModel.Image|IcoPath is empty and exception when calling Icon() for result <{Result.Title}> of plugin <{Result.PluginDirectory}>", e);
return ImageLoader.DefaultImage;
}
}
if (ImageLoader.CacheContainImage(imagePath))
{
// will get here either when icoPath has value\icon delegate is null\when had exception in delegate
return ImageLoader.Load(imagePath);
image = ImageLoader.Load(imagePath);
return;
}
return await Task.Run(() => ImageLoader.Load(imagePath));
// We need to modify the property not field here to trigger the OnPropertyChanged event
Image = await Task.Run(() => ImageLoader.Load(imagePath)).ConfigureAwait(false);
}
public Result Result { get; }
public override bool Equals(object obj)
{
var r = obj as ResultViewModel;
if (r != null)
{
return Result.Equals(r.Result);
}
else
{
return false;
}
return obj is ResultViewModel r && Result.Equals(r.Result);
}
public override int GetHashCode()

View file

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

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

View file

@ -10,6 +10,10 @@
<system:String x:Key="plugin_explorer_deletefilefoldersuccess">Deletion successful</system:String>
<system:String x:Key="plugin_explorer_deletefilefoldersuccess_detail">Successfully deleted the {0}</system:String>
<system:String x:Key="plugin_explorer_globalActionKeywordInvalid">Assigning the global action keyword could bring up too many results during search. Please choose a specific action keyword</system:String>
<system:String x:Key="plugin_explorer_windowsSearchServiceNotRunning">The required service for Windows Index Search does not appear to be running</system:String>
<system:String x:Key="plugin_explorer_windowsSearchServiceFix">To fix this, start the Windows Search service. Select here to remove this warning</system:String>
<system:String x:Key="plugin_explorer_alternative">The warning message has been switched off. As an alternative for searching files and folders, would you like to install Everything plugin?{0}{0}Select 'Yes' to install Everything plugin, or 'No' to return</system:String>
<system:String x:Key="plugin_explorer_alternative_title">Explorer Alternative</system:String>
<!--Controls-->
<system:String x:Key="plugin_explorer_delete">Delete</system:String>
@ -19,8 +23,14 @@
<system:String x:Key="plugin_explorer_quickaccesslinks_header">Quick Access Links</system:String>
<system:String x:Key="plugin_explorer_indexsearchexcludedpaths_header">Index Search Excluded Paths</system:String>
<system:String x:Key="plugin_explorer_manageindexoptions">Indexing Options</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_search">Search Activation:</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_search">Search:</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_pathsearch">Path Search:</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_filecontentsearch">File Content Search:</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_indexsearch">Index Search:</system:String>
<system:String x:Key="plugin_explorer_actionkeyword_current">Current Action Keyword:</system:String>
<system:String x:Key="plugin_explorer_actionkeyword_done">Done</system:String>
<system:String x:Key="plugin_explorer_actionkeyword_enabled">Enabled</system:String>
<system:String x:Key="plugin_explorer_actionkeyword_enabled_tooltip">When disabled Flow will not execute this search option, and will additionally revert back to '*' to free up the action keyword</system:String>
<!--Plugin Infos-->
<system:String x:Key="plugin_explorer_plugin_name">Explorer</system:String>

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_manageindexoptions">索引选项</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_search">搜索激活:</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_pathsearch">路径搜索激活:</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_filecontentsearch">文件内容搜索:</system:String>
<!--Plugin Infos-->

View file

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

View file

@ -21,7 +21,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
RecurseSubdirectories = true
}, query, search, criteria, token);
return DirectorySearch(new EnumerationOptions(), query, search, criteria, token); // null will be passed as default
return DirectorySearch(new EnumerationOptions(), query, search, criteria,
token); // null will be passed as default
}
public static string ConstructSearchCriteria(string search)
@ -57,7 +58,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
{
var directoryInfo = new System.IO.DirectoryInfo(path);
foreach (var fileSystemInfo in directoryInfo.EnumerateFileSystemInfos(searchCriteria, enumerationOption))
foreach (var fileSystemInfo in directoryInfo.EnumerateFileSystemInfos(searchCriteria, enumerationOption)
)
{
if (fileSystemInfo is System.IO.DirectoryInfo)
{
@ -74,17 +76,10 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
}
catch (Exception e)
{
if (!(e is ArgumentException))
throw e;
Log.Exception("Flow.Plugin.Explorer.", nameof(DirectoryInfoSearch), e);
results.Add(new Result {Title = e.Message, Score = 501});
return results;
#if DEBUG // Please investigate and handle error from DirectoryInfo search
#else
Log.Exception($"|Flow.Launcher.Plugin.Explorer.DirectoryInfoSearch|Error from performing DirectoryInfoSearch", e);
#endif
}
// Initial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection.

View file

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

View file

@ -10,10 +10,12 @@ namespace Flow.Launcher.Plugin.Explorer.Search
public static class ResultManager
{
private static PluginInitContext Context;
private static Settings Settings { get; set; }
public static void Init(PluginInitContext context)
public static void Init(PluginInitContext context, Settings settings)
{
Context = context;
Settings = settings;
}
internal static Result CreateFolderResult(string title, string subtitle, string path, Query query, int score = 0, bool showIndexState = false, bool windowsIndexed = false)
@ -26,7 +28,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
TitleHighlightData = StringMatcher.FuzzySearch(query.Search, title).MatchData,
Action = c =>
{
if (c.SpecialKeyState.CtrlPressed)
if (c.SpecialKeyState.CtrlPressed || (!Settings.PathSearchKeywordEnabled && !Settings.SearchActionKeywordEnabled))
{
try
{
@ -39,17 +41,25 @@ namespace Flow.Launcher.Plugin.Explorer.Search
return false;
}
}
// one of it is enabled
var keyword = Settings.SearchActionKeywordEnabled ? Settings.SearchActionKeyword : Settings.PathSearchActionKeyword;
keyword = keyword == Query.GlobalPluginWildcardSign ? string.Empty : keyword + " ";
string changeTo = path.EndsWith(Constants.DirectorySeperator) ? path : path + Constants.DirectorySeperator;
Context.API.ChangeQuery(string.IsNullOrEmpty(query.ActionKeyword) ?
changeTo :
query.ActionKeyword + " " + changeTo);
Context.API.ChangeQuery($"{keyword}{changeTo}");
return false;
},
Score = score,
TitleToolTip = Constants.ToolTipOpenDirectory,
SubTitleToolTip = Constants.ToolTipOpenDirectory,
ContextData = new SearchResult { Type = ResultType.Folder, FullPath = path, ShowIndexState = showIndexState, WindowsIndexed = windowsIndexed }
SubTitleToolTip = path,
ContextData = new SearchResult
{
Type = ResultType.Folder,
FullPath = path,
ShowIndexState = showIndexState,
WindowsIndexed = windowsIndexed
}
};
}
@ -57,7 +67,10 @@ namespace Flow.Launcher.Plugin.Explorer.Search
{
var retrievedDirectoryPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path);
var folderName = retrievedDirectoryPath.TrimEnd(Constants.DirectorySeperator).Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None).Last();
var folderName = retrievedDirectoryPath.TrimEnd(Constants.DirectorySeperator).Split(new[]
{
Path.DirectorySeparatorChar
}, StringSplitOptions.None).Last();
if (retrievedDirectoryPath.EndsWith(":\\"))
{
@ -81,7 +94,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
{
Title = title,
SubTitle = $"Use > to search within {subtitleFolderName}, " +
$"* to search for file extensions or >* to combine both searches.",
$"* to search for file extensions or >* to combine both searches.",
IcoPath = retrievedDirectoryPath,
Score = 500,
Action = c =>
@ -91,7 +104,13 @@ namespace Flow.Launcher.Plugin.Explorer.Search
},
TitleToolTip = retrievedDirectoryPath,
SubTitleToolTip = retrievedDirectoryPath,
ContextData = new SearchResult { Type = ResultType.Folder, FullPath = retrievedDirectoryPath, ShowIndexState = true, WindowsIndexed = windowsIndexed }
ContextData = new SearchResult
{
Type = ResultType.Folder,
FullPath = retrievedDirectoryPath,
ShowIndexState = true,
WindowsIndexed = windowsIndexed
}
};
}
@ -125,8 +144,14 @@ namespace Flow.Launcher.Plugin.Explorer.Search
return true;
},
TitleToolTip = Constants.ToolTipOpenContainingFolder,
SubTitleToolTip = Constants.ToolTipOpenContainingFolder,
ContextData = new SearchResult { Type = ResultType.File, FullPath = filePath, ShowIndexState = showIndexState, WindowsIndexed = windowsIndexed }
SubTitleToolTip = filePath,
ContextData = new SearchResult
{
Type = ResultType.File,
FullPath = filePath,
ShowIndexState = showIndexState,
WindowsIndexed = windowsIndexed
}
};
return result;
}
@ -148,4 +173,4 @@ namespace Flow.Launcher.Plugin.Explorer.Search
Folder,
File
}
}
}

View file

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

View file

@ -5,9 +5,11 @@ using System;
using System.Collections.Generic;
using System.Data.OleDb;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
{
@ -17,20 +19,20 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
// Reserved keywords in oleDB
private const string reservedStringPattern = @"^[`\@\#\^,\&\/\\\$\%_]+$";
internal async static Task<List<Result>> ExecuteWindowsIndexSearchAsync(string indexQueryString, string connectionString, Query query, CancellationToken token)
internal static async Task<List<Result>> ExecuteWindowsIndexSearchAsync(string indexQueryString, string connectionString, Query query, CancellationToken token)
{
var results = new List<Result>();
var fileResults = new List<Result>();
try
{
using var conn = new OleDbConnection(connectionString);
await using var conn = new OleDbConnection(connectionString);
await conn.OpenAsync(token);
token.ThrowIfCancellationRequested();
using var command = new OleDbCommand(indexQueryString, conn);
await using var command = new OleDbCommand(indexQueryString, conn);
// Results return as an OleDbDataReader.
using var dataReaderResults = await command.ExecuteReaderAsync(token) as OleDbDataReader;
await using var dataReaderResults = await command.ExecuteReaderAsync(token) as OleDbDataReader;
token.ThrowIfCancellationRequested();
if (dataReaderResults.HasRows)
@ -42,18 +44,18 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
{
// # is URI syntax for the fragment component, need to be encoded so LocalPath returns complete path
var encodedFragmentPath = dataReaderResults
.GetString(1)
.Replace("#", "%23", StringComparison.OrdinalIgnoreCase);
.GetString(1)
.Replace("#", "%23", StringComparison.OrdinalIgnoreCase);
var path = new Uri(encodedFragmentPath).LocalPath;
if (dataReaderResults.GetString(2) == "Directory")
{
results.Add(ResultManager.CreateFolderResult(
dataReaderResults.GetString(0),
path,
path,
query, 0, true, true));
dataReaderResults.GetString(0),
path,
path,
query, 0, true, true));
}
else
{
@ -63,6 +65,11 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
}
}
}
catch (OperationCanceledException)
{
// return empty result when cancelled
return results;
}
catch (InvalidOperationException e)
{
// Internal error from ExecuteReader(): Connection closed.
@ -79,22 +86,36 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
return results;
}
internal async static Task<List<Result>> WindowsIndexSearchAsync(string searchString, string connectionString,
Func<string, string> constructQuery,
List<AccessLink> exclusionList,
Query query,
CancellationToken token)
internal async static Task<List<Result>> WindowsIndexSearchAsync(
string searchString,
Func<CSearchQueryHelper> createQueryHelper,
Func<string, string> constructQuery,
List<AccessLink> exclusionList,
Query query,
CancellationToken token)
{
var regexMatch = Regex.Match(searchString, reservedStringPattern);
if (regexMatch.Success)
return new List<Result>();
try
{
var constructedQuery = constructQuery(searchString);
var constructedQuery = constructQuery(searchString);
return RemoveResultsInExclusionList(
await ExecuteWindowsIndexSearchAsync(constructedQuery, connectionString, query, token).ConfigureAwait(false),
return RemoveResultsInExclusionList(
await ExecuteWindowsIndexSearchAsync(constructedQuery, createQueryHelper().ConnectionString, query, token).ConfigureAwait(false),
exclusionList,
token);
}
catch (COMException)
{
// Occurs because the Windows Indexing (WSearch) is turned off in services and unable to be used by Explorer plugin
if (!SearchManager.Settings.WarnWindowsSearchServiceOff)
return new List<Result>();
return ResultForWindexSearchOff(query.RawQuery);
}
}
private static List<Result> RemoveResultsInExclusionList(List<Result> results, List<AccessLink> exclusionList, CancellationToken token)
@ -132,9 +153,66 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
internal static bool PathIsIndexed(string path)
{
var csm = new CSearchManager();
var indexManager = csm.GetCatalog("SystemIndex").GetCrawlScopeManager();
return indexManager.IncludedInCrawlScope(path) > 0;
try
{
var csm = new CSearchManager();
var indexManager = csm.GetCatalog("SystemIndex").GetCrawlScopeManager();
return indexManager.IncludedInCrawlScope(path) > 0;
}
catch(COMException)
{
// Occurs because the Windows Indexing (WSearch) is turned off in services and unable to be used by Explorer plugin
return false;
}
}
private static List<Result> ResultForWindexSearchOff(string rawQuery)
{
var api = SearchManager.Context.API;
return new List<Result>
{
new Result
{
Title = api.GetTranslation("plugin_explorer_windowsSearchServiceNotRunning"),
SubTitle = api.GetTranslation("plugin_explorer_windowsSearchServiceFix"),
Action = c =>
{
SearchManager.Settings.WarnWindowsSearchServiceOff = false;
var pluginsManagerPlugin= api.GetAllPlugins().FirstOrDefault(x => x.Metadata.ID == "9f8f9b14-2518-4907-b211-35ab6290dee7");
var actionKeywordCount = pluginsManagerPlugin.Metadata.ActionKeywords.Count;
if (actionKeywordCount > 1)
LogException("PluginsManager's action keyword has increased to more than 1, this does not allow for determining the " +
"right action keyword. Explorer's code for managing Windows Search service not running exception needs to be updated",
new InvalidOperationException());
if (MessageBox.Show(string.Format(api.GetTranslation("plugin_explorer_alternative"), Environment.NewLine),
api.GetTranslation("plugin_explorer_alternative_title"),
MessageBoxButton.YesNo) == MessageBoxResult.Yes
&& actionKeywordCount == 1)
{
api.ChangeQuery(string.Format("{0} install everything", pluginsManagerPlugin.Metadata.ActionKeywords[0]));
}
else
{
// Clears the warning message because same query string will not alter the displayed result list
api.ChangeQuery(string.Empty);
api.ChangeQuery(rawQuery);
}
var mainWindow = Application.Current.MainWindow;
mainWindow.Visibility = Visibility.Visible;
mainWindow.Focus();
return false;
},
IcoPath = Constants.ExplorerIconImagePath
}
};
}
private static void LogException(string message, Exception e)

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 Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
using System;
using System.Collections.Generic;
using System.IO;
namespace Flow.Launcher.Plugin.Explorer
{
@ -18,7 +21,59 @@ namespace Flow.Launcher.Plugin.Explorer
public List<AccessLink> IndexSearchExcludedSubdirectoryPaths { get; set; } = new List<AccessLink>();
public string SearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign;
public bool SearchActionKeywordEnabled { get; set; } = true;
public string FileContentSearchActionKeyword { get; set; } = Constants.DefaultContentSearchActionKeyword;
public string PathSearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign;
public bool PathSearchKeywordEnabled { get; set; }
public string IndexSearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign;
public bool IndexOnlySearchKeywordEnabled { get; set; }
public bool WarnWindowsSearchServiceOff { get; set; } = true;
internal enum ActionKeyword
{
SearchActionKeyword,
PathSearchActionKeyword,
FileContentSearchActionKeyword,
IndexSearchActionKeyword
}
internal string GetActionKeyword(ActionKeyword actionKeyword) => actionKeyword switch
{
ActionKeyword.SearchActionKeyword => SearchActionKeyword,
ActionKeyword.PathSearchActionKeyword => PathSearchActionKeyword,
ActionKeyword.FileContentSearchActionKeyword => FileContentSearchActionKeyword,
ActionKeyword.IndexSearchActionKeyword => IndexSearchActionKeyword
};
internal void SetActionKeyword(ActionKeyword actionKeyword, string keyword) => _ = actionKeyword switch
{
ActionKeyword.SearchActionKeyword => SearchActionKeyword = keyword,
ActionKeyword.PathSearchActionKeyword => PathSearchActionKeyword = keyword,
ActionKeyword.FileContentSearchActionKeyword => FileContentSearchActionKeyword = keyword,
ActionKeyword.IndexSearchActionKeyword => IndexSearchActionKeyword = keyword,
_ => throw new ArgumentOutOfRangeException(nameof(actionKeyword), actionKeyword, "Unexpected property")
};
internal bool? GetActionKeywordEnabled(ActionKeyword actionKeyword) => actionKeyword switch
{
ActionKeyword.SearchActionKeyword => SearchActionKeywordEnabled,
ActionKeyword.PathSearchActionKeyword => PathSearchKeywordEnabled,
ActionKeyword.IndexSearchActionKeyword => IndexOnlySearchKeywordEnabled,
_ => null
};
internal void SetActionKeywordEnabled(ActionKeyword actionKeyword, bool enable) => _ = actionKeyword switch
{
ActionKeyword.SearchActionKeyword => SearchActionKeywordEnabled = enable,
ActionKeyword.PathSearchActionKeyword => PathSearchKeywordEnabled = enable,
ActionKeyword.IndexSearchActionKeyword => IndexOnlySearchKeywordEnabled = enable,
_ => throw new ArgumentOutOfRangeException(nameof(actionKeyword), actionKeyword, "Unexpected property")
};
}
}

View file

@ -41,18 +41,15 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
Process.Start(psi);
}
internal void UpdateActionKeyword(string newActionKeyword, string oldActionKeyword)
internal void UpdateActionKeyword(Settings.ActionKeyword modifiedActionKeyword, string newActionKeyword, string oldActionKeyword)
{
PluginManager.ReplaceActionKeyword(Context.CurrentPluginMetadata.ID, oldActionKeyword, newActionKeyword);
if (Settings.FileContentSearchActionKeyword == oldActionKeyword)
Settings.FileContentSearchActionKeyword = newActionKeyword;
if (Settings.SearchActionKeyword == oldActionKeyword)
Settings.SearchActionKeyword = newActionKeyword;
}
internal bool IsActionKeywordAlreadyAssigned(string newActionKeyword) => PluginManager.ActionKeywordRegistered(newActionKeyword);
internal bool IsActionKeywordAlreadyAssigned(string newActionKeyword)
{
return PluginManager.ActionKeywordRegistered(newActionKeyword);
}
internal bool IsNewActionKeywordGlobal(string newActionKeyword) => newActionKeyword == Query.GlobalPluginWildcardSign;
}

View file

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

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.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace Flow.Launcher.Plugin.Explorer.Views
{
@ -21,65 +15,102 @@ namespace Flow.Launcher.Plugin.Explorer.Views
{
private SettingsViewModel settingsViewModel;
private ActionKeywordView currentActionKeyword;
public ActionKeywordView CurrentActionKeyword { get; set; }
private List<ActionKeywordView> actionKeywordListView;
public ActionKeywordSetting(SettingsViewModel settingsViewModel, List<ActionKeywordView> actionKeywordListView, ActionKeywordView selectedActionKeyword)
public string ActionKeyword
{
InitializeComponent();
this.settingsViewModel = settingsViewModel;
currentActionKeyword = selectedActionKeyword;
txtCurrentActionKeyword.Text = selectedActionKeyword.Keyword;
this.actionKeywordListView = actionKeywordListView;
get => _actionKeyword;
set
{
// Set Enable to be true if user change ActionKeyword
if (Enabled is not null)
Enabled = true;
_actionKeyword = value;
}
}
private void OnConfirmButtonClick(object sender, RoutedEventArgs e)
public bool? Enabled { get; set; }
private string _actionKeyword;
public Visibility Visible =>
CurrentActionKeyword.Enabled is not null ? Visibility.Visible : Visibility.Collapsed;
public ActionKeywordSetting(SettingsViewModel settingsViewModel,
ActionKeywordView selectedActionKeyword)
{
var newActionKeyword = txtCurrentActionKeyword.Text;
this.settingsViewModel = settingsViewModel;
if (string.IsNullOrEmpty(newActionKeyword))
return;
CurrentActionKeyword = selectedActionKeyword;
if (newActionKeyword == currentActionKeyword.Keyword)
ActionKeyword = selectedActionKeyword.Keyword;
Enabled = selectedActionKeyword.Enabled;
InitializeComponent();
TxtCurrentActionKeyword.Focus();
}
private void OnDoneButtonClick(object sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(ActionKeyword))
ActionKeyword = Query.GlobalPluginWildcardSign;
if (CurrentActionKeyword.Keyword == ActionKeyword && CurrentActionKeyword.Enabled == Enabled)
{
Close();
return;
}
if (CurrentActionKeyword.KeywordProperty == Settings.ActionKeyword.FileContentSearchActionKeyword
&& ActionKeyword == Query.GlobalPluginWildcardSign)
{
MessageBox.Show(
settingsViewModel.Context.API.GetTranslation("plugin_explorer_globalActionKeywordInvalid"));
return;
}
if (settingsViewModel.IsNewActionKeywordGlobal(newActionKeyword)
&& currentActionKeyword.Description
== settingsViewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_filecontentsearch"))
{
MessageBox.Show(settingsViewModel.Context.API.GetTranslation("plugin_explorer_globalActionKeywordInvalid"));
var oldActionKeyword = CurrentActionKeyword.Keyword;
return;
}
if (!settingsViewModel.IsActionKeywordAlreadyAssigned(newActionKeyword))
// == because of nullable
if (Enabled == false || !settingsViewModel.IsActionKeywordAlreadyAssigned(ActionKeyword))
{
settingsViewModel.UpdateActionKeyword(newActionKeyword, currentActionKeyword.Keyword);
// Update View Data
CurrentActionKeyword.Keyword = Enabled == true ? ActionKeyword : Query.GlobalPluginWildcardSign;
CurrentActionKeyword.Enabled = Enabled;
actionKeywordListView.Where(x => x.Description == currentActionKeyword.Description).FirstOrDefault().Keyword = newActionKeyword;
switch (Enabled)
{
// reset to global so it does not take up an action keyword when disabled
// not for null Enable plugin
case false when oldActionKeyword != Query.GlobalPluginWildcardSign:
settingsViewModel.UpdateActionKeyword(CurrentActionKeyword.KeywordProperty,
Query.GlobalPluginWildcardSign, oldActionKeyword);
break;
default:
settingsViewModel.UpdateActionKeyword(CurrentActionKeyword.KeywordProperty,
CurrentActionKeyword.Keyword, oldActionKeyword);
break;
}
Close();
return;
}
// The keyword is not valid, so show message
MessageBox.Show(settingsViewModel.Context.API.GetTranslation("newActionKeywordsHasBeenAssigned"));
}
private void OnCancelButtonClick(object sender, RoutedEventArgs e)
private void TxtCurrentActionKeyword_OnKeyDown(object sender, KeyEventArgs e)
{
Close();
return;
if (e.Key == Key.Enter)
{
DownButton.Focus();
OnDoneButtonClick(sender, e);
e.Handled = true;
}
}
}
}
}

View file

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

View file

@ -35,20 +35,20 @@ namespace Flow.Launcher.Plugin.Explorer.Views
actionKeywordsListView = new List<ActionKeywordView>
{
new ActionKeywordView()
{
Description = viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_search"),
Keyword = this.viewModel.Settings.SearchActionKeyword
},
new ActionKeywordView()
{
Description = viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_filecontentsearch"),
Keyword = this.viewModel.Settings.FileContentSearchActionKeyword
}
new(Settings.ActionKeyword.SearchActionKeyword,
viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_search")),
new(Settings.ActionKeyword.FileContentSearchActionKeyword,
viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_filecontentsearch")),
new(Settings.ActionKeyword.PathSearchActionKeyword,
viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_pathsearch")),
new(Settings.ActionKeyword.IndexSearchActionKeyword,
viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_indexsearch"))
};
lbxActionKeywords.ItemsSource = actionKeywordsListView;
ActionKeywordView.Init(viewModel.Settings);
RefreshView();
}
@ -71,9 +71,9 @@ namespace Flow.Launcher.Plugin.Explorer.Views
&& btnEdit.Visibility == Visibility.Hidden)
btnEdit.Visibility = Visibility.Visible;
if ((lbxAccessLinks.Items.Count == 0 && lbxExcludedPaths.Items.Count == 0)
&& btnDelete.Visibility == Visibility.Visible
&& btnEdit.Visibility == Visibility.Visible)
if (lbxAccessLinks.Items.Count == 0 && lbxExcludedPaths.Items.Count == 0
&& btnDelete.Visibility == Visibility.Visible
&& btnEdit.Visibility == Visibility.Visible)
{
btnDelete.Visibility = Visibility.Hidden;
btnEdit.Visibility = Visibility.Hidden;
@ -122,7 +122,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views
private void expActionKeywords_Collapsed(object sender, RoutedEventArgs e)
{
if (!expActionKeywords.IsExpanded)
expActionKeywords.Height = Double.NaN;
expActionKeywords.Height = double.NaN;
}
private void expAccessLinks_Click(object sender, RoutedEventArgs e)
@ -135,20 +135,20 @@ namespace Flow.Launcher.Plugin.Explorer.Views
if (expActionKeywords.IsExpanded)
expActionKeywords.IsExpanded = false;
RefreshView();
}
private void expAccessLinks_Collapsed(object sender, RoutedEventArgs e)
{
if (!expAccessLinks.IsExpanded)
expAccessLinks.Height = Double.NaN;
expAccessLinks.Height = double.NaN;
}
private void expExcludedPaths_Click(object sender, RoutedEventArgs e)
{
if (expExcludedPaths.IsExpanded)
expAccessLinks.Height = Double.NaN;
expAccessLinks.Height = double.NaN;
if (expAccessLinks.IsExpanded)
expAccessLinks.IsExpanded = false;
@ -161,11 +161,12 @@ namespace Flow.Launcher.Plugin.Explorer.Views
private void btnDelete_Click(object sender, RoutedEventArgs e)
{
var selectedRow = lbxAccessLinks.SelectedItem as AccessLink?? lbxExcludedPaths.SelectedItem as AccessLink;
var selectedRow = lbxAccessLinks.SelectedItem as AccessLink ?? lbxExcludedPaths.SelectedItem as AccessLink;
if (selectedRow != null)
{
string msg = string.Format(viewModel.Context.API.GetTranslation("plugin_explorer_delete_folder_link"), selectedRow.Path);
string msg = string.Format(viewModel.Context.API.GetTranslation("plugin_explorer_delete_folder_link"),
selectedRow.Path);
if (MessageBox.Show(msg, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
@ -191,7 +192,8 @@ namespace Flow.Launcher.Plugin.Explorer.Views
{
var selectedActionKeyword = lbxActionKeywords.SelectedItem as ActionKeywordView;
var actionKeywordWindow = new ActionKeywordSetting(viewModel, actionKeywordsListView, selectedActionKeyword);
var actionKeywordWindow = new ActionKeywordSetting(viewModel,
selectedActionKeyword);
actionKeywordWindow.ShowDialog();
@ -199,7 +201,8 @@ namespace Flow.Launcher.Plugin.Explorer.Views
}
else
{
var selectedRow = lbxAccessLinks.SelectedItem as AccessLink ?? lbxExcludedPaths.SelectedItem as AccessLink;
var selectedRow = lbxAccessLinks.SelectedItem as AccessLink ??
lbxExcludedPaths.SelectedItem as AccessLink;
if (selectedRow != null)
{
@ -215,7 +218,8 @@ namespace Flow.Launcher.Plugin.Explorer.Views
if (expExcludedPaths.IsExpanded)
{
var link = viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.First(x => x.Path == selectedRow.Path);
var link = viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.First(x =>
x.Path == selectedRow.Path);
link.Path = folderBrowserDialog.SelectedPath;
}
}
@ -235,10 +239,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views
var folderBrowserDialog = new FolderBrowserDialog();
if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
{
var newAccessLink = new AccessLink
{
Path = folderBrowserDialog.SelectedPath
};
var newAccessLink = new AccessLink {Path = folderBrowserDialog.SelectedPath};
AddAccessLink(newAccessLink);
}
@ -259,10 +260,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views
{
if (Directory.Exists(s))
{
var newFolderLink = new AccessLink
{
Path = s
};
var newFolderLink = new AccessLink {Path = s};
AddAccessLink(newFolderLink);
}
@ -275,7 +273,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views
private void AddAccessLink(AccessLink newAccessLink)
{
if (expAccessLinks.IsExpanded
&& !viewModel.Settings.QuickAccessLinks.Any(x => x.Path == newAccessLink.Path))
&& !viewModel.Settings.QuickAccessLinks.Any(x => x.Path == newAccessLink.Path))
{
if (viewModel.Settings.QuickAccessLinks == null)
viewModel.Settings.QuickAccessLinks = new List<AccessLink>();
@ -313,8 +311,35 @@ namespace Flow.Launcher.Plugin.Explorer.Views
public class ActionKeywordView
{
public string Description { get; set; }
private static Settings _settings;
public string Keyword { get; set; }
public static void Init(Settings settings)
{
_settings = settings;
}
internal ActionKeywordView(Settings.ActionKeyword actionKeyword, string description)
{
KeywordProperty = actionKeyword;
Description = description;
}
public string Description { get; private init; }
public string Color => Enabled ?? true ? "Black" : "Gray";
internal Settings.ActionKeyword KeywordProperty { get; }
public string Keyword
{
get => _settings.GetActionKeyword(KeywordProperty);
set => _settings.SetActionKeyword(KeywordProperty, value);
}
public bool? Enabled
{
get => _settings.GetActionKeywordEnabled(KeywordProperty);
set => _settings.SetActionKeywordEnabled(KeywordProperty,
value ?? throw new ArgumentException("Unexpected null value"));
}
}
}
}

View file

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

View file

@ -17,7 +17,8 @@ namespace Flow.Launcher.Plugin.PluginsManager
public List<Result> LoadContextMenus(Result selectedResult)
{
var pluginManifestInfo = selectedResult.ContextData as UserPlugin;
if(selectedResult.ContextData is not UserPlugin pluginManifestInfo)
return new List<Result>();
return new List<Result>
{
@ -50,7 +51,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
IcoPath = "Images\\request.png",
Action = _ =>
{
// standard UrlSourceCode format in PluginsManifest's plugins.json file: https://github.com/jjw24/WoxDictionary/tree/master
// standard UrlSourceCode format in PluginsManifest's plugins.json file: https://github.com/jjw24/Flow.Launcher.Plugin.Putty/tree/master
var link = pluginManifestInfo.UrlSourceCode.StartsWith("https://github.com")
? pluginManifestInfo.UrlSourceCode.Replace("/tree/master", "/issues/new/choose")
: pluginManifestInfo.UrlSourceCode;

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_exists">This plugin has an update, would you like to see it?</system:String>
<system:String x:Key="plugin_pluginsmanager_update_alreadyexists">This plugin is already installed</system:String>
<system:String x:Key="plugin_pluginsmanager_update_failed_title">Plugin Manifest Download Failed</system:String>
<system:String x:Key="plugin_pluginsmanager_update_failed_subtitle">Please check if you can connect to github.com. This error means you may not be able to install or update plugins.</system:String>
<!--Controls-->

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_exists">Tento plugin má dostupnú aktualizáciu, chcete ju zobraziť?</system:String>
<system:String x:Key="plugin_pluginsmanager_update_alreadyexists">Tento plugin je už nainštalovaný</system:String>
<system:String x:Key="plugin_pluginsmanager_update_failed_title">Stiahnutie manifestu pluginu zlyhalo</system:String>
<system:String x:Key="plugin_pluginsmanager_update_failed_subtitle">Skontrolujte, či sa môžete pripojiť na github.com. Táto chyba znamená, že pravdepodobne nemôžete pluginy inštalovať alebo aktualizovať.</system:String>
<!--Controls-->

View file

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

View file

@ -15,7 +15,7 @@ namespace Flow.Launcher.Plugin.PluginsManager.Models
{
try
{
await using var jsonStream = await Http.GetStreamAsync("https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/main/plugins.json")
await using var jsonStream = await Http.GetStreamAsync("https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/plugin_api_v2/plugins.json")
.ConfigureAwait(false);
UserPlugins = await JsonSerializer.DeserializeAsync<List<UserPlugin>>(jsonStream).ConfigureAwait(false);

View file

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

View file

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

View file

@ -13,8 +13,11 @@
<system:String x:Key="flowlauncher_plugin_program_reindex">Reindex</system:String>
<system:String x:Key="flowlauncher_plugin_program_indexing">Indexing</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_start">Index Start Menu</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_start_tooltip">When enabled, Flow will load programs from the start menu</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_registry">Index Registry</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_registry_tooltip">When enabled, Flow will load programs from the registry</system:String>
<system:String x:Key="flowlauncher_plugin_program_enable_description">Enable Program Description</system:String>
<system:String x:Key="flowlauncher_plugin_program_enable_description_tooltip">Disabling this will also stop Flow from searching via the program desciption</system:String>
<system:String x:Key="flowlauncher_plugin_program_suffixes_header">Suffixes</system:String>
<system:String x:Key="flowlauncher_plugin_program_max_depth_header">Max Depth</system:String>

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_reindex">Reindexovať</system:String>
<system:String x:Key="flowlauncher_plugin_program_indexing">Indexovanie</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_start">Indexovať Ponuku Štart</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_registry">Indexovať Registry</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_start">Indexovať ponuku Štart</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_start_tooltip">Ak je povolené, Flow načíta programy z ponuky Štart</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_registry">Indexovať databázu Registry</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_registry_tooltip">Ak je povolené, Flow načíta programy z databázy Registry</system:String>
<system:String x:Key="flowlauncher_plugin_program_enable_description">Povoliť popis programu</system:String>
<system:String x:Key="flowlauncher_plugin_program_enable_description_tooltip">Zakázaním tejto funkcie sa tiež zastaví vyhľadávanie popisu programu cez Flow</system:String>
<system:String x:Key="flowlauncher_plugin_program_suffixes_header">Prípony</system:String>
<system:String x:Key="flowlauncher_plugin_program_max_depth_header">Max. hĺbka</system:String>

View file

@ -20,27 +20,6 @@ namespace Flow.Launcher.Plugin.Program.Logger
{
public const string DirectoryName = "Logs";
static ProgramLogger()
{
var path = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Version);
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
var configuration = new LoggingConfiguration();
var target = new FileTarget();
configuration.AddTarget("file", target);
target.FileName = path.Replace(@"\", "/") + "/${shortdate}.txt";
#if DEBUG
var rule = new LoggingRule("*", LogLevel.Debug, target);
#else
var rule = new LoggingRule("*", LogLevel.Error, target);
#endif
configuration.LoggingRules.Add(rule);
LogManager.Configuration = configuration;
}
/// <summary>
/// Logs an exception
/// </summary>
@ -48,8 +27,6 @@ namespace Flow.Launcher.Plugin.Program.Logger
internal static void LogException(string classname, string callingMethodName, string loadingProgramPath,
string interpretationMessage, Exception e)
{
Debug.WriteLine($"ERROR{classname}|{callingMethodName}|{loadingProgramPath}|{interpretationMessage}");
var logger = LogManager.GetLogger("");
var innerExceptionNumber = 1;
@ -103,6 +80,7 @@ namespace Flow.Launcher.Plugin.Program.Logger
{
var logger = LogManager.GetLogger("");
logger.Error(e, $"fail to log exception in program logger, parts length is too small: {parts.Length}, message: {message}");
return;
}
var classname = parts[0];

View file

@ -1,4 +1,5 @@
using System.Windows;
using System;
using System.Windows;
namespace Flow.Launcher.Plugin.Program
{
@ -20,18 +21,21 @@ namespace Flow.Launcher.Plugin.Program
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(tbSuffixes.Text))
var suffixes = tbSuffixes.Text.Split(Settings.SuffixSeperator, StringSplitOptions.RemoveEmptyEntries);
if (suffixes.Length == 0)
{
string warning = context.API.GetTranslation("flowlauncher_plugin_program_suffixes_cannot_empty");
MessageBox.Show(warning);
return;
}
_settings.ProgramSuffixes = tbSuffixes.Text.Split(Settings.SuffixSeperator);
_settings.ProgramSuffixes = suffixes;
string msg = context.API.GetTranslation("flowlauncher_plugin_program_update_file_suffixes");
MessageBox.Show(msg);
DialogResult = true;
}
}
}
}

View file

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

View file

@ -17,6 +17,7 @@ using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.Program.Logger;
using Rect = System.Windows.Rect;
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.Infrastructure.Logger;
namespace Flow.Launcher.Plugin.Program.Programs
{
@ -81,6 +82,11 @@ namespace Flow.Launcher.Plugin.Program.Programs
Apps = new List<Application>().ToArray();
}
if (Marshal.ReleaseComObject(stream) > 0)
{
Log.Error("Flow.Launcher.Plugin.Program.Programs.UWP", "AppxManifest.xml was leaked");
}
}
@ -559,7 +565,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
}
else
{
ProgramLogger.LogException($"|UWP|ImageFromPath|{path}" +
ProgramLogger.LogException($"|UWP|ImageFromPath|{(string.IsNullOrEmpty(path) ? "Not Avaliable" : path)}" +
$"|Unable to get logo for {UserModelId} from {path} and" +
$" located in {Package.Location}", new FileNotFoundException());
return new BitmapImage(new Uri(Constant.MissingImgIcon));

View file

@ -16,9 +16,9 @@
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Stretch">
<StackPanel Orientation="Vertical" Width="250">
<CheckBox Name="StartMenuEnabled" IsChecked="{Binding EnableStartMenuSource}" Margin="5" Content="{DynamicResource flowlauncher_plugin_program_index_start}" />
<CheckBox Name="RegistryEnabled" IsChecked="{Binding EnableRegistrySource}" Margin="5" Content="{DynamicResource flowlauncher_plugin_program_index_registry}" />
<CheckBox Name="DescriptionEnabled" IsChecked="{Binding EnableDescription}" Margin="5" Content="{DynamicResource flowlauncher_plugin_program_enable_description}" />
<CheckBox Name="StartMenuEnabled" IsChecked="{Binding EnableStartMenuSource}" Margin="5" Content="{DynamicResource flowlauncher_plugin_program_index_start}" ToolTip="{DynamicResource flowlauncher_plugin_program_index_start_tooltip}" />
<CheckBox Name="RegistryEnabled" IsChecked="{Binding EnableRegistrySource}" Margin="5" Content="{DynamicResource flowlauncher_plugin_program_index_registry}" ToolTip="{DynamicResource flowlauncher_plugin_program_index_registry_tooltip}" />
<CheckBox Name="DescriptionEnabled" IsChecked="{Binding EnableDescription}" Margin="5" Content="{DynamicResource flowlauncher_plugin_program_enable_description}" ToolTip="{DynamicResource flowlauncher_plugin_program_enable_description_tooltip}" />
</StackPanel>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<Button Height="31" HorizontalAlignment="Right" Margin="10" Width="100" x:Name="btnLoadAllProgramSource" Click="btnLoadAllProgramSource_OnClick" Content="{DynamicResource flowlauncher_plugin_program_all_programs}" />

View file

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

View file

@ -271,8 +271,8 @@ namespace Flow.Launcher.Plugin.Shell
public void Init(PluginInitContext context)
{
this.context = context;
context.API.GlobalKeyboardEvent += API_GlobalKeyboardEvent;
_settings = context.API.LoadSettingJsonStorage<Settings>();
context.API.GlobalKeyboardEvent += API_GlobalKeyboardEvent;
}
bool API_GlobalKeyboardEvent(int keyevent, int vkcode, SpecialKeyState state)
@ -298,7 +298,11 @@ namespace Flow.Launcher.Plugin.Shell
private void OnWinRPressed()
{
context.API.ChangeQuery($"{context.CurrentPluginMetadata.ActionKeywords[0]}{Plugin.Query.TermSeperater}");
Application.Current.MainWindow.Visibility = Visibility.Visible;
// show the main window and set focus to the query box
Window mainWindow = Application.Current.MainWindow;
mainWindow.Visibility = Visibility.Visible;
mainWindow.Focus();
}
public Control CreateSettingPanel()

View file

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

View file

@ -1,6 +1,7 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="flowlauncher_plugin_websearch_open_search_in">Otvoriť vyhľadávanie v:</system:String>
<system:String x:Key="flowlauncher_plugin_websearch_new_window">Nové okno</system:String>
<system:String x:Key="flowlauncher_plugin_websearch_new_tab">Nová karta</system:String>

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>
---
<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
![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)
## Running Flow Launcher
## Getting Started
| [Windows 7 and up](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest) |
| ---------------------------------------------------------------------------------- |
### Installation
| [Windows 7+ installer](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest/download/Flow-Launcher-Setup.exe) | [Portable](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest/download/Flow-Launcher-Portable.zip) | `WinGet install "Flow Launcher"` |
| --------------------------------- | --------------------------------- | --------------------------------- |
Windows may complain about security due to code not being signed, this will be completed at a later stage. If you downloaded from this repo, you are good to continue the set up.
@ -45,52 +59,47 @@ Windows may complain about security due to code not being signed, this will be c
- Open context menu: on the selected result, press <kbd>Ctrl</kbd>+<kbd>O</kbd>/<kbd>Shift</kbd>+<kbd>Enter</kbd>.
- Cancel/Return to previous screen: <kbd>Esc</kbd>.
- Install/Uninstall/Update plugins: in the search window, type `pm` `install`/`uninstall`/`update` + the plugin name.
- Saved user settings are located:
- Type `flow user data` to open your saved user settings folder. They are located at:
- If using roaming: `%APPDATA%\FlowLauncher`
- If using portable, by default: `%localappdata%\FlowLauncher\app-<VersionOfYourFlowLauncher>\UserData`
- Logs are saved along with your user settings folder.
- Type `open log location` to open your logs folder, they are saved along with your user settings folder.
[More tips](https://flow-launcher.github.io/docs/#/usage-tips)
### Integrations
- If you use Python plugins:
- Install [Python3](https://www.python.org/downloads/), download `.exe` installer.
- Add Python to `%PATH%` or set it in flow's settings.
- Use `pip` to install `flowlauncher`, open cmd and type `pip install flowlauncher`.
- The Python plugin may require additional modules to be installed, please ensure you check by visiting the plugin's website via `pm install` + plugin name, go to context menu and select `Open website`.
- Start to launch your Python plugins.
- Flow searches files and contents via Windows Index Search, to use Everything: `pm install everything`.
### Plugins
## Plugins
Flow searches files and contents via Windows Index Search, to use **Everything**: `pm install everything`.
There is a [list of actively maintained plugins](https://github.com/Flow-Launcher/docs/blob/main/Plugins.md) in the documentation section, some of which are integrated from other launchers.
If you are using Python plugins, flow will prompt to either select the location or allow Python (Embeddable) to be automatic downloaded for use.
## Status
Vist [here](https://flow-launcher.github.io/docs/#/plugins) for our plugin portfolio.
Flow is under heavy development, but the code base is stable, so contributions are very welcome. If you would like to help maintain it, please do not hesistate to get in touch.
If you are keen to write your own plugin for flow, please take a look at our plugin development documentation for [C#](https://flow-launcher.github.io/docs/#/develop-dotnet-plugins) or [Python](https://flow-launcher.github.io/docs/#/develop-py-plugins)
## Contributing
## Questions/Suggestions
We welcome all contributions. If you are unsure of a change you want to make, simply put an issue in for discussion, otherwise feel free to put in a pull request.
You will find the main goals of flow placed under Projects board, so feel free to contribute on that. If you would like to make small incremental changes, feel free to do so as well.
Get in touch if you like to join the Flow-Launcher Team and help build this great tool.
## Question/Suggestion
Yes please, submit an issue to let us know.
Yes please, let us know in the [Q&A](https://github.com/Flow-Launcher/Flow.Launcher/discussions/categories/q-a) section.
**Join our community on [Discord](https://discord.gg/AvgAQgh)!**
## Developing/Debugging
## Development
### Status
Flow is under heavy development, but the code base is stable, so contributions are very welcome. If you would like to help maintain it, please do not hesistate to get in touch.
### Contributing
We welcome all contributions. If you are unsure of a change you want to make, let us know in the [Discussions](https://github.com/Flow-Launcher/Flow.Launcher/discussions/categories/ideas), otherwise feel free to put in a pull request.
You will find the main goals of flow placed under the [Projects board](https://github.com/Flow-Launcher/Flow.Launcher/projects), so feel free to contribute on that. If you would like to make small incremental changes, feel free to do so as well.
Get in touch if you like to join the Flow-Launcher Team and help build this great tool.
### Developing/Debugging
Flow Launcher's target framework is .Net 5
Install Visual Studio 2019
Install .Net 5 SDK via Visual Studio installer or manually from [here](https://dotnet.microsoft.com/download/dotnet/thank-you/sdk-5.0.103-windows-x64-installer)
## Documentation
Visit [here](https://flow-launcher.github.io/docs) for more information on usage, development and design documentations

View file

@ -47,10 +47,20 @@ function Delete-Unused ($path, $config) {
Remove-Item -Path $target -Include "*.xml" -Recurse
}
function Remove-CreateDumpExe ($path, $config) {
$target = "$path\Output\$config"
$depjson = Get-Content $target\Flow.Launcher.deps.json -raw
$depjson -replace '(?s)(.createdump.exe": {.*?}.*?\n)\s*', "" | Out-File $target\Flow.Launcher.deps.json -Encoding UTF8
Remove-Item -Path $target -Include "*createdump.exe" -Recurse
}
function Validate-Directory ($output) {
New-Item $output -ItemType Directory -Force
}
function Pack-Squirrel-Installer ($path, $version, $output) {
# msbuild based installer generation is not working in appveyor, not sure why
Write-Host "Begin pack squirrel installer"
@ -79,7 +89,7 @@ function Pack-Squirrel-Installer ($path, $version, $output) {
Move-Item $temp\* $output -Force
Remove-Item $temp
$file = "$output\Flow-Launcher-v$version.exe"
$file = "$output\Flow-Launcher-Setup.exe"
Write-Host "Filename: $file"
Move-Item "$output\Setup.exe" $file -Force
@ -97,6 +107,13 @@ function Publish-Self-Contained ($p) {
dotnet publish -c Release $csproj /p:PublishProfile=$profile
}
function Publish-Portable ($outputLocation, $version) {
& $outputLocation\Flow-Launcher-Setup.exe --silent | Out-Null
mkdir "$env:LocalAppData\FlowLauncher\app-$version\UserData"
Compress-Archive -Path $env:LocalAppData\FlowLauncher -DestinationPath $outputLocation\Flow-Launcher-Portable.zip
}
function Main {
$p = Build-Path
$v = Build-Version
@ -108,10 +125,14 @@ function Main {
Publish-Self-Contained $p
Remove-CreateDumpExe $p $config
$o = "$p\Output\Packages"
Validate-Directory $o
Pack-Squirrel-Installer $p $v $o
Publish-Portable $o $v
}
}
Main
Main

View file

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

View file

@ -1,4 +1,4 @@
version: '1.7.2.{build}'
version: '1.8.1.{build}'
init:
- ps: |
@ -14,6 +14,8 @@ assembly_info:
assembly_file_version: $(flowVersion)
assembly_informational_version: $(flowVersion)
skip_branch_with_pr: true
skip_commits:
files:
- '*.md'
@ -34,6 +36,8 @@ artifacts:
name: Plugin nupkg
- path: 'Output\Packages\Flow-Launcher-*.exe'
name: Squirrel Installer
- path: Output\Packages\Flow-Launcher-Portable.zip
name: Portable Version
- path: 'Output\Packages\FlowLauncher-*-full.nupkg'
name: Squirrel nupkg
- path: 'Output\Packages\RELEASES'
@ -45,13 +49,13 @@ deploy:
api_key:
secure: n80IeWR3pN81p0w4uXq4mO0TdTXoJSHHFL+yTB9YBJ0Wni2DjZGYwOFdaWzW4hRi
on:
branch: master
APPVEYOR_REPO_TAG: true
- provider: GitHub
release: v$(flowVersion)
auth_token:
secure: ij4UeXUYQBDJxn2YRAAhUOjklOGVKDB87Hn5J8tKIzj13yatoI7sLM666QDQFEgv
artifact: Squirrel Installer, Squirrel nupkg, Squirrel RELEASES
artifact: Squirrel Installer, Portable Version, Squirrel nupkg, Squirrel RELEASES
draft: true
force_update: true
on:
@ -61,7 +65,19 @@ deploy:
release: v$(flowVersion)
auth_token:
secure: ij4UeXUYQBDJxn2YRAAhUOjklOGVKDB87Hn5J8tKIzj13yatoI7sLM666QDQFEgv
artifact: Squirrel Installer, Squirrel nupkg, Squirrel RELEASES
artifact: Squirrel Installer, Portable Version, Squirrel nupkg, Squirrel RELEASES
force_update: true
on:
APPVEYOR_REPO_TAG: true
APPVEYOR_REPO_TAG: true
environment:
winget_token:
secure: HKfVT2FYZITAG0qqMCePYhIem5a/gzvAgYDSlr6RlXfGmeBUOANUtgJ9X6fNroxN
on_success:
- ps: |
if ($env:APPVEYOR_REPO_BRANCH -eq "master" -and $env:APPVEYOR_REPO_TAG -eq "true")
{
iwr https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe
.\wingetcreate.exe update Flow-Launcher.Flow-Launcher -s true -u https://github.com/Flow-Launcher/Flow.Launcher/releases/download/v$env:flowVersion/Flow-Launcher-v$env:flowVersion.exe -v $env:flowVersion -t $env:winget_token
}