Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
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
50 changes: 26 additions & 24 deletions Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);

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<List<UserPlugin>>(jsonStream).ConfigureAwait(false);
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
{
Expand Down
8 changes: 8 additions & 0 deletions Flow.Launcher.Infrastructure/Http/Http.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,5 +153,13 @@ public static async Task<Stream> GetStreamAsync([NotNull] string url, Cancellati
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);
}
}
}
6 changes: 3 additions & 3 deletions Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -53,8 +53,8 @@ public List<Result> 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;
Expand Down
47 changes: 9 additions & 38 deletions Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand All @@ -24,64 +24,41 @@ 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<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)
{
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()
};
Expand All @@ -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;
}
}
}
Loading