mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into rename-file
This commit is contained in:
commit
35474f2cbc
20 changed files with 689 additions and 118 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ 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
|
||||
{
|
||||
|
|
@ -693,8 +693,13 @@ 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
|
||||
|
|
@ -704,33 +709,46 @@ namespace Flow.Launcher.Core.Plugin
|
|||
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);
|
||||
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
|
||||
|
|
@ -748,12 +766,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}";
|
||||
|
|
@ -797,13 +819,17 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
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)
|
||||
|
|
@ -872,6 +898,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
ModifiedPlugins.Add(plugin.ID);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
|
|||
|
|
@ -398,6 +398,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]
|
||||
|
|
|
|||
|
|
@ -23,8 +23,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);
|
||||
|
||||
|
|
@ -49,7 +49,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);
|
||||
|
|
@ -65,7 +65,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.
|
||||
|
|
@ -97,7 +97,7 @@ namespace Flow.Launcher.Plugin
|
|||
/// Show the MainWindow when hiding
|
||||
/// </summary>
|
||||
void ShowMainWindow();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Focus the query text box in the main window
|
||||
/// </summary>
|
||||
|
|
@ -115,7 +115,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;
|
||||
|
||||
|
|
@ -171,7 +171,7 @@ namespace Flow.Launcher.Plugin
|
|||
string GetTranslation(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Get all loaded plugins
|
||||
/// Get all loaded plugins
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
List<PluginPair> GetAllPlugins();
|
||||
|
|
@ -229,7 +229,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>
|
||||
|
|
@ -237,7 +237,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>
|
||||
|
|
@ -305,8 +305,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 = "");
|
||||
|
||||
|
|
@ -393,7 +393,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);
|
||||
|
|
@ -547,8 +547,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,
|
||||
|
|
@ -558,7 +560,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
|
||||
|
|
@ -567,8 +572,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
|
||||
|
|
@ -603,7 +610,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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -211,6 +211,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
|
||||
|
|
|
|||
|
|
@ -135,6 +135,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>
|
||||
|
||||
<!-- Setting Plugin -->
|
||||
<system:String x:Key="searchplugin">Search Plugin</system:String>
|
||||
|
|
@ -173,6 +177,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>
|
||||
|
|
@ -188,6 +198,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -567,13 +567,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
|
||||
|
|
|
|||
|
|
@ -225,6 +225,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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
@ -409,6 +442,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)
|
||||
{
|
||||
|
|
@ -430,6 +471,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
return false;
|
||||
}
|
||||
|
||||
var anyPluginSuccess = false;
|
||||
await Task.WhenAll(resultsForUpdate.Select(async plugin =>
|
||||
{
|
||||
var downloadToFilePath = Path.Combine(Path.GetTempPath(),
|
||||
|
|
@ -447,8 +489,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)
|
||||
{
|
||||
|
|
@ -461,6 +506,8 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
}
|
||||
}));
|
||||
|
||||
if (!anyPluginSuccess) return false;
|
||||
|
||||
if (Settings.AutoRestartAfterChanging)
|
||||
{
|
||||
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
|
||||
|
|
@ -556,6 +603,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>
|
||||
|
|
@ -597,14 +658,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,
|
||||
|
|
@ -644,7 +708,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}",
|
||||
|
|
@ -652,10 +716,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)
|
||||
{
|
||||
|
|
@ -677,6 +744,8 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
plugin.Name));
|
||||
Context.API.LogException(ClassName, e.Message, e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal List<Result> RequestUninstall(string search)
|
||||
|
|
@ -691,6 +760,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)
|
||||
{
|
||||
|
|
@ -712,7 +789,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();
|
||||
|
|
@ -745,7 +825,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
return Search(results, search);
|
||||
}
|
||||
|
||||
private async Task UninstallAsync(PluginMetadata plugin)
|
||||
private async Task<bool> UninstallAsync(PluginMetadata plugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -753,13 +833,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