diff --git a/LanMountainDesktop.PluginSdk/SettingsPageCategory.cs b/LanMountainDesktop.PluginSdk/SettingsPageCategory.cs index 5b52baa..0f1d125 100644 --- a/LanMountainDesktop.PluginSdk/SettingsPageCategory.cs +++ b/LanMountainDesktop.PluginSdk/SettingsPageCategory.cs @@ -6,5 +6,6 @@ public enum SettingsPageCategory Appearance = 10, Components = 20, Plugins = 30, + PluginMarket = 35, About = 40 } diff --git a/LanMountainDesktop/Helpers/PluginMarketMarkdownHelper.cs b/LanMountainDesktop/Helpers/PluginMarketMarkdownHelper.cs new file mode 100644 index 0000000..b5015f0 --- /dev/null +++ b/LanMountainDesktop/Helpers/PluginMarketMarkdownHelper.cs @@ -0,0 +1,47 @@ +using System; +using System.Diagnostics; +using System.Windows.Input; +using CommunityToolkit.Mvvm.Input; +using Markdown.Avalonia; + +namespace LanMountainDesktop.Helpers; + +public static class PluginMarketMarkdownHelper +{ + private static Markdown.Avalonia.Markdown? _engine; + + public static ICommand OpenLinkCommand { get; } = new RelayCommand(OpenLink); + + public static Markdown.Avalonia.Markdown Engine => _engine ??= new Markdown.Avalonia.Markdown + { + HyperlinkCommand = OpenLinkCommand + }; + + private static void OpenLink(object? parameter) + { + var url = parameter switch + { + Uri uri => uri.ToString(), + string text => text, + _ => null + }; + + if (string.IsNullOrWhiteSpace(url)) + { + return; + } + + try + { + Process.Start(new ProcessStartInfo + { + FileName = url, + UseShellExecute = true + }); + } + catch + { + // Ignore browser launch failures inside the markdown viewer. + } + } +} diff --git a/LanMountainDesktop/LanMountainDesktop.csproj b/LanMountainDesktop/LanMountainDesktop.csproj index 4393448..21ffe83 100644 --- a/LanMountainDesktop/LanMountainDesktop.csproj +++ b/LanMountainDesktop/LanMountainDesktop.csproj @@ -48,6 +48,7 @@ + diff --git a/LanMountainDesktop/Localization/en-US.json b/LanMountainDesktop/Localization/en-US.json index 49c1da4..02caa3d 100644 --- a/LanMountainDesktop/Localization/en-US.json +++ b/LanMountainDesktop/Localization/en-US.json @@ -359,9 +359,10 @@ "settings.plugins.runtime_desc": "Review plugin runtime state and load results.", "settings.plugins.runtime_hint": "This page shows discovery status, load results, and runtime diagnostics for installed plugins.", "settings.plugins.runtime_status": "Plugin runtime status will appear here after plugin discovery completes.", - "settings.plugins.description": "Manage installed plugins and browse marketplace packages.", - "settings.plugins.initial_status": "Refresh plugin state to see the latest installed and marketplace entries.", + "settings.plugins.description": "Manage installed plugins and review their runtime state.", + "settings.plugins.initial_status": "Refresh plugin state to see the latest installed plugins.", "settings.plugins.refresh_button": "Refresh Plugins", + "settings.plugins.refresh_success_installed_format": "Loaded {0} installed plugins.", "settings.plugins.refresh_success_format": "Loaded {0} installed plugins and {1} marketplace entries.", "settings.plugins.refresh_failed": "Failed to load plugin market index.", "settings.plugins.marketplace_header": "Marketplace", @@ -437,7 +438,7 @@ "market.card.loaded": "Loaded", "market.card.pending_restart": "Restart required", "market.detail.placeholder": "Select a plugin on the left to inspect details.", - "market.detail.author": "Author", + "market.detail.author": "Publisher", "market.detail.version": "Version", "market.detail.api_version": "API Version", "market.detail.min_host_version": "Minimum Host Version", @@ -456,6 +457,11 @@ "market.detail.homepage": "Homepage", "market.detail.repository": "Repository", "market.detail.release_notes": "Release Notes", + "market.detail.dependencies": "Dependencies", + "market.detail.dependencies_empty": "No shared contract dependencies were declared by this plugin.", + "market.detail.readme_loading": "Loading README...", + "market.detail.readme_empty": "README is empty.", + "market.detail.readme_error_format": "README could not be loaded: {0}", "market.detail.state.not_installed": "Not installed", "market.detail.state.update_available": "Update available", "market.detail.state.installed": "Installed", @@ -464,6 +470,7 @@ "market.button.update": "Update", "market.button.installed": "Installed", "market.button.installing": "Installing...", + "market.button.restart": "Restart to apply", "button.component_library": "Edit Desktop", "tooltip.component_library": "Edit Desktop", "component_library.title": "Widgets", diff --git a/LanMountainDesktop/Localization/zh-CN.json b/LanMountainDesktop/Localization/zh-CN.json index d9f586d..94d08a6 100644 --- a/LanMountainDesktop/Localization/zh-CN.json +++ b/LanMountainDesktop/Localization/zh-CN.json @@ -364,9 +364,10 @@ "settings.plugins.runtime_desc": "查看插件运行时状态、加载结果与诊断信息。", "settings.plugins.runtime_hint": "这里展示已安装插件的发现结果、加载状态和运行时诊断信息。", "settings.plugins.runtime_status": "插件扫描完成后,运行时状态会显示在这里。", - "settings.plugins.description": "管理已安装插件并浏览插件市场。", - "settings.plugins.initial_status": "刷新插件状态以查看最新的已安装插件和市场条目。", + "settings.plugins.description": "管理已安装插件并查看其运行时状态。", + "settings.plugins.initial_status": "刷新插件状态以查看最新的已安装插件。", "settings.plugins.refresh_button": "刷新插件", + "settings.plugins.refresh_success_installed_format": "已加载 {0} 个已安装插件。", "settings.plugins.refresh_success_format": "已加载 {0} 个已安装插件和 {1} 个市场条目。", "settings.plugins.refresh_failed": "加载插件市场索引失败。", "settings.plugins.marketplace_header": "插件市场", @@ -442,7 +443,7 @@ "market.card.loaded": "已加载", "market.card.pending_restart": "需要重启", "market.detail.placeholder": "从左侧选择一个插件以查看详情。", - "market.detail.author": "作者", + "market.detail.author": "发行者", "market.detail.version": "版本", "market.detail.api_version": "API 版本", "market.detail.min_host_version": "最低宿主版本", @@ -461,6 +462,11 @@ "market.detail.homepage": "主页", "market.detail.repository": "仓库", "market.detail.release_notes": "发布说明", + "market.detail.dependencies": "依赖项", + "market.detail.dependencies_empty": "该插件没有声明 SharedContracts 依赖项。", + "market.detail.readme_loading": "正在加载 README...", + "market.detail.readme_empty": "README 为空。", + "market.detail.readme_error_format": "README 加载失败:{0}", "market.detail.state.not_installed": "未安装", "market.detail.state.update_available": "可更新", "market.detail.state.installed": "已安装", @@ -469,6 +475,7 @@ "market.button.update": "更新", "market.button.installed": "已安装", "market.button.installing": "安装中...", + "market.button.restart": "重启后应用", "button.component_library": "桌面编辑", "tooltip.component_library": "桌面编辑", "component_library.title": "桌面编辑", diff --git a/LanMountainDesktop/Services/Settings/SettingsContracts.cs b/LanMountainDesktop/Services/Settings/SettingsContracts.cs index b7c20ac..650da15 100644 --- a/LanMountainDesktop/Services/Settings/SettingsContracts.cs +++ b/LanMountainDesktop/Services/Settings/SettingsContracts.cs @@ -39,6 +39,10 @@ public sealed record WeatherSettingsState( public sealed record RegionSettingsState(string LanguageCode, string? TimeZoneId); public sealed record UpdateSettingsState(bool AutoCheckUpdates, bool IncludePrereleaseUpdates, string UpdateChannel); public sealed record PluginManagementSettingsState(IReadOnlyList DisabledPluginIds); +public sealed record PluginMarketDependencyInfo( + string Id, + string Version, + string AssemblyName); public sealed record PluginMarketPluginInfo( string Id, string Name, @@ -55,6 +59,7 @@ public sealed record PluginMarketPluginInfo( string HomepageUrl, string RepositoryUrl, IReadOnlyList Tags, + IReadOnlyList Dependencies, DateTimeOffset PublishedAt, DateTimeOffset UpdatedAt); public sealed record PluginMarketIndexResult( diff --git a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs index ca6f12b..02bef72 100644 --- a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs +++ b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs @@ -725,6 +725,12 @@ internal sealed class PluginMarketSettingsService : IPluginMarketSettingsService entry.HomepageUrl, entry.RepositoryUrl, entry.Tags, + entry.SharedContracts + .Select(contract => new PluginMarketDependencyInfo( + contract.Id, + contract.Version, + contract.AssemblyName)) + .ToArray(), entry.PublishedAt, entry.UpdatedAt); }) diff --git a/LanMountainDesktop/Services/Settings/SettingsPageRegistry.cs b/LanMountainDesktop/Services/Settings/SettingsPageRegistry.cs index e24606f..59f4cd5 100644 --- a/LanMountainDesktop/Services/Settings/SettingsPageRegistry.cs +++ b/LanMountainDesktop/Services/Settings/SettingsPageRegistry.cs @@ -5,6 +5,7 @@ using System.Reflection; using Avalonia.Controls; using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Plugins; +using LanMountainDesktop.Services.PluginMarket; using LanMountainDesktop.Services; using LanMountainDesktop.ViewModels; using LanMountainDesktop.Views.SettingsPages; @@ -180,6 +181,8 @@ internal sealed class SettingsPageRegistry : ISettingsPageRegistry, IDisposable services.AddSingleton(_localizationService); services.AddSingleton(_ => HostLocationServiceProvider.GetOrCreate()); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); var pluginRuntime = _pluginRuntimeAccessor(); if (pluginRuntime is not null) diff --git a/LanMountainDesktop/Styles/SettingsCardStyles.axaml b/LanMountainDesktop/Styles/SettingsCardStyles.axaml index c092ccd..c9786c4 100644 --- a/LanMountainDesktop/Styles/SettingsCardStyles.axaml +++ b/LanMountainDesktop/Styles/SettingsCardStyles.axaml @@ -238,6 +238,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LanMountainDesktop/Views/SettingsPages/PluginMarketSettingsPage.axaml.cs b/LanMountainDesktop/Views/SettingsPages/PluginMarketSettingsPage.axaml.cs new file mode 100644 index 0000000..cff8e8f --- /dev/null +++ b/LanMountainDesktop/Views/SettingsPages/PluginMarketSettingsPage.axaml.cs @@ -0,0 +1,63 @@ +using LanMountainDesktop.PluginSdk; +using LanMountainDesktop.Services; +using LanMountainDesktop.Services.PluginMarket; +using LanMountainDesktop.Services.Settings; +using LanMountainDesktop.ViewModels; + +namespace LanMountainDesktop.Views.SettingsPages; + +[SettingsPageInfo( + "plugin-market", + "Plugin Market", + SettingsPageCategory.PluginMarket, + IconKey = "ShoppingBag", + SortOrder = 35, + TitleLocalizationKey = "settings.plugin_market.title", + DescriptionLocalizationKey = "settings.plugin_market.subtitle")] +public partial class PluginMarketSettingsPage : SettingsPageBase +{ + public PluginMarketSettingsPage() + : this(CreateDefaultViewModel()) + { + } + + public PluginMarketSettingsPage(PluginMarketSettingsPageViewModel viewModel) + { + ViewModel = viewModel; + ViewModel.RestartRequested += OnRestartRequested; + ViewModel.DetailsRequested += OnDetailsRequested; + DataContext = ViewModel; + InitializeComponent(); + } + + public PluginMarketSettingsPageViewModel ViewModel { get; } + + public override async void OnNavigatedTo(object? parameter) + { + await ViewModel.InitializeAsync(); + } + + private static PluginMarketSettingsPageViewModel CreateDefaultViewModel() + { + var settingsFacade = HostSettingsFacadeProvider.GetOrCreate(); + var localizationService = new LocalizationService(); + return new PluginMarketSettingsPageViewModel( + settingsFacade, + localizationService, + new AirAppMarketIconService(), + new AirAppMarketReadmeService()); + } + + private void OnRestartRequested(string? reason) + { + RequestRestart(reason ?? ViewModel.RestartRequiredMessage); + } + + private async void OnDetailsRequested(PluginMarketItemViewModel item) + { + var detailViewModel = ViewModel.CreateDetailViewModel(item); + var drawer = new PluginMarketDetailDrawer(detailViewModel); + OpenDrawer(drawer, detailViewModel.DrawerTitle); + await detailViewModel.InitializeAsync(); + } +} diff --git a/LanMountainDesktop/Views/SettingsPages/PluginsSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/PluginsSettingsPage.axaml index 67549be..730893a 100644 --- a/LanMountainDesktop/Views/SettingsPages/PluginsSettingsPage.axaml +++ b/LanMountainDesktop/Views/SettingsPages/PluginsSettingsPage.axaml @@ -9,8 +9,6 @@ x:DataType="vm:PluginsSettingsPageViewModel"> - - @@ -24,7 +22,6 @@ - @@ -56,37 +53,6 @@ - - - - - - - - - - - - - - - - - - - - -