diff --git a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs index a9585f6a4f9..fab1b3e8f00 100644 --- a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs +++ b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs @@ -2,6 +2,8 @@ using Flow.Launcher.Infrastructure.Logger; using System; using System.Collections.Generic; +using System.Net; +using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -10,43 +12,43 @@ namespace Flow.Launcher.Core.ExternalPlugins { public static class PluginsManifest { - static PluginsManifest() - { - UpdateTask = UpdateManifestAsync(); - } - - public static List UserPlugins { get; private set; } = new List(); - - public static Task UpdateTask { get; private set; } + private const string manifestFileUrl = "https://cdn.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json"; private static readonly SemaphoreSlim manifestUpdateLock = new(1); - public static Task UpdateManifestAsync() - { - if (manifestUpdateLock.CurrentCount == 0) - { - return UpdateTask; - } + private static string latestEtag = ""; - return UpdateTask = DownloadManifestAsync(); - } + public static List UserPlugins { get; private set; } = new List(); - private static async Task DownloadManifestAsync() + public static async Task UpdateManifestAsync(CancellationToken token = default) { try { - await manifestUpdateLock.WaitAsync().ConfigureAwait(false); + await manifestUpdateLock.WaitAsync(token).ConfigureAwait(false); + + var request = new HttpRequestMessage(HttpMethod.Get, manifestFileUrl); + request.Headers.Add("If-None-Match", latestEtag); + + var response = await Http.SendAsync(request, token).ConfigureAwait(false); - await using var jsonStream = await Http.GetStreamAsync("https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/plugin_api_v2/plugins.json") - .ConfigureAwait(false); + if (response.StatusCode == HttpStatusCode.OK) + { + Log.Info($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Fetched plugins from manifest repo"); - UserPlugins = await JsonSerializer.DeserializeAsync>(jsonStream).ConfigureAwait(false); + var json = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false); + + UserPlugins = await JsonSerializer.DeserializeAsync>(json, cancellationToken: token).ConfigureAwait(false); + + latestEtag = response.Headers.ETag.Tag; + } + else if (response.StatusCode != HttpStatusCode.NotModified) + { + Log.Warn($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Http response for manifest file was {response.StatusCode}"); + } } catch (Exception e) { - Log.Exception("|PluginManagement.GetManifest|Encountered error trying to download plugins manifest", e); - - UserPlugins = new List(); + Log.Exception($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Http request failed", e); } finally { diff --git a/Flow.Launcher.Infrastructure/Http/Http.cs b/Flow.Launcher.Infrastructure/Http/Http.cs index b45b6adcde1..9f4146b7be3 100644 --- a/Flow.Launcher.Infrastructure/Http/Http.cs +++ b/Flow.Launcher.Infrastructure/Http/Http.cs @@ -153,5 +153,13 @@ public static async Task GetStreamAsync([NotNull] string url, Cancellati var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); return await response.Content.ReadAsStreamAsync(); } + + /// + /// Asynchrously send an HTTP request. + /// + public static async Task SendAsync(HttpRequestMessage request, CancellationToken token = default) + { + return await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); + } } } diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs index a20e5a26731..37f0a126efd 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs @@ -2,7 +2,7 @@ using Flow.Launcher.Infrastructure.UserSettings; using System; using System.Collections.Generic; -using System.Text; +using System.Text.RegularExpressions; namespace Flow.Launcher.Plugin.PluginsManager { @@ -53,8 +53,8 @@ public List LoadContextMenus(Result selectedResult) { // standard UrlSourceCode format in PluginsManifest's plugins.json file: https://github.com/jjw24/Flow.Launcher.Plugin.Putty/tree/master var link = pluginManifestInfo.UrlSourceCode.StartsWith("https://github.com") - ? pluginManifestInfo.UrlSourceCode.Replace("/tree/master", "/issues/new/choose") - : pluginManifestInfo.UrlSourceCode; + ? Regex.Replace(pluginManifestInfo.UrlSourceCode, @"\/tree\/\w+$", "") + "/issues/new/choose" + : pluginManifestInfo.UrlSourceCode; Context.API.OpenUrl(link); return true; diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs index 8aaae95d65c..57c8574039f 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs @@ -12,7 +12,7 @@ namespace Flow.Launcher.Plugin.PluginsManager { - public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n, IAsyncReloadable + public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n { internal PluginInitContext Context { get; set; } @@ -24,28 +24,20 @@ public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n, I internal PluginsManager pluginManager; - private DateTime lastUpdateTime = DateTime.MinValue; - public Control CreateSettingPanel() { return new PluginsManagerSettings(viewModel); } - public Task InitAsync(PluginInitContext context) + public async Task InitAsync(PluginInitContext context) { Context = context; Settings = context.API.LoadSettingJsonStorage(); viewModel = new SettingsViewModel(context, Settings); contextMenu = new ContextMenu(Context); pluginManager = new PluginsManager(Context, Settings); - _manifestUpdateTask = pluginManager - .UpdateManifestAsync(true) - .ContinueWith(_ => - { - lastUpdateTime = DateTime.Now; - }, TaskContinuationOptions.OnlyOnRanToCompletion); - return Task.CompletedTask; + await pluginManager.UpdateManifestAsync(); } public List LoadContextMenus(Result selectedResult) @@ -53,35 +45,20 @@ public List LoadContextMenus(Result selectedResult) return contextMenu.LoadContextMenus(selectedResult); } - private Task _manifestUpdateTask = Task.CompletedTask; - public async Task> QueryAsync(Query query, CancellationToken token) { - var search = query.Search; - - if (string.IsNullOrWhiteSpace(search)) + if (string.IsNullOrWhiteSpace(query.Search)) return pluginManager.GetDefaultHotKeys(); - if ((DateTime.Now - lastUpdateTime).TotalHours > 12 && _manifestUpdateTask.IsCompleted) // 12 hours - { - _manifestUpdateTask = pluginManager.UpdateManifestAsync().ContinueWith(t => - { - lastUpdateTime = DateTime.Now; - }, TaskContinuationOptions.OnlyOnRanToCompletion); - } - - return search switch + return query.FirstSearch.ToLower() switch { //search could be url, no need ToLower() when passed in - var s when s.StartsWith(Settings.HotKeyInstall, StringComparison.OrdinalIgnoreCase) - => await pluginManager.RequestInstallOrUpdate(search, token), - var s when s.StartsWith(Settings.HotkeyUninstall, StringComparison.OrdinalIgnoreCase) - => pluginManager.RequestUninstall(search), - var s when s.StartsWith(Settings.HotkeyUpdate, StringComparison.OrdinalIgnoreCase) - => await pluginManager.RequestUpdate(search, token), + Settings.InstallCommand => await pluginManager.RequestInstallOrUpdate(query.SecondToEndSearch, token), + Settings.UninstallCommand => pluginManager.RequestUninstall(query.SecondToEndSearch), + Settings.UpdateCommand => await pluginManager.RequestUpdate(query.SecondToEndSearch, token), _ => pluginManager.GetDefaultHotKeys().Where(hotkey => { - hotkey.Score = StringMatcher.FuzzySearch(search, hotkey.Title).Score; + hotkey.Score = StringMatcher.FuzzySearch(query.Search, hotkey.Title).Score; return hotkey.Score > 0; }).ToList() }; @@ -96,11 +73,5 @@ public string GetTranslatedPluginDescription() { return Context.API.GetTranslation("plugin_pluginsmanager_plugin_description"); } - - public async Task ReloadDataAsync() - { - await pluginManager.UpdateManifestAsync(); - lastUpdateTime = DateTime.Now; - } } } \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs index 53e994d3423..bbf155e23c8 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs @@ -51,7 +51,7 @@ internal PluginsManager(PluginInitContext context, Settings settings) private Task _downloadManifestTask = Task.CompletedTask; - internal Task UpdateManifestAsync(bool silent = false) + internal Task UpdateManifestAsync(CancellationToken token = default, bool silent = false) { if (_downloadManifestTask.Status == TaskStatus.Running) { @@ -59,7 +59,7 @@ internal Task UpdateManifestAsync(bool silent = false) } else { - _downloadManifestTask = PluginsManifest.UpdateTask; + _downloadManifestTask = PluginsManifest.UpdateManifestAsync(token); if (!silent) _downloadManifestTask.ContinueWith(_ => Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_update_failed_title"), @@ -75,31 +75,34 @@ internal List GetDefaultHotKeys() { new Result() { - Title = Settings.HotKeyInstall, + Title = Settings.InstallCommand, IcoPath = icoPath, + AutoCompleteText = $"{Context.CurrentPluginMetadata.ActionKeyword} {Settings.InstallCommand} ", Action = _ => { - Context.API.ChangeQuery("pm install "); + Context.API.ChangeQuery($"{Context.CurrentPluginMetadata.ActionKeyword} {Settings.InstallCommand} "); return false; } }, new Result() { - Title = Settings.HotkeyUninstall, + Title = Settings.UninstallCommand, IcoPath = icoPath, + AutoCompleteText = $"{Context.CurrentPluginMetadata.ActionKeyword} {Settings.UninstallCommand} ", Action = _ => { - Context.API.ChangeQuery("pm uninstall "); + Context.API.ChangeQuery($"{Context.CurrentPluginMetadata.ActionKeyword} {Settings.UninstallCommand} "); return false; } }, new Result() { - Title = Settings.HotkeyUpdate, + Title = Settings.UpdateCommand, IcoPath = icoPath, + AutoCompleteText = $"{Context.CurrentPluginMetadata.ActionKeyword} {Settings.UpdateCommand} ", Action = _ => { - Context.API.ChangeQuery("pm update "); + Context.API.ChangeQuery($"{Context.CurrentPluginMetadata.ActionKeyword} {Settings.UpdateCommand} "); return false; } } @@ -119,7 +122,7 @@ internal async Task InstallOrUpdate(UserPlugin plugin) Context .API .ChangeQuery( - $"{Context.CurrentPluginMetadata.ActionKeywords.FirstOrDefault()} {Settings.HotkeyUpdate} {plugin.Name}"); + $"{Context.CurrentPluginMetadata.ActionKeywords.FirstOrDefault()} {Settings.UpdateCommand} {plugin.Name}"); var mainWindow = Application.Current.MainWindow; mainWindow.Show(); @@ -181,22 +184,7 @@ internal async Task InstallOrUpdate(UserPlugin plugin) internal async ValueTask> RequestUpdate(string search, CancellationToken token) { - if (!PluginsManifest.UserPlugins.Any()) - { - await UpdateManifestAsync(); - } - - token.ThrowIfCancellationRequested(); - - var autocompletedResults = AutoCompleteReturnAllResults(search, - Settings.HotkeyUpdate, - "Update", - "Select a plugin to update"); - - if (autocompletedResults.Any()) - return autocompletedResults; - - var uninstallSearch = search.Replace(Settings.HotkeyUpdate, string.Empty, StringComparison.OrdinalIgnoreCase).TrimStart(); + await UpdateManifestAsync(token); var resultsForUpdate = from existingPlugin in Context.API.GetAllPlugins() @@ -285,7 +273,7 @@ await Http.DownloadAsync(x.PluginNewUserPlugin.UrlDownload, downloadToFilePath) } }); - return Search(results, uninstallSearch); + return Search(results, search); } internal bool PluginExists(string id) @@ -369,20 +357,13 @@ private bool InstallSourceKnown(string url) return url.StartsWith(acceptedSource) && Context.API.GetAllPlugins().Any(x => x.Metadata.Website.StartsWith(contructedUrlPart)); } - internal async ValueTask> RequestInstallOrUpdate(string searchName, CancellationToken token) + internal async ValueTask> RequestInstallOrUpdate(string search, CancellationToken token) { - if (!PluginsManifest.UserPlugins.Any()) - { - await UpdateManifestAsync(); - } + await UpdateManifestAsync(token); - token.ThrowIfCancellationRequested(); - - var searchNameWithoutKeyword = searchName.Replace(Settings.HotKeyInstall, string.Empty, StringComparison.OrdinalIgnoreCase).Trim(); - - if (Uri.IsWellFormedUriString(searchNameWithoutKeyword, UriKind.Absolute) - && searchNameWithoutKeyword.Split('.').Last() == zip) - return InstallFromWeb(searchNameWithoutKeyword); + if (Uri.IsWellFormedUriString(search, UriKind.Absolute) + && search.Split('.').Last() == zip) + return InstallFromWeb(search); var results = PluginsManifest @@ -408,7 +389,7 @@ internal async ValueTask> RequestInstallOrUpdate(string searchName, ContextData = x }); - return Search(results, searchNameWithoutKeyword); + return Search(results, search); } private void Install(UserPlugin plugin, string downloadedFilePath) @@ -468,16 +449,6 @@ private void Install(UserPlugin plugin, string downloadedFilePath) internal List RequestUninstall(string search) { - var autocompletedResults = AutoCompleteReturnAllResults(search, - Settings.HotkeyUninstall, - "Uninstall", - "Select a plugin to uninstall"); - - if (autocompletedResults.Any()) - return autocompletedResults; - - var uninstallSearch = search.Replace(Settings.HotkeyUninstall, string.Empty, StringComparison.OrdinalIgnoreCase).TrimStart(); - var results = Context.API .GetAllPlugins() .Select(x => @@ -508,7 +479,7 @@ internal List RequestUninstall(string search) } }); - return Search(results, uninstallSearch); + return Search(results, search); } private void Uninstall(PluginMetadata plugin, bool removedSetting = true) @@ -523,36 +494,6 @@ private void Uninstall(PluginMetadata plugin, bool removedSetting = true) using var _ = File.CreateText(Path.Combine(plugin.PluginDirectory, "NeedDelete.txt")); } - private List AutoCompleteReturnAllResults(string search, string hotkey, string title, string subtitle) - { - if (!string.IsNullOrEmpty(search) - && hotkey.StartsWith(search) - && (hotkey != search || !search.StartsWith(hotkey))) - { - return - new List - { - new Result - { - Title = title, - IcoPath = icoPath, - SubTitle = subtitle, - Action = e => - { - Context - .API - .ChangeQuery( - $"{Context.CurrentPluginMetadata.ActionKeywords.FirstOrDefault()} {hotkey} "); - - return false; - } - } - }; - } - - return new List(); - } - private bool SameOrLesserPluginVersionExists(string metadataPath) { var newMetadata = JsonSerializer.Deserialize(File.ReadAllText(metadataPath)); diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs index a951010c0b1..6fd4e51acfd 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs @@ -6,11 +6,11 @@ namespace Flow.Launcher.Plugin.PluginsManager { internal class Settings { - internal string HotKeyInstall { get; set; } = "install"; + internal const string InstallCommand = "install"; - internal string HotkeyUninstall { get; set; } = "uninstall"; + internal const string UninstallCommand = "uninstall"; - internal string HotkeyUpdate { get; set; } = "update"; + internal const string UpdateCommand = "update"; public bool WarnFromUnknownSource { get; set; } = true; } diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json b/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json index 0900ddb6ca1..c8d5bd8a331 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json @@ -6,7 +6,7 @@ "Name": "Plugins Manager", "Description": "Management of installing, uninstalling or updating Flow Launcher plugins", "Author": "Jeremy Wu", - "Version": "1.11.2", + "Version": "1.12.0", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.PluginsManager.dll",