diff --git a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs index e3f0e2a2f28..d691bc39f4d 100644 --- a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs +++ b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs @@ -2,58 +2,66 @@ using Flow.Launcher.Infrastructure.Logger; using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using FastCache; namespace Flow.Launcher.Core.ExternalPlugins { public static class PluginsManifest { - private const string manifestFileUrl = "https://cdn.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json"; + private const string ManifestFileUrl = + "https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/plugin_api_v2/plugins.json"; - private static readonly SemaphoreSlim manifestUpdateLock = new(1); + private static string _latestEtag = ""; - private static string latestEtag = ""; - public static List UserPlugins { get; private set; } = new List(); - - public static async Task UpdateManifestAsync(CancellationToken token = default) + public static async ValueTask> RetrieveManifestAsync() { - try + if (Cached>.TryGet(0, out var cached)) { - await manifestUpdateLock.WaitAsync(token).ConfigureAwait(false); - - var request = new HttpRequestMessage(HttpMethod.Get, manifestFileUrl); - request.Headers.Add("If-None-Match", latestEtag); + return cached; + } - using var response = await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false); + var result = await UpdateManifestAsync(); - if (response.StatusCode == HttpStatusCode.OK) - { - Log.Info($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Fetched plugins from manifest repo"); + return result; + } - await using var json = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false); + public static async Task> UpdateManifestAsync(CancellationToken token = default) + { + var request = new HttpRequestMessage(HttpMethod.Get, ManifestFileUrl); + request.Headers.Add("If-None-Match", _latestEtag); - UserPlugins = await JsonSerializer.DeserializeAsync>(json, cancellationToken: token).ConfigureAwait(false); + using var response = await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, 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) + if (response.StatusCode == HttpStatusCode.OK) { - Log.Exception($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Http request failed", e); + Log.Info($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Fetched plugins from manifest repo"); + + await using var json = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false); + + var plugins = await JsonSerializer + .DeserializeAsync>(json, cancellationToken: token).ConfigureAwait(false); + + _latestEtag = response.Headers.ETag!.Tag; + if (plugins.Any()) + Cached>.Save(0, plugins, TimeSpan.FromHours(6)); + return plugins; } - finally + + if (response.StatusCode != HttpStatusCode.NotModified) { - manifestUpdateLock.Release(); + Log.Warn( + $"|PluginsManifest.{nameof(UpdateManifestAsync)}|Http response for manifest file was {response.StatusCode}"); } + + return new List(); } } } diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj index 8d06e71c51b..58c9aadd86a 100644 --- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj +++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj @@ -54,6 +54,7 @@ + diff --git a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs index 47a5cd67235..d5c055543a1 100644 --- a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs +++ b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs @@ -65,7 +65,6 @@ public SettingWindowViewModel(Updater updater, IPortable portable) break; } }; - } public Settings Settings { get; set; } @@ -107,13 +106,15 @@ public bool StartFlowLauncherOnSystemStartup } catch (Exception e) { - Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"), e.Message); + Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"), + e.Message); } } } // This is only required to set at startup. When portable mode enabled/disabled a restart is always required private bool _portableMode = DataLocation.PortableDataLocationInUse(); + public bool PortableMode { get => _portableMode; @@ -182,6 +183,7 @@ public class LastQueryMode : BaseModel } private List _lastQueryModes = new List(); + public List LastQueryModes { get @@ -190,6 +192,7 @@ public List LastQueryModes { _lastQueryModes = InitLastQueryModes(); } + return _lastQueryModes; } } @@ -197,17 +200,16 @@ public List LastQueryModes private List InitLastQueryModes() { var modes = new List(); - var enums = (Infrastructure.UserSettings.LastQueryMode[])Enum.GetValues(typeof(Infrastructure.UserSettings.LastQueryMode)); + var enums = (Infrastructure.UserSettings.LastQueryMode[])Enum.GetValues( + typeof(Infrastructure.UserSettings.LastQueryMode)); foreach (var e in enums) { var key = $"LastQuery{e}"; var display = _translater.GetTranslation(key); - var m = new LastQueryMode - { - Display = display, Value = e, - }; + var m = new LastQueryMode { Display = display, Value = e, }; modes.Add(m); } + return modes; } @@ -264,15 +266,15 @@ public List QuerySearchPrecisionStrings public List OpenResultModifiersList => new List { - KeyConstant.Alt, - KeyConstant.Ctrl, - $"{KeyConstant.Ctrl}+{KeyConstant.Alt}" + KeyConstant.Alt, KeyConstant.Ctrl, $"{KeyConstant.Ctrl}+{KeyConstant.Alt}" }; + private Internationalization _translater => InternationalizationManager.Instance; public List Languages => _translater.LoadAvailableLanguages(); public IEnumerable MaxResultsRange => Enumerable.Range(2, 16); - public string AlwaysPreviewToolTip => string.Format(_translater.GetTranslation("AlwaysPreviewToolTip"), Settings.PreviewHotkey); + public string AlwaysPreviewToolTip => + string.Format(_translater.GetTranslation("AlwaysPreviewToolTip"), Settings.PreviewHotkey); public string TestProxy() { @@ -282,6 +284,7 @@ public string TestProxy() { return InternationalizationManager.Instance.GetTranslation("serverCantBeEmpty"); } + if (Settings.Proxy.Port <= 0) { return InternationalizationManager.Instance.GetTranslation("portCantBeEmpty"); @@ -300,6 +303,7 @@ public string TestProxy() Credentials = new NetworkCredential(proxyUserName, Settings.Proxy.Password) }; } + try { var response = (HttpWebResponse)request.GetResponse(); @@ -330,21 +334,35 @@ public IList PluginViewModels get => PluginManager.AllPlugins .OrderBy(x => x.Metadata.Disabled) .ThenBy(y => y.Metadata.Name) - .Select(p => new PluginViewModel - { - PluginPair = p - }) + .Select(p => new PluginViewModel { PluginPair = p }) .ToList(); } + private bool externalPluginsLoaded; + private IList externalPlugins = new List(); + public IList ExternalPlugins { get { - return LabelMaker(PluginsManifest.UserPlugins); + if (!externalPluginsLoaded) + _ = RetrieveManifestAsync(); + return externalPlugins; + } + set + { + externalPlugins = value; + OnPropertyChanged(); } } + private async Task RetrieveManifestAsync() + { + externalPluginsLoaded = true; + await PluginsManifest.RetrieveManifestAsync(); + ExternalPlugins = LabelMaker(await PluginsManifest.RetrieveManifestAsync()); + } + private IList LabelMaker(IList list) { return list.Select(p => new PluginStoreItemViewModel(p)) @@ -380,8 +398,7 @@ private async Task RefreshExternalPluginsAsync() await PluginsManifest.UpdateManifestAsync(); OnPropertyChanged(nameof(ExternalPlugins)); } - - + internal void DisplayPluginQuery(string queryToDisplay, PluginPair plugin, int actionKeywordPosition = 0) { @@ -455,12 +472,10 @@ public List ColorSchemes { var key = $"ColorScheme{e}"; var display = _translater.GetTranslation(key); - var m = new ColorScheme - { - Display = display, Value = e, - }; + var m = new ColorScheme { Display = display, Value = e, }; modes.Add(m); } + return modes; } } @@ -481,13 +496,10 @@ public List SearchWindowScreens { var key = $"SearchWindowScreen{e}"; var display = _translater.GetTranslation(key); - var m = new SearchWindowScreen - { - Display = display, - Value = e, - }; + var m = new SearchWindowScreen { Display = display, Value = e, }; modes.Add(m); } + return modes; } } @@ -508,12 +520,10 @@ public List SearchWindowAligns { var key = $"SearchWindowAlign{e}"; var display = _translater.GetTranslation(key); - var m = new SearchWindowAlign - { - Display = display, Value = e, - }; + var m = new SearchWindowAlign { Display = display, Value = e, }; modes.Add(m); } + return modes; } } @@ -528,6 +538,7 @@ public List ScreenNumbers { screenNumbers.Add(i); } + return screenNumbers; } } @@ -654,10 +665,7 @@ public Brush PreviewBackground bitmap.DecodePixelWidth = 800; bitmap.DecodePixelHeight = 600; bitmap.EndInit(); - var brush = new ImageBrush(bitmap) - { - Stretch = Stretch.UniformToFill - }; + var brush = new ImageBrush(bitmap) { Stretch = Stretch.UniformToFill }; return brush; } else @@ -678,26 +686,33 @@ public ResultsViewModel PreviewResults new Result { Title = InternationalizationManager.Instance.GetTranslation("SampleTitleExplorer"), - SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleExplorer"), - IcoPath = Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.Explorer\Images\explorer.png") + SubTitle = + InternationalizationManager.Instance.GetTranslation("SampleSubTitleExplorer"), + IcoPath = Path.Combine(Constant.ProgramDirectory, + @"Plugins\Flow.Launcher.Plugin.Explorer\Images\explorer.png") }, new Result { Title = InternationalizationManager.Instance.GetTranslation("SampleTitleWebSearch"), - SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleWebSearch"), - IcoPath = Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.WebSearch\Images\web_search.png") + SubTitle = + InternationalizationManager.Instance.GetTranslation("SampleSubTitleWebSearch"), + IcoPath = Path.Combine(Constant.ProgramDirectory, + @"Plugins\Flow.Launcher.Plugin.WebSearch\Images\web_search.png") }, new Result { Title = InternationalizationManager.Instance.GetTranslation("SampleTitleProgram"), SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleProgram"), - IcoPath = Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.Program\Images\program.png") + IcoPath = Path.Combine(Constant.ProgramDirectory, + @"Plugins\Flow.Launcher.Plugin.Program\Images\program.png") }, new Result { Title = InternationalizationManager.Instance.GetTranslation("SampleTitleProcessKiller"), - SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleProcessKiller"), - IcoPath = Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.ProcessKiller\Images\app.png") + SubTitle = + InternationalizationManager.Instance.GetTranslation("SampleSubTitleProcessKiller"), + IcoPath = Path.Combine(Constant.ProgramDirectory, + @"Plugins\Flow.Launcher.Plugin.ProcessKiller\Images\app.png") } }; var vm = new ResultsViewModel(Settings); @@ -853,6 +868,7 @@ public bool EditSelectedCustomShortcut() SelectedCustomShortcut = item; return true; } + return false; } @@ -881,6 +897,7 @@ public bool ShortcutExists(string key) public string Documentation => Constant.Documentation; public string Docs => Constant.Docs; public string Github => Constant.GitHub; + public string Version { get @@ -895,7 +912,9 @@ public string Version } } } - public string ActivatedTimes => string.Format(_translater.GetTranslation("about_activate_times"), Settings.ActivateTimes); + + public string ActivatedTimes => + string.Format(_translater.GetTranslation("about_activate_times"), Settings.ActivateTimes); public string CheckLogFolder { @@ -903,7 +922,8 @@ public string CheckLogFolder { var logFiles = GetLogFiles(); long size = logFiles.Sum(file => file.Length); - return string.Format("{0} ({1})", _translater.GetTranslation("clearlogfolder"), BytesToReadableString(size)); + return string.Format("{0} ({1})", _translater.GetTranslation("clearlogfolder"), + BytesToReadableString(size)); } } @@ -940,10 +960,7 @@ internal void OpenLogFolder() internal static string BytesToReadableString(long bytes) { const int scale = 1024; - string[] orders = new string[] - { - "GB", "MB", "KB", "B" - }; + string[] orders = new string[] { "GB", "MB", "KB", "B" }; long max = (long)Math.Pow(scale, orders.Length - 1); foreach (string order in orders) @@ -953,6 +970,7 @@ internal static string BytesToReadableString(long bytes) max /= scale; } + return "0 B"; } diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs index cd554e4d0a7..2a1952f739b 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs @@ -33,8 +33,6 @@ public async Task InitAsync(PluginInitContext context) viewModel = new SettingsViewModel(context, Settings); contextMenu = new ContextMenu(Context); pluginManager = new PluginsManager(Context, Settings); - - _ = pluginManager.UpdateManifestAsync(); } public List LoadContextMenus(Result selectedResult) diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs index d74ec70b595..7b1c83418cb 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs @@ -184,13 +184,11 @@ internal async Task InstallOrUpdateAsync(UserPlugin plugin) internal async ValueTask> RequestUpdateAsync(string search, CancellationToken token) { - await UpdateManifestAsync(token); - var resultsForUpdate = from existingPlugin in Context.API.GetAllPlugins() - join pluginFromManifest in PluginsManifest.UserPlugins + join pluginFromManifest in (await PluginsManifest.RetrieveManifestAsync()) on existingPlugin.Metadata.ID equals pluginFromManifest.ID - where existingPlugin.Metadata.Version.CompareTo(pluginFromManifest.Version) < + where String.Compare(existingPlugin.Metadata.Version, pluginFromManifest.Version, StringComparison.InvariantCulture) < 0 // if current version precedes manifest version select new @@ -359,15 +357,13 @@ private bool InstallSourceKnown(string url) internal async ValueTask> RequestInstallOrUpdate(string search, CancellationToken token) { - await UpdateManifestAsync(token); - if (Uri.IsWellFormedUriString(search, UriKind.Absolute) && search.Split('.').Last() == zip) return InstallFromWeb(search); var results = - PluginsManifest - .UserPlugins + (await PluginsManifest + .RetrieveManifestAsync()) .Select(x => new Result {