Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
949344a
Add internal model for plugin management
Jack251970 May 22, 2025
76736b7
Fix typo
Jack251970 May 22, 2025
c6c7ff8
Handle default
Jack251970 May 22, 2025
6044f87
Do not restart on failure
Jack251970 May 22, 2025
383c0ae
Improve code quality
Jack251970 May 23, 2025
406c29f
Merge branch 'dev' into plugin_store_item_vm_null
Jack251970 Jun 1, 2025
6bf7f00
Add unknown source warning setting
Jack251970 Jun 1, 2025
f9e7b82
Merge branch 'dev' into plugin_store_item_vm_null
Jack251970 Jun 3, 2025
248b098
Support installing from local path
Jack251970 Jun 9, 2025
e79239e
Merge branch 'dev' into plugin_store_item_vm_null
Jack251970 Jun 13, 2025
19c8104
Merge branch 'dev' into plugin_store_item_vm_null
Jack251970 Jun 15, 2025
a948636
Merge branch 'dev' into plugin_store_item_vm_null
Jack251970 Jun 28, 2025
7c3c768
Add type for card elements inside card group
Jack251970 Jun 29, 2025
73a6fb6
Move codes to new place
Jack251970 Jun 29, 2025
135fd03
Improve code quality
Jack251970 Jun 29, 2025
c5dd19e
Use Microsoft.Win32.OpenFileDialog instead
Jack251970 Jun 29, 2025
104b4b2
Improve code quality
Jack251970 Jun 29, 2025
3e9e91d
Fix possible exception when extracting zip file
Jack251970 Jun 29, 2025
a3a0c59
Check url nullability
Jack251970 Jun 29, 2025
bdb3616
Improve string resource
Jack251970 Jun 29, 2025
8e6a410
Use url host
Jack251970 Jun 29, 2025
19cb3ea
Fix typos
Jack251970 Jun 29, 2025
9e868e7
Move plugin installer location
Jack251970 Jun 29, 2025
dafb0ca
Remove unused using
Jack251970 Jun 30, 2025
ea25a66
Fix an issue that after uninstalling pm, store no longer fetches plug…
Jack251970 Jun 30, 2025
5b8b84a
Fix code comments
Jack251970 Jun 30, 2025
01e749a
Fix an issue that store install/uninstall same plugin without restart…
Jack251970 Jun 30, 2025
6318bbe
Fix an issue that pm install/uninstall same plugin without restart sa…
Jack251970 Jun 30, 2025
4e4b59e
Merge branch 'dev' into plugin_store_item_vm_null
jjw24 Jun 30, 2025
1bb7286
Show error message instead of message
Jack251970 Jul 1, 2025
a8bc55d
Add return for UninstallPluginAsync
Jack251970 Jul 1, 2025
ce6c2cb
Add return value for api functions
Jack251970 Jul 1, 2025
2a4bd50
Resolve conflicts
Jack251970 Jul 1, 2025
71043be
Fix string format issue
Jack251970 Jul 1, 2025
07947c5
Fix an issue that pm install/uninstall same plugin without restart sa…
Jack251970 Jul 1, 2025
5bfdc58
Change the notification message title about plugin already installed/…
Jack251970 Jul 1, 2025
650a156
Improve strings
Jack251970 Jul 1, 2025
e1f9d60
Merge branch 'dev' into plugin_store_item_vm_null
jjw24 Jul 3, 2025
ae206c3
update Plugin Store & Plugins Manager texts
jjw24 Jul 3, 2025
c06e6d1
Check modified state when updating plugins
Jack251970 Jul 3, 2025
a0dff3f
Merge branch 'dev' into plugin_store_item_vm_null
Jack251970 Jul 5, 2025
052bbb9
Fix format
Jack251970 Jul 6, 2025
c23eb50
Merge branch 'dev' into plugin_store_item_vm_null
Jack251970 Jul 6, 2025
edb1450
Fix build issue
Jack251970 Jul 6, 2025
a63c8b0
updates to method summary and correct spell check errors
jjw24 Jul 7, 2025
236bff1
fix spelling
jjw24 Jul 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Flow.Launcher.Infrastructure/UserSettings/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ public bool ShowHomePage
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]
Expand Down
18 changes: 18 additions & 0 deletions Flow.Launcher/Languages/en.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
11 changes: 11 additions & 0 deletions Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
270 changes: 246 additions & 24 deletions Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,35 +1,45 @@
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;

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>();

Check warning on line 19 in Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Ioc` is not a recognized word. (unrecognized-spelling)

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 => _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 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 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";
Expand All @@ -41,15 +51,15 @@
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;
}
Expand All @@ -59,11 +69,223 @@
}

[RelayCommand]
private void ShowCommandQuery(string action)
private async Task ShowCommandQueryAsync(string action)
{
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)
{
var actionKeyword = PluginManagerData.Metadata.ActionKeywords.Any() ? PluginManagerData.Metadata.ActionKeywords[0] + " " : String.Empty;
App.API.ChangeQuery($"{actionKeyword}{action} {_plugin.Name}");
App.API.ShowMainWindow();
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,

Check warning on line 264 in Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`prg` is not a recognized word. (unrecognized-spelling)
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);
}
}
}
}
Loading
Loading