Merge pull request #3827 from Flow-Launcher/plugin_update

Add Support for Automatically Checking Plugin Updates
This commit is contained in:
Jeremy Wu 2025-07-15 22:51:53 +10:00 committed by GitHub
commit fa6301b2de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 164 additions and 5 deletions

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
@ -277,6 +278,100 @@ public static class PluginInstaller
}
}
/// <summary>
/// Updates the plugin to the latest version available from its source.
/// </summary>
/// <param name="silentUpdate">If true, do not show any messages when there is no update available.</param>
/// <param name="usePrimaryUrlOnly">If true, only use the primary URL for updates.</param>
/// <param name="token">Cancellation token to cancel the update operation.</param>
/// <returns></returns>
public static async Task CheckForPluginUpdatesAsync(bool silentUpdate = true, bool usePrimaryUrlOnly = false, CancellationToken token = default)
{
// Update the plugin manifest
await API.UpdatePluginManifestAsync(usePrimaryUrlOnly, token);
// Get all plugins that can be updated
var resultsForUpdate = (
from existingPlugin in API.GetAllPlugins()
join pluginUpdateSource in API.GetPluginManifest()
on existingPlugin.Metadata.ID equals pluginUpdateSource.ID
where string.Compare(existingPlugin.Metadata.Version, pluginUpdateSource.Version,
StringComparison.InvariantCulture) <
0 // if current version precedes version of the plugin from update source (e.g. PluginsManifest)
&& !API.PluginModified(existingPlugin.Metadata.ID)
select
new PluginUpdateInfo()
{
ID = existingPlugin.Metadata.ID,
Name = existingPlugin.Metadata.Name,
Author = existingPlugin.Metadata.Author,
CurrentVersion = existingPlugin.Metadata.Version,
NewVersion = pluginUpdateSource.Version,
IcoPath = existingPlugin.Metadata.IcoPath,
PluginExistingMetadata = existingPlugin.Metadata,
PluginNewUserPlugin = pluginUpdateSource
}).ToList();
// No updates
if (!resultsForUpdate.Any())
{
if (!silentUpdate)
{
API.ShowMsg(API.GetTranslation("updateNoResultTitle"), API.GetTranslation("updateNoResultSubtitle"));
}
return;
}
// If all plugins are modified, just return
if (resultsForUpdate.All(x => API.PluginModified(x.ID)))
{
return;
}
// Show message box with button to update all plugins
API.ShowMsgWithButton(
API.GetTranslation("updateAllPluginsTitle"),
API.GetTranslation("updateAllPluginsButtonContent"),
() =>
{
UpdateAllPlugins(resultsForUpdate);
},
string.Join(", ", resultsForUpdate.Select(x => x.PluginExistingMetadata.Name)));
}
private static void UpdateAllPlugins(IEnumerable<PluginUpdateInfo> resultsForUpdate)
{
_ = Task.WhenAll(resultsForUpdate.Select(async plugin =>
{
var downloadToFilePath = Path.Combine(Path.GetTempPath(), $"{plugin.Name}-{plugin.NewVersion}.zip");
try
{
using var cts = new CancellationTokenSource();
await DownloadFileAsync(
$"{API.GetTranslation("DownloadingPlugin")} {plugin.PluginNewUserPlugin.Name}",
plugin.PluginNewUserPlugin.UrlDownload, downloadToFilePath, cts);
// check if user cancelled download before installing plugin
if (cts.IsCancellationRequested)
{
return;
}
if (!await API.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin, downloadToFilePath))
{
return;
}
}
catch (Exception e)
{
API.LogException(ClassName, "Failed to update plugin", e);
API.ShowMsgError(API.GetTranslation("ErrorUpdatingPlugin"));
}
}));
}
/// <summary>
/// Downloads a file from a URL to a local path, optionally showing a progress box and handling cancellation.
/// </summary>
@ -350,4 +445,16 @@ public static class PluginInstaller
x.Metadata.Website.StartsWith(constructedUrlPart)
);
}
private record PluginUpdateInfo
{
public string ID { get; init; }
public string Name { get; init; }
public string Author { get; init; }
public string CurrentVersion { get; init; }
public string NewVersion { get; init; }
public string IcoPath { get; init; }
public PluginMetadata PluginExistingMetadata { get; init; }
public UserPlugin PluginNewUserPlugin { get; init; }
}
}

View file

@ -233,6 +233,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public bool AutoRestartAfterChanging { get; set; } = false;
public bool ShowUnknownSourceWarning { get; set; } = true;
public bool AutoUpdatePlugins { get; set; } = true;
public int CustomExplorerIndex { get; set; } = 0;

View file

@ -239,6 +239,7 @@ namespace Flow.Launcher
AutoStartup();
AutoUpdates();
AutoPluginUpdates();
API.SaveAppAllSettings();
API.LogInfo(ClassName, "End Flow Launcher startup ----------------------------------------------------");
@ -251,7 +252,7 @@ namespace Flow.Launcher
/// Check startup only for Release
/// </summary>
[Conditional("RELEASE")]
private void AutoStartup()
private static void AutoStartup()
{
// we try to enable auto-startup on first launch, or reenable if it was removed
// but the user still has the setting set
@ -272,7 +273,7 @@ namespace Flow.Launcher
}
[Conditional("RELEASE")]
private void AutoUpdates()
private static void AutoUpdates()
{
_ = Task.Run(async () =>
{
@ -289,6 +290,23 @@ namespace Flow.Launcher
});
}
private static void AutoPluginUpdates()
{
_ = Task.Run(async () =>
{
if (_settings.AutoUpdatePlugins)
{
// check plugin updates every 5 hour
var timer = new PeriodicTimer(TimeSpan.FromHours(5));
await PluginInstaller.CheckForPluginUpdatesAsync();
while (await timer.WaitForNextTickAsync())
// check updates on startup
await PluginInstaller.CheckForPluginUpdatesAsync();
}
});
}
#endregion
#region Register Events

View file

@ -93,6 +93,7 @@
<system:String x:Key="typingStartEn">Always Start Typing in English Mode</system:String>
<system:String x:Key="typingStartEnTooltip">Temporarily change your input method to English mode when activating Flow.</system:String>
<system:String x:Key="autoUpdates">Auto Update</system:String>
<system:String x:Key="autoUpdatesTooltip">Automatically check and update the app when available</system:String>
<system:String x:Key="select">Select</system:String>
<system:String x:Key="hideOnStartup">Hide Flow Launcher on startup</system:String>
<system:String x:Key="hideOnStartupToolTip">Flow Launcher search window is hidden in the tray after starting up.</system:String>
@ -117,7 +118,7 @@
<system:String x:Key="DoublePinyinSchemasXingKongJianDao">Xing Kong Jian Dao</system:String>
<system:String x:Key="DoublePinyinSchemasDaNiu">Da Niu</system:String>
<system:String x:Key="DoublePinyinSchemasXiaoLang">Xiao Lang</system:String>
<system:String x:Key="AlwaysPreview">Always Preview</system:String>
<system:String x:Key="AlwaysPreviewToolTip">Always open preview panel when Flow activates. Press {0} to toggle preview.</system:String>
<system:String x:Key="shadowEffectNotAllowed">Shadow effect is not allowed while current theme has blur effect enabled</system:String>
@ -150,6 +151,8 @@
<system:String x:Key="autoRestartAfterChangingToolTip">Restart Flow Launcher automatically after installing/uninstalling/updating plugin via Plugin Store</system:String>
<system:String x:Key="showUnknownSourceWarning">Show unknown source warning</system:String>
<system:String x:Key="showUnknownSourceWarningToolTip">Show warning when installing plugins from unknown sources</system:String>
<system:String x:Key="autoUpdatePlugins">Auto update plugins</system:String>
<system:String x:Key="autoUpdatePluginsToolTip">Automatically check plugin updates and notify if there are any updates available</system:String>
<!-- Setting Plugin -->
<system:String x:Key="searchplugin">Search Plugin</system:String>
@ -231,6 +234,11 @@
<system:String x:Key="ZipFiles">Zip files</system:String>
<system:String x:Key="SelectZipFile">Please select zip file</system:String>
<system:String x:Key="installLocalPluginTooltip">Install plugin from local path</system:String>
<system:String x:Key="updateNoResultTitle">No update available</system:String>
<system:String x:Key="updateNoResultSubtitle">All plugins are up to date</system:String>
<system:String x:Key="updateAllPluginsTitle">Plugin updates available</system:String>
<system:String x:Key="updateAllPluginsButtonContent">Update all plugins</system:String>
<system:String x:Key="checkPluginUpdatesTooltip">Check plugin updates</system:String>
<!-- Setting Theme -->
<system:String x:Key="theme">Theme</system:String>

View file

@ -109,6 +109,12 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel
await PluginInstaller.InstallPluginAndCheckRestartAsync(file);
}
[RelayCommand]
private async Task CheckPluginUpdatesAsync()
{
await PluginInstaller.CheckForPluginUpdatesAsync(silentUpdate: false);
}
private static string GetFileFromDialog(string title, string filter = "")
{
var dlg = new Microsoft.Win32.OpenFileDialog

View file

@ -182,7 +182,8 @@
<cc:Card
Title="{DynamicResource autoUpdates}"
Margin="0 14 0 0"
Icon="&#xecc5;">
Icon="&#xecc5;"
Sub="{DynamicResource autoUpdatesTooltip}">
<ui:ToggleSwitch
IsOn="{Binding AutoUpdates}"
OffContent="{DynamicResource disable}"
@ -241,12 +242,23 @@
Title="{DynamicResource showUnknownSourceWarning}"
Icon="&#xE7BA;"
Sub="{DynamicResource showUnknownSourceWarningToolTip}"
Type="Last">
Type="Middle">
<ui:ToggleSwitch
IsOn="{Binding Settings.ShowUnknownSourceWarning}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
<cc:Card
Title="{DynamicResource autoUpdatePlugins}"
Icon="&#xecc5;"
Sub="{DynamicResource autoUpdatePluginsToolTip}"
Type="Last">
<ui:ToggleSwitch
IsOn="{Binding Settings.AutoUpdatePlugins}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
</cc:CardGroup>
<cc:ExCard

View file

@ -99,6 +99,13 @@
ToolTip="{DynamicResource installLocalPluginTooltip}">
<ui:FontIcon FontSize="14" Glyph="&#xE8DA;" />
</Button>
<Button
Height="34"
Margin="0 0 10 0"
Command="{Binding CheckPluginUpdatesCommand}"
ToolTip="{DynamicResource checkPluginUpdatesTooltip}">
<ui:FontIcon FontSize="14" Glyph="&#xecc5;" />
</Button>
<TextBox
Name="PluginStoreFilterTextbox"
Width="150"