Move codes to new place

This commit is contained in:
Jack251970 2025-06-29 15:48:08 +08:00
parent 7c3c7680c0
commit 73a6fb6f65
5 changed files with 291 additions and 270 deletions

View file

@ -2,12 +2,10 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
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.Core.ExternalPlugins;
using Flow.Launcher.Infrastructure;
@ -26,8 +24,6 @@ namespace Flow.Launcher.Core.Plugin
{
private static readonly string ClassName = nameof(PluginManager);
private static readonly Settings FlowSettings = Ioc.Default.GetRequiredService<Settings>();
private static IEnumerable<PluginPair> _contextMenuPlugins;
private static IEnumerable<PluginPair> _homePlugins;
@ -561,213 +557,6 @@ namespace Flow.Launcher.Core.Plugin
await UninstallPluginAsync(plugin, removePluginFromSettings, removePluginSettings, true);
}
public static async Task InstallPluginAndCheckRestartAsync(UserPlugin newPlugin)
{
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;
}
else
{
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"Plugin {newPlugin.ID} zip file not found at {filePath}", filePath);
}
API.InstallPlugin(newPlugin, filePath);
if (!newPlugin.IsFromLocalInstallPath)
{
File.Delete(filePath);
}
}
}
catch (Exception e)
{
API.LogException(ClassName, "Failed to install plugin", e);
API.ShowMsgError(API.GetTranslation("ErrorInstallingPlugin"));
return; // dont restart on failure
}
if (FlowSettings.AutoRestartAfterChanging)
{
API.RestartApp();
}
else
{
API.ShowMsg(
API.GetTranslation("installbtn"),
string.Format(
API.GetTranslation(
"InstallSuccessNoRestart"),
newPlugin.Name));
}
}
public static async Task InstallPluginAndCheckRestartAsync(string filePath)
{
UserPlugin plugin;
try
{
using ZipArchive archive = ZipFile.OpenRead(filePath);
var pluginJsonPath = archive.Entries.FirstOrDefault(x => x.Name == "plugin.json") ??
throw new FileNotFoundException("The zip file does not contain a plugin.json file.");
var pluginJsonEntry = archive.GetEntry(pluginJsonPath.ToString()) ??
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 (FlowSettings.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);
}
public static async Task UninstallPluginAndCheckRestartAsync(PluginMetadata oldPlugin)
{
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
{
await API.UninstallPluginAsync(oldPlugin, removePluginSettings);
}
catch (Exception e)
{
API.LogException(ClassName, "Failed to uninstall plugin", e);
API.ShowMsgError(API.GetTranslation("ErrorUninstallingPlugin"));
return; // dont restart on failure
}
if (FlowSettings.AutoRestartAfterChanging)
{
API.RestartApp();
}
else
{
API.ShowMsg(
API.GetTranslation("uninstallbtn"),
string.Format(
API.GetTranslation(
"UninstallSuccessNoRestart"),
oldPlugin.Name));
}
}
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;
}
else
{
await API.UpdatePluginAsync(oldPlugin, newPlugin, filePath);
}
}
catch (Exception e)
{
API.LogException(ClassName, "Failed to update plugin", e);
API.ShowMsgError(API.GetTranslation("ErrorUpdatingPlugin"));
return; // dont restart on failure
}
if (FlowSettings.AutoRestartAfterChanging)
{
API.RestartApp();
}
else
{
API.ShowMsg(
API.GetTranslation("updatebtn"),
string.Format(
API.GetTranslation(
"UpdateSuccessNoRestart"),
newPlugin.Name));
}
}
#endregion
#region Internal functions
@ -915,59 +704,6 @@ namespace Flow.Launcher.Core.Plugin
}
}
internal static async Task DownloadFileAsync(string prgBoxTitle, 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(prgBoxTitle,
async (reportProgress) =>
{
if (reportProgress == null)
{
// when reportProgress is null, it means there is expcetion 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);
}
}
private static bool InstallSourceKnown(string url)
{
var pieces = url.Split('/');
if (pieces.Length < 4)
return false;
var author = pieces[3];
var acceptedSource = "https://github.com";
var constructedUrlPart = string.Format("{0}/{1}/", acceptedSource, author);
return url.StartsWith(acceptedSource) &&
API.GetAllPlugins().Any(x =>
!string.IsNullOrEmpty(x.Metadata.Website) &&
x.Metadata.Website.StartsWith(constructedUrlPart)
);
}
#endregion
}
}

View file

@ -0,0 +1,283 @@
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.Helper;
/// <summary>
/// Helper class for installing, updating, and uninstalling plugins.
/// </summary>
public static class PluginInstallationHelper
{
private static readonly string ClassName = nameof(PluginInstallationHelper);
private static readonly Settings Settings = Ioc.Default.GetRequiredService<Settings>();
public static async Task InstallPluginAndCheckRestartAsync(UserPlugin newPlugin)
{
if (App.API.ShowMsgBox(
string.Format(
App.API.GetTranslation("InstallPromptSubtitle"),
newPlugin.Name, newPlugin.Author, Environment.NewLine),
App.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(
$"{App.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;
}
else
{
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"Plugin {newPlugin.ID} zip file not found at {filePath}", filePath);
}
App.API.InstallPlugin(newPlugin, filePath);
if (!newPlugin.IsFromLocalInstallPath)
{
File.Delete(filePath);
}
}
}
catch (Exception e)
{
App.API.LogException(ClassName, "Failed to install plugin", e);
App.API.ShowMsgError(App.API.GetTranslation("ErrorInstallingPlugin"));
return; // dont restart on failure
}
if (Settings.AutoRestartAfterChanging)
{
App.API.RestartApp();
}
else
{
App.API.ShowMsg(
App.API.GetTranslation("installbtn"),
string.Format(
App.API.GetTranslation(
"InstallSuccessNoRestart"),
newPlugin.Name));
}
}
public static async Task InstallPluginAndCheckRestartAsync(string filePath)
{
UserPlugin plugin;
try
{
using ZipArchive archive = ZipFile.OpenRead(filePath);
var pluginJsonPath = archive.Entries.FirstOrDefault(x => x.Name == "plugin.json") ??
throw new FileNotFoundException("The zip file does not contain a plugin.json file.");
var pluginJsonEntry = archive.GetEntry(pluginJsonPath.ToString()) ??
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)
{
App.API.LogException(ClassName, "Failed to validate zip file", e);
App.API.ShowMsgError(App.API.GetTranslation("ZipFileNotHavePluginJson"));
return;
}
if (Settings.ShowUnknownSourceWarning)
{
if (!InstallSourceKnown(plugin.Website)
&& App.API.ShowMsgBox(string.Format(
App.API.GetTranslation("InstallFromUnknownSourceSubtitle"), Environment.NewLine),
App.API.GetTranslation("InstallFromUnknownSourceTitle"),
MessageBoxButton.YesNo) == MessageBoxResult.No)
return;
}
await InstallPluginAndCheckRestartAsync(plugin);
}
public static async Task UninstallPluginAndCheckRestartAsync(PluginMetadata oldPlugin)
{
if (App.API.ShowMsgBox(
string.Format(
App.API.GetTranslation("UninstallPromptSubtitle"),
oldPlugin.Name, oldPlugin.Author, Environment.NewLine),
App.API.GetTranslation("UninstallPromptTitle"),
button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return;
var removePluginSettings = App.API.ShowMsgBox(
App.API.GetTranslation("KeepPluginSettingsSubtitle"),
App.API.GetTranslation("KeepPluginSettingsTitle"),
button: MessageBoxButton.YesNo) == MessageBoxResult.No;
try
{
await App.API.UninstallPluginAsync(oldPlugin, removePluginSettings);
}
catch (Exception e)
{
App.API.LogException(ClassName, "Failed to uninstall plugin", e);
App.API.ShowMsgError(App.API.GetTranslation("ErrorUninstallingPlugin"));
return; // dont restart on failure
}
if (Settings.AutoRestartAfterChanging)
{
App.API.RestartApp();
}
else
{
App.API.ShowMsg(
App.API.GetTranslation("uninstallbtn"),
string.Format(
App.API.GetTranslation(
"UninstallSuccessNoRestart"),
oldPlugin.Name));
}
}
public static async Task UpdatePluginAndCheckRestartAsync(UserPlugin newPlugin, PluginMetadata oldPlugin)
{
if (App.API.ShowMsgBox(
string.Format(
App.API.GetTranslation("UpdatePromptSubtitle"),
oldPlugin.Name, oldPlugin.Author, Environment.NewLine),
App.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(
$"{App.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;
}
else
{
await App.API.UpdatePluginAsync(oldPlugin, newPlugin, filePath);
}
}
catch (Exception e)
{
App.API.LogException(ClassName, "Failed to update plugin", e);
App.API.ShowMsgError(App.API.GetTranslation("ErrorUpdatingPlugin"));
return; // dont restart on failure
}
if (Settings.AutoRestartAfterChanging)
{
App.API.RestartApp();
}
else
{
App.API.ShowMsg(
App.API.GetTranslation("updatebtn"),
string.Format(
App.API.GetTranslation(
"UpdateSuccessNoRestart"),
newPlugin.Name));
}
}
private static async Task DownloadFileAsync(string prgBoxTitle, 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 App.API.ShowProgressBoxAsync(prgBoxTitle,
async (reportProgress) =>
{
if (reportProgress == null)
{
// when reportProgress is null, it means there is expcetion with the progress box
// so we record it with exceptionHappened and return so that progress box will close instantly
exceptionHappened = true;
return;
}
else
{
await App.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 App.API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false);
}
else
{
await App.API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false);
}
}
private static bool InstallSourceKnown(string url)
{
var pieces = url.Split('/');
if (pieces.Length < 4)
return false;
var author = pieces[3];
var acceptedSource = "https://github.com";
var constructedUrlPart = string.Format("{0}/{1}/", acceptedSource, author);
return url.StartsWith(acceptedSource) &&
App.API.GetAllPlugins().Any(x =>
!string.IsNullOrEmpty(x.Metadata.Website) &&
x.Metadata.Website.StartsWith(constructedUrlPart)
);
}
}

View file

@ -4,7 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Helper;
using Flow.Launcher.Plugin;
using Flow.Launcher.ViewModel;
@ -107,7 +107,7 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel
$"{App.API.GetTranslation("ZipFiles")} (*.zip)|*.zip");
if (!string.IsNullOrEmpty(file))
await PluginManager.InstallPluginAndCheckRestartAsync(file);
await PluginInstallationHelper.InstallPluginAndCheckRestartAsync(file);
}
private static string GetFileFromDialog(string title, string filter = "")

View file

@ -2,6 +2,7 @@
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Helper;
using Flow.Launcher.Plugin;
using Version = SemanticVersioning.Version;
@ -65,13 +66,13 @@ namespace Flow.Launcher.ViewModel
switch (action)
{
case "install":
await PluginManager.InstallPluginAndCheckRestartAsync(_newPlugin);
await PluginInstallationHelper.InstallPluginAndCheckRestartAsync(_newPlugin);
break;
case "uninstall":
await PluginManager.UninstallPluginAndCheckRestartAsync(_oldPluginPair.Metadata);
await PluginInstallationHelper.UninstallPluginAndCheckRestartAsync(_oldPluginPair.Metadata);
break;
case "update":
await PluginManager.UpdatePluginAndCheckRestartAsync(_newPlugin, _oldPluginPair.Metadata);
await PluginInstallationHelper.UpdatePluginAndCheckRestartAsync(_newPlugin, _oldPluginPair.Metadata);
break;
default:
break;

View file

@ -5,6 +5,7 @@ using System.Windows.Media;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure.Image;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
@ -172,7 +173,7 @@ namespace Flow.Launcher.ViewModel
[RelayCommand]
private async Task OpenDeletePluginWindowAsync()
{
await PluginManager.UninstallPluginAndCheckRestartAsync(PluginPair.Metadata);
await PluginInstallationHelper.UninstallPluginAndCheckRestartAsync(PluginPair.Metadata);
}
[RelayCommand]