-
-
Notifications
You must be signed in to change notification settings - Fork 381
Add internal model for plugin management #3572
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Jack251970
wants to merge
41
commits into
dev
Choose a base branch
from
plugin_store_item_vm_null
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
41 commits
Select commit
Hold shift + click to select a range
949344a
Add internal model for plugin management
Jack251970 76736b7
Fix typo
Jack251970 c6c7ff8
Handle default
Jack251970 6044f87
Do not restart on failure
Jack251970 383c0ae
Improve code quality
Jack251970 406c29f
Merge branch 'dev' into plugin_store_item_vm_null
Jack251970 6bf7f00
Add unknown source warning setting
Jack251970 f9e7b82
Merge branch 'dev' into plugin_store_item_vm_null
Jack251970 248b098
Support installing from local path
Jack251970 e79239e
Merge branch 'dev' into plugin_store_item_vm_null
Jack251970 19c8104
Merge branch 'dev' into plugin_store_item_vm_null
Jack251970 a948636
Merge branch 'dev' into plugin_store_item_vm_null
Jack251970 7c3c768
Add type for card elements inside card group
Jack251970 73a6fb6
Move codes to new place
Jack251970 135fd03
Improve code quality
Jack251970 c5dd19e
Use Microsoft.Win32.OpenFileDialog instead
Jack251970 104b4b2
Improve code quality
Jack251970 3e9e91d
Fix possible exception when extracting zip file
Jack251970 a3a0c59
Check url nullability
Jack251970 bdb3616
Improve string resource
Jack251970 8e6a410
Use url host
Jack251970 19cb3ea
Fix typos
Jack251970 9e868e7
Move plugin installer location
Jack251970 dafb0ca
Remove unused using
Jack251970 ea25a66
Fix an issue that after uninstalling pm, store no longer fetches plug…
Jack251970 5b8b84a
Fix code comments
Jack251970 01e749a
Fix an issue that store install/uninstall same plugin without restart…
Jack251970 6318bbe
Fix an issue that pm install/uninstall same plugin without restart sa…
Jack251970 4e4b59e
Merge branch 'dev' into plugin_store_item_vm_null
jjw24 1bb7286
Show error message instead of message
Jack251970 a8bc55d
Add return for UninstallPluginAsync
Jack251970 ce6c2cb
Add return value for api functions
Jack251970 2a4bd50
Resolve conflicts
Jack251970 71043be
Fix string format issue
Jack251970 07947c5
Fix an issue that pm install/uninstall same plugin without restart sa…
Jack251970 5bfdc58
Change the notification message title about plugin already installed/…
Jack251970 650a156
Improve strings
Jack251970 e1f9d60
Merge branch 'dev' into plugin_store_item_vm_null
jjw24 ae206c3
update Plugin Store & Plugins Manager texts
jjw24 c06e6d1
Check modified state when updating plugins
Jack251970 a0dff3f
Merge branch 'dev' into plugin_store_item_vm_null
Jack251970 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,317 @@ | ||
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> | ||
/// Helper 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>(); | ||
|
||
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)); | ||
} | ||
} | ||
|
||
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); | ||
} | ||
|
||
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)); | ||
} | ||
} | ||
|
||
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)); | ||
} | ||
} | ||
|
||
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 API.ShowProgressBoxAsync(prgBoxTitle, | ||
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); | ||
} | ||
Jack251970 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}, 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) | ||
{ | ||
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) | ||
); | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.