mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'DotNet5Upgrade' of github.com:taooceros/Flow.Launcher into DotNet5Upgrade
This commit is contained in:
commit
f66eabdb1b
36 changed files with 793 additions and 308 deletions
|
|
@ -53,6 +53,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Droplex" Version="1.2.0" />
|
||||
<PackageReference Include="FSharp.Core" Version="4.7.1" />
|
||||
<PackageReference Include="squirrel.windows" Version="1.5.2" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Droplex;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
|
|
@ -22,7 +23,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings)
|
||||
{
|
||||
var dotnetPlugins = DotNetPlugins(metadatas);
|
||||
var pythonPlugins = PythonPlugins(metadatas, settings.PythonDirectory);
|
||||
var pythonPlugins = PythonPlugins(metadatas, settings);
|
||||
var executablePlugins = ExecutablePlugins(metadatas);
|
||||
var plugins = dotnetPlugins.Concat(pythonPlugins).Concat(executablePlugins).ToList();
|
||||
return plugins;
|
||||
|
|
@ -113,11 +114,14 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return plugins;
|
||||
}
|
||||
|
||||
public static IEnumerable<PluginPair> PythonPlugins(List<PluginMetadata> source, string pythonDirectory)
|
||||
public static IEnumerable<PluginPair> PythonPlugins(List<PluginMetadata> source, PluginsSettings settings)
|
||||
{
|
||||
// try to set Constant.PythonPath, either from
|
||||
if (!source.Any(o => o.Language.ToUpper() == AllowedLanguage.Python))
|
||||
return new List<PluginPair>();
|
||||
|
||||
// Try setting Constant.PythonPath first, either from
|
||||
// PATH or from the given pythonDirectory
|
||||
if (string.IsNullOrEmpty(pythonDirectory))
|
||||
if (string.IsNullOrEmpty(settings.PythonDirectory))
|
||||
{
|
||||
var paths = Environment.GetEnvironmentVariable(PATH);
|
||||
if (paths != null)
|
||||
|
|
@ -129,47 +133,103 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
if (pythonInPath)
|
||||
{
|
||||
Constant.PythonPath = PythonExecutable;
|
||||
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.PythonPlugins|Python can't be found in PATH.");
|
||||
Log.Error("PluginsLoader","Failed to set Python path despite the environment variable PATH is found", "PythonPlugins");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("|PluginsLoader.PythonPlugins|PATH environment variable is not set.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var path = Path.Combine(pythonDirectory, PythonExecutable);
|
||||
var path = Path.Combine(settings.PythonDirectory, PythonExecutable);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
Constant.PythonPath = path;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"|PluginsLoader.PythonPlugins|Can't find python executable in {path}");
|
||||
Log.Error("PluginsLoader",$"Tried to automatically set from Settings.PythonDirectory " +
|
||||
$"but can't find python executable in {path}", "PythonPlugins");
|
||||
}
|
||||
}
|
||||
|
||||
// if we have a path to the python executable,
|
||||
// load every python plugin pair.
|
||||
if (String.IsNullOrEmpty(Constant.PythonPath))
|
||||
if (string.IsNullOrEmpty(settings.PythonDirectory))
|
||||
{
|
||||
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).");
|
||||
Log.Error("PluginsLoader",
|
||||
$"Not able to successfully set Python path, the PythonDirectory variable is still an empty string.",
|
||||
"PythonPlugins");
|
||||
|
||||
return new List<PluginPair>();
|
||||
}
|
||||
else
|
||||
{
|
||||
return source
|
||||
.Where(o => o.Language.ToUpper() == AllowedLanguage.Python)
|
||||
.Select(metadata => new PluginPair
|
||||
{
|
||||
Plugin = new PythonPlugin(Constant.PythonPath),
|
||||
Metadata = metadata
|
||||
});
|
||||
}
|
||||
|
||||
return source
|
||||
.Where(o => o.Language.ToUpper() == AllowedLanguage.Python)
|
||||
.Select(metadata => new PluginPair
|
||||
{
|
||||
Plugin = new PythonPlugin(Constant.PythonPath),
|
||||
Metadata = metadata
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ namespace Flow.Launcher.Infrastructure.Http
|
|||
{
|
||||
try
|
||||
{
|
||||
using var response = await client.GetAsync(url, token);
|
||||
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
|
||||
|
|
@ -135,7 +135,7 @@ namespace Flow.Launcher.Infrastructure.Http
|
|||
public static async Task<Stream> GetStreamAsync([NotNull] string url, CancellationToken token = default)
|
||||
{
|
||||
Log.Debug($"|Http.Get|Url <{url}>");
|
||||
var response = await client.GetAsync(url, token);
|
||||
var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
return await response.Content.ReadAsStreamAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
public class JsonStrorage<T> where T : new()
|
||||
{
|
||||
private readonly JsonSerializerOptions _serializerSettings;
|
||||
private T _data;
|
||||
protected T _data;
|
||||
// need a new directory name
|
||||
public const string DirectoryName = "Settings";
|
||||
public const string FileSuffix = ".json";
|
||||
|
|
|
|||
|
|
@ -15,5 +15,10 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
|
||||
FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}");
|
||||
}
|
||||
|
||||
public PluginJsonStorage(T data) : this()
|
||||
{
|
||||
_data = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using JetBrains.Annotations;
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
|
@ -88,17 +89,86 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Fuzzy Search the string with the given query. This is the core search mechanism Flow uses
|
||||
/// </summary>
|
||||
/// <param name="query">Query string</param>
|
||||
/// <param name="stringToCompare">The string that will be compared against the query</param>
|
||||
/// <returns>Match results</returns>
|
||||
MatchResult FuzzySearch(string query, string stringToCompare);
|
||||
|
||||
/// <summary>
|
||||
/// Http download the spefic url and return as string
|
||||
/// </summary>
|
||||
/// <param name="url">URL to call Http Get</param>
|
||||
/// <param name="token">Cancellation Token</param>
|
||||
/// <returns>Task to get string result</returns>
|
||||
Task<string> HttpGetStringAsync(string url, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Http download the spefic url and return as stream
|
||||
/// </summary>
|
||||
/// <param name="url">URL to call Http Get</param>
|
||||
/// <param name="token">Cancellation Token</param>
|
||||
/// <returns>Task to get stream result</returns>
|
||||
Task<Stream> HttpGetStreamAsync(string url, CancellationToken token = default);
|
||||
|
||||
Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath);
|
||||
/// <summary>
|
||||
/// Download the specific url to a cretain file path
|
||||
/// </summary>
|
||||
/// <param name="url">URL to download file</param>
|
||||
/// <param name="token">place to store file</param>
|
||||
/// <returns>Task showing the progress</returns>
|
||||
Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Add ActionKeyword for specific plugin
|
||||
/// </summary>
|
||||
/// <param name="pluginId">ID for plugin that needs to add action keyword</param>
|
||||
/// <param name="newActionKeyword">The actionkeyword that is supposed to be added</param>
|
||||
void AddActionKeyword(string pluginId, string newActionKeyword);
|
||||
|
||||
/// <summary>
|
||||
/// Remove ActionKeyword for specific plugin
|
||||
/// </summary>
|
||||
/// <param name="pluginId">ID for plugin that needs to remove action keyword</param>
|
||||
/// <param name="newActionKeyword">The actionkeyword that is supposed to be removed</param>
|
||||
void RemoveActionKeyword(string pluginId, string oldActionKeyword);
|
||||
|
||||
/// <summary>
|
||||
/// Log debug message
|
||||
/// Message will only be logged in Debug mode
|
||||
/// </summary>
|
||||
void LogDebug(string className, string message, [CallerMemberName] string methodName = "");
|
||||
|
||||
/// <summary>
|
||||
/// Log info message
|
||||
/// </summary>
|
||||
void LogInfo(string className, string message, [CallerMemberName] string methodName = "");
|
||||
|
||||
/// <summary>
|
||||
/// Log warning message
|
||||
/// </summary>
|
||||
void LogWarn(string className, string message, [CallerMemberName] string methodName = "");
|
||||
|
||||
/// <summary>
|
||||
/// Log an Exception. Will throw if in debug mode so developer will be aware,
|
||||
/// otherwise logs the eror message. This is the primary logging method used for Flow
|
||||
/// </summary>
|
||||
void LogException(string className, string message, Exception e, [CallerMemberName] string methodName = "");
|
||||
|
||||
/// <summary>
|
||||
/// Load JsonStorage for current plugin. This is the method used to load settings from json in Flow
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type for deserialization</typeparam>
|
||||
/// <returns></returns>
|
||||
T LoadJsonStorage<T>() where T : new();
|
||||
|
||||
/// <summary>
|
||||
/// Save JsonStorage for current plugin. This is the method used to save settings to json in Flow
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type for Serialization</typeparam>
|
||||
/// <returns></returns>
|
||||
void SaveJsonStorage<T>(T setting) where T : new();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Save plugin settings/cache,
|
||||
|
|
@ -66,6 +66,9 @@
|
|||
<Content Include="Images\*.svg">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Images\*.ico">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
BIN
Flow.Launcher/Images/app.ico
Normal file
BIN
Flow.Launcher/Images/app.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
|
|
@ -121,6 +121,7 @@
|
|||
<system:String x:Key="newActionKeywordsCannotBeEmpty">New Action Keyword can't be empty</system:String>
|
||||
<system:String x:Key="newActionKeywordsHasBeenAssigned">This new Action Keyword is already assigned to another plugin, please choose a different one</system:String>
|
||||
<system:String x:Key="success">Success</system:String>
|
||||
<system:String x:Key="completedSuccessfully">Completed successfully</system:String>
|
||||
<system:String x:Key="actionkeyword_tips">Use * if you don't want to specify an action keyword</system:String>
|
||||
|
||||
<!--Custom Query Hotkey Dialog-->
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
<Window.InputBindings>
|
||||
<KeyBinding Key="Escape" Command="{Binding EscCommand}"></KeyBinding>
|
||||
<KeyBinding Key="F1" Command="{Binding StartHelpCommand}"></KeyBinding>
|
||||
<KeyBinding Key="F5" Command="{Binding ReloadPluginDataCommand}"></KeyBinding>
|
||||
<KeyBinding Key="Tab" Command="{Binding SelectNextItemCommand}"></KeyBinding>
|
||||
<KeyBinding Key="Tab" Modifiers="Shift" Command="{Binding SelectPrevItemCommand}"></KeyBinding>
|
||||
<KeyBinding Key="N" Modifiers="Ctrl" Command="{Binding SelectNextItemCommand}"></KeyBinding>
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ namespace Flow.Launcher
|
|||
}
|
||||
if (!File.Exists(iconPath))
|
||||
{
|
||||
imgIco.Source = ImageLoader.Load(Path.Combine(Infrastructure.Constant.ProgramDirectory, "Images\\app.png"));
|
||||
imgIco.Source = ImageLoader.Load(Path.Combine(Constant.ProgramDirectory, "Images\\app.png"));
|
||||
}
|
||||
else {
|
||||
imgIco.Source = ImageLoader.Load(iconPath);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ using System.Threading;
|
|||
using System.IO;
|
||||
using Flow.Launcher.Infrastructure.Http;
|
||||
using JetBrains.Annotations;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.Storage;
|
||||
|
||||
namespace Flow.Launcher
|
||||
{
|
||||
|
|
@ -61,18 +64,15 @@ namespace Flow.Launcher
|
|||
// which will cause ungraceful exit
|
||||
SaveAppAllSettings();
|
||||
|
||||
// Restart requires Squirrel's Update.exe to be present in the parent folder,
|
||||
// it is only published from the project's release pipeline. When debugging without it,
|
||||
// the project may not restart or just terminates. This is expected.
|
||||
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
||||
}
|
||||
|
||||
public void RestarApp()
|
||||
{
|
||||
RestartApp();
|
||||
}
|
||||
public void RestarApp() => RestartApp();
|
||||
|
||||
public void CheckForNewUpdate()
|
||||
{
|
||||
_settingsVM.UpdateApp();
|
||||
}
|
||||
public void CheckForNewUpdate() => _settingsVM.UpdateApp();
|
||||
|
||||
public void SaveAppAllSettings()
|
||||
{
|
||||
|
|
@ -82,15 +82,9 @@ namespace Flow.Launcher
|
|||
ImageLoader.Save();
|
||||
}
|
||||
|
||||
public Task ReloadAllPluginData()
|
||||
{
|
||||
return PluginManager.ReloadData();
|
||||
}
|
||||
public Task ReloadAllPluginData() => PluginManager.ReloadData();
|
||||
|
||||
public void ShowMsg(string title, string subTitle = "", string iconPath = "")
|
||||
{
|
||||
ShowMsg(title, subTitle, iconPath, true);
|
||||
}
|
||||
public void ShowMsg(string title, string subTitle = "", string iconPath = "") => ShowMsg(title, subTitle, iconPath, true);
|
||||
|
||||
public void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true)
|
||||
{
|
||||
|
|
@ -109,54 +103,40 @@ namespace Flow.Launcher
|
|||
});
|
||||
}
|
||||
|
||||
public void StartLoadingBar()
|
||||
{
|
||||
_mainVM.ProgressBarVisibility = Visibility.Visible;
|
||||
}
|
||||
public void StartLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Visible;
|
||||
|
||||
public void StopLoadingBar()
|
||||
{
|
||||
_mainVM.ProgressBarVisibility = Visibility.Collapsed;
|
||||
}
|
||||
public void StopLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Collapsed;
|
||||
|
||||
public string GetTranslation(string key)
|
||||
{
|
||||
return InternationalizationManager.Instance.GetTranslation(key);
|
||||
}
|
||||
public string GetTranslation(string key) => InternationalizationManager.Instance.GetTranslation(key);
|
||||
|
||||
public List<PluginPair> GetAllPlugins()
|
||||
{
|
||||
return PluginManager.AllPlugins.ToList();
|
||||
}
|
||||
|
||||
public event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent;
|
||||
public List<PluginPair> GetAllPlugins() => PluginManager.AllPlugins.ToList();
|
||||
|
||||
public MatchResult FuzzySearch(string query, string stringToCompare) => StringMatcher.FuzzySearch(query, stringToCompare);
|
||||
|
||||
public Task<string> HttpGetStringAsync(string url, CancellationToken token = default)
|
||||
{
|
||||
return Http.GetAsync(url);
|
||||
}
|
||||
public Task<string> HttpGetStringAsync(string url, CancellationToken token = default) => Http.GetAsync(url);
|
||||
|
||||
public Task<Stream> HttpGetStreamAsync(string url, CancellationToken token = default)
|
||||
{
|
||||
return Http.GetStreamAsync(url);
|
||||
}
|
||||
public Task<Stream> HttpGetStreamAsync(string url, CancellationToken token = default) => Http.GetStreamAsync(url);
|
||||
|
||||
public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath)
|
||||
{
|
||||
return Http.DownloadAsync(url, filePath);
|
||||
}
|
||||
public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default) => Http.DownloadAsync(url, filePath, token);
|
||||
|
||||
public void AddActionKeyword(string pluginId, string newActionKeyword)
|
||||
{
|
||||
PluginManager.AddActionKeyword(pluginId, newActionKeyword);
|
||||
}
|
||||
public void AddActionKeyword(string pluginId, string newActionKeyword) => PluginManager.AddActionKeyword(pluginId, newActionKeyword);
|
||||
|
||||
public void RemoveActionKeyword(string pluginId, string oldActionKeyword) => PluginManager.RemoveActionKeyword(pluginId, oldActionKeyword);
|
||||
|
||||
public void LogDebug(string className, string message, [CallerMemberName] string methodName = "") => Log.Debug(className, message, methodName);
|
||||
|
||||
public void LogInfo(string className, string message, [CallerMemberName] string methodName = "") => Log.Info(className, message, methodName);
|
||||
|
||||
public void LogWarn(string className, string message, [CallerMemberName] string methodName = "") => Log.Warn(className, message, methodName);
|
||||
|
||||
public void LogException(string className, string message, Exception e, [CallerMemberName] string methodName = "") => Log.Exception(className, message, e, methodName);
|
||||
|
||||
public T LoadJsonStorage<T>() where T : new() => new PluginJsonStorage<T>().Load();
|
||||
|
||||
public void SaveJsonStorage<T>(T setting) where T : new() => new PluginJsonStorage<T>(setting).Save();
|
||||
|
||||
public event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent;
|
||||
|
||||
public void RemoveActionKeyword(string pluginId, string oldActionKeyword)
|
||||
{
|
||||
PluginManager.RemoveActionKeyword(pluginId, oldActionKeyword);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
|
||||
x:Class="Flow.Launcher.SettingWindow"
|
||||
mc:Ignorable="d"
|
||||
Icon="Images\app.png"
|
||||
Icon="Images\app.ico"
|
||||
Title="{DynamicResource flowlauncher_settings}"
|
||||
ResizeMode="NoResize"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ using Flow.Launcher.Plugin;
|
|||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using Flow.Launcher.Storage;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace Flow.Launcher.ViewModel
|
||||
{
|
||||
|
|
@ -46,7 +47,7 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
private readonly Internationalization _translator = InternationalizationManager.Instance;
|
||||
|
||||
private BufferBlock<ResultsForUpdate> _resultsUpdateQueue;
|
||||
private ChannelWriter<ResultsForUpdate> _resultsUpdateChannelWriter;
|
||||
private Task _resultsViewUpdateTask;
|
||||
|
||||
#endregion
|
||||
|
|
@ -85,29 +86,32 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
private void RegisterViewUpdate()
|
||||
{
|
||||
_resultsUpdateQueue = new BufferBlock<ResultsForUpdate>();
|
||||
var resultUpdateChannel = Channel.CreateUnbounded<ResultsForUpdate>();
|
||||
_resultsUpdateChannelWriter = resultUpdateChannel.Writer;
|
||||
_resultsViewUpdateTask =
|
||||
Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted);
|
||||
|
||||
|
||||
async Task updateAction()
|
||||
{
|
||||
var queue = new Dictionary<string, ResultsForUpdate>();
|
||||
while (await _resultsUpdateQueue.OutputAvailableAsync())
|
||||
var channelReader = resultUpdateChannel.Reader;
|
||||
|
||||
// it is not supposed to be false because it won't be complete
|
||||
while (await channelReader.WaitToReadAsync())
|
||||
{
|
||||
queue.Clear();
|
||||
await Task.Delay(20);
|
||||
while (_resultsUpdateQueue.TryReceive(out var item))
|
||||
while (channelReader.TryRead(out var item))
|
||||
{
|
||||
if (!item.Token.IsCancellationRequested)
|
||||
queue[item.ID] = item;
|
||||
}
|
||||
|
||||
UpdateResultView(queue.Values);
|
||||
queue.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
;
|
||||
Log.Error("MainViewModel", "Unexpected ResultViewUpdate ends");
|
||||
};
|
||||
|
||||
void continueAction(Task t)
|
||||
{
|
||||
|
|
@ -115,8 +119,8 @@ namespace Flow.Launcher.ViewModel
|
|||
throw t.Exception;
|
||||
#else
|
||||
Log.Error($"Error happen in task dealing with viewupdate for results. {t.Exception}");
|
||||
_resultsViewUpdateTask =
|
||||
Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted);
|
||||
_resultsViewUpdateTask =
|
||||
Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
@ -131,7 +135,10 @@ namespace Flow.Launcher.ViewModel
|
|||
if (e.Query.RawQuery == QueryText) // TODO: allow cancellation
|
||||
{
|
||||
PluginManager.UpdatePluginMetadata(e.Results, pair.Metadata, e.Query);
|
||||
_resultsUpdateQueue.Post(new ResultsForUpdate(e.Results, pair.Metadata, e.Query, _updateToken));
|
||||
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(e.Results, pair.Metadata, e.Query, _updateToken)))
|
||||
{
|
||||
Log.Error("MainViewModel", "Unable to add item to Result Update Queue");
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -225,6 +232,25 @@ namespace Flow.Launcher.ViewModel
|
|||
SelectedResults = Results;
|
||||
}
|
||||
});
|
||||
|
||||
ReloadPluginDataCommand = new RelayCommand(_ =>
|
||||
{
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -313,6 +339,7 @@ namespace Flow.Launcher.ViewModel
|
|||
public ICommand LoadContextMenuCommand { get; set; }
|
||||
public ICommand LoadHistoryCommand { get; set; }
|
||||
public ICommand OpenResultCommand { get; set; }
|
||||
public ICommand ReloadPluginDataCommand { get; set; }
|
||||
|
||||
public string OpenResultCommandModifiers { get; private set; }
|
||||
|
||||
|
|
@ -512,9 +539,12 @@ namespace Flow.Launcher.ViewModel
|
|||
await Task.Yield();
|
||||
|
||||
var results = await PluginManager.QueryForPlugin(plugin, query, currentCancellationToken);
|
||||
if (!currentCancellationToken.IsCancellationRequested && results != null)
|
||||
_resultsUpdateQueue.Post(new ResultsForUpdate(results, plugin.Metadata, query,
|
||||
currentCancellationToken));
|
||||
if (currentCancellationToken.IsCancellationRequested || results == null) return;
|
||||
|
||||
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),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ using System.Threading;
|
|||
|
||||
namespace Flow.Launcher.ViewModel
|
||||
{
|
||||
public class ResultsForUpdate
|
||||
public struct ResultsForUpdate
|
||||
{
|
||||
public List<Result> Results { get; }
|
||||
|
||||
|
|
@ -16,13 +16,6 @@ namespace Flow.Launcher.ViewModel
|
|||
public Query Query { get; }
|
||||
public CancellationToken Token { get; }
|
||||
|
||||
public ResultsForUpdate(List<Result> results, string resultID, CancellationToken token)
|
||||
{
|
||||
Results = results;
|
||||
ID = resultID;
|
||||
Token = token;
|
||||
}
|
||||
|
||||
public ResultsForUpdate(List<Result> results, PluginMetadata metadata, Query query, CancellationToken token)
|
||||
{
|
||||
Results = results;
|
||||
|
|
|
|||
|
|
@ -183,13 +183,12 @@ namespace Flow.Launcher.ViewModel
|
|||
private List<ResultViewModel> NewResults(List<Result> newRawResults, string resultId)
|
||||
{
|
||||
if (newRawResults.Count == 0)
|
||||
return Results.ToList();
|
||||
return Results;
|
||||
|
||||
var results = Results as IEnumerable<ResultViewModel>;
|
||||
|
||||
var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings));
|
||||
|
||||
return results.Where(r => r.Result.PluginID != resultId)
|
||||
return Results.Where(r => r.Result.PluginID != resultId)
|
||||
.Concat(newResults)
|
||||
.OrderByDescending(r => r.Result.Score)
|
||||
.ToList();
|
||||
|
|
@ -198,11 +197,9 @@ namespace Flow.Launcher.ViewModel
|
|||
private List<ResultViewModel> NewResults(IEnumerable<ResultsForUpdate> resultsForUpdates)
|
||||
{
|
||||
if (!resultsForUpdates.Any())
|
||||
return Results.ToList();
|
||||
return Results;
|
||||
|
||||
var results = Results as IEnumerable<ResultViewModel>;
|
||||
|
||||
return results.Where(r => r != null && !resultsForUpdates.Any(u => u.Metadata.ID == r.Result.PluginID))
|
||||
return Results.Where(r => r != null && !resultsForUpdates.Any(u => u.ID == r.Result.PluginID))
|
||||
.Concat(resultsForUpdates.SelectMany(u => u.Results, (u, r) => new ResultViewModel(r, _settings)))
|
||||
.OrderByDescending(rv => rv.Result.Score)
|
||||
.ToList();
|
||||
|
|
|
|||
BIN
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Images/copylink.png
Normal file
BIN
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Images/copylink.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -12,4 +12,6 @@
|
|||
<system:String x:Key="flowlauncher_plugin_browserbookmark_settings_newTab">New tab</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_browserbookmark_settings_setBrowserFromPath">Set browser from path:</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_browserbookmark_settings_choose">Choose</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_browserbookmark_copyurl_title">Copy url</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_browserbookmark_copyurl_subtitle">Copy the bookmark's url to clipboard</system:String>
|
||||
</ResourceDictionary>
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.Storage;
|
||||
using Flow.Launcher.Plugin.BrowserBookmark.Commands;
|
||||
using Flow.Launcher.Plugin.BrowserBookmark.Models;
|
||||
|
|
@ -9,7 +12,7 @@ using Flow.Launcher.Plugin.SharedCommands;
|
|||
|
||||
namespace Flow.Launcher.Plugin.BrowserBookmark
|
||||
{
|
||||
public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, ISavable
|
||||
public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, ISavable, IContextMenu
|
||||
{
|
||||
private PluginInitContext context;
|
||||
|
||||
|
|
@ -60,7 +63,8 @@ namespace Flow.Launcher.Plugin.BrowserBookmark
|
|||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
ContextData = new BookmarkAttributes { Url = c.Url }
|
||||
}).Where(r => r.Score > 0);
|
||||
return returnList.ToList();
|
||||
}
|
||||
|
|
@ -84,7 +88,8 @@ namespace Flow.Launcher.Plugin.BrowserBookmark
|
|||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
ContextData = new BookmarkAttributes { Url = c.Url }
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
|
|
@ -115,5 +120,39 @@ namespace Flow.Launcher.Plugin.BrowserBookmark
|
|||
{
|
||||
_storage.Save();
|
||||
}
|
||||
|
||||
public List<Result> LoadContextMenus(Result selectedResult)
|
||||
{
|
||||
return new List<Result>() {
|
||||
new Result
|
||||
{
|
||||
Title = context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copyurl_title"),
|
||||
SubTitle = context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copyurl_subtitle"),
|
||||
Action = _ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Clipboard.SetDataObject(((BookmarkAttributes)selectedResult.ContextData).Url);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
var message = "Failed to set url in clipboard";
|
||||
Log.Exception("Main",message, e, "LoadContextMenus");
|
||||
|
||||
context.API.ShowMsg(message);
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
IcoPath = "Images\\copylink.png"
|
||||
}};
|
||||
}
|
||||
|
||||
internal class BookmarkAttributes
|
||||
{
|
||||
internal string Url { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"Name": "Browser Bookmarks",
|
||||
"Description": "Search your browser bookmarks",
|
||||
"Author": "qianlifeng, Ioannis G.",
|
||||
"Version": "1.3.2",
|
||||
"Version": "1.4.0",
|
||||
"Language": "csharp",
|
||||
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
|
||||
"ExecuteFileName": "Flow.Launcher.Plugin.BrowserBookmark.dll",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\Flow.Launcher.Core\Flow.Launcher.Core.csproj" />
|
||||
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -37,20 +37,10 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
Settings = viewModel.Settings;
|
||||
contextMenu = new ContextMenu(Context);
|
||||
pluginManager = new PluginsManager(Context, Settings);
|
||||
var updateManifestTask = pluginManager.UpdateManifest();
|
||||
_ = updateManifestTask.ContinueWith(t =>
|
||||
{
|
||||
if (t.IsCompletedSuccessfully)
|
||||
{
|
||||
lastUpdateTime = DateTime.Now;
|
||||
}
|
||||
else
|
||||
{
|
||||
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.", pluginManager.icoPath, false);
|
||||
}
|
||||
});
|
||||
_ = pluginManager.UpdateManifest().ContinueWith(_ =>
|
||||
{
|
||||
lastUpdateTime = DateTime.Now;
|
||||
}, TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
|
@ -69,15 +59,17 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
|
||||
if ((DateTime.Now - lastUpdateTime).TotalHours > 12) // 12 hours
|
||||
{
|
||||
await pluginManager.UpdateManifest();
|
||||
lastUpdateTime = DateTime.Now;
|
||||
_ = pluginManager.UpdateManifest().ContinueWith(t =>
|
||||
{
|
||||
lastUpdateTime = DateTime.Now;
|
||||
}, TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||
}
|
||||
|
||||
return search switch
|
||||
{
|
||||
var s when s.StartsWith(Settings.HotKeyInstall) => await pluginManager.RequestInstallOrUpdate(s, token),
|
||||
var s when s.StartsWith(Settings.HotkeyUninstall) => pluginManager.RequestUninstall(s),
|
||||
var s when s.StartsWith(Settings.HotkeyUpdate) => pluginManager.RequestUpdate(s),
|
||||
var s when s.StartsWith(Settings.HotkeyUpdate) => await pluginManager.RequestUpdate(s, token),
|
||||
_ => pluginManager.GetDefaultHotKeys().Where(hotkey =>
|
||||
{
|
||||
hotkey.Score = StringMatcher.FuzzySearch(search, hotkey.Title).Score;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.Http;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
|
|
@ -47,9 +48,23 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
Settings = settings;
|
||||
}
|
||||
|
||||
internal async Task UpdateManifest()
|
||||
private Task _downloadManifestTask = Task.CompletedTask;
|
||||
|
||||
|
||||
internal Task UpdateManifest()
|
||||
{
|
||||
await pluginsManifest.DownloadManifest();
|
||||
if (_downloadManifestTask.Status == TaskStatus.Running)
|
||||
{
|
||||
return _downloadManifestTask;
|
||||
}
|
||||
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),
|
||||
TaskContinuationOptions.OnlyOnFaulted);
|
||||
}
|
||||
}
|
||||
|
||||
internal List<Result> GetDefaultHotKeys()
|
||||
|
|
@ -150,8 +165,15 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
Context.API.RestartApp();
|
||||
}
|
||||
|
||||
internal List<Result> RequestUpdate(string search)
|
||||
internal async ValueTask<List<Result>> RequestUpdate(string search, CancellationToken token)
|
||||
{
|
||||
if (!pluginsManifest.UserPlugins.Any())
|
||||
{
|
||||
await UpdateManifest();
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var autocompletedResults = AutoCompleteReturnAllResults(search,
|
||||
Settings.HotkeyUpdate,
|
||||
"Update",
|
||||
|
|
@ -275,20 +297,14 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
.ToList();
|
||||
}
|
||||
|
||||
private Task _downloadManifestTask = Task.CompletedTask;
|
||||
|
||||
internal async ValueTask<List<Result>> RequestInstallOrUpdate(string searchName, CancellationToken token)
|
||||
{
|
||||
if (!pluginsManifest.UserPlugins.Any() &&
|
||||
_downloadManifestTask.Status != TaskStatus.Running)
|
||||
if (!pluginsManifest.UserPlugins.Any())
|
||||
{
|
||||
_downloadManifestTask = pluginsManifest.DownloadManifest();
|
||||
await UpdateManifest();
|
||||
}
|
||||
|
||||
await _downloadManifestTask;
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return null;
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var searchNameWithoutKeyword = searchName.Replace(Settings.HotKeyInstall, string.Empty).Trim();
|
||||
|
||||
|
|
@ -400,6 +416,9 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
|
||||
private void Uninstall(PluginMetadata plugin)
|
||||
{
|
||||
PluginManager.Settings.Plugins.Remove(plugin.ID);
|
||||
PluginManager.AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID);
|
||||
|
||||
// Marked for deletion. Will be deleted on next start up
|
||||
using var _ = File.CreateText(Path.Combine(plugin.PluginDirectory, "NeedDelete.txt"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
"Name": "Plugins Manager",
|
||||
"Description": "Management of installing, uninstalling or updating Flow Launcher plugins",
|
||||
"Author": "Jeremy Wu",
|
||||
"Version": "1.6.3",
|
||||
"Version": "1.7.0",
|
||||
"Language": "csharp",
|
||||
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
|
||||
"ExecuteFileName": "Flow.Launcher.Plugin.PluginsManager.dll",
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -40,17 +40,6 @@
|
|||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="AppxPackagingTlb">
|
||||
<HintPath>.\AppxPackagingTlb.dll</HintPath>
|
||||
<EmbedInteropTypes>True</EmbedInteropTypes>
|
||||
</Reference>
|
||||
<Reference Include="ShObjIdlTlb">
|
||||
<HintPath>.\ShObjIdlTlb.dll</HintPath>
|
||||
<EmbedInteropTypes>True</EmbedInteropTypes>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Languages\*.xaml">
|
||||
|
|
|
|||
|
|
@ -228,7 +228,6 @@ namespace Flow.Launcher.Plugin.Program
|
|||
|
||||
public static void StartProcess(Func<ProcessStartInfo, Process> runProcess, ProcessStartInfo info)
|
||||
{
|
||||
bool hide;
|
||||
try
|
||||
{
|
||||
runProcess(info);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Flow.Launcher.Plugin.Program.Programs
|
||||
{
|
||||
public class ApplicationActivationHelper
|
||||
{
|
||||
// Reference : https://github.com/MicrosoftEdge/edge-launcher/blob/108e63df0b4cb5cd9d5e45aa7a264690851ec51d/MIcrosoftEdgeLauncherCsharp/Program.cs
|
||||
public enum ActivateOptions
|
||||
{
|
||||
None = 0x00000000,
|
||||
DesignMode = 0x00000001,
|
||||
NoErrorUI = 0x00000002,
|
||||
NoSplashScreen = 0x00000004,
|
||||
}
|
||||
|
||||
/// ApplicationActivationManager
|
||||
[ComImport, Guid("2e941141-7f97-4756-ba1d-9decde894a3d"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
interface IApplicationActivationManager
|
||||
{
|
||||
IntPtr ActivateApplication([In] string appUserModelId, [In] string arguments, [In] ActivateOptions options, [Out] out uint processId);
|
||||
IntPtr ActivateForFile([In] string appUserModelId, [In] IntPtr /*IShellItemArray* */ itemArray, [In] string verb, [Out] out uint processId);
|
||||
IntPtr ActivateForProtocol([In] string appUserModelId, [In] IntPtr /* IShellItemArray* */itemArray, [Out] out uint processId);
|
||||
}
|
||||
|
||||
// Application Activation Manager Class
|
||||
[ComImport, Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C")]
|
||||
public class ApplicationActivationManager : IApplicationActivationManager
|
||||
{
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)/*, PreserveSig*/]
|
||||
public extern IntPtr ActivateApplication([In] string appUserModelId, [In] string arguments, [In] ActivateOptions options, [Out] out uint processId);
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
|
||||
public extern IntPtr ActivateForFile([In] string appUserModelId, [In] IntPtr /*IShellItemArray* */ itemArray, [In] string verb, [Out] out uint processId);
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
|
||||
public extern IntPtr ActivateForProtocol([In] string appUserModelId, [In] IntPtr /* IShellItemArray* */itemArray, [Out] out uint processId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.ComTypes;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace Flow.Launcher.Plugin.Program.Programs
|
||||
{
|
||||
public class AppxPackageHelper
|
||||
{
|
||||
// This function returns a list of attributes of applications
|
||||
public List<IAppxManifestApplication> getAppsFromManifest(IStream stream)
|
||||
{
|
||||
List<IAppxManifestApplication> apps = new List<IAppxManifestApplication>();
|
||||
var appxFactory = new AppxFactory();
|
||||
var reader = ((IAppxFactory)appxFactory).CreateManifestReader(stream);
|
||||
var manifestApps = reader.GetApplications();
|
||||
while (manifestApps.GetHasCurrent())
|
||||
{
|
||||
string appListEntry;
|
||||
var manifestApp = manifestApps.GetCurrent();
|
||||
manifestApp.GetStringValue("AppListEntry", out appListEntry);
|
||||
if (appListEntry != "none")
|
||||
{
|
||||
apps.Add(manifestApp);
|
||||
}
|
||||
manifestApps.MoveNext();
|
||||
}
|
||||
return apps;
|
||||
}
|
||||
|
||||
// Reference : https://stackoverflow.com/questions/32122679/getting-icon-of-modern-windows-app-from-a-desktop-application
|
||||
[Guid("5842a140-ff9f-4166-8f5c-62f5b7b0c781"), ComImport]
|
||||
public class AppxFactory
|
||||
{
|
||||
}
|
||||
|
||||
[Guid("BEB94909-E451-438B-B5A7-D79E767B75D8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAppxFactory
|
||||
{
|
||||
void _VtblGap0_2(); // skip 2 methods
|
||||
IAppxManifestReader CreateManifestReader(IStream inputStream);
|
||||
}
|
||||
|
||||
[Guid("4E1BD148-55A0-4480-A3D1-15544710637C"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAppxManifestReader
|
||||
{
|
||||
void _VtblGap0_1(); // skip 1 method
|
||||
IAppxManifestProperties GetProperties();
|
||||
void _VtblGap1_5(); // skip 5 methods
|
||||
IAppxManifestApplicationsEnumerator GetApplications();
|
||||
}
|
||||
|
||||
[Guid("9EB8A55A-F04B-4D0D-808D-686185D4847A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAppxManifestApplicationsEnumerator
|
||||
{
|
||||
IAppxManifestApplication GetCurrent();
|
||||
bool GetHasCurrent();
|
||||
bool MoveNext();
|
||||
}
|
||||
|
||||
[Guid("5DA89BF4-3773-46BE-B650-7E744863B7E8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAppxManifestApplication
|
||||
{
|
||||
[PreserveSig]
|
||||
int GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string name, [MarshalAs(UnmanagedType.LPWStr)] out string value);
|
||||
|
||||
[PreserveSig]
|
||||
int GetAppUserModelId([MarshalAs(UnmanagedType.LPWStr)] out string value);
|
||||
}
|
||||
|
||||
[Guid("03FAF64D-F26F-4B2C-AAF7-8FE7789B8BCA"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAppxManifestProperties
|
||||
{
|
||||
[PreserveSig]
|
||||
int GetBoolValue([MarshalAs(UnmanagedType.LPWStr)]string name, out bool value);
|
||||
[PreserveSig]
|
||||
int GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string name, [MarshalAs(UnmanagedType.LPWStr)] out string value);
|
||||
}
|
||||
}
|
||||
}
|
||||
132
Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs
Normal file
132
Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.IO;
|
||||
using Accessibility;
|
||||
using System.Runtime.InteropServices.ComTypes;
|
||||
using System.Security.Policy;
|
||||
|
||||
namespace Flow.Launcher.Plugin.Program.Programs
|
||||
{
|
||||
class ShellLinkHelper
|
||||
{
|
||||
[Flags()]
|
||||
public enum SLGP_FLAGS
|
||||
{
|
||||
SLGP_SHORTPATH = 0x1,
|
||||
SLGP_UNCPRIORITY = 0x2,
|
||||
SLGP_RAWPATH = 0x4
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
|
||||
public struct WIN32_FIND_DATAW
|
||||
{
|
||||
public uint dwFileAttributes;
|
||||
public long ftCreationTime;
|
||||
public long ftLastAccessTime;
|
||||
public long ftLastWriteTime;
|
||||
public uint nFileSizeHigh;
|
||||
public uint nFileSizeLow;
|
||||
public uint dwReserved0;
|
||||
public uint dwReserved1;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
|
||||
public string cFileName;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
|
||||
public string cAlternateFileName;
|
||||
}
|
||||
|
||||
[Flags()]
|
||||
public enum SLR_FLAGS
|
||||
{
|
||||
SLR_NO_UI = 0x1,
|
||||
SLR_ANY_MATCH = 0x2,
|
||||
SLR_UPDATE = 0x4,
|
||||
SLR_NOUPDATE = 0x8,
|
||||
SLR_NOSEARCH = 0x10,
|
||||
SLR_NOTRACK = 0x20,
|
||||
SLR_NOLINKINFO = 0x40,
|
||||
SLR_INVOKE_MSI = 0x80
|
||||
}
|
||||
|
||||
|
||||
// Reference : http://www.pinvoke.net/default.aspx/Interfaces.IShellLinkW
|
||||
/// The IShellLink interface allows Shell links to be created, modified, and resolved
|
||||
[ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("000214F9-0000-0000-C000-000000000046")]
|
||||
interface IShellLinkW
|
||||
{
|
||||
/// <summary>Retrieves the path and file name of a Shell link object</summary>
|
||||
void GetPath([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, ref WIN32_FIND_DATAW pfd, SLGP_FLAGS fFlags);
|
||||
/// <summary>Retrieves the list of item identifiers for a Shell link object</summary>
|
||||
void GetIDList(out IntPtr ppidl);
|
||||
/// <summary>Sets the pointer to an item identifier list (PIDL) for a Shell link object.</summary>
|
||||
void SetIDList(IntPtr pidl);
|
||||
/// <summary>Retrieves the description string for a Shell link object</summary>
|
||||
void GetDescription([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
|
||||
/// <summary>Sets the description for a Shell link object. The description can be any application-defined string</summary>
|
||||
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
|
||||
/// <summary>Retrieves the name of the working directory for a Shell link object</summary>
|
||||
void GetWorkingDirectory([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
|
||||
/// <summary>Sets the name of the working directory for a Shell link object</summary>
|
||||
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
|
||||
/// <summary>Retrieves the command-line arguments associated with a Shell link object</summary>
|
||||
void GetArguments([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
|
||||
/// <summary>Sets the command-line arguments for a Shell link object</summary>
|
||||
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
|
||||
/// <summary>Retrieves the hot key for a Shell link object</summary>
|
||||
void GetHotkey(out short pwHotkey);
|
||||
/// <summary>Sets a hot key for a Shell link object</summary>
|
||||
void SetHotkey(short wHotkey);
|
||||
/// <summary>Retrieves the show command for a Shell link object</summary>
|
||||
void GetShowCmd(out int piShowCmd);
|
||||
/// <summary>Sets the show command for a Shell link object. The show command sets the initial show state of the window.</summary>
|
||||
void SetShowCmd(int iShowCmd);
|
||||
/// <summary>Retrieves the location (path and index) of the icon for a Shell link object</summary>
|
||||
void GetIconLocation([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath,
|
||||
int cchIconPath, out int piIcon);
|
||||
/// <summary>Sets the location (path and index) of the icon for a Shell link object</summary>
|
||||
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
|
||||
/// <summary>Sets the relative path to the Shell link object</summary>
|
||||
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved);
|
||||
/// <summary>Attempts to find the target of a Shell link, even if it has been moved or renamed</summary>
|
||||
void Resolve(ref Accessibility._RemotableHandle hwnd, SLR_FLAGS fFlags);
|
||||
/// <summary>Sets the path and file name of a Shell link object</summary>
|
||||
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
|
||||
}
|
||||
|
||||
[ComImport(), Guid("00021401-0000-0000-C000-000000000046")]
|
||||
public class ShellLink
|
||||
{
|
||||
}
|
||||
|
||||
// To initialize the app description
|
||||
public String description = String.Empty;
|
||||
|
||||
|
||||
// Retrieve the target path using Shell Link
|
||||
public string retrieveTargetPath(string path)
|
||||
{
|
||||
var link = new ShellLink();
|
||||
const int STGM_READ = 0;
|
||||
((IPersistFile)link).Load(path, STGM_READ);
|
||||
var hwnd = new _RemotableHandle();
|
||||
((IShellLinkW)link).Resolve(ref hwnd, 0);
|
||||
|
||||
const int MAX_PATH = 260;
|
||||
StringBuilder buffer = new StringBuilder(MAX_PATH);
|
||||
|
||||
var data = new WIN32_FIND_DATAW();
|
||||
((IShellLinkW)link).GetPath(buffer, buffer.Capacity, ref data, SLGP_FLAGS.SLGP_SHORTPATH);
|
||||
var target = buffer.ToString();
|
||||
|
||||
// To set the app description
|
||||
if (!String.IsNullOrEmpty(target))
|
||||
{
|
||||
buffer = new StringBuilder(MAX_PATH);
|
||||
((IShellLinkW)link).GetDescription(buffer, MAX_PATH);
|
||||
description = buffer.ToString();
|
||||
}
|
||||
return target;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ using System.Diagnostics;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.ComTypes;
|
||||
using System.Security.Principal;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -12,11 +13,8 @@ using System.Windows.Media.Imaging;
|
|||
using System.Xml.Linq;
|
||||
using Windows.ApplicationModel;
|
||||
using Windows.Management.Deployment;
|
||||
using AppxPackaing;
|
||||
using Shell;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Plugin.Program.Logger;
|
||||
using IStream = AppxPackaing.IStream;
|
||||
using Rect = System.Windows.Rect;
|
||||
using Flow.Launcher.Plugin.SharedModels;
|
||||
|
||||
|
|
@ -52,33 +50,27 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
|
||||
private void InitializeAppInfo()
|
||||
{
|
||||
AppxPackageHelper _helper = new AppxPackageHelper();
|
||||
var path = Path.Combine(Location, "AppxManifest.xml");
|
||||
|
||||
var namespaces = XmlNamespaces(path);
|
||||
InitPackageVersion(namespaces);
|
||||
|
||||
var appxFactory = new AppxFactory();
|
||||
IStream stream;
|
||||
const uint noAttribute = 0x80;
|
||||
const Stgm exclusiveRead = Stgm.Read | Stgm.ShareExclusive;
|
||||
var hResult = SHCreateStreamOnFileEx(path, exclusiveRead, noAttribute, false, null, out stream);
|
||||
var hResult = SHCreateStreamOnFileEx(path, exclusiveRead, noAttribute, false, null, out IStream stream);
|
||||
|
||||
if (hResult == Hresult.Ok)
|
||||
{
|
||||
var reader = appxFactory.CreateManifestReader(stream);
|
||||
var manifestApps = reader.GetApplications();
|
||||
var apps = new List<Application>();
|
||||
while (manifestApps.GetHasCurrent() != 0)
|
||||
|
||||
List<AppxPackageHelper.IAppxManifestApplication> _apps = _helper.getAppsFromManifest(stream);
|
||||
foreach(var _app in _apps)
|
||||
{
|
||||
var manifestApp = manifestApps.GetCurrent();
|
||||
var appListEntry = manifestApp.GetStringValue("AppListEntry");
|
||||
if (appListEntry != "none")
|
||||
{
|
||||
var app = new Application(manifestApp, this);
|
||||
apps.Add(app);
|
||||
}
|
||||
manifestApps.MoveNext();
|
||||
var app = new Application(_app, this);
|
||||
apps.Add(app);
|
||||
}
|
||||
|
||||
Apps = apps.Where(a => a.AppListEntry != "none").ToArray();
|
||||
}
|
||||
else
|
||||
|
|
@ -262,6 +254,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
public string UserModelId { get; set; }
|
||||
public string BackgroundColor { get; set; }
|
||||
|
||||
public string EntryPoint { get; set; }
|
||||
public string Name => DisplayName;
|
||||
public string Location => Package.Location;
|
||||
|
||||
|
|
@ -358,15 +351,14 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
|
||||
private async void Launch(IPublicAPI api)
|
||||
{
|
||||
var appManager = new ApplicationActivationManager();
|
||||
uint unusedPid;
|
||||
var appManager = new ApplicationActivationHelper.ApplicationActivationManager();
|
||||
const string noArgs = "";
|
||||
const ACTIVATEOPTIONS noFlags = ACTIVATEOPTIONS.AO_NONE;
|
||||
const ApplicationActivationHelper.ActivateOptions noFlags = ApplicationActivationHelper.ActivateOptions.None;
|
||||
await Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
appManager.ActivateApplication(UserModelId, noArgs, noFlags, out unusedPid);
|
||||
appManager.ActivateApplication(UserModelId, noArgs, noFlags, out _);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
|
@ -377,13 +369,24 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
});
|
||||
}
|
||||
|
||||
public Application(IAppxManifestApplication manifestApp, UWP package)
|
||||
public Application(AppxPackageHelper.IAppxManifestApplication manifestApp, UWP package)
|
||||
{
|
||||
UserModelId = manifestApp.GetAppUserModelId();
|
||||
UniqueIdentifier = manifestApp.GetAppUserModelId();
|
||||
DisplayName = manifestApp.GetStringValue("DisplayName");
|
||||
Description = manifestApp.GetStringValue("Description");
|
||||
BackgroundColor = manifestApp.GetStringValue("BackgroundColor");
|
||||
// This is done because we cannot use the keyword 'out' along with a property
|
||||
|
||||
manifestApp.GetAppUserModelId(out string tmpUserModelId);
|
||||
manifestApp.GetAppUserModelId(out string tmpUniqueIdentifier);
|
||||
manifestApp.GetStringValue("DisplayName", out string tmpDisplayName);
|
||||
manifestApp.GetStringValue("Description", out string tmpDescription);
|
||||
manifestApp.GetStringValue("BackgroundColor", out string tmpBackgroundColor);
|
||||
manifestApp.GetStringValue("EntryPoint", out string tmpEntryPoint);
|
||||
|
||||
UserModelId = tmpUserModelId;
|
||||
UniqueIdentifier = tmpUniqueIdentifier;
|
||||
DisplayName = tmpDisplayName;
|
||||
Description = tmpDescription;
|
||||
BackgroundColor = tmpBackgroundColor;
|
||||
EntryPoint = tmpEntryPoint;
|
||||
|
||||
Package = package;
|
||||
|
||||
DisplayName = ResourceFromPri(package.FullName, package.Name, DisplayName);
|
||||
|
|
@ -451,7 +454,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
return $"{prefix}//{packageName}{key}";
|
||||
}
|
||||
|
||||
internal string LogoUriFromManifest(IAppxManifestApplication app)
|
||||
internal string LogoUriFromManifest(AppxPackageHelper.IAppxManifestApplication app)
|
||||
{
|
||||
var logoKeyFromVersion = new Dictionary<PackageVersion, string>
|
||||
{
|
||||
|
|
@ -462,7 +465,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
if (logoKeyFromVersion.ContainsKey(Package.Version))
|
||||
{
|
||||
var key = logoKeyFromVersion[Package.Version];
|
||||
var logoUri = app.GetStringValue(key);
|
||||
app.GetStringValue(key, out string logoUri);
|
||||
return logoUri;
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
|
@ -8,11 +7,13 @@ using System.Security;
|
|||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Win32;
|
||||
using Shell;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Plugin.Program.Logger;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using Flow.Launcher.Plugin.SharedModels;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using System.Diagnostics;
|
||||
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
|
||||
|
||||
namespace Flow.Launcher.Plugin.Program.Programs
|
||||
{
|
||||
|
|
@ -23,6 +24,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
public string UniqueIdentifier { get; set; }
|
||||
public string IcoPath { get; set; }
|
||||
public string FullPath { get; set; }
|
||||
public string LnkResolvedPath { get; set; }
|
||||
public string ParentDirectory { get; set; }
|
||||
public string ExecutableName { get; set; }
|
||||
public string Description { get; set; }
|
||||
|
|
@ -31,49 +33,64 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
public string Location => ParentDirectory;
|
||||
|
||||
private const string ShortcutExtension = "lnk";
|
||||
private const string ApplicationReferenceExtension = "appref-ms";
|
||||
private const string ExeExtension = "exe";
|
||||
|
||||
|
||||
public Result Result(string query, IPublicAPI api)
|
||||
{
|
||||
string title;
|
||||
MatchResult matchResult;
|
||||
|
||||
var nameMatchResult = StringMatcher.FuzzySearch(query, Name);
|
||||
var descriptionMatchResult = StringMatcher.FuzzySearch(query, Description);
|
||||
|
||||
var pathMatchResult = new MatchResult(false, 0, new List<int>(), 0);
|
||||
if (ExecutableName != null) // only lnk program will need this one
|
||||
pathMatchResult = StringMatcher.FuzzySearch(query, ExecutableName);
|
||||
|
||||
MatchResult matchResult = nameMatchResult;
|
||||
|
||||
if (nameMatchResult.Score < descriptionMatchResult.Score)
|
||||
matchResult = descriptionMatchResult;
|
||||
|
||||
if (!matchResult.IsSearchPrecisionScoreMet())
|
||||
{
|
||||
if (pathMatchResult.IsSearchPrecisionScoreMet())
|
||||
matchResult = pathMatchResult;
|
||||
else return null;
|
||||
}
|
||||
|
||||
// We suppose Name won't be null
|
||||
if (Description == null || Name.StartsWith(Description))
|
||||
{
|
||||
title = Name;
|
||||
matchResult = StringMatcher.FuzzySearch(query, title);
|
||||
}
|
||||
else if (Description.StartsWith(Name))
|
||||
{
|
||||
title = Description;
|
||||
matchResult = StringMatcher.FuzzySearch(query, Description);
|
||||
}
|
||||
else
|
||||
{
|
||||
title = $"{Name}: {Description}";
|
||||
var nameMatch = StringMatcher.FuzzySearch(query, Name);
|
||||
var desciptionMatch = StringMatcher.FuzzySearch(query, Description);
|
||||
if (desciptionMatch.Score > nameMatch.Score)
|
||||
|
||||
if (matchResult == descriptionMatchResult)
|
||||
{
|
||||
for (int i = 0; i < desciptionMatch.MatchData.Count; i++)
|
||||
for (int i = 0; i < descriptionMatchResult.MatchData.Count; i++)
|
||||
{
|
||||
desciptionMatch.MatchData[i] += Name.Length + 2; // 2 is ": "
|
||||
matchResult.MatchData[i] += Name.Length + 2; // 2 is ": "
|
||||
}
|
||||
matchResult = desciptionMatch;
|
||||
}
|
||||
else matchResult = nameMatch;
|
||||
}
|
||||
|
||||
if (!matchResult.Success) return null;
|
||||
|
||||
if (matchResult == pathMatchResult)
|
||||
{
|
||||
// path Match won't have valid highlight data
|
||||
matchResult.MatchData = new List<int>();
|
||||
}
|
||||
|
||||
var result = new Result
|
||||
{
|
||||
Title = title,
|
||||
SubTitle = FullPath,
|
||||
SubTitle = LnkResolvedPath ?? FullPath,
|
||||
IcoPath = IcoPath,
|
||||
Score = matchResult.Score,
|
||||
TitleHighlightData = matchResult.MatchData,
|
||||
|
|
@ -82,7 +99,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
{
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
FileName = FullPath,
|
||||
FileName = LnkResolvedPath ?? FullPath,
|
||||
WorkingDirectory = ParentDirectory,
|
||||
UseShellExecute = true
|
||||
};
|
||||
|
|
@ -144,19 +161,19 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
Action = _ =>
|
||||
{
|
||||
var args = !string.IsNullOrWhiteSpace(Main._settings.CustomizedArgs)
|
||||
? Main._settings.CustomizedArgs
|
||||
.Replace("%s",$"\"{ParentDirectory}\"")
|
||||
.Replace("%f",$"\"{FullPath}\"")
|
||||
: Main._settings.CustomizedExplorer==Settings.Explorer
|
||||
? $"/select,\"{FullPath}\""
|
||||
: Settings.ExplorerArgs;
|
||||
? Main._settings.CustomizedArgs
|
||||
.Replace("%s", $"\"{ParentDirectory}\"")
|
||||
.Replace("%f", $"\"{FullPath}\"")
|
||||
: Main._settings.CustomizedExplorer == Settings.Explorer
|
||||
? $"/select,\"{FullPath}\""
|
||||
: Settings.ExplorerArgs;
|
||||
|
||||
Main.StartProcess(Process.Start,
|
||||
new ProcessStartInfo(
|
||||
!string.IsNullOrWhiteSpace(Main._settings.CustomizedExplorer)
|
||||
? Main._settings.CustomizedExplorer
|
||||
: Settings.Explorer,
|
||||
args));
|
||||
new ProcessStartInfo(
|
||||
!string.IsNullOrWhiteSpace(Main._settings.CustomizedExplorer)
|
||||
? Main._settings.CustomizedExplorer
|
||||
: Settings.Explorer,
|
||||
args));
|
||||
|
||||
return true;
|
||||
},
|
||||
|
|
@ -167,10 +184,9 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
}
|
||||
|
||||
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return ExecutableName;
|
||||
return Name;
|
||||
}
|
||||
|
||||
private static Win32 Win32Program(string path)
|
||||
|
|
@ -193,7 +209,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
|
||||
{
|
||||
ProgramLogger.LogException($"|Win32|Win32Program|{path}" +
|
||||
$"|Permission denied when trying to load the program from {path}", e);
|
||||
$"|Permission denied when trying to load the program from {path}", e);
|
||||
|
||||
return new Win32() { Valid = false, Enabled = false };
|
||||
}
|
||||
|
|
@ -204,27 +220,21 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
var program = Win32Program(path);
|
||||
try
|
||||
{
|
||||
var link = new ShellLink();
|
||||
const uint STGM_READ = 0;
|
||||
((IPersistFile)link).Load(path, STGM_READ);
|
||||
var hwnd = new _RemotableHandle();
|
||||
link.Resolve(ref hwnd, 0);
|
||||
|
||||
const int MAX_PATH = 260;
|
||||
StringBuilder buffer = new StringBuilder(MAX_PATH);
|
||||
ShellLinkHelper _helper = new ShellLinkHelper();
|
||||
string target = _helper.retrieveTargetPath(path);
|
||||
|
||||
var data = new _WIN32_FIND_DATAW();
|
||||
const uint SLGP_SHORTPATH = 1;
|
||||
link.GetPath(buffer, buffer.Capacity, ref data, SLGP_SHORTPATH);
|
||||
var target = buffer.ToString();
|
||||
if (!string.IsNullOrEmpty(target))
|
||||
{
|
||||
var extension = Extension(target);
|
||||
if (extension == ExeExtension && File.Exists(target))
|
||||
{
|
||||
buffer = new StringBuilder(MAX_PATH);
|
||||
link.GetDescription(buffer, MAX_PATH);
|
||||
var description = buffer.ToString();
|
||||
program.LnkResolvedPath = program.FullPath;
|
||||
program.FullPath = Path.GetFullPath(target).ToLower();
|
||||
program.ExecutableName = Path.GetFileName(target);
|
||||
|
||||
var description = _helper.description;
|
||||
if (!string.IsNullOrEmpty(description))
|
||||
{
|
||||
program.Description = description;
|
||||
|
|
@ -239,13 +249,15 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return program;
|
||||
}
|
||||
catch (COMException e)
|
||||
{
|
||||
// C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\MiracastView.lnk always cause exception
|
||||
ProgramLogger.LogException($"|Win32|LnkProgram|{path}" +
|
||||
"|Error caused likely due to trying to get the description of the program", e);
|
||||
"|Error caused likely due to trying to get the description of the program",
|
||||
e);
|
||||
|
||||
program.Valid = false;
|
||||
return program;
|
||||
|
|
@ -275,7 +287,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
|
||||
{
|
||||
ProgramLogger.LogException($"|Win32|ExeProgram|{path}" +
|
||||
$"|Permission denied when trying to load the program from {path}", e);
|
||||
$"|Permission denied when trying to load the program from {path}", e);
|
||||
|
||||
return new Win32() { Valid = false, Enabled = false };
|
||||
}
|
||||
|
|
@ -284,28 +296,13 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
private static IEnumerable<string> ProgramPaths(string directory, string[] suffixes)
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
return new string[] { };
|
||||
try
|
||||
{
|
||||
var paths = Directory.EnumerateFiles(directory, "*", new EnumerationOptions
|
||||
{
|
||||
IgnoreInaccessible = true,
|
||||
RecurseSubdirectories = true
|
||||
})
|
||||
.Where(x => suffixes.Contains(Extension(x)));
|
||||
return Enumerable.Empty<string>();
|
||||
|
||||
return paths;
|
||||
}
|
||||
catch (DirectoryNotFoundException e)
|
||||
return Directory.EnumerateFiles(directory, "*", new EnumerationOptions
|
||||
{
|
||||
ProgramLogger.LogException($"Directory not found {directory}", e);
|
||||
return new string[] { };
|
||||
}
|
||||
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
|
||||
{
|
||||
ProgramLogger.LogException($"Permission denied {directory}", e);
|
||||
return new string[] { };
|
||||
}
|
||||
IgnoreInaccessible = true,
|
||||
RecurseSubdirectories = true
|
||||
}).Where(x => suffixes.Contains(Extension(x)));
|
||||
}
|
||||
|
||||
private static string Extension(string path)
|
||||
|
|
@ -321,14 +318,13 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
}
|
||||
}
|
||||
|
||||
private static ParallelQuery<Win32> UnregisteredPrograms(List<Settings.ProgramSource> sources, string[] suffixes)
|
||||
private static IEnumerable<Win32> UnregisteredPrograms(List<Settings.ProgramSource> sources, string[] suffixes)
|
||||
{
|
||||
var paths = sources.Where(s => Directory.Exists(s.Location) && s.Enabled)
|
||||
.SelectMany(s => ProgramPaths(s.Location, suffixes))
|
||||
.Where(t1 => !Main._settings.DisabledProgramSources.Any(x => t1 == x.UniqueIdentifier))
|
||||
var paths = ExceptDisabledSource(sources.Where(s => Directory.Exists(s.Location) && s.Enabled)
|
||||
.SelectMany(s => ProgramPaths(s.Location, suffixes)), x => x)
|
||||
.Distinct();
|
||||
|
||||
var programs = paths.AsParallel().Select(x => Extension(x) switch
|
||||
var programs = paths.Select(x => Extension(x) switch
|
||||
{
|
||||
ExeExtension => ExeProgram(x),
|
||||
ShortcutExtension => LnkProgram(x),
|
||||
|
|
@ -339,7 +335,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
return programs;
|
||||
}
|
||||
|
||||
private static ParallelQuery<Win32> StartMenuPrograms(string[] suffixes)
|
||||
private static IEnumerable<Win32> StartMenuPrograms(string[] suffixes)
|
||||
{
|
||||
var disabledProgramsList = Main._settings.DisabledProgramSources;
|
||||
|
||||
|
|
@ -350,53 +346,49 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
|
||||
var toFilter = paths1.Concat(paths2);
|
||||
|
||||
var programs = toFilter
|
||||
.AsParallel()
|
||||
.Where(t1 => !disabledProgramsList.Any(x => x.UniqueIdentifier == t1))
|
||||
.Distinct()
|
||||
.Select(x => Extension(x) switch
|
||||
{
|
||||
ShortcutExtension => LnkProgram(x),
|
||||
_ => Win32Program(x)
|
||||
}).Where(x => x.Valid);
|
||||
var programs = ExceptDisabledSource(toFilter.Distinct())
|
||||
.Select(x => Extension(x) switch
|
||||
{
|
||||
ShortcutExtension => LnkProgram(x),
|
||||
_ => Win32Program(x)
|
||||
}).Where(x => x.Valid);
|
||||
return programs;
|
||||
}
|
||||
|
||||
private static ParallelQuery<Win32> AppPathsPrograms(string[] suffixes)
|
||||
private static IEnumerable<Win32> AppPathsPrograms(string[] suffixes)
|
||||
{
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/ee872121
|
||||
const string appPaths = @"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths";
|
||||
var programs = new List<Win32>();
|
||||
using (var root = Registry.LocalMachine.OpenSubKey(appPaths))
|
||||
|
||||
IEnumerable<string> toFilter = Enumerable.Empty<string>();
|
||||
|
||||
using var rootMachine = Registry.LocalMachine.OpenSubKey(appPaths);
|
||||
using var rootUser = Registry.CurrentUser.OpenSubKey(appPaths);
|
||||
|
||||
if (rootMachine != null)
|
||||
{
|
||||
if (root != null)
|
||||
{
|
||||
programs.AddRange(GetProgramsFromRegistry(root));
|
||||
}
|
||||
}
|
||||
using (var root = Registry.CurrentUser.OpenSubKey(appPaths))
|
||||
{
|
||||
if (root != null)
|
||||
{
|
||||
programs.AddRange(GetProgramsFromRegistry(root));
|
||||
}
|
||||
toFilter = toFilter.Concat(GetPathFromRegistry(rootMachine));
|
||||
}
|
||||
|
||||
var disabledProgramsList = Main._settings.DisabledProgramSources;
|
||||
var toFilter = programs.AsParallel().Where(p => suffixes.Contains(Extension(p.ExecutableName)));
|
||||
if (rootUser != null)
|
||||
{
|
||||
toFilter = toFilter.Concat(GetPathFromRegistry(rootUser));
|
||||
}
|
||||
|
||||
var filtered = toFilter.Where(t1 => !disabledProgramsList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier)).Select(t1 => t1);
|
||||
|
||||
return filtered;
|
||||
toFilter = toFilter.Distinct().Where(p => suffixes.Contains(Extension(p)));
|
||||
|
||||
var filtered = ExceptDisabledSource(toFilter);
|
||||
|
||||
return filtered.Select(GetProgramFromPath).ToList(); // ToList due to disposing issue
|
||||
}
|
||||
|
||||
private static IEnumerable<Win32> GetProgramsFromRegistry(RegistryKey root)
|
||||
private static IEnumerable<string> GetPathFromRegistry(RegistryKey root)
|
||||
{
|
||||
return root
|
||||
.GetSubKeyNames()
|
||||
.Select(x => GetProgramPathFromRegistrySubKeys(root, x))
|
||||
.Distinct()
|
||||
.Select(x => GetProgramFromPath(x));
|
||||
.GetSubKeyNames()
|
||||
.Select(x => GetProgramPathFromRegistrySubKeys(root, x))
|
||||
.Distinct();
|
||||
}
|
||||
|
||||
private static string GetProgramPathFromRegistrySubKeys(RegistryKey root, string subkey)
|
||||
|
|
@ -422,7 +414,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
|
||||
{
|
||||
ProgramLogger.LogException($"|Win32|GetProgramPathFromRegistrySubKeys|{path}" +
|
||||
$"|Permission denied when trying to load the program from {path}", e);
|
||||
$"|Permission denied when trying to load the program from {path}", e);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
|
@ -431,27 +423,74 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
private static Win32 GetProgramFromPath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return new Win32();
|
||||
return null;
|
||||
|
||||
path = Environment.ExpandEnvironmentVariables(path);
|
||||
|
||||
if (!File.Exists(path))
|
||||
return new Win32();
|
||||
return null;
|
||||
|
||||
var entry = Win32Program(path);
|
||||
entry.ExecutableName = Path.GetFileName(path);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
public static IEnumerable<string> ExceptDisabledSource(IEnumerable<string> sources)
|
||||
{
|
||||
return ExceptDisabledSource(sources, x => x);
|
||||
}
|
||||
|
||||
public static IEnumerable<TSource> ExceptDisabledSource<TSource>(IEnumerable<TSource> sources,
|
||||
Func<TSource, string> keySelector)
|
||||
{
|
||||
return Main._settings.DisabledProgramSources.Count == 0
|
||||
? sources
|
||||
: ExceptDisabledSourceEnumerable(sources, keySelector);
|
||||
|
||||
static IEnumerable<TSource> ExceptDisabledSourceEnumerable(IEnumerable<TSource> elements,
|
||||
Func<TSource, string> selector)
|
||||
{
|
||||
var set = Main._settings.DisabledProgramSources.Select(x => x.UniqueIdentifier).ToHashSet();
|
||||
|
||||
foreach (var element in elements)
|
||||
{
|
||||
if (!set.Contains(selector(element)))
|
||||
yield return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<T> DistinctBy<T, R>(IEnumerable<T> source, Func<T, R> selector)
|
||||
{
|
||||
var set = new HashSet<R>();
|
||||
foreach (var item in source)
|
||||
{
|
||||
if (set.Add(selector(item)))
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
private static Win32[] ProgramsHasher(IEnumerable<Win32> programs)
|
||||
{
|
||||
return programs.GroupBy(p => p.FullPath.ToLower())
|
||||
.SelectMany(g =>
|
||||
{
|
||||
if (g.Count() > 1)
|
||||
return DistinctBy(g.Where(p => !string.IsNullOrEmpty(p.Description)), x => x.Description);
|
||||
return g;
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
|
||||
public static Win32[] All(Settings settings)
|
||||
{
|
||||
try
|
||||
{
|
||||
var programs = new List<Win32>().AsParallel();
|
||||
var programs = Enumerable.Empty<Win32>();
|
||||
|
||||
var unregistered = UnregisteredPrograms(settings.ProgramSources, settings.ProgramSuffixes);
|
||||
programs = programs.Concat(unregistered);
|
||||
|
||||
if (settings.EnableRegistrySource)
|
||||
{
|
||||
var appPaths = AppPathsPrograms(settings.ProgramSuffixes);
|
||||
|
|
@ -464,7 +503,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
programs = programs.Concat(startMenu);
|
||||
}
|
||||
|
||||
return programs.ToArray();
|
||||
|
||||
return ProgramsHasher(programs.Where(p => p != null));
|
||||
}
|
||||
#if DEBUG //This is to make developer aware of any unhandled exception and add in handling.
|
||||
catch (Exception e)
|
||||
|
|
@ -478,9 +518,9 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
{
|
||||
ProgramLogger.LogException("|Win32|All|Not available|An unexpected error occurred", e);
|
||||
|
||||
return new Win32[0];
|
||||
return Array.Empty<Win32>();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
|
@ -4,7 +4,7 @@
|
|||
"Name": "Program",
|
||||
"Description": "Search programs in Flow.Launcher",
|
||||
"Author": "qianlifeng",
|
||||
"Version": "1.4.0",
|
||||
"Version": "1.4.2",
|
||||
"Language": "csharp",
|
||||
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
|
||||
"ExecuteFileName": "Flow.Launcher.Plugin.Program.dll",
|
||||
|
|
|
|||
|
|
@ -38,10 +38,11 @@ Flow Launcher. Dedicated to make your workflow flow more seamlessly. Aimed at be
|
|||
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.
|
||||
|
||||
**Integrations**
|
||||
- If you use Python plugins:
|
||||
- Install [Python3](https://www.python.org/downloads/), download `.exe` installer.
|
||||
- Python plugins:
|
||||
- Once a Python plugin has been detected at start up, you will be prompted to either select the location or allow Python 3.9.1 to automatic download and install.
|
||||
- Add Python to `%PATH%` or set it in flow's settings.
|
||||
- Use `pip` to install `flowlauncher`, cmd in `pip install flowlauncher`.
|
||||
- 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, download the plugin [here](https://github.com/Flow-Launcher/Flow.Launcher.Plugin.Everything/releases/latest).
|
||||
|
||||
|
|
@ -51,6 +52,7 @@ 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`/`pm uninstall`/`pm update` + the plugin name.
|
||||
- Press `F5` while in the query window to reload all plugin data.
|
||||
- Saved user settings are located:
|
||||
- If using roaming: `%APPDATA%\FlowLauncher`
|
||||
- If using portable, by default: `%localappdata%\FlowLauncher\app-<VersionOfYourFlowLauncher>\UserData`
|
||||
|
|
|
|||
Loading…
Reference in a new issue