Merge pull request #972 from Flow-Launcher/refactor_plugin_manifest

Improvements to PluginManager plugin
This commit is contained in:
Jeremy Wu 2022-01-14 08:12:32 +11:00 committed by GitHub
commit b9048f50f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 71 additions and 149 deletions

View file

@ -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<UserPlugin> UserPlugins { get; private set; } = new List<UserPlugin>();
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<UserPlugin> UserPlugins { get; private set; } = new List<UserPlugin>();
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);
await using var jsonStream = await Http.GetStreamAsync("https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/plugin_api_v2/plugins.json")
.ConfigureAwait(false);
var request = new HttpRequestMessage(HttpMethod.Get, manifestFileUrl);
request.Headers.Add("If-None-Match", latestEtag);
UserPlugins = await JsonSerializer.DeserializeAsync<List<UserPlugin>>(jsonStream).ConfigureAwait(false);
var response = await Http.SendAsync(request, token).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.OK)
{
Log.Info($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Fetched plugins from manifest repo");
var json = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false);
UserPlugins = await JsonSerializer.DeserializeAsync<List<UserPlugin>>(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<UserPlugin>();
Log.Exception($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Http request failed", e);
}
finally
{

View file

@ -153,5 +153,13 @@ namespace Flow.Launcher.Infrastructure.Http
var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
return await response.Content.ReadAsStreamAsync();
}
/// <summary>
/// Asynchrously send an HTTP request.
/// </summary>
public static async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token = default)
{
return await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
}
}
}

View file

@ -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 @@ namespace Flow.Launcher.Plugin.PluginsManager
{
// 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;

View file

@ -12,7 +12,7 @@ using System.Windows;
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 @@ namespace Flow.Launcher.Plugin.PluginsManager
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<Settings>();
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<Result> LoadContextMenus(Result selectedResult)
@ -53,35 +45,20 @@ namespace Flow.Launcher.Plugin.PluginsManager
return contextMenu.LoadContextMenus(selectedResult);
}
private Task _manifestUpdateTask = Task.CompletedTask;
public async Task<List<Result>> 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 @@ namespace Flow.Launcher.Plugin.PluginsManager
{
return Context.API.GetTranslation("plugin_pluginsmanager_plugin_description");
}
public async Task ReloadDataAsync()
{
await pluginManager.UpdateManifestAsync();
lastUpdateTime = DateTime.Now;
}
}
}

View file

@ -51,7 +51,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
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 @@ namespace Flow.Launcher.Plugin.PluginsManager
}
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 @@ namespace Flow.Launcher.Plugin.PluginsManager
{
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 @@ namespace Flow.Launcher.Plugin.PluginsManager
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 @@ namespace Flow.Launcher.Plugin.PluginsManager
internal async ValueTask<List<Result>> 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 @@ namespace Flow.Launcher.Plugin.PluginsManager
}
});
return Search(results, uninstallSearch);
return Search(results, search);
}
internal bool PluginExists(string id)
@ -369,20 +357,13 @@ namespace Flow.Launcher.Plugin.PluginsManager
return url.StartsWith(acceptedSource) && Context.API.GetAllPlugins().Any(x => x.Metadata.Website.StartsWith(contructedUrlPart));
}
internal async ValueTask<List<Result>> RequestInstallOrUpdate(string searchName, CancellationToken token)
internal async ValueTask<List<Result>> 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 @@ namespace Flow.Launcher.Plugin.PluginsManager
ContextData = x
});
return Search(results, searchNameWithoutKeyword);
return Search(results, search);
}
private void Install(UserPlugin plugin, string downloadedFilePath)
@ -468,16 +449,6 @@ namespace Flow.Launcher.Plugin.PluginsManager
internal List<Result> 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 @@ namespace Flow.Launcher.Plugin.PluginsManager
}
});
return Search(results, uninstallSearch);
return Search(results, search);
}
private void Uninstall(PluginMetadata plugin, bool removedSetting = true)
@ -523,36 +494,6 @@ namespace Flow.Launcher.Plugin.PluginsManager
using var _ = File.CreateText(Path.Combine(plugin.PluginDirectory, "NeedDelete.txt"));
}
private List<Result> AutoCompleteReturnAllResults(string search, string hotkey, string title, string subtitle)
{
if (!string.IsNullOrEmpty(search)
&& hotkey.StartsWith(search)
&& (hotkey != search || !search.StartsWith(hotkey)))
{
return
new List<Result>
{
new Result
{
Title = title,
IcoPath = icoPath,
SubTitle = subtitle,
Action = e =>
{
Context
.API
.ChangeQuery(
$"{Context.CurrentPluginMetadata.ActionKeywords.FirstOrDefault()} {hotkey} ");
return false;
}
}
};
}
return new List<Result>();
}
private bool SameOrLesserPluginVersionExists(string metadataPath)
{
var newMetadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(metadataPath));

View file

@ -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;
}

View file

@ -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",