-
-
Notifications
You must be signed in to change notification settings - Fork 634
Added custom plugins manifest URL option for self hosting #4585
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
46122c6
f4cedd0
27816e0
ba14e05
3e33375
6e2b8c8
89e59a7
e17169f
4da57d5
93173aa
9f3ef81
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,18 +4,16 @@ | |
| using System.Threading.Tasks; | ||
| using Flow.Launcher.Plugin; | ||
| using Flow.Launcher.Core.Plugin; | ||
| using Flow.Launcher.Infrastructure.UserSettings; | ||
|
|
||
| namespace Flow.Launcher.Core.ExternalPlugins | ||
| { | ||
| public static class PluginsManifest | ||
| { | ||
| private static readonly string ClassName = nameof(PluginsManifest); | ||
|
|
||
| private static readonly CommunityPluginStore mainPluginStore = | ||
| new("https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/main/plugins.json", | ||
| "https://fastly.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@main/plugins.json", | ||
| "https://gcore.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@main/plugins.json", | ||
| "https://cdn.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@main/plugins.json"); | ||
| private static CommunityPluginStore mainPluginStore; | ||
| private static string lastCustomUrl = string.Empty; | ||
|
|
||
| private static readonly SemaphoreSlim manifestUpdateLock = new(1); | ||
|
|
||
|
|
@@ -24,14 +22,48 @@ public static class PluginsManifest | |
|
|
||
| public static List<UserPlugin> UserPlugins { get; private set; } | ||
|
|
||
| public static async Task<bool> UpdateManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default) | ||
| public static async Task<bool> UpdateManifestAsync(Settings settings, bool usePrimaryUrlOnly = false, CancellationToken token = default) | ||
| { | ||
| bool lockAcquired = false; | ||
| var lockAcquired = false; | ||
| try | ||
| { | ||
| var defaultUrls = new[] | ||
| { | ||
| "https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/main/plugins.json", | ||
| "https://fastly.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@main/plugins.json", | ||
| "https://gcore.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@main/plugins.json", | ||
| "https://cdn.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@main/plugins.json" | ||
| }; | ||
|
|
||
| await manifestUpdateLock.WaitAsync(token).ConfigureAwait(false); | ||
| lockAcquired = true; | ||
|
|
||
| var customUrl = settings.PluginSettings.PluginsManifestUrl?.Trim() ?? string.Empty; | ||
|
|
||
| if (mainPluginStore == null || lastCustomUrl != customUrl) | ||
| { | ||
|
Comment on lines
+43
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the setting changes from the default or a previous custom source, this replaces Useful? React with 👍 / 👎. |
||
| if (!string.IsNullOrEmpty(customUrl)) | ||
| { | ||
| if (Uri.TryCreate(customUrl, UriKind.Absolute, out var uri) | ||
| && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps) | ||
| && !string.IsNullOrEmpty(uri.Host)) | ||
| { | ||
| mainPluginStore = new(customUrl); | ||
| } | ||
| else | ||
| { | ||
| PublicApi.Instance.LogWarn(ClassName, $"Invalid custom plugins manifest URL: {customUrl}. Using default URLs."); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the user enters a rejected URL containing credentials or a token, such as Useful? React with 👍 / 👎. |
||
| mainPluginStore = new(defaultUrls[0], defaultUrls[1..]); | ||
| } | ||
| } | ||
| else | ||
| { | ||
| mainPluginStore = new(defaultUrls[0], defaultUrls[1..]); | ||
| } | ||
| lastCustomUrl = customUrl; | ||
|
Jack251970 marked this conversation as resolved.
|
||
| lastFetchedAt = DateTime.MinValue; | ||
| } | ||
|
|
||
| if (UserPlugins == null || usePrimaryUrlOnly || DateTime.Now.Subtract(lastFetchedAt) >= fetchTimeout) | ||
| { | ||
| var results = await mainPluginStore.FetchAsync(token, usePrimaryUrlOnly).ConfigureAwait(false); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| using System.Diagnostics; | ||
| using System.Diagnostics; | ||
| using System.IO; | ||
| using System.Reflection; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| using Flow.Launcher.Core.ExternalPlugins; | ||
| using NUnit.Framework; | ||
| using NUnit.Framework.Legacy; | ||
|
|
||
| namespace Flow.Launcher.Test; | ||
|
|
||
| public class CommunityPluginSourceTest | ||
| { | ||
| [Test] | ||
| public void ManifestFileUrlForLogging_OmitsCredentialsQueryAndFragment() | ||
| { | ||
| const string manifestUrl = | ||
| "https://username:password@example.com:8443/private/plugins.json?token=secret#fragment"; | ||
| var source = new CommunityPluginSource(manifestUrl); | ||
|
|
||
| ClassicAssert.AreEqual(manifestUrl, source.ManifestFileUrl); | ||
| ClassicAssert.AreEqual( | ||
| "https://example.com:8443/private/plugins.json", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: This expected value will not match the implementation. Prompt for AI agents |
||
| source.ManifestFileUrlForLogging); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -504,6 +504,15 @@ | |
| </StackPanel> | ||
| </ui:SettingsCard> | ||
|
|
||
| <ui:SettingsCard Margin="0 4 0 0" Header="{DynamicResource pluginManifestUrl}"> | ||
| <StackPanel Orientation="Horizontal"> | ||
| <TextBox | ||
| Width="370" | ||
| Height="34" | ||
| Text="{Binding Settings.PluginSettings.PluginsManifestUrl, TargetNullValue='None', UpdateSourceTrigger=PropertyChanged}" /> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: This binding only writes the typed URL into the settings object; nothing reacts to PluginsManifestUrl changing, so the plugin catalog is not refreshed when the user edits the URL (UpdatePluginManifestAsync is only called at startup, from the Plugin Store refresh button, and from the plugins settings pane). The user keeps seeing the stale catalog until a manual refresh or restart. Also, with UpdateSourceTrigger=PropertyChanged each keystroke commits a partial URL into the in-memory settings, which is then persisted when the settings window closes (SettingWindow.xaml.cs OnClosed calls _settings.Save()), so half-typed values can be written to disk. Prompt for AI agents |
||
| </StackPanel> | ||
| </ui:SettingsCard> | ||
|
|
||
| <ui:SettingsCard | ||
| Margin="0 14 0 0" | ||
| Description="{DynamicResource typingStartEnTooltip}" | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.