Merge branch 'dev' into FixUnexpectedErrorNotice

This commit is contained in:
Jeremy Wu 2021-02-15 21:13:24 +11:00
commit c2dd536b9b
45 changed files with 792 additions and 312 deletions

View file

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

View file

@ -15,6 +15,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.Plugin
@ -42,6 +43,7 @@ namespace Flow.Launcher.Core.Plugin
public class JsonRPCQueryResponseModel : JsonRPCResponseModel
{
[JsonPropertyName("result")]
public new List<JsonRPCResult> Result { get; set; }
}

View file

@ -147,47 +147,42 @@ namespace Flow.Launcher.Core.Plugin
{
try
{
using (var process = Process.Start(startInfo))
using var process = Process.Start(startInfo);
if (process == null)
{
if (process != null)
Log.Error("|JsonRPCPlugin.Execute|Can't start new process");
return string.Empty;
}
using var standardOutput = process.StandardOutput;
var result = standardOutput.ReadToEnd();
if (string.IsNullOrEmpty(result))
{
using (var standardError = process.StandardError)
{
using (var standardOutput = process.StandardOutput)
var error = standardError.ReadToEnd();
if (!string.IsNullOrEmpty(error))
{
var result = standardOutput.ReadToEnd();
if (string.IsNullOrEmpty(result))
{
using (var standardError = process.StandardError)
{
var error = standardError.ReadToEnd();
if (!string.IsNullOrEmpty(error))
{
Log.Error($"|JsonRPCPlugin.Execute|{error}");
return string.Empty;
}
else
{
Log.Error("|JsonRPCPlugin.Execute|Empty standard output and standard error.");
return string.Empty;
}
}
}
else if (result.StartsWith("DEBUG:"))
{
MessageBox.Show(new Form { TopMost = true }, result.Substring(6));
return string.Empty;
}
else
{
return result;
}
Log.Error($"|JsonRPCPlugin.Execute|{error}");
return string.Empty;
}
else
{
Log.Error("|JsonRPCPlugin.Execute|Empty standard output and standard error.");
return string.Empty;
}
}
else
{
Log.Error("|JsonRPCPlugin.Execute|Can't start new process");
return string.Empty;
}
}
else if (result.StartsWith("DEBUG:"))
{
MessageBox.Show(new Form { TopMost = true }, result.Substring(6));
return string.Empty;
}
else
{
return result;
}
}
catch (Exception e)
{

View file

@ -183,6 +183,8 @@ namespace Flow.Launcher.Core.Plugin
throw new ArgumentOutOfRangeException();
}
token.ThrowIfCancellationRequested();
if (results == null)
return results;
UpdatePluginMetadata(results, metadata, query);
metadata.QueryCount += 1;

View file

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

View file

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

View file

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

View file

@ -15,5 +15,10 @@ namespace Flow.Launcher.Infrastructure.Storage
FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}");
}
public PluginJsonStorage(T data) : this()
{
_data = data;
}
}
}

View file

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

View file

@ -1,4 +1,4 @@
namespace Flow.Launcher.Infrastructure.Storage
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Save plugin settings/cache,

View file

@ -66,6 +66,9 @@
<Content Include="Images\*.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Images\*.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View file

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

View file

@ -40,8 +40,8 @@
<!--Setting Plugin-->
<system:String x:Key="plugin">Plugin</system:String>
<system:String x:Key="browserMorePlugins">Nájsť ďalšie pluginy</system:String>
<system:String x:Key="enable">Povol</system:String>
<system:String x:Key="disable">Zakázať</system:String>
<system:String x:Key="enable">Povolené</system:String>
<system:String x:Key="disable">Zakáza</system:String>
<system:String x:Key="actionKeywords">Skratka akcie</system:String>
<system:String x:Key="currentActionKeywords">Aktuálna akcia skratky:</system:String>
<system:String x:Key="newActionKeyword">Nová akcia skratky:</system:String>

View file

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

View file

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

View file

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

View file

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

View file

@ -89,7 +89,6 @@ namespace Flow.Launcher.ViewModel
_resultsViewUpdateTask =
Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted);
async Task updateAction()
{
var queue = new Dictionary<string, ResultsForUpdate>();
@ -105,9 +104,7 @@ namespace Flow.Launcher.ViewModel
UpdateResultView(queue.Values);
}
}
;
};
void continueAction(Task t)
{
@ -115,8 +112,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
}
}
@ -225,6 +222,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 +329,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; }
@ -363,7 +380,7 @@ namespace Flow.Launcher.ViewModel
}
if (!match.IsSearchPrecisionScoreMet()) return false;
r.Score = match.Score;
return true;
@ -512,7 +529,7 @@ namespace Flow.Launcher.ViewModel
await Task.Yield();
var results = await PluginManager.QueryForPlugin(plugin, query, currentCancellationToken);
if (!currentCancellationToken.IsCancellationRequested)
if (!currentCancellationToken.IsCancellationRequested && results != null)
_resultsUpdateQueue.Post(new ResultsForUpdate(results, plugin.Metadata, query,
currentCancellationToken));
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

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

View file

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

View file

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

View file

@ -6,7 +6,7 @@
<system:String x:Key="flowlauncher_plugin_caculator_plugin_description">Spracúva matematické operácie.(Skúste 5*3-2 vo flowlauncheri)</system:String>
<system:String x:Key="flowlauncher_plugin_calculator_not_a_number">Nie je číslo (NaN)</system:String>
<system:String x:Key="flowlauncher_plugin_calculator_expression_not_complete">Nesprávny alebo neúplný výraz (Nezabudli ste na zátvorky?)</system:String>
<system:String x:Key="flowlauncher_plugin_calculator_copy_number_to_clipboard">Kopírovať toto číslo do schránky</system:String>
<system:String x:Key="flowlauncher_plugin_calculator_copy_number_to_clipboard">Kopírovať výsledok do schránky</system:String>
<system:String x:Key="flowlauncher_plugin_calculator_output_decimal_seperator">Oddeľovač des. miest</system:String>
<system:String x:Key="flowlauncher_plugin_calculator_output_decimal_seperator_help">Oddeľovač desatinných miest použitý vo výsledku.</system:String>
<system:String x:Key="flowlauncher_plugin_calculator_decimal_seperator_use_system_locale">Použiť podľa systému</system:String>

View file

@ -1,10 +1,8 @@
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.SharedCommands;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows;
namespace Flow.Launcher.Plugin.Explorer.Search

View file

@ -25,7 +25,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
{
Title = Context.API.GetTranslation("plugin_pluginsmanager_plugin_contextmenu_openwebsite_title"),
SubTitle = Context.API.GetTranslation("plugin_pluginsmanager_plugin_contextmenu_openwebsite_subtitle"),
IcoPath = "Images\\website.png",
IcoPath = selectedResult.IcoPath,
Action = _ =>
{
SharedCommands.SearchWeb.NewTabInBrowser(pluginManifestInfo.Website);
@ -63,7 +63,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
{
Title = Context.API.GetTranslation("plugin_pluginsmanager_plugin_contextmenu_pluginsmanifest_title"),
SubTitle = Context.API.GetTranslation("plugin_pluginsmanager_plugin_contextmenu_pluginsmanifest_subtitle"),
IcoPath = selectedResult.IcoPath,
IcoPath = "Images\\manifestsite.png",
Action = _ =>
{
SharedCommands.SearchWeb.NewTabInBrowser("https://github.com/Flow-Launcher/Flow.Launcher.PluginsManifest");

View file

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

View file

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

View file

@ -7,7 +7,7 @@
<system:String x:Key="plugin_pluginsmanager_please_wait">Čakajte, prosím…</system:String>
<system:String x:Key="plugin_pluginsmanager_download_success">Úspešne stiahnuté</system:String>
<system:String x:Key="plugin_pluginsmanager_uninstall_prompt">{0} od {1} {2}{3}Chcete odinštalovať tento plugin? Po odinštalovaní sa Flow automaticky reštartuje.</system:String>
<system:String x:Key="plugin_pluginsmanager_install_prompt">{0} by {1} {2}{3}Chcete nainštalovať tento plugin? Po odinštalovaní sa Flow automaticky reštartuje.</system:String>
<system:String x:Key="plugin_pluginsmanager_install_prompt">{0} by {1} {2}{3}Chcete nainštalovať tento plugin? Po nainštalovaní sa Flow automaticky reštartuje.</system:String>
<system:String x:Key="plugin_pluginsmanager_install_title">Inštalovať plugin</system:String>
<system:String x:Key="plugin_pluginsmanager_uninstall_title">Odinštalovať plugin</system:String>
<system:String x:Key="plugin_pluginsmanager_install_errormetadatafile">Inštalácia zlyhala: nepodarilo sa nájsť metadáta súboru plugin.json nového pluginu</system:String>

View file

@ -1,8 +1,10 @@
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin.PluginsManager.Models;
using Flow.Launcher.Plugin.SharedCommands;
using System;
using System.Collections.Generic;
using System.IO;
@ -342,7 +344,9 @@ namespace Flow.Launcher.Plugin.PluginsManager
var zipFilePath = Path.Combine(tempFolderPath, Path.GetFileName(downloadedFilePath));
File.Move(downloadedFilePath, zipFilePath);
File.Copy(downloadedFilePath, zipFilePath);
File.Delete(downloadedFilePath);
Utilities.UnZip(zipFilePath, tempFolderPluginPath, true);
@ -360,7 +364,9 @@ namespace Flow.Launcher.Plugin.PluginsManager
string newPluginPath = Path.Combine(DataLocation.PluginsDirectory, $"{plugin.Name}-{plugin.Version}");
Directory.Move(pluginFolderPath, newPluginPath);
FilesFolders.CopyAll(pluginFolderPath, newPluginPath);
Directory.Delete(pluginFolderPath, true);
}
internal List<Result> RequestUninstall(string search)
@ -410,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"));
}

View file

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

View file

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

View file

@ -228,7 +228,6 @@ namespace Flow.Launcher.Plugin.Program
public static void StartProcess(Func<ProcessStartInfo, Process> runProcess, ProcessStartInfo info)
{
bool hide;
try
{
runProcess(info);

View file

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

View file

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

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

View file

@ -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
@ -254,6 +246,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;
@ -350,15 +343,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)
{
@ -369,13 +361,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);
@ -443,7 +446,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>
{
@ -454,7 +457,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

View file

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

View file

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

View file

@ -4,7 +4,7 @@
<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>
<system:String x:Key="flowlaucnher_plugin_websearch_set_browser_path">Zadajte cestu k prehliadaču:</system:String>
<system:String x:Key="flowlaucnher_plugin_websearch_set_browser_path">Cesta k prehliadaču:</system:String>
<system:String x:Key="flowlauncher_plugin_websearch_choose">Vybrať</system:String>
<system:String x:Key="flowlauncher_plugin_websearch_delete">Odstrániť</system:String>
<system:String x:Key="flowlauncher_plugin_websearch_edit">Upraviť</system:String>

View file

@ -20,10 +20,12 @@ Flow Launcher. Dedicated to make your workflow flow more seamlessly. Aimed at be
- Search everything from applications, files, bookmarks, YouTube, Twitter and more. All from the comfort of your keyboard without ever touching the mouse.
- Search for file contents.
- Support search using environment variable paths
- Support search using environment variable paths.
- Run batch and PowerShell commands as Administrator or a different user.
- Support languages from Chinese to Italian and more.
- Support of wide range of plugins.
- Support wide range of plugins.
- Prioritise the order of each plugin's results.
- Save file or folder locations for quick access.
- Fully portable.
[<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)
@ -36,22 +38,25 @@ 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).
**Usage**
- Open flow's search window: <kbd>Alt</kbd>+<kbd>Space</kbd> is the default hotkey.
- Open context menu: <kbd>Ctrl</kbd>+<kbd>O</kbd>/<kbd>Shift</kbd>+<kbd>Enter</kbd>.
- Search programs, bookmarks and control panel items with acronyms eg. `gk` or `gp` for GitKraken Preview; fuzzy search eg. `code` or `vis` for Visual Studio Code.
- 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`
- Logs are saved along with your user settings folder
- Logs are saved along with your user settings folder.
## Status

View file

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

View file

@ -1,4 +1,4 @@
version: '1.7.0.{build}'
version: '1.7.1.{build}'
init:
- ps: |