Add internal model for plugin management

This commit is contained in:
Jack251970 2025-05-22 17:32:33 +08:00
parent 60de423fc3
commit 949344a51e
5 changed files with 280 additions and 44 deletions

View file

@ -176,6 +176,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public bool ShowHistoryResultsForHomePage { get; set; } = false;
public int MaxHistoryResultsToShowForHomePage { get; set; } = 5;
public bool AutoRestartAfterChanging { get; set; } = false;
public int CustomExplorerIndex { get; set; } = 0;
[JsonIgnore]

View file

@ -131,6 +131,8 @@
<system:String x:Key="historyResultsForHomePage">Show History Results in Home Page</system:String>
<system:String x:Key="historyResultsCountForHomePage">Maximum History Results Shown in Home Page</system:String>
<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="autoRestartAfterChanging">Automatically restart after changing plugins</system:String>
<system:String x:Key="autoRestartAfterChangingToolTip">Automatically restart Flow Launcher after installing/uninstalling/updating plugins</system:String>
<!-- Setting Plugin -->
<system:String x:Key="searchplugin">Search Plugin</system:String>
@ -184,6 +186,22 @@
<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 udpate</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>
<!-- Setting Theme -->
<system:String x:Key="theme">Theme</system:String>

View file

@ -202,6 +202,17 @@
</cc:Card>
</cc:CardGroup>
<cc:Card
Title="{DynamicResource autoRestartAfterChanging}"
Margin="0 14 0 0"
Icon="&#xF83E;"
Sub="{DynamicResource autoRestartAfterChangingToolTip}">
<ui:ToggleSwitch
IsOn="{Binding Settings.AutoRestartAfterChanging}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
<cc:ExCard
Title="{DynamicResource searchDelay}"
Margin="0 14 0 0"

View file

@ -1,7 +1,12 @@
using System;
using System.Linq;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Version = SemanticVersioning.Version;
@ -9,27 +14,32 @@ namespace Flow.Launcher.ViewModel
{
public partial class PluginStoreItemViewModel : BaseModel
{
private PluginPair PluginManagerData => PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7");
private static readonly string ClassName = nameof(PluginStoreItemViewModel);
private static readonly Settings Settings = Ioc.Default.GetRequiredService<Settings>();
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 +51,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 +69,223 @@ 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 InstallPluginAsync(_newPlugin);
break;
case "uninstall":
await UninstallPluginAsync(_oldPluginPair.Metadata);
break;
case "update":
await UpdatePluginAsync(_newPlugin, _oldPluginPair.Metadata);
break;
}
}
internal static async Task InstallPluginAsync(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"));
}
if (Settings.AutoRestartAfterChanging)
{
App.API.RestartApp();
}
else
{
App.API.ShowMsg(
App.API.GetTranslation("installbtn"),
string.Format(
App.API.GetTranslation(
"InstallSuccessNoRestart"),
newPlugin.Name));
}
}
internal static async Task UninstallPluginAsync(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"));
}
if (Settings.AutoRestartAfterChanging)
{
App.API.RestartApp();
}
else
{
App.API.ShowMsg(
App.API.GetTranslation("uninstallbtn"),
string.Format(
App.API.GetTranslation(
"UninstallSuccessNoRestart"),
oldPlugin.Name));
}
}
internal static async Task UpdatePluginAsync(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"));
}
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);
}
}
}
}

View file

@ -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 PluginStoreItemViewModel.UninstallPluginAsync(PluginPair.Metadata);
}
[RelayCommand]