mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into administrator_mode
This commit is contained in:
commit
758d025c59
24 changed files with 773 additions and 140 deletions
4
.github/actions/spelling/expect.txt
vendored
4
.github/actions/spelling/expect.txt
vendored
|
|
@ -99,3 +99,7 @@ pluginsmanager
|
|||
alreadyexists
|
||||
Softpedia
|
||||
img
|
||||
Reloadable
|
||||
metadatas
|
||||
WMP
|
||||
VSTHRD
|
||||
|
|
|
|||
1
.github/actions/spelling/patterns.txt
vendored
1
.github/actions/spelling/patterns.txt
vendored
|
|
@ -133,3 +133,4 @@
|
|||
\bPortuguês (Brasil)\b
|
||||
\bčeština\b
|
||||
\bPortuguês\b
|
||||
\bIoc\b
|
||||
|
|
|
|||
353
Flow.Launcher.Core/Plugin/PluginInstaller.cs
Normal file
353
Flow.Launcher.Core/Plugin/PluginInstaller.cs
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin;
|
||||
|
||||
/// <summary>
|
||||
/// Class for installing, updating, and uninstalling plugins.
|
||||
/// </summary>
|
||||
public static class PluginInstaller
|
||||
{
|
||||
private static readonly string ClassName = nameof(PluginInstaller);
|
||||
|
||||
private static readonly Settings Settings = Ioc.Default.GetRequiredService<Settings>();
|
||||
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
/// <summary>
|
||||
/// Installs a plugin and restarts the application if required by settings. Prompts user for confirmation and handles download if needed.
|
||||
/// </summary>
|
||||
/// <param name="newPlugin">The plugin to install.</param>
|
||||
/// <returns>A Task representing the asynchronous install operation.</returns>
|
||||
public static async Task InstallPluginAndCheckRestartAsync(UserPlugin newPlugin)
|
||||
{
|
||||
if (API.PluginModified(newPlugin.ID))
|
||||
{
|
||||
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), newPlugin.Name),
|
||||
API.GetTranslation("pluginModifiedAlreadyMessage"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (API.ShowMsgBox(
|
||||
string.Format(
|
||||
API.GetTranslation("InstallPromptSubtitle"),
|
||||
newPlugin.Name, newPlugin.Author, Environment.NewLine),
|
||||
API.GetTranslation("InstallPromptTitle"),
|
||||
button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return;
|
||||
|
||||
try
|
||||
{
|
||||
// at minimum should provide a name, but handle plugin that is not downloaded from plugins manifest and is a url download
|
||||
var downloadFilename = string.IsNullOrEmpty(newPlugin.Version)
|
||||
? $"{newPlugin.Name}-{Guid.NewGuid()}.zip"
|
||||
: $"{newPlugin.Name}-{newPlugin.Version}.zip";
|
||||
|
||||
var filePath = Path.Combine(Path.GetTempPath(), downloadFilename);
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
|
||||
if (!newPlugin.IsFromLocalInstallPath)
|
||||
{
|
||||
await DownloadFileAsync(
|
||||
$"{API.GetTranslation("DownloadingPlugin")} {newPlugin.Name}",
|
||||
newPlugin.UrlDownload, filePath, cts);
|
||||
}
|
||||
else
|
||||
{
|
||||
filePath = newPlugin.LocalInstallPath;
|
||||
}
|
||||
|
||||
// check if user cancelled download before installing plugin
|
||||
if (cts.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw new FileNotFoundException($"Plugin {newPlugin.ID} zip file not found at {filePath}", filePath);
|
||||
}
|
||||
|
||||
if (!API.InstallPlugin(newPlugin, filePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newPlugin.IsFromLocalInstallPath)
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, "Failed to install plugin", e);
|
||||
API.ShowMsgError(API.GetTranslation("ErrorInstallingPlugin"));
|
||||
return; // do not restart on failure
|
||||
}
|
||||
|
||||
if (Settings.AutoRestartAfterChanging)
|
||||
{
|
||||
API.RestartApp();
|
||||
}
|
||||
else
|
||||
{
|
||||
API.ShowMsg(
|
||||
API.GetTranslation("installbtn"),
|
||||
string.Format(
|
||||
API.GetTranslation(
|
||||
"InstallSuccessNoRestart"),
|
||||
newPlugin.Name));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs a plugin from a local zip file and restarts the application if required by settings. Validates the zip and prompts user for confirmation.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path to the plugin zip file.</param>
|
||||
/// <returns>A Task representing the asynchronous install operation.</returns>
|
||||
public static async Task InstallPluginAndCheckRestartAsync(string filePath)
|
||||
{
|
||||
UserPlugin plugin;
|
||||
try
|
||||
{
|
||||
using ZipArchive archive = ZipFile.OpenRead(filePath);
|
||||
var pluginJsonEntry = archive.Entries.FirstOrDefault(x => x.Name == "plugin.json") ??
|
||||
throw new FileNotFoundException("The zip file does not contain a plugin.json file.");
|
||||
|
||||
using Stream stream = pluginJsonEntry.Open();
|
||||
plugin = JsonSerializer.Deserialize<UserPlugin>(stream);
|
||||
plugin.IcoPath = "Images\\zipfolder.png";
|
||||
plugin.LocalInstallPath = filePath;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, "Failed to validate zip file", e);
|
||||
API.ShowMsgError(API.GetTranslation("ZipFileNotHavePluginJson"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (API.PluginModified(plugin.ID))
|
||||
{
|
||||
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), plugin.Name),
|
||||
API.GetTranslation("pluginModifiedAlreadyMessage"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Settings.ShowUnknownSourceWarning)
|
||||
{
|
||||
if (!InstallSourceKnown(plugin.Website)
|
||||
&& API.ShowMsgBox(string.Format(
|
||||
API.GetTranslation("InstallFromUnknownSourceSubtitle"), Environment.NewLine),
|
||||
API.GetTranslation("InstallFromUnknownSourceTitle"),
|
||||
MessageBoxButton.YesNo) == MessageBoxResult.No)
|
||||
return;
|
||||
}
|
||||
|
||||
await InstallPluginAndCheckRestartAsync(plugin);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uninstalls a plugin and restarts the application if required by settings. Prompts user for confirmation and whether to keep plugin settings.
|
||||
/// </summary>
|
||||
/// <param name="oldPlugin">The plugin metadata to uninstall.</param>
|
||||
/// <returns>A Task representing the asynchronous uninstall operation.</returns>
|
||||
public static async Task UninstallPluginAndCheckRestartAsync(PluginMetadata oldPlugin)
|
||||
{
|
||||
if (API.PluginModified(oldPlugin.ID))
|
||||
{
|
||||
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), oldPlugin.Name),
|
||||
API.GetTranslation("pluginModifiedAlreadyMessage"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (API.ShowMsgBox(
|
||||
string.Format(
|
||||
API.GetTranslation("UninstallPromptSubtitle"),
|
||||
oldPlugin.Name, oldPlugin.Author, Environment.NewLine),
|
||||
API.GetTranslation("UninstallPromptTitle"),
|
||||
button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return;
|
||||
|
||||
var removePluginSettings = API.ShowMsgBox(
|
||||
API.GetTranslation("KeepPluginSettingsSubtitle"),
|
||||
API.GetTranslation("KeepPluginSettingsTitle"),
|
||||
button: MessageBoxButton.YesNo) == MessageBoxResult.No;
|
||||
|
||||
try
|
||||
{
|
||||
if (!await API.UninstallPluginAsync(oldPlugin, removePluginSettings))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, "Failed to uninstall plugin", e);
|
||||
API.ShowMsgError(API.GetTranslation("ErrorUninstallingPlugin"));
|
||||
return; // don not restart on failure
|
||||
}
|
||||
|
||||
if (Settings.AutoRestartAfterChanging)
|
||||
{
|
||||
API.RestartApp();
|
||||
}
|
||||
else
|
||||
{
|
||||
API.ShowMsg(
|
||||
API.GetTranslation("uninstallbtn"),
|
||||
string.Format(
|
||||
API.GetTranslation(
|
||||
"UninstallSuccessNoRestart"),
|
||||
oldPlugin.Name));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a plugin to a new version and restarts the application if required by settings. Prompts user for confirmation and handles download if needed.
|
||||
/// </summary>
|
||||
/// <param name="newPlugin">The new plugin version to install.</param>
|
||||
/// <param name="oldPlugin">The existing plugin metadata to update.</param>
|
||||
/// <returns>A Task representing the asynchronous update operation.</returns>
|
||||
public static async Task UpdatePluginAndCheckRestartAsync(UserPlugin newPlugin, PluginMetadata oldPlugin)
|
||||
{
|
||||
if (API.ShowMsgBox(
|
||||
string.Format(
|
||||
API.GetTranslation("UpdatePromptSubtitle"),
|
||||
oldPlugin.Name, oldPlugin.Author, Environment.NewLine),
|
||||
API.GetTranslation("UpdatePromptTitle"),
|
||||
button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return;
|
||||
|
||||
try
|
||||
{
|
||||
var filePath = Path.Combine(Path.GetTempPath(), $"{newPlugin.Name}-{newPlugin.Version}.zip");
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
|
||||
if (!newPlugin.IsFromLocalInstallPath)
|
||||
{
|
||||
await DownloadFileAsync(
|
||||
$"{API.GetTranslation("DownloadingPlugin")} {newPlugin.Name}",
|
||||
newPlugin.UrlDownload, filePath, cts);
|
||||
}
|
||||
else
|
||||
{
|
||||
filePath = newPlugin.LocalInstallPath;
|
||||
}
|
||||
|
||||
// check if user cancelled download before installing plugin
|
||||
if (cts.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await API.UpdatePluginAsync(oldPlugin, newPlugin, filePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, "Failed to update plugin", e);
|
||||
API.ShowMsgError(API.GetTranslation("ErrorUpdatingPlugin"));
|
||||
return; // do not restart on failure
|
||||
}
|
||||
|
||||
if (Settings.AutoRestartAfterChanging)
|
||||
{
|
||||
API.RestartApp();
|
||||
}
|
||||
else
|
||||
{
|
||||
API.ShowMsg(
|
||||
API.GetTranslation("updatebtn"),
|
||||
string.Format(
|
||||
API.GetTranslation(
|
||||
"UpdateSuccessNoRestart"),
|
||||
newPlugin.Name));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a file from a URL to a local path, optionally showing a progress box and handling cancellation.
|
||||
/// </summary>
|
||||
/// <param name="progressBoxTitle">The title for the progress box.</param>
|
||||
/// <param name="downloadUrl">The URL to download from.</param>
|
||||
/// <param name="filePath">The local file path to save to.</param>
|
||||
/// <param name="cts">Cancellation token source for cancelling the download.</param>
|
||||
/// <param name="deleteFile">Whether to delete the file if it already exists.</param>
|
||||
/// <param name="showProgress">Whether to show a progress box during download.</param>
|
||||
/// <returns>A Task representing the asynchronous download operation.</returns>
|
||||
private static async Task DownloadFileAsync(string progressBoxTitle, string downloadUrl, string filePath, CancellationTokenSource cts, bool deleteFile = true, bool showProgress = true)
|
||||
{
|
||||
if (deleteFile && File.Exists(filePath))
|
||||
File.Delete(filePath);
|
||||
|
||||
if (showProgress)
|
||||
{
|
||||
var exceptionHappened = false;
|
||||
await API.ShowProgressBoxAsync(progressBoxTitle,
|
||||
async (reportProgress) =>
|
||||
{
|
||||
if (reportProgress == null)
|
||||
{
|
||||
// when reportProgress is null, it means there is exception with the progress box
|
||||
// so we record it with exceptionHappened and return so that progress box will close instantly
|
||||
exceptionHappened = true;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
await API.HttpDownloadAsync(downloadUrl, filePath, reportProgress, cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
}, cts.Cancel);
|
||||
|
||||
// if exception happened while downloading and user does not cancel downloading,
|
||||
// we need to redownload the plugin
|
||||
if (exceptionHappened && (!cts.IsCancellationRequested))
|
||||
await API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the plugin install source is a known/approved source (e.g., GitHub and matches an existing plugin author).
|
||||
/// </summary>
|
||||
/// <param name="url">The URL to check.</param>
|
||||
/// <returns>True if the source is known, otherwise false.</returns>
|
||||
private static bool InstallSourceKnown(string url)
|
||||
{
|
||||
if (string.IsNullOrEmpty(url))
|
||||
return false;
|
||||
|
||||
var pieces = url.Split('/');
|
||||
|
||||
if (pieces.Length < 4)
|
||||
return false;
|
||||
|
||||
var author = pieces[3];
|
||||
var acceptedHost = "github.com";
|
||||
var acceptedSource = "https://github.com";
|
||||
var constructedUrlPart = string.Format("{0}/{1}/", acceptedSource, author);
|
||||
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || uri.Host != acceptedHost)
|
||||
return false;
|
||||
|
||||
return API.GetAllPlugins().Any(x =>
|
||||
!string.IsNullOrEmpty(x.Metadata.Website) &&
|
||||
x.Metadata.Website.StartsWith(constructedUrlPart)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,15 +18,12 @@ using ISavable = Flow.Launcher.Plugin.ISavable;
|
|||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// The entry for managing Flow Launcher plugins
|
||||
/// Class for co-ordinating and managing all plugin lifecycle.
|
||||
/// </summary>
|
||||
public static class PluginManager
|
||||
{
|
||||
private static readonly string ClassName = nameof(PluginManager);
|
||||
|
||||
private static IEnumerable<PluginPair> _contextMenuPlugins;
|
||||
private static IEnumerable<PluginPair> _homePlugins;
|
||||
|
||||
public static List<PluginPair> AllPlugins { get; private set; }
|
||||
public static readonly HashSet<PluginPair> GlobalPlugins = new();
|
||||
public static readonly Dictionary<string, PluginPair> NonGlobalPlugins = new();
|
||||
|
|
@ -36,8 +33,12 @@ namespace Flow.Launcher.Core.Plugin
|
|||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
private static PluginsSettings Settings;
|
||||
private static List<PluginMetadata> _metadatas;
|
||||
private static readonly List<string> _modifiedPlugins = new();
|
||||
private static readonly ConcurrentBag<string> ModifiedPlugins = new();
|
||||
|
||||
private static IEnumerable<PluginPair> _contextMenuPlugins;
|
||||
private static IEnumerable<PluginPair> _homePlugins;
|
||||
private static IEnumerable<PluginPair> _resultUpdatePlugin;
|
||||
private static IEnumerable<PluginPair> _translationPlugins;
|
||||
|
||||
/// <summary>
|
||||
/// Directories that will hold Flow Launcher plugin directory
|
||||
|
|
@ -173,12 +174,18 @@ namespace Flow.Launcher.Core.Plugin
|
|||
/// <param name="settings"></param>
|
||||
public static void LoadPlugins(PluginsSettings settings)
|
||||
{
|
||||
_metadatas = PluginConfig.Parse(Directories);
|
||||
var metadatas = PluginConfig.Parse(Directories);
|
||||
Settings = settings;
|
||||
Settings.UpdatePluginSettings(_metadatas);
|
||||
AllPlugins = PluginsLoader.Plugins(_metadatas, Settings);
|
||||
Settings.UpdatePluginSettings(metadatas);
|
||||
AllPlugins = PluginsLoader.Plugins(metadatas, Settings);
|
||||
// Since dotnet plugins need to get assembly name first, we should update plugin directory after loading plugins
|
||||
UpdatePluginDirectory(_metadatas);
|
||||
UpdatePluginDirectory(metadatas);
|
||||
|
||||
// Initialize plugin enumerable after all plugins are initialized
|
||||
_contextMenuPlugins = GetPluginsForInterface<IContextMenu>();
|
||||
_homePlugins = GetPluginsForInterface<IAsyncHomeQuery>();
|
||||
_resultUpdatePlugin = GetPluginsForInterface<IResultUpdated>();
|
||||
_translationPlugins = GetPluginsForInterface<IPluginI18n>();
|
||||
}
|
||||
|
||||
private static void UpdatePluginDirectory(List<PluginMetadata> metadatas)
|
||||
|
|
@ -248,9 +255,6 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
await Task.WhenAll(InitTasks);
|
||||
|
||||
_contextMenuPlugins = GetPluginsForInterface<IContextMenu>();
|
||||
_homePlugins = GetPluginsForInterface<IAsyncHomeQuery>();
|
||||
|
||||
foreach (var plugin in AllPlugins)
|
||||
{
|
||||
// set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin
|
||||
|
|
@ -409,16 +413,26 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return AllPlugins.FirstOrDefault(o => o.Metadata.ID == id);
|
||||
}
|
||||
|
||||
public static IEnumerable<PluginPair> GetPluginsForInterface<T>() where T : IFeatures
|
||||
private static IEnumerable<PluginPair> GetPluginsForInterface<T>() where T : IFeatures
|
||||
{
|
||||
// Handle scenario where this is called before all plugins are instantiated, e.g. language change on startup
|
||||
return AllPlugins?.Where(p => p.Plugin is T) ?? Array.Empty<PluginPair>();
|
||||
}
|
||||
|
||||
public static IList<PluginPair> GetResultUpdatePlugin()
|
||||
{
|
||||
return _resultUpdatePlugin.Where(p => !PluginModified(p.Metadata.ID)).ToList();
|
||||
}
|
||||
|
||||
public static IList<PluginPair> GetTranslationPlugins()
|
||||
{
|
||||
return _translationPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
|
||||
}
|
||||
|
||||
public static List<Result> GetContextMenusForPlugin(Result result)
|
||||
{
|
||||
var results = new List<Result>();
|
||||
var pluginPair = _contextMenuPlugins.FirstOrDefault(o => o.Metadata.ID == result.PluginID);
|
||||
var pluginPair = _contextMenuPlugins.Where(p => !PluginModified(p.Metadata.ID)).FirstOrDefault(o => o.Metadata.ID == result.PluginID);
|
||||
if (pluginPair != null)
|
||||
{
|
||||
var plugin = (IContextMenu)pluginPair.Plugin;
|
||||
|
|
@ -446,7 +460,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
public static bool IsHomePlugin(string id)
|
||||
{
|
||||
return _homePlugins.Any(p => p.Metadata.ID == id);
|
||||
return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).Any(p => p.Metadata.ID == id);
|
||||
}
|
||||
|
||||
public static bool ActionKeywordRegistered(string actionKeyword)
|
||||
|
|
@ -536,44 +550,62 @@ namespace Flow.Launcher.Core.Plugin
|
|||
private static bool SameOrLesserPluginVersionExists(string metadataPath)
|
||||
{
|
||||
var newMetadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(metadataPath));
|
||||
|
||||
if (!Version.TryParse(newMetadata.Version, out var newVersion))
|
||||
return true; // If version is not valid, we assume it is lesser than any existing version
|
||||
|
||||
return AllPlugins.Any(x => x.Metadata.ID == newMetadata.ID
|
||||
&& newMetadata.Version.CompareTo(x.Metadata.Version) <= 0);
|
||||
&& Version.TryParse(x.Metadata.Version, out var version)
|
||||
&& newVersion <= version);
|
||||
}
|
||||
|
||||
#region Public functions
|
||||
|
||||
public static bool PluginModified(string id)
|
||||
{
|
||||
return _modifiedPlugins.Contains(id);
|
||||
return ModifiedPlugins.Contains(id);
|
||||
}
|
||||
|
||||
public static async Task UpdatePluginAsync(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
|
||||
public static async Task<bool> UpdatePluginAsync(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
|
||||
{
|
||||
InstallPlugin(newVersion, zipFilePath, checkModified:false);
|
||||
await UninstallPluginAsync(existingVersion, removePluginFromSettings:false, removePluginSettings:false, checkModified: false);
|
||||
_modifiedPlugins.Add(existingVersion.ID);
|
||||
if (PluginModified(existingVersion.ID))
|
||||
{
|
||||
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), existingVersion.Name),
|
||||
API.GetTranslation("pluginModifiedAlreadyMessage"));
|
||||
return false;
|
||||
}
|
||||
|
||||
var installSuccess = InstallPlugin(newVersion, zipFilePath, checkModified: false);
|
||||
if (!installSuccess) return false;
|
||||
|
||||
var uninstallSuccess = await UninstallPluginAsync(existingVersion, removePluginFromSettings: false, removePluginSettings: false, checkModified: false);
|
||||
if (!uninstallSuccess) return false;
|
||||
|
||||
ModifiedPlugins.Add(existingVersion.ID);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void InstallPlugin(UserPlugin plugin, string zipFilePath)
|
||||
public static bool InstallPlugin(UserPlugin plugin, string zipFilePath)
|
||||
{
|
||||
InstallPlugin(plugin, zipFilePath, checkModified: true);
|
||||
return InstallPlugin(plugin, zipFilePath, checkModified: true);
|
||||
}
|
||||
|
||||
public static async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginSettings = false)
|
||||
public static async Task<bool> UninstallPluginAsync(PluginMetadata plugin, bool removePluginSettings = false)
|
||||
{
|
||||
await UninstallPluginAsync(plugin, removePluginFromSettings: true, removePluginSettings: removePluginSettings, checkModified: true);
|
||||
return await UninstallPluginAsync(plugin, removePluginFromSettings: true, removePluginSettings: removePluginSettings, checkModified: true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal functions
|
||||
|
||||
internal static void InstallPlugin(UserPlugin plugin, string zipFilePath, bool checkModified)
|
||||
internal static bool InstallPlugin(UserPlugin plugin, string zipFilePath, bool checkModified)
|
||||
{
|
||||
if (checkModified && PluginModified(plugin.ID))
|
||||
{
|
||||
// Distinguish exception from installing same or less version
|
||||
throw new ArgumentException($"Plugin {plugin.Name} {plugin.ID} has been modified.", nameof(plugin));
|
||||
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), plugin.Name),
|
||||
API.GetTranslation("pluginModifiedAlreadyMessage"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Unzip plugin files to temp folder
|
||||
|
|
@ -591,12 +623,16 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
if (string.IsNullOrEmpty(metadataJsonFilePath) || string.IsNullOrEmpty(pluginFolderPath))
|
||||
{
|
||||
throw new FileNotFoundException($"Unable to find plugin.json from the extracted zip file, or this path {pluginFolderPath} does not exist");
|
||||
API.ShowMsgError(string.Format(API.GetTranslation("failedToInstallPluginTitle"), plugin.Name),
|
||||
string.Format(API.GetTranslation("fileNotFoundMessage"), pluginFolderPath));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (SameOrLesserPluginVersionExists(metadataJsonFilePath))
|
||||
{
|
||||
throw new InvalidOperationException($"A plugin with the same ID and version already exists, or the version is greater than this downloaded plugin {plugin.Name}");
|
||||
API.ShowMsgError(string.Format(API.GetTranslation("failedToInstallPluginTitle"), plugin.Name),
|
||||
API.GetTranslation("pluginExistAlreadyMessage"));
|
||||
return false;
|
||||
}
|
||||
|
||||
var folderName = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}";
|
||||
|
|
@ -638,15 +674,19 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
if (checkModified)
|
||||
{
|
||||
_modifiedPlugins.Add(plugin.ID);
|
||||
ModifiedPlugins.Add(plugin.ID);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified)
|
||||
internal static async Task<bool> UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified)
|
||||
{
|
||||
if (checkModified && PluginModified(plugin.ID))
|
||||
{
|
||||
throw new ArgumentException($"Plugin {plugin.Name} has been modified");
|
||||
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), plugin.Name),
|
||||
API.GetTranslation("pluginModifiedAlreadyMessage"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (removePluginSettings || removePluginFromSettings)
|
||||
|
|
@ -700,6 +740,12 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
Settings.RemovePluginSettings(plugin.ID);
|
||||
AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID);
|
||||
GlobalPlugins.RemoveWhere(p => p.Metadata.ID == plugin.ID);
|
||||
var keysToRemove = NonGlobalPlugins.Where(p => p.Value.Metadata.ID == plugin.ID).Select(p => p.Key).ToList();
|
||||
foreach (var key in keysToRemove)
|
||||
{
|
||||
NonGlobalPlugins.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Marked for deletion. Will be deleted on next start up
|
||||
|
|
@ -707,8 +753,10 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
if (checkModified)
|
||||
{
|
||||
_modifiedPlugins.Add(plugin.ID);
|
||||
ModifiedPlugins.Add(plugin.ID);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
|
||||
private void AddPluginLanguageDirectories()
|
||||
{
|
||||
foreach (var plugin in PluginManager.GetPluginsForInterface<IPluginI18n>())
|
||||
foreach (var plugin in PluginManager.GetTranslationPlugins())
|
||||
{
|
||||
var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location;
|
||||
var dir = Path.GetDirectoryName(location);
|
||||
|
|
@ -278,7 +278,8 @@ namespace Flow.Launcher.Core.Resource
|
|||
|
||||
private void UpdatePluginMetadataTranslations()
|
||||
{
|
||||
foreach (var p in PluginManager.GetPluginsForInterface<IPluginI18n>())
|
||||
// Update plugin metadata name & description
|
||||
foreach (var p in PluginManager.GetTranslationPlugins())
|
||||
{
|
||||
if (p.Plugin is not IPluginI18n pluginI18N) return;
|
||||
try
|
||||
|
|
|
|||
|
|
@ -203,6 +203,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
|
||||
public int MaxHistoryResultsToShowForHomePage { get; set; } = 5;
|
||||
|
||||
public bool AutoRestartAfterChanging { get; set; } = false;
|
||||
public bool ShowUnknownSourceWarning { get; set; } = true;
|
||||
|
||||
public int CustomExplorerIndex { get; set; } = 0;
|
||||
|
||||
[JsonIgnore]
|
||||
|
|
|
|||
|
|
@ -796,6 +796,42 @@ namespace Flow.Launcher.Infrastructure
|
|||
|
||||
#endregion
|
||||
|
||||
#region Win32 Dark Mode
|
||||
|
||||
/*
|
||||
* Inspired by https://github.com/ysc3839/win32-darkmode
|
||||
*/
|
||||
|
||||
[DllImport("uxtheme.dll", EntryPoint = "#135", SetLastError = true)]
|
||||
private static extern int SetPreferredAppMode(int appMode);
|
||||
|
||||
public static void EnableWin32DarkMode(string colorScheme)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Undocumented API from Windows 10 1809
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
|
||||
Environment.OSVersion.Version.Build >= 17763)
|
||||
{
|
||||
var flag = colorScheme switch
|
||||
{
|
||||
Constant.Light => 3, // ForceLight
|
||||
Constant.Dark => 2, // ForceDark
|
||||
Constant.System => 1, // AllowDark
|
||||
_ => 0 // Default
|
||||
};
|
||||
_ = SetPreferredAppMode(flag);
|
||||
}
|
||||
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors on unsupported OS
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Administrator Mode
|
||||
|
||||
public static bool IsAdministrator()
|
||||
|
|
@ -950,5 +986,6 @@ cleanup:
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
/// <param name="query">query text</param>
|
||||
/// <param name="requery">
|
||||
/// Force requery. By default, Flow Launcher will not fire query if your query is same with existing one.
|
||||
/// Set this to <see langword="true"/> to force Flow Launcher requerying
|
||||
/// Force requery. By default, Flow Launcher will not fire query if your query is same with existing one.
|
||||
/// Set this to <see langword="true"/> to force Flow Launcher re-querying
|
||||
/// </param>
|
||||
void ChangeQuery(string query, bool requery = false);
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
/// <param name="text">Text to save on clipboard</param>
|
||||
/// <param name="directCopy">When true it will directly copy the file/folder from the path specified in text</param>
|
||||
/// <param name="showDefaultNotification">Whether to show the default notification from this method after copy is done.
|
||||
/// <param name="showDefaultNotification">Whether to show the default notification from this method after copy is done.
|
||||
/// It will show file/folder/text is copied successfully.
|
||||
/// Turn this off to show your own notification after copy is done.</param>>
|
||||
public void CopyToClipboard(string text, bool directCopy = false, bool showDefaultNotification = true);
|
||||
|
|
@ -74,7 +74,7 @@ namespace Flow.Launcher.Plugin
|
|||
void SavePluginSettings();
|
||||
|
||||
/// <summary>
|
||||
/// Reloads any Plugins that have the
|
||||
/// Reloads any Plugins that have the
|
||||
/// IReloadable implemented. It refeshes
|
||||
/// Plugin's in memory data with new content
|
||||
/// added by user.
|
||||
|
|
@ -106,7 +106,7 @@ namespace Flow.Launcher.Plugin
|
|||
/// Show the MainWindow when hiding
|
||||
/// </summary>
|
||||
void ShowMainWindow();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Focus the query text box in the main window
|
||||
/// </summary>
|
||||
|
|
@ -124,7 +124,7 @@ namespace Flow.Launcher.Plugin
|
|||
bool IsMainWindowVisible();
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the visibility of the main window has changed. Currently, the plugin will continue to be subscribed even if it is turned off.
|
||||
/// Invoked when the visibility of the main window has changed. Currently, the plugin will continue to be subscribed even if it is turned off.
|
||||
/// </summary>
|
||||
event VisibilityChangedEventHandler VisibilityChanged;
|
||||
|
||||
|
|
@ -180,7 +180,7 @@ namespace Flow.Launcher.Plugin
|
|||
string GetTranslation(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Get all loaded plugins
|
||||
/// Get all loaded plugins
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
List<PluginPair> GetAllPlugins();
|
||||
|
|
@ -238,7 +238,7 @@ namespace Flow.Launcher.Plugin
|
|||
MatchResult FuzzySearch(string query, string stringToCompare);
|
||||
|
||||
/// <summary>
|
||||
/// Http download the spefic url and return as string
|
||||
/// Http download the specific url and return as string
|
||||
/// </summary>
|
||||
/// <param name="url">URL to call Http Get</param>
|
||||
/// <param name="token">Cancellation Token</param>
|
||||
|
|
@ -246,7 +246,7 @@ namespace Flow.Launcher.Plugin
|
|||
Task<string> HttpGetStringAsync(string url, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Http download the spefic url and return as stream
|
||||
/// Http download the specific url and return as stream
|
||||
/// </summary>
|
||||
/// <param name="url">URL to call Http Get</param>
|
||||
/// <param name="token">Cancellation Token</param>
|
||||
|
|
@ -314,8 +314,8 @@ namespace Flow.Launcher.Plugin
|
|||
void LogError(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
|
||||
/// 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 = "");
|
||||
|
||||
|
|
@ -402,7 +402,7 @@ namespace Flow.Launcher.Plugin
|
|||
|
||||
/// <summary>
|
||||
/// Reloads the query.
|
||||
/// When current results are from context menu or history, it will go back to query results before requerying.
|
||||
/// When current results are from context menu or history, it will go back to query results before re-querying.
|
||||
/// </summary>
|
||||
/// <param name="reselect">Choose the first result after reload if true; keep the last selected result if false. Default is true.</param>
|
||||
public void ReQuery(bool reselect = true);
|
||||
|
|
@ -556,8 +556,10 @@ namespace Flow.Launcher.Plugin
|
|||
/// <param name="zipFilePath">
|
||||
/// Path to the zip file containing the plugin. It will be unzipped to the temporary directory, removed and installed.
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
public Task UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath);
|
||||
/// <returns>
|
||||
/// True if the plugin is updated successfully, false otherwise.
|
||||
/// </returns>
|
||||
public Task<bool> UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath);
|
||||
|
||||
/// <summary>
|
||||
/// Install a plugin. By default will remove the zip file if installation is from url,
|
||||
|
|
@ -567,7 +569,10 @@ namespace Flow.Launcher.Plugin
|
|||
/// <param name="zipFilePath">
|
||||
/// Path to the zip file containing the plugin. It will be unzipped to the temporary directory, removed and installed.
|
||||
/// </param>
|
||||
public void InstallPlugin(UserPlugin plugin, string zipFilePath);
|
||||
/// <returns>
|
||||
/// True if the plugin is installed successfully, false otherwise.
|
||||
/// </returns>
|
||||
public bool InstallPlugin(UserPlugin plugin, string zipFilePath);
|
||||
|
||||
/// <summary>
|
||||
/// Uninstall a plugin
|
||||
|
|
@ -576,8 +581,10 @@ namespace Flow.Launcher.Plugin
|
|||
/// <param name="removePluginSettings">
|
||||
/// Plugin has their own settings. If this is set to true, the plugin settings will be removed.
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
public Task UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false);
|
||||
/// <returns>
|
||||
/// True if the plugin is updated successfully, false otherwise.
|
||||
/// </returns>
|
||||
public Task<bool> UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false);
|
||||
|
||||
/// <summary>
|
||||
/// Log debug message of the time taken to execute a method
|
||||
|
|
@ -612,7 +619,7 @@ namespace Flow.Launcher.Plugin
|
|||
bool IsApplicationDarkTheme();
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the actual theme of the application has changed. Currently, the plugin will continue to be subscribed even if it is turned off.
|
||||
/// Invoked when the actual theme of the application has changed. Currently, the plugin will continue to be subscribed even if it is turned off.
|
||||
/// </summary>
|
||||
event ActualApplicationThemeChangedEventHandler ActualApplicationThemeChanged;
|
||||
|
||||
|
|
|
|||
|
|
@ -197,6 +197,9 @@ namespace Flow.Launcher
|
|||
|
||||
Notification.Install();
|
||||
|
||||
// Enable Win32 dark mode if the system is in dark mode before creating all windows
|
||||
Win32Helper.EnableWin32DarkMode(_settings.ColorScheme);
|
||||
|
||||
Ioc.Default.GetRequiredService<Portable>().PreStartCleanUpAfterPortabilityUpdate();
|
||||
|
||||
API.LogInfo(ClassName, "Begin Flow Launcher startup ----------------------------------------------------");
|
||||
|
|
@ -217,6 +220,9 @@ namespace Flow.Launcher
|
|||
|
||||
Http.Proxy = _settings.Proxy;
|
||||
|
||||
// Initialize plugin manifest before initializing plugins so that they can use the manifest instantly
|
||||
await API.UpdatePluginManifestAsync();
|
||||
|
||||
await PluginManager.InitializePluginsAsync();
|
||||
|
||||
// Change language after all plugins are initialized because we need to update plugin title based on their api
|
||||
|
|
|
|||
|
|
@ -134,6 +134,10 @@
|
|||
<system:String x:Key="homeToggleBoxToolTip">This can only be edited if plugin supports Home feature and Home Page is enabled.</system:String>
|
||||
<system:String x:Key="showAtTopmost">Show Search Window at Foremost</system:String>
|
||||
<system:String x:Key="showAtTopmostToolTip">Overrides other programs' 'Always on Top' setting and displays Flow in the foremost position.</system:String>
|
||||
<system:String x:Key="autoRestartAfterChanging">Restart after modifying plugin via Plugin Store</system:String>
|
||||
<system:String x:Key="autoRestartAfterChangingToolTip">Restart Flow Launcher automatically after installing/uninstalling/updating plugin via Plugin Store</system:String>
|
||||
<system:String x:Key="showUnknownSourceWarning">Show unknown source warning</system:String>
|
||||
<system:String x:Key="showUnknownSourceWarningToolTip">Show warning when installing plugins from unknown sources</system:String>
|
||||
<system:String x:Key="alwaysRunAsAdministrator">Always run as administrator</system:String>
|
||||
<system:String x:Key="alwaysRunAsAdministratorToolTip">Run Flow Launcher as administrator on startup</system:String>
|
||||
<system:String x:Key="runAsAdministratorChange">Administrator Mode Change</system:String>
|
||||
|
|
@ -176,6 +180,12 @@
|
|||
<system:String x:Key="failedToRemovePluginSettingsMessage">Plugins: {0} - Fail to remove plugin settings files, please remove them manually</system:String>
|
||||
<system:String x:Key="failedToRemovePluginCacheTitle">Fail to remove plugin cache</system:String>
|
||||
<system:String x:Key="failedToRemovePluginCacheMessage">Plugins: {0} - Fail to remove plugin cache files, please remove them manually</system:String>
|
||||
<system:String x:Key="pluginModifiedAlreadyTitle">{0} modified already</system:String>
|
||||
<system:String x:Key="pluginModifiedAlreadyMessage">Please restart Flow before making any further changes</system:String>
|
||||
<system:String x:Key="failedToInstallPluginTitle">Fail to install {0}</system:String>
|
||||
<system:String x:Key="failedToUninstallPluginTitle">Fail to uninstall {0}</system:String>
|
||||
<system:String x:Key="fileNotFoundMessage">Unable to find plugin.json from the extracted zip file, or this path {0} does not exist</system:String>
|
||||
<system:String x:Key="pluginExistAlreadyMessage">A plugin with the same ID and version already exists, or the version is greater than this downloaded plugin</system:String>
|
||||
|
||||
<!-- Setting Plugin Store -->
|
||||
<system:String x:Key="pluginStore">Plugin Store</system:String>
|
||||
|
|
@ -191,6 +201,28 @@
|
|||
<system:String x:Key="LabelNew">New Version</system:String>
|
||||
<system:String x:Key="LabelNewToolTip">This plugin has been updated within the last 7 days</system:String>
|
||||
<system:String x:Key="LabelUpdateToolTip">New Update is Available</system:String>
|
||||
<system:String x:Key="ErrorInstallingPlugin">Error installing plugin</system:String>
|
||||
<system:String x:Key="ErrorUninstallingPlugin">Error uninstalling plugin</system:String>
|
||||
<system:String x:Key="ErrorUpdatingPlugin">Error updating plugin</system:String>
|
||||
<system:String x:Key="KeepPluginSettingsTitle">Keep plugin settings</system:String>
|
||||
<system:String x:Key="KeepPluginSettingsSubtitle">Do you want to keep the settings of the plugin for the next usage?</system:String>
|
||||
<system:String x:Key="InstallSuccessNoRestart">Plugin {0} successfully installed. Please restart Flow.</system:String>
|
||||
<system:String x:Key="UninstallSuccessNoRestart">Plugin {0} successfully uninstalled. Please restart Flow.</system:String>
|
||||
<system:String x:Key="UpdateSuccessNoRestart">Plugin {0} successfully updated. Please restart Flow.</system:String>
|
||||
<system:String x:Key="InstallPromptTitle">Plugin install</system:String>
|
||||
<system:String x:Key="InstallPromptSubtitle">{0} by {1} {2}{2}Would you like to install this plugin?</system:String>
|
||||
<system:String x:Key="UninstallPromptTitle">Plugin uninstall</system:String>
|
||||
<system:String x:Key="UninstallPromptSubtitle">{0} by {1} {2}{2}Would you like to uninstall this plugin?</system:String>
|
||||
<system:String x:Key="UpdatePromptTitle">Plugin update</system:String>
|
||||
<system:String x:Key="UpdatePromptSubtitle">{0} by {1} {2}{2}Would you like to update this plugin?</system:String>
|
||||
<system:String x:Key="DownloadingPlugin">Downloading plugin</system:String>
|
||||
<system:String x:Key="AutoRestartAfterChange">Automatically restart after installing/uninstalling/updating plugins in plugin store</system:String>
|
||||
<system:String x:Key="ZipFileNotHavePluginJson">Zip file does not have a valid plugin.json configuration</system:String>
|
||||
<system:String x:Key="InstallFromUnknownSourceTitle">Installing from an unknown source</system:String>
|
||||
<system:String x:Key="InstallFromUnknownSourceSubtitle">This plugin is from an unknown source and it may contain potential risks!{0}{0}Please ensure you understand where this plugin is from and that it is safe.{0}{0}Would you like to continue still?{0}{0}(You can switch off this warning in general section of setting window)</system:String>
|
||||
<system:String x:Key="ZipFiles">Zip files</system:String>
|
||||
<system:String x:Key="SelectZipFile">Please select zip file</system:String>
|
||||
<system:String x:Key="installLocalPluginTooltip">Install plugin from local path</system:String>
|
||||
|
||||
<!-- Setting Theme -->
|
||||
<system:String x:Key="theme">Theme</system:String>
|
||||
|
|
|
|||
|
|
@ -19,32 +19,32 @@ namespace Flow.Launcher
|
|||
|
||||
public static async Task ShowAsync(string caption, Func<Action<double>, Task> reportProgressAsync, Action cancelProgress = null)
|
||||
{
|
||||
ProgressBoxEx prgBox = null;
|
||||
ProgressBoxEx progressBox = null;
|
||||
try
|
||||
{
|
||||
if (!Application.Current.Dispatcher.CheckAccess())
|
||||
{
|
||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
prgBox = new ProgressBoxEx(cancelProgress)
|
||||
progressBox = new ProgressBoxEx(cancelProgress)
|
||||
{
|
||||
Title = caption
|
||||
};
|
||||
prgBox.TitleTextBlock.Text = caption;
|
||||
prgBox.Show();
|
||||
progressBox.TitleTextBlock.Text = caption;
|
||||
progressBox.Show();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
prgBox = new ProgressBoxEx(cancelProgress)
|
||||
progressBox = new ProgressBoxEx(cancelProgress)
|
||||
{
|
||||
Title = caption
|
||||
};
|
||||
prgBox.TitleTextBlock.Text = caption;
|
||||
prgBox.Show();
|
||||
progressBox.TitleTextBlock.Text = caption;
|
||||
progressBox.Show();
|
||||
}
|
||||
|
||||
await reportProgressAsync(prgBox.ReportProgress).ConfigureAwait(false);
|
||||
await reportProgressAsync(progressBox.ReportProgress).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
@ -58,12 +58,12 @@ namespace Flow.Launcher
|
|||
{
|
||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
prgBox?.Close();
|
||||
progressBox?.Close();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
prgBox?.Close();
|
||||
progressBox?.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -564,13 +564,13 @@ namespace Flow.Launcher
|
|||
|
||||
public bool PluginModified(string id) => PluginManager.PluginModified(id);
|
||||
|
||||
public Task UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath) =>
|
||||
public Task<bool> UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath) =>
|
||||
PluginManager.UpdatePluginAsync(pluginMetadata, plugin, zipFilePath);
|
||||
|
||||
public void InstallPlugin(UserPlugin plugin, string zipFilePath) =>
|
||||
public bool InstallPlugin(UserPlugin plugin, string zipFilePath) =>
|
||||
PluginManager.InstallPlugin(plugin, zipFilePath);
|
||||
|
||||
public Task UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false) =>
|
||||
public Task<bool> UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false) =>
|
||||
PluginManager.UninstallPluginAsync(pluginMetadata, removePluginSettings);
|
||||
|
||||
public long StopwatchLogDebug(string className, string message, Action action, [CallerMemberName] string methodName = "") =>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.ViewModel;
|
||||
|
||||
|
|
@ -96,6 +98,35 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel
|
|||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task InstallPluginAsync()
|
||||
{
|
||||
var file = GetFileFromDialog(
|
||||
App.API.GetTranslation("SelectZipFile"),
|
||||
$"{App.API.GetTranslation("ZipFiles")} (*.zip)|*.zip");
|
||||
|
||||
if (!string.IsNullOrEmpty(file))
|
||||
await PluginInstaller.InstallPluginAndCheckRestartAsync(file);
|
||||
}
|
||||
|
||||
private static string GetFileFromDialog(string title, string filter = "")
|
||||
{
|
||||
var dlg = new Microsoft.Win32.OpenFileDialog
|
||||
{
|
||||
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "\\Downloads",
|
||||
Multiselect = false,
|
||||
CheckFileExists = true,
|
||||
CheckPathExists = true,
|
||||
Title = title,
|
||||
Filter = filter
|
||||
};
|
||||
var result = dlg.ShowDialog();
|
||||
if (result == true)
|
||||
return dlg.FileName;
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public bool SatisfiesFilter(PluginStoreItemViewModel plugin)
|
||||
{
|
||||
// Check plugin language
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
|
|||
};
|
||||
Settings.ColorScheme = value;
|
||||
_ = _theme.RefreshFrameAsync();
|
||||
Win32Helper.EnableWin32DarkMode(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -234,6 +234,30 @@
|
|||
</cc:Card>
|
||||
</cc:CardGroup>
|
||||
|
||||
<cc:CardGroup Margin="0 14 0 0">
|
||||
<cc:Card
|
||||
Title="{DynamicResource autoRestartAfterChanging}"
|
||||
Icon=""
|
||||
Sub="{DynamicResource autoRestartAfterChangingToolTip}"
|
||||
Type="First">
|
||||
<ui:ToggleSwitch
|
||||
IsOn="{Binding Settings.AutoRestartAfterChanging}"
|
||||
OffContent="{DynamicResource disable}"
|
||||
OnContent="{DynamicResource enable}" />
|
||||
</cc:Card>
|
||||
|
||||
<cc:Card
|
||||
Title="{DynamicResource showUnknownSourceWarning}"
|
||||
Icon=""
|
||||
Sub="{DynamicResource showUnknownSourceWarningToolTip}"
|
||||
Type="Last">
|
||||
<ui:ToggleSwitch
|
||||
IsOn="{Binding Settings.ShowUnknownSourceWarning}"
|
||||
OffContent="{DynamicResource disable}"
|
||||
OnContent="{DynamicResource enable}" />
|
||||
</cc:Card>
|
||||
</cc:CardGroup>
|
||||
|
||||
<cc:ExCard
|
||||
Title="{DynamicResource searchDelay}"
|
||||
Margin="0 14 0 0"
|
||||
|
|
|
|||
|
|
@ -92,6 +92,13 @@
|
|||
</ui:MenuFlyout>
|
||||
</ui:FlyoutService.Flyout>
|
||||
</Button>
|
||||
<Button
|
||||
Height="34"
|
||||
Margin="0 0 10 0"
|
||||
Command="{Binding InstallPluginCommand}"
|
||||
ToolTip="{DynamicResource installLocalPluginTooltip}">
|
||||
<ui:FontIcon FontSize="14" Glyph="" />
|
||||
</Button>
|
||||
<TextBox
|
||||
Name="PluginStoreFilterTextbox"
|
||||
Width="150"
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
public void RegisterResultsUpdatedEvent()
|
||||
{
|
||||
foreach (var pair in PluginManager.GetPluginsForInterface<IResultUpdated>())
|
||||
foreach (var pair in PluginManager.GetResultUpdatePlugin())
|
||||
{
|
||||
var plugin = (IResultUpdated)pair.Plugin;
|
||||
plugin.ResultsUpdated += (s, e) =>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
|
@ -9,27 +9,28 @@ namespace Flow.Launcher.ViewModel
|
|||
{
|
||||
public partial class PluginStoreItemViewModel : BaseModel
|
||||
{
|
||||
private PluginPair PluginManagerData => PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7");
|
||||
private readonly UserPlugin _newPlugin;
|
||||
private readonly PluginPair _oldPluginPair;
|
||||
|
||||
public PluginStoreItemViewModel(UserPlugin plugin)
|
||||
{
|
||||
_plugin = plugin;
|
||||
_newPlugin = plugin;
|
||||
_oldPluginPair = PluginManager.GetPluginForId(plugin.ID);
|
||||
}
|
||||
|
||||
private UserPlugin _plugin;
|
||||
public string ID => _newPlugin.ID;
|
||||
public string Name => _newPlugin.Name;
|
||||
public string Description => _newPlugin.Description;
|
||||
public string Author => _newPlugin.Author;
|
||||
public string Version => _newPlugin.Version;
|
||||
public string Language => _newPlugin.Language;
|
||||
public string Website => _newPlugin.Website;
|
||||
public string UrlDownload => _newPlugin.UrlDownload;
|
||||
public string UrlSourceCode => _newPlugin.UrlSourceCode;
|
||||
public string IcoPath => _newPlugin.IcoPath;
|
||||
|
||||
public string ID => _plugin.ID;
|
||||
public string Name => _plugin.Name;
|
||||
public string Description => _plugin.Description;
|
||||
public string Author => _plugin.Author;
|
||||
public string Version => _plugin.Version;
|
||||
public string Language => _plugin.Language;
|
||||
public string Website => _plugin.Website;
|
||||
public string UrlDownload => _plugin.UrlDownload;
|
||||
public string UrlSourceCode => _plugin.UrlSourceCode;
|
||||
public string IcoPath => _plugin.IcoPath;
|
||||
|
||||
public bool LabelInstalled => PluginManager.GetPluginForId(_plugin.ID) != null;
|
||||
public bool LabelUpdate => LabelInstalled && new Version(_plugin.Version) > new Version(PluginManager.GetPluginForId(_plugin.ID).Metadata.Version);
|
||||
public bool LabelInstalled => _oldPluginPair != null;
|
||||
public bool LabelUpdate => LabelInstalled && new Version(_newPlugin.Version) > new Version(_oldPluginPair.Metadata.Version);
|
||||
|
||||
internal const string None = "None";
|
||||
internal const string RecentlyUpdated = "RecentlyUpdated";
|
||||
|
|
@ -41,15 +42,15 @@ namespace Flow.Launcher.ViewModel
|
|||
get
|
||||
{
|
||||
string category = None;
|
||||
if (DateTime.Now - _plugin.LatestReleaseDate < TimeSpan.FromDays(7))
|
||||
if (DateTime.Now - _newPlugin.LatestReleaseDate < TimeSpan.FromDays(7))
|
||||
{
|
||||
category = RecentlyUpdated;
|
||||
}
|
||||
if (DateTime.Now - _plugin.DateAdded < TimeSpan.FromDays(7))
|
||||
if (DateTime.Now - _newPlugin.DateAdded < TimeSpan.FromDays(7))
|
||||
{
|
||||
category = NewRelease;
|
||||
}
|
||||
if (PluginManager.GetPluginForId(_plugin.ID) != null)
|
||||
if (_oldPluginPair != null)
|
||||
{
|
||||
category = Installed;
|
||||
}
|
||||
|
|
@ -59,11 +60,22 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ShowCommandQuery(string action)
|
||||
private async Task ShowCommandQueryAsync(string action)
|
||||
{
|
||||
var actionKeyword = PluginManagerData.Metadata.ActionKeywords.Any() ? PluginManagerData.Metadata.ActionKeywords[0] + " " : String.Empty;
|
||||
App.API.ChangeQuery($"{actionKeyword}{action} {_plugin.Name}");
|
||||
App.API.ShowMainWindow();
|
||||
switch (action)
|
||||
{
|
||||
case "install":
|
||||
await PluginInstaller.InstallPluginAndCheckRestartAsync(_newPlugin);
|
||||
break;
|
||||
case "uninstall":
|
||||
await PluginInstaller.UninstallPluginAndCheckRestartAsync(_oldPluginPair.Metadata);
|
||||
break;
|
||||
case "update":
|
||||
await PluginInstaller.UpdatePluginAndCheckRestartAsync(_newPlugin, _oldPluginPair.Metadata);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
|
@ -32,21 +31,6 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
}
|
||||
|
||||
private static string PluginManagerActionKeyword
|
||||
{
|
||||
get
|
||||
{
|
||||
var keyword = PluginManager
|
||||
.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7")
|
||||
.Metadata.ActionKeywords.FirstOrDefault();
|
||||
return keyword switch
|
||||
{
|
||||
null or "*" => string.Empty,
|
||||
_ => keyword
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadIconAsync()
|
||||
{
|
||||
Image = await App.API.LoadImageAsync(PluginPair.Metadata.IcoPath);
|
||||
|
|
@ -186,10 +170,9 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenDeletePluginWindow()
|
||||
private async Task OpenDeletePluginWindowAsync()
|
||||
{
|
||||
App.API.ChangeQuery($"{PluginManagerActionKeyword} uninstall {PluginPair.Metadata.Name}".Trim(), true);
|
||||
App.API.ShowMainWindow();
|
||||
await PluginInstaller.UninstallPluginAndCheckRestartAsync(PluginPair.Metadata);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
|
|
|||
|
|
@ -45,10 +45,15 @@
|
|||
<system:String x:Key="plugin_pluginsmanager_update_success_no_restart">Plugin {0} successfully updated. Please restart Flow.</system:String>
|
||||
<system:String x:Key="plugin_pluginsmanager_update_all_success_no_restart">{0} plugins successfully updated. Please restart Flow.</system:String>
|
||||
<system:String x:Key="plugin_pluginsmanager_plugin_modified_error">Plugin {0} has already been modified. Please restart Flow before making any further changes.</system:String>
|
||||
<system:String x:Key="plugin_pluginsmanager_plugin_modified_error_title">{0} modified already</system:String>
|
||||
<system:String x:Key="plugin_pluginsmanager_plugin_modified_error_message">Please restart Flow before making any further changes</system:String>
|
||||
|
||||
<system:String x:Key="plugin_pluginsmanager_invalid_zip_title">Invalid zip installer file</system:String>
|
||||
<system:String x:Key="plugin_pluginsmanager_invalid_zip_subtitle">Please check if there is a plugin.json in {0}</system:String>
|
||||
|
||||
<!-- Plugin Infos -->
|
||||
<system:String x:Key="plugin_pluginsmanager_plugin_name">Plugins Manager</system:String>
|
||||
<system:String x:Key="plugin_pluginsmanager_plugin_description">Management of installing, uninstalling or updating Flow Launcher plugins</system:String>
|
||||
<system:String x:Key="plugin_pluginsmanager_plugin_description">Install, uninstall or update Flow Launcher plugins via the search window</system:String>
|
||||
<system:String x:Key="plugin_pluginsmanager_unknown_author">Unknown Author</system:String>
|
||||
|
||||
<!-- Context menu items -->
|
||||
|
|
@ -63,5 +68,5 @@
|
|||
|
||||
<!-- Settings menu items -->
|
||||
<system:String x:Key="plugin_pluginsmanager_plugin_settings_unknown_source">Install from unknown source warning</system:String>
|
||||
<system:String x:Key="plugin_pluginsmanager_plugin_settings_auto_restart">Automatically restart Flow Launcher after installing/uninstalling/updating plugins</system:String>
|
||||
<system:String x:Key="plugin_pluginsmanager_plugin_settings_auto_restart">Restart Flow Launcher automatically after installing/uninstalling/updating plugin via Plugins Manager</system:String>
|
||||
</ResourceDictionary>
|
||||
|
|
@ -114,6 +114,14 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
return;
|
||||
}
|
||||
|
||||
if (Context.API.PluginModified(plugin.ID))
|
||||
{
|
||||
Context.API.ShowMsgError(
|
||||
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_plugin_modified_error_title"), plugin.Name),
|
||||
Context.API.GetTranslation("plugin_pluginsmanager_plugin_modified_error_message"));
|
||||
return;
|
||||
}
|
||||
|
||||
string message;
|
||||
if (Settings.AutoRestartAfterChanging)
|
||||
{
|
||||
|
|
@ -158,7 +166,8 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
if (cts.IsCancellationRequested)
|
||||
return;
|
||||
else
|
||||
Install(plugin, filePath);
|
||||
if (!Install(plugin, filePath))
|
||||
return;
|
||||
}
|
||||
catch (HttpRequestException e)
|
||||
{
|
||||
|
|
@ -196,7 +205,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
}
|
||||
}
|
||||
|
||||
private async Task DownloadFileAsync(string prgBoxTitle, string downloadUrl, string filePath, CancellationTokenSource cts, bool deleteFile = true, bool showProgress = true)
|
||||
private async Task DownloadFileAsync(string progressBoxTitle, string downloadUrl, string filePath, CancellationTokenSource cts, bool deleteFile = true, bool showProgress = true)
|
||||
{
|
||||
if (deleteFile && File.Exists(filePath))
|
||||
File.Delete(filePath);
|
||||
|
|
@ -204,12 +213,12 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
if (showProgress)
|
||||
{
|
||||
var exceptionHappened = false;
|
||||
await Context.API.ShowProgressBoxAsync(prgBoxTitle,
|
||||
await Context.API.ShowProgressBoxAsync(progressBoxTitle,
|
||||
async (reportProgress) =>
|
||||
{
|
||||
if (reportProgress == null)
|
||||
{
|
||||
// when reportProgress is null, it means there is expcetion with the progress box
|
||||
// when reportProgress is null, it means there is exception with the progress box
|
||||
// so we record it with exceptionHappened and return so that progress box will close instantly
|
||||
exceptionHappened = true;
|
||||
return;
|
||||
|
|
@ -242,6 +251,18 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
if (FilesFolders.IsZipFilePath(search, checkFileExists: true))
|
||||
{
|
||||
pluginFromLocalPath = Utilities.GetPluginInfoFromZip(search);
|
||||
|
||||
if (pluginFromLocalPath == null) return new List<Result>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Title = Context.API.GetTranslation("plugin_pluginsmanager_invalid_zip_title"),
|
||||
SubTitle = string.Format(Context.API.GetTranslation("plugin_pluginsmanager_invalid_zip_subtitle"),
|
||||
search),
|
||||
IcoPath = icoPath
|
||||
}
|
||||
};
|
||||
|
||||
pluginFromLocalPath.LocalInstallPath = search;
|
||||
updateFromLocalPath = true;
|
||||
}
|
||||
|
|
@ -261,6 +282,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
select
|
||||
new
|
||||
{
|
||||
existingPlugin.Metadata.ID,
|
||||
pluginUpdateSource.Name,
|
||||
pluginUpdateSource.Author,
|
||||
CurrentVersion = existingPlugin.Metadata.Version,
|
||||
|
|
@ -290,6 +312,14 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
IcoPath = x.IcoPath,
|
||||
Action = e =>
|
||||
{
|
||||
if (Context.API.PluginModified(x.ID))
|
||||
{
|
||||
Context.API.ShowMsgError(
|
||||
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_plugin_modified_error_title"), x.Name),
|
||||
Context.API.GetTranslation("plugin_pluginsmanager_plugin_modified_error_message"));
|
||||
return false;
|
||||
}
|
||||
|
||||
string message;
|
||||
if (Settings.AutoRestartAfterChanging)
|
||||
{
|
||||
|
|
@ -340,8 +370,11 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
}
|
||||
else
|
||||
{
|
||||
await Context.API.UpdatePluginAsync(x.PluginExistingMetadata, x.PluginNewUserPlugin,
|
||||
downloadToFilePath);
|
||||
if (!await Context.API.UpdatePluginAsync(x.PluginExistingMetadata, x.PluginNewUserPlugin,
|
||||
downloadToFilePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Settings.AutoRestartAfterChanging)
|
||||
{
|
||||
|
|
@ -406,6 +439,14 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
IcoPath = icoPath,
|
||||
AsyncAction = async e =>
|
||||
{
|
||||
if (resultsForUpdate.All(x => Context.API.PluginModified(x.ID)))
|
||||
{
|
||||
Context.API.ShowMsgError(Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"),
|
||||
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_plugin_modified_error"),
|
||||
string.Join(" ", resultsForUpdate.Select(x => x.Name))));
|
||||
return false;
|
||||
}
|
||||
|
||||
string message;
|
||||
if (Settings.AutoRestartAfterChanging)
|
||||
{
|
||||
|
|
@ -427,6 +468,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
return false;
|
||||
}
|
||||
|
||||
var anyPluginSuccess = false;
|
||||
await Task.WhenAll(resultsForUpdate.Select(async plugin =>
|
||||
{
|
||||
var downloadToFilePath = Path.Combine(Path.GetTempPath(),
|
||||
|
|
@ -444,8 +486,11 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
if (cts.IsCancellationRequested)
|
||||
return;
|
||||
else
|
||||
await Context.API.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin,
|
||||
downloadToFilePath);
|
||||
if (!await Context.API.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin,
|
||||
downloadToFilePath))
|
||||
return;
|
||||
|
||||
anyPluginSuccess = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -458,6 +503,8 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
}
|
||||
}));
|
||||
|
||||
if (!anyPluginSuccess) return false;
|
||||
|
||||
if (Settings.AutoRestartAfterChanging)
|
||||
{
|
||||
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
|
||||
|
|
@ -559,6 +606,20 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
{
|
||||
var plugin = Utilities.GetPluginInfoFromZip(localPath);
|
||||
|
||||
if (plugin == null)
|
||||
{
|
||||
return new List<Result>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Title = Context.API.GetTranslation("plugin_pluginsmanager_invalid_zip_title"),
|
||||
SubTitle = string.Format(Context.API.GetTranslation("plugin_pluginsmanager_invalid_zip_subtitle"),
|
||||
localPath),
|
||||
IcoPath = icoPath
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
plugin.LocalInstallPath = localPath;
|
||||
|
||||
return new List<Result>
|
||||
|
|
@ -600,14 +661,17 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
return false;
|
||||
|
||||
var author = pieces[3];
|
||||
var acceptedHost = "github.com";
|
||||
var acceptedSource = "https://github.com";
|
||||
var constructedUrlPart = string.Format("{0}/{1}/", acceptedSource, author);
|
||||
|
||||
return url.StartsWith(acceptedSource) &&
|
||||
Context.API.GetAllPlugins().Any(x =>
|
||||
!string.IsNullOrEmpty(x.Metadata.Website) &&
|
||||
x.Metadata.Website.StartsWith(constructedUrlPart)
|
||||
);
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || uri.Host != acceptedHost)
|
||||
return false;
|
||||
|
||||
return Context.API.GetAllPlugins().Any(x =>
|
||||
!string.IsNullOrEmpty(x.Metadata.Website) &&
|
||||
x.Metadata.Website.StartsWith(constructedUrlPart)
|
||||
);
|
||||
}
|
||||
|
||||
internal async ValueTask<List<Result>> RequestInstallOrUpdateAsync(string search, CancellationToken token,
|
||||
|
|
@ -649,7 +713,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
return Search(results, search);
|
||||
}
|
||||
|
||||
private void Install(UserPlugin plugin, string downloadedFilePath)
|
||||
private bool Install(UserPlugin plugin, string downloadedFilePath)
|
||||
{
|
||||
if (!File.Exists(downloadedFilePath))
|
||||
throw new FileNotFoundException($"Plugin {plugin.ID} zip file not found at {downloadedFilePath}",
|
||||
|
|
@ -657,10 +721,13 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
|
||||
try
|
||||
{
|
||||
Context.API.InstallPlugin(plugin, downloadedFilePath);
|
||||
if (!Context.API.InstallPlugin(plugin, downloadedFilePath))
|
||||
return false;
|
||||
|
||||
if (!plugin.IsFromLocalInstallPath)
|
||||
File.Delete(downloadedFilePath);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (FileNotFoundException e)
|
||||
{
|
||||
|
|
@ -682,6 +749,8 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
plugin.Name));
|
||||
Context.API.LogException(ClassName, e.Message, e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal List<Result> RequestUninstall(string search)
|
||||
|
|
@ -696,6 +765,14 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
IcoPath = x.Metadata.IcoPath,
|
||||
AsyncAction = async e =>
|
||||
{
|
||||
if (Context.API.PluginModified(x.Metadata.ID))
|
||||
{
|
||||
Context.API.ShowMsgError(
|
||||
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_plugin_modified_error_title"), x.Metadata.Name),
|
||||
Context.API.GetTranslation("plugin_pluginsmanager_plugin_modified_error_message"));
|
||||
return false;
|
||||
}
|
||||
|
||||
string message;
|
||||
if (Settings.AutoRestartAfterChanging)
|
||||
{
|
||||
|
|
@ -717,7 +794,10 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
||||
{
|
||||
Context.API.HideMainWindow();
|
||||
await UninstallAsync(x.Metadata);
|
||||
if (!await UninstallAsync(x.Metadata))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Settings.AutoRestartAfterChanging)
|
||||
{
|
||||
Context.API.RestartApp();
|
||||
|
|
@ -742,7 +822,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
return Search(results, search);
|
||||
}
|
||||
|
||||
private async Task UninstallAsync(PluginMetadata plugin)
|
||||
private async Task<bool> UninstallAsync(PluginMetadata plugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -750,13 +830,14 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
Context.API.GetTranslation("plugin_pluginsmanager_keep_plugin_settings_subtitle"),
|
||||
Context.API.GetTranslation("plugin_pluginsmanager_keep_plugin_settings_title"),
|
||||
button: MessageBoxButton.YesNo) == MessageBoxResult.No;
|
||||
await Context.API.UninstallPluginAsync(plugin, removePluginSettings);
|
||||
return await Context.API.UninstallPluginAsync(plugin, removePluginSettings);
|
||||
}
|
||||
catch (ArgumentException e)
|
||||
{
|
||||
Context.API.LogException(ClassName, e.Message, e);
|
||||
Context.API.ShowMsgError(Context.API.GetTranslation("plugin_pluginsmanager_uninstall_error_title"),
|
||||
Context.API.GetTranslation("plugin_pluginsmanager_plugin_modified_error"));
|
||||
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_plugin_modified_error"), plugin.Name));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,9 +65,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
|
||||
using (ZipArchive archive = System.IO.Compression.ZipFile.OpenRead(filePath))
|
||||
{
|
||||
var pluginJsonPath = archive.Entries.FirstOrDefault(x => x.Name == "plugin.json").ToString();
|
||||
ZipArchiveEntry pluginJsonEntry = archive.GetEntry(pluginJsonPath);
|
||||
|
||||
var pluginJsonEntry = archive.Entries.FirstOrDefault(x => x.Name == "plugin.json");
|
||||
if (pluginJsonEntry != null)
|
||||
{
|
||||
using Stream stream = pluginJsonEntry.Open();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"pm"
|
||||
],
|
||||
"Name": "Plugins Manager",
|
||||
"Description": "Management of installing, uninstalling or updating Flow Launcher plugins",
|
||||
"Description": "Install, uninstall or update Flow Launcher plugins via the search window",
|
||||
"Author": "Jeremy Wu",
|
||||
"Version": "1.0.0",
|
||||
"Language": "csharp",
|
||||
|
|
|
|||
Loading…
Reference in a new issue