mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Add internal model for plugin management
This commit is contained in:
parent
60de423fc3
commit
949344a51e
5 changed files with 280 additions and 44 deletions
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -202,6 +202,17 @@
|
|||
</cc:Card>
|
||||
</cc:CardGroup>
|
||||
|
||||
<cc:Card
|
||||
Title="{DynamicResource autoRestartAfterChanging}"
|
||||
Margin="0 14 0 0"
|
||||
Icon=""
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Reference in a new issue