From 145172447cda540854d6f307d92b98a3b62a3b3d Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Tue, 19 May 2026 23:04:10 -0500 Subject: [PATCH 1/2] feat: add IUpdateCheckable opt-in capability to plugin contract --- src/SharpFM.Plugin/IUpdateCheckable.cs | 29 ++++++++++++ src/SharpFM.Plugin/UpdateCheckResult.cs | 30 ++++++++++++ .../PluginInterfaceTests.cs | 47 +++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 src/SharpFM.Plugin/IUpdateCheckable.cs create mode 100644 src/SharpFM.Plugin/UpdateCheckResult.cs diff --git a/src/SharpFM.Plugin/IUpdateCheckable.cs b/src/SharpFM.Plugin/IUpdateCheckable.cs new file mode 100644 index 0000000..6954c85 --- /dev/null +++ b/src/SharpFM.Plugin/IUpdateCheckable.cs @@ -0,0 +1,29 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace SharpFM.Plugin; + +/// +/// Opt-in capability for plugins that have a remote update channel. Implementing +/// this interface is independent of — a plugin opts in by +/// implementing both. Plugins that ship with the host binary (or otherwise have +/// no separate release cadence) should not implement this; the host will simply +/// display without a check-for-updates affordance. +/// +public interface IUpdateCheckable +{ + /// + /// Query the plugin's update channel and return what the host should surface + /// in the About dialog. Implementations choose the channel (public manifest, + /// licensing endpoint, anonymous GitHub releases for a public repo, etc.) — + /// the host has no knowledge of any specific plugin's source. + /// + /// + /// Implementations should fail silently on network errors, returning a + /// result with set to + /// false rather than throwing. A thrown exception is treated by the + /// host as a check failure and silently suppressed. + /// + /// + Task CheckForUpdatesAsync(CancellationToken ct); +} diff --git a/src/SharpFM.Plugin/UpdateCheckResult.cs b/src/SharpFM.Plugin/UpdateCheckResult.cs new file mode 100644 index 0000000..3eb9bf2 --- /dev/null +++ b/src/SharpFM.Plugin/UpdateCheckResult.cs @@ -0,0 +1,30 @@ +using System; + +namespace SharpFM.Plugin; + +/// +/// Outcome of an call. +/// All three optional fields may be null when +/// is false. +/// +/// +/// true when the channel reports a newer version than what the plugin +/// is currently running. +/// +/// +/// Human-readable version string of the latest release (e.g. "2.0.0"). +/// Format is the plugin's choice; the host displays it verbatim. +/// +/// +/// Where to point the user to obtain the update — typically a release notes +/// or download page. Opened in the system browser on click. +/// +/// +/// Optional short summary surfaced alongside the version (e.g. release +/// highlights). The host shows it as plain text; do not include markup. +/// +public sealed record UpdateCheckResult( + bool UpdateAvailable, + string? LatestVersion, + Uri? ReleaseUrl, + string? Notes); diff --git a/tests/SharpFM.Plugin.Tests/PluginInterfaceTests.cs b/tests/SharpFM.Plugin.Tests/PluginInterfaceTests.cs index 0f2cdcf..19261e3 100644 --- a/tests/SharpFM.Plugin.Tests/PluginInterfaceTests.cs +++ b/tests/SharpFM.Plugin.Tests/PluginInterfaceTests.cs @@ -27,6 +27,53 @@ public void IPanelPlugin_Extends_IDisposable() Assert.True(typeof(IDisposable).IsAssignableFrom(typeof(IPanelPlugin))); } + // --- IUpdateCheckable opt-in capability --- + + [Fact] + public void IUpdateCheckable_IsPublicInterface() + { + var t = typeof(IUpdateCheckable); + Assert.True(t.IsInterface); + Assert.True(t.IsPublic); + } + + [Fact] + public void IUpdateCheckable_DoesNotExtend_IPlugin() + { + // Capability interface, not a plugin sub-shape. A plugin opts in by + // implementing both IPlugin and IUpdateCheckable. + Assert.False(typeof(IPlugin).IsAssignableFrom(typeof(IUpdateCheckable))); + } + + [Fact] + public void IUpdateCheckable_DeclaresCheckForUpdatesAsync() + { + var method = typeof(IUpdateCheckable).GetMethod("CheckForUpdatesAsync"); + Assert.NotNull(method); + Assert.Equal(typeof(Task), method!.ReturnType); + var parameters = method.GetParameters(); + Assert.Single(parameters); + Assert.Equal(typeof(CancellationToken), parameters[0].ParameterType); + } + + [Fact] + public void UpdateCheckResult_RecordEquality() + { + var a = new UpdateCheckResult(true, "2.0.0", new Uri("https://example.com/r/2.0.0"), "notes"); + var b = new UpdateCheckResult(true, "2.0.0", new Uri("https://example.com/r/2.0.0"), "notes"); + Assert.Equal(a, b); + } + + [Fact] + public void UpdateCheckResult_NullablesAllowed_WhenNoUpdate() + { + var none = new UpdateCheckResult(false, null, null, null); + Assert.False(none.UpdateAvailable); + Assert.Null(none.LatestVersion); + Assert.Null(none.ReleaseUrl); + Assert.Null(none.Notes); + } + // --- ClipData record --- [Fact] From 6a4164517792323ae7441c0310f31ff53cdc9961 Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Tue, 19 May 2026 23:04:17 -0500 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20add=20Help=20=E2=86=92=20About=20wi?= =?UTF-8?q?th=20host=20and=20per-plugin=20update=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/SharpFM/Dialogs/AboutDialog.axaml | 102 +++++++++++ src/SharpFM/Dialogs/AboutDialog.axaml.cs | 78 +++++++++ src/SharpFM/MainWindow.axaml | 3 + src/SharpFM/MainWindow.axaml.cs | 31 ++++ src/SharpFM/Services/HostUpdateCheck.cs | 121 +++++++++++++ src/SharpFM/ViewModels/AboutEntryViewModel.cs | 93 ++++++++++ src/SharpFM/ViewModels/AboutViewModel.cs | 36 ++++ .../Services/HostUpdateCheckTests.cs | 162 ++++++++++++++++++ .../ViewModels/AboutEntryViewModelTests.cs | 110 ++++++++++++ .../ViewModels/AboutViewModelTests.cs | 90 ++++++++++ 10 files changed, 826 insertions(+) create mode 100644 src/SharpFM/Dialogs/AboutDialog.axaml create mode 100644 src/SharpFM/Dialogs/AboutDialog.axaml.cs create mode 100644 src/SharpFM/Services/HostUpdateCheck.cs create mode 100644 src/SharpFM/ViewModels/AboutEntryViewModel.cs create mode 100644 src/SharpFM/ViewModels/AboutViewModel.cs create mode 100644 tests/SharpFM.Tests/Services/HostUpdateCheckTests.cs create mode 100644 tests/SharpFM.Tests/ViewModels/AboutEntryViewModelTests.cs create mode 100644 tests/SharpFM.Tests/ViewModels/AboutViewModelTests.cs diff --git a/src/SharpFM/Dialogs/AboutDialog.axaml b/src/SharpFM/Dialogs/AboutDialog.axaml new file mode 100644 index 0000000..a7e7c12 --- /dev/null +++ b/src/SharpFM/Dialogs/AboutDialog.axaml @@ -0,0 +1,102 @@ + + + + + + + + + +