From 9d8d853ed4e7668da86eb01f9a5e86e66743f185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=BA=E5=95=86=E7=84=97=E8=92=9F=E9=95=BF?= <104508795+ztzpro@users.noreply.github.com> Date: Wed, 30 Sep 2026 08:01:55 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(plugin):=20=E4=BF=AE=E5=A4=8D=E5=BC=80?= =?UTF-8?q?=E6=9C=BA=E9=A2=84=E5=8A=A0=E8=BD=BD=E5=8B=BE=E9=80=89=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E4=B8=8D=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 通过 PluginHost 统一更新预加载持久化状态与运行时插件实例状态 - 修复刷新插件列表后“开机预加载”复选框被重置为未勾选的问题 - 确保后续启动预加载扫描能够读取最新的用户设置 --- WinPieGestures/Plugin/PluginHost.cs | 23 +++++++++++++++++++++++ WinPieGestures/SettingsWindow.xaml.cs | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/WinPieGestures/Plugin/PluginHost.cs b/WinPieGestures/Plugin/PluginHost.cs index 70db529d..72d750b1 100644 --- a/WinPieGestures/Plugin/PluginHost.cs +++ b/WinPieGestures/Plugin/PluginHost.cs @@ -401,6 +401,29 @@ public static bool Enable(string pluginId, out string error) return true; } + /// + /// 设置插件的启动预加载偏好,并同步当前运行时实例。 + /// + /// PluginRegistryStore 负责持久化,而运行中的 + /// 还持有一份登记快照。两者必须同时更新,否则设置页刷新时会从旧快照读回 + /// IsPreload = false,并且后续的预加载扫描也会漏掉刚勾选的插件。 + /// + /// + public static void SetPreload(string pluginId, bool preload) + { + if (string.IsNullOrWhiteSpace(pluginId)) return; + + PluginRegistryStore.SetPreload(pluginId, preload); + + lock (Gate) + { + if (Instances.TryGetValue(pluginId, out PluginInstance? instance)) + { + instance.Entry.Preload = preload; + } + } + } + /// 是否需要重启宿主才能释放该插件的旧运行时。 public static bool IsRestartRequired(string pluginId) { diff --git a/WinPieGestures/SettingsWindow.xaml.cs b/WinPieGestures/SettingsWindow.xaml.cs index 92c3f058..052fbe58 100644 --- a/WinPieGestures/SettingsWindow.xaml.cs +++ b/WinPieGestures/SettingsWindow.xaml.cs @@ -8083,7 +8083,7 @@ private void PluginRowPreloadCheckBox_Click(object sender, RoutedEventArgs e) } bool desired = box.IsChecked == true; - PluginRegistryStore.SetPreload(pluginId, desired); + PluginHost.SetPreload(pluginId, desired); if (desired && !PluginHost.PreloadNow(pluginId, out string preloadError)) { System.Windows.MessageBox.Show(this, From 5b91f1850feb3a90a9b889b23318dcb739cafca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=BA=E5=95=86=E7=84=97=E8=92=9F=E9=95=BF?= <104508795+ztzpro@users.noreply.github.com> Date: Wed, 30 Sep 2026 08:03:53 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(plugin):=20=E5=A2=9E=E5=8A=A0=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E8=AF=A6=E6=83=85=E9=A1=B5=E4=B8=8E=E5=AE=98=E6=96=B9?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E6=9C=AC=E5=9C=B0=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为官方插件和本地已安装插件增加统一的详情入口,展示功能描述、功能列表与插件信息 - 从本地 plugin.json 或官方 catalog 读取静态功能元数据,未加载插件时不触发程序集加载 - 在用户主动刷新或引导安装后缓存官方目录,重新打开设置页时可离线读取 - 显示官方目录的缓存更新时间,并兼容缺少新增详情字段的旧 catalog --- WinPieGestures/I18n.cs | 18 ++ WinPieGestures/Plugin/OfficialPluginClient.cs | 128 +++++++++++++ .../Plugin/OfficialPluginListItem.cs | 10 +- WinPieGestures/Plugin/PluginDetailModel.cs | 176 ++++++++++++++++++ WinPieGestures/Plugin/PluginListItem.cs | 4 + WinPieGestures/Plugin/PluginPaths.cs | 3 + WinPieGestures/PluginDetailWindow.xaml | 60 ++++++ WinPieGestures/PluginDetailWindow.xaml.cs | 52 ++++++ WinPieGestures/SettingsWindow.xaml | 26 +-- WinPieGestures/SettingsWindow.xaml.cs | 61 ++++++ 10 files changed, 524 insertions(+), 14 deletions(-) create mode 100644 WinPieGestures/Plugin/PluginDetailModel.cs create mode 100644 WinPieGestures/PluginDetailWindow.xaml create mode 100644 WinPieGestures/PluginDetailWindow.xaml.cs diff --git a/WinPieGestures/I18n.cs b/WinPieGestures/I18n.cs index 40b4534e..2c9039dd 100644 --- a/WinPieGestures/I18n.cs +++ b/WinPieGestures/I18n.cs @@ -2064,6 +2064,23 @@ void Add(string key, string zhCn, string? zhTw = null, string? en = null, string Add("PluginsRestartTitle", "重启 StarPie", "重新啟動 StarPie", "Restart StarPie", "StarPie を再起動"); Add("PluginsRestartFailed", "重启 StarPie 失败:{0}", "重新啟動 StarPie 失敗:{0}", "Failed to restart StarPie: {0}", "StarPie の再起動に失敗しました:{0}"); Add("PluginsCardSettingsButton", "⚙ 设置", "⚙ 設定", "⚙ Settings", "⚙ 設定"); + Add("PluginsDetailButton", "详情", "詳細資料", "Details", "詳細"); + Add("PluginsDetailTitle", "插件详情", "外掛詳細資料", "Plugin details", "プラグイン詳細"); + Add("PluginsDetailDescription", "功能描述", "功能描述", "Description", "機能説明"); + Add("PluginsDetailFeatures", "包含功能", "包含功能", "Included features", "含まれる機能"); + Add("PluginsDetailCapabilities", "所需权限", "所需權限", "Required capabilities", "必要な権限"); + Add("PluginsDetailMetadata", "插件信息", "外掛資訊", "Plugin information", "プラグイン情報"); + Add("PluginsDetailAuthor", "作者", "作者", "Author", "作者"); + Add("PluginsDetailLicense", "许可证", "授權條款", "License", "ライセンス"); + Add("PluginsDetailVersion", "版本", "版本", "Version", "バージョン"); + Add("PluginsDetailSource", "来源", "來源", "Source", "提供元"); + Add("PluginsDetailPluginId", "插件 ID", "外掛 ID", "Plugin ID", "プラグイン ID"); + Add("PluginsDetailTargetFramework", "运行框架", "執行框架", "Target framework", "ターゲット フレームワーク"); + Add("PluginsDetailInstallPath", "安装路径", "安裝路徑", "Install path", "インストール パス"); + Add("PluginsDetailDependencies", "插件依赖", "外掛相依", "Dependencies", "依存関係"); + Add("PluginsDetailNoFeatures", "该插件暂未声明具体功能列表。", "此插件尚未宣告具體功能列表。", "This plugin has not declared a detailed feature list.", "このプラグインは詳細な機能一覧を宣言していません。"); + Add("PluginsDetailSourceOfficial", "官方插件", "官方外掛", "Official plugin", "公式プラグイン"); + Add("PluginsDetailSourceLocal", "本地安装", "本機安裝", "Local installation", "ローカルインストール"); Add("PluginsCardSigned", "已签名", "已簽章", "signed", "署名済み"); Add("PluginsCardUninstallButton", "🗑 卸载", "🗑 解除安裝", "🗑 Uninstall", "🗑 アンインストール"); Add("PluginsCardUnsigned", "未签名", "未簽章", "unsigned", "未署名"); @@ -2120,6 +2137,7 @@ void Add(string key, string zhCn, string? zhTw = null, string? en = null, string Add("PluginsOfficialActionInstalled", "已安装", "已安裝", "Installed", "インストール済み"); Add("PluginsOfficialActionUpdate", "⬆️ 更新", "⬆️ 更新", "⬆️ Update", "⬆️ 更新"); Add("PluginsOfficialCatalogInfo", "目录 {0} · {1} 个模块 · 来源 StarPie-Official-Plugins", "目錄 {0} · {1} 個模組 · 來源 StarPie-Official-Plugins", "Catalog {0} · {1} modules · from StarPie-Official-Plugins", "カタログ {0} · {1} モジュール · 提供元 StarPie-Official-Plugins"); + Add("PluginsOfficialCatalogCached", "官方插件目录:已缓存 · 更新于 {0} · {1} 个模块", "官方外掛目錄:已快取 · 更新於 {0} · {1} 個模組", "Official plugin catalog: cached · updated {0} · {1} modules", "公式プラグインカタログ:キャッシュ済み · 更新 {0} · {1} モジュール"); Add("PluginsOfficialClaimSeparator", "、", "、", ", ", "、"); Add("PluginsOfficialHeader", "官方插件", "官方外掛", "Official plugins", "公式プラグイン"); Add("PluginsOfficialInstallFailed", "官方插件 {0} 安装失败:\n\n{1}", "官方外掛 {0} 安裝失敗:\n\n{1}", "Failed to install official plugin {0}:\n\n{1}", "公式プラグイン {0} のインストールに失敗しました:\n\n{1}"); diff --git a/WinPieGestures/Plugin/OfficialPluginClient.cs b/WinPieGestures/Plugin/OfficialPluginClient.cs index 8a9de722..c8865dce 100644 --- a/WinPieGestures/Plugin/OfficialPluginClient.cs +++ b/WinPieGestures/Plugin/OfficialPluginClient.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net.Http; using System.Security.Cryptography; +using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -37,12 +38,35 @@ internal sealed class OfficialPluginModule public string Sha256 { get; set; } = ""; public long Size { get; set; } public string ApiVersion { get; set; } = ""; + public string TargetFramework { get; set; } = ""; public string MinHostVersion { get; set; } = ""; public string? MaxHostVersion { get; set; } public List TypeClaims { get; set; } = new(); public List Capabilities { get; set; } = new(); + public string Description { get; set; } = ""; + public string Author { get; set; } = ""; + public string? Homepage { get; set; } + public string License { get; set; } = ""; + public string? Icon { get; set; } + public List Tags { get; set; } = new(); + public List Features { get; set; } = new(); } +internal sealed class OfficialPluginFeature +{ + public string Id { get; set; } = ""; + public string Name { get; set; } = ""; + public string Description { get; set; } = ""; + public string Category { get; set; } = ""; + public string? IconKey { get; set; } +} + +internal sealed class OfficialPluginCatalogCache +{ + public int CacheVersion { get; set; } = 1; + public DateTimeOffset CachedAt { get; set; } + public OfficialPluginCatalog Catalog { get; set; } = new(); +} internal sealed class OfficialPluginInstallResult { @@ -92,9 +116,104 @@ public static async Task FetchCatalogAsync(CancellationTo ValidateCatalog(catalog); if (!string.Equals(catalog!.ReleaseTag, releaseTag, StringComparison.Ordinal)) throw new InvalidDataException($"官方 catalog 的 releaseTag 与发布标签不一致:{catalog.ReleaseTag} / {releaseTag}。"); + + // 下载成功后写入本地缓存。缓存失败不应把成功的目录刷新误报成失败。 + try + { + SaveCachedCatalog(catalog); + } + catch (Exception ex) + { + AppLogger.LogWarn($"[plugin] 保存官方插件目录缓存失败:{ex.Message}"); + } + return catalog; } + public static bool TryLoadCachedCatalog( + out OfficialPluginCatalog catalog, + out DateTimeOffset cachedAt, + out string error) + { + catalog = null!; + cachedAt = default; + error = ""; + + string path = PluginPaths.OfficialCatalogCacheFile; + if (!File.Exists(path)) return false; + + try + { + OfficialPluginCatalogCache? cache = JsonSerializer.Deserialize( + File.ReadAllText(path), JsonOptions); + if (cache?.Catalog == null) + { + error = "官方插件目录缓存为空。"; + return false; + } + if (cache.CacheVersion != 1) + { + error = "官方插件目录缓存版本不受支持。"; + return false; + } + + ValidateCatalog(cache.Catalog); + catalog = cache.Catalog; + cachedAt = cache.CachedAt == default + ? new DateTimeOffset(File.GetLastWriteTimeUtc(path)) + : cache.CachedAt; + return true; + } + catch (Exception ex) + { + error = ex.Message; + return false; + } + } + + public static DateTimeOffset? GetCachedCatalogTimestamp() + { + try + { + string path = PluginPaths.OfficialCatalogCacheFile; + return File.Exists(path) ? new DateTimeOffset(File.GetLastWriteTimeUtc(path)) : null; + } + catch + { + return null; + } + } + + private static void SaveCachedCatalog(OfficialPluginCatalog catalog) + { + Directory.CreateDirectory(PluginPaths.Root); + string target = PluginPaths.OfficialCatalogCacheFile; + string temporary = target + "." + Guid.NewGuid().ToString("N") + ".tmp"; + var cache = new OfficialPluginCatalogCache + { + CacheVersion = 1, + CachedAt = DateTimeOffset.Now, + Catalog = catalog, + }; + + try + { + string json = JsonSerializer.Serialize(cache, JsonOptions); + File.WriteAllText(temporary, json, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); + File.Move(temporary, target, overwrite: true); + } + finally + { + try + { + if (File.Exists(temporary)) File.Delete(temporary); + } + catch + { + } + } + } + private static async Task FetchLatestReleaseTagAsync(CancellationToken cancellationToken) { string feed = await Http.GetStringAsync(ReleasesFeedUrl, cancellationToken).ConfigureAwait(false); @@ -374,6 +493,15 @@ private static void ValidateCatalog(OfficialPluginCatalog? catalog) throw new InvalidDataException($"插件 {module.Id} 的包大小不合法。"); if (module.Sha256.Length != 64 || !module.Sha256.All(Uri.IsHexDigit)) throw new InvalidDataException($"插件 {module.Id} 的 SHA-256 不合法。"); + + var featureIds = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (OfficialPluginFeature feature in module.Features ?? new List()) + { + if (string.IsNullOrWhiteSpace(feature.Id) || string.IsNullOrWhiteSpace(feature.Name)) + throw new InvalidDataException($"插件 {module.Id} 的功能条目缺少 id 或 name。"); + if (!featureIds.Add(feature.Id)) + throw new InvalidDataException($"插件 {module.Id} 含有重复功能:{feature.Id}。"); + } } } } diff --git a/WinPieGestures/Plugin/OfficialPluginListItem.cs b/WinPieGestures/Plugin/OfficialPluginListItem.cs index c6f0d945..566a1825 100644 --- a/WinPieGestures/Plugin/OfficialPluginListItem.cs +++ b/WinPieGestures/Plugin/OfficialPluginListItem.cs @@ -12,17 +12,21 @@ internal sealed class OfficialPluginListItem public OfficialPluginModule Module { get; } public string DisplayName => Module.Name; public string VersionText => string.IsNullOrWhiteSpace(Module.Version) ? "" : $"v{Module.Version}"; - public string SummaryText => $"{Module.Id} | {(Module.TypeClaims.Count == 0 - ? I18n.T("PluginsOfficialSummaryFallback") - : string.Join(I18n.T("PluginsOfficialClaimSeparator"), Module.TypeClaims))}"; + public string SummaryText => !string.IsNullOrWhiteSpace(Module.Description) + ? Module.Description + : $"{Module.Id} | {(Module.TypeClaims.Count == 0 + ? I18n.T("PluginsOfficialSummaryFallback") + : string.Join(I18n.T("PluginsOfficialClaimSeparator"), Module.TypeClaims))}"; public string StateText { get; } public string InstallButtonText { get; } + public string DetailsText { get; } public bool CanInstall { get; } public bool IsInstalled { get; } public OfficialPluginListItem(OfficialPluginModule module, string? installedVersion) { Module = module; + DetailsText = I18n.T("PluginsDetailButton"); IsInstalled = !string.IsNullOrWhiteSpace(installedVersion); if (!IsInstalled) { diff --git a/WinPieGestures/Plugin/PluginDetailModel.cs b/WinPieGestures/Plugin/PluginDetailModel.cs new file mode 100644 index 00000000..cee2b134 --- /dev/null +++ b/WinPieGestures/Plugin/PluginDetailModel.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using StarPie.Plugin; + +namespace WinPieGestures.Plugins; + +internal sealed class PluginFeatureItem +{ + public string Id { get; init; } = ""; + public string Name { get; init; } = ""; + public string Description { get; init; } = ""; + public string Category { get; init; } = ""; + public string? IconKey { get; init; } +} + +internal sealed class PluginDetailModel +{ + public string DisplayName { get; init; } = ""; + public string VersionText { get; init; } = ""; + public string Description { get; init; } = ""; + public string Author { get; init; } = ""; + public string License { get; init; } = ""; + public string Homepage { get; init; } = ""; + public string PluginId { get; init; } = ""; + public string SourceText { get; init; } = ""; + public string StateText { get; init; } = ""; + public string TargetFramework { get; init; } = ""; + public string CapabilitiesText { get; init; } = ""; + public string DependenciesText { get; init; } = ""; + public string InstallPath { get; init; } = ""; + public string Sha256Text { get; init; } = ""; + public IReadOnlyList Features { get; init; } = Array.Empty(); + + public bool HasFeatures => Features.Count > 0; + public bool HasCapabilities => !string.IsNullOrWhiteSpace(CapabilitiesText); + public bool HasDependencies => !string.IsNullOrWhiteSpace(DependenciesText); +} + +internal static class PluginDetailModelBuilder +{ + public static PluginDetailModel FromLocal(PluginInstance instance) + { + PluginManifest? manifest = instance.Scan.Manifest; + List features = PluginFeatureReader.Read(instance); + + if (features.Count == 0) + { + features = PluginHost.Catalog.SnapshotActions() + .Where(action => string.Equals(action.PluginId, instance.PluginId, StringComparison.OrdinalIgnoreCase)) + .OrderBy(action => action.Category, StringComparer.CurrentCultureIgnoreCase) + .ThenBy(action => action.DisplayName, StringComparer.CurrentCultureIgnoreCase) + .Select(action => new PluginFeatureItem + { + Id = action.ShortId, + Name = action.DisplayName, + Description = action.Description, + Category = action.Category, + IconKey = action.IconKey, + }) + .ToList(); + } + + return new PluginDetailModel + { + DisplayName = !string.IsNullOrWhiteSpace(instance.Entry.Name) + ? instance.Entry.Name + : (manifest?.Name ?? instance.PluginId), + VersionText = string.IsNullOrWhiteSpace(instance.Entry.Version) ? "" : $"v{instance.Entry.Version}", + Description = !string.IsNullOrWhiteSpace(manifest?.Description) ? manifest!.Description : instance.Entry.Description, + Author = !string.IsNullOrWhiteSpace(manifest?.Author) ? manifest!.Author : instance.Entry.Author, + License = !string.IsNullOrWhiteSpace(manifest?.License) ? manifest!.License : instance.Entry.License, + Homepage = manifest?.Homepage ?? "", + PluginId = instance.PluginId, + SourceText = instance.Entry.Official ? I18n.T("PluginsDetailSourceOfficial") : I18n.T("PluginsDetailSourceLocal"), + StateText = PluginListItem.DescribeState(instance).Text, + TargetFramework = instance.Scan.TargetFramework, + CapabilitiesText = string.Join(I18n.T("PluginsEnumSeparator"), manifest?.Capabilities ?? new List()), + DependenciesText = manifest?.Dependencies == null + ? "" + : string.Join(I18n.T("PluginsEnumSeparator"), manifest.Dependencies.Select(dependency => + string.IsNullOrWhiteSpace(dependency.VersionRange) ? dependency.Id : $"{dependency.Id} {dependency.VersionRange}")), + InstallPath = instance.Directory, + Sha256Text = instance.Scan.Sha256, + Features = features, + }; + } + + public static PluginDetailModel FromOfficial(OfficialPluginModule module, PluginInstance? installed) + { + PluginDetailModel? local = installed == null ? null : FromLocal(installed); + IReadOnlyList features = local?.Features.Count > 0 + ? local.Features + : (module.Features ?? new List()).Select(feature => new PluginFeatureItem + { + Id = feature.Id, + Name = feature.Name, + Description = feature.Description, + Category = feature.Category, + IconKey = feature.IconKey, + }).ToList(); + + return new PluginDetailModel + { + DisplayName = !string.IsNullOrWhiteSpace(module.Name) ? module.Name : (local?.DisplayName ?? module.Id), + VersionText = string.IsNullOrWhiteSpace(module.Version) ? (local?.VersionText ?? "") : $"v{module.Version}", + Description = !string.IsNullOrWhiteSpace(module.Description) ? module.Description : (local?.Description ?? ""), + Author = !string.IsNullOrWhiteSpace(module.Author) ? module.Author : (local?.Author ?? ""), + License = !string.IsNullOrWhiteSpace(module.License) ? module.License : (local?.License ?? ""), + Homepage = module.Homepage ?? local?.Homepage ?? "", + PluginId = module.Id, + SourceText = I18n.T("PluginsDetailSourceOfficial"), + StateText = local?.StateText ?? I18n.T("PluginsOfficialStateNotInstalled"), + TargetFramework = local?.TargetFramework ?? module.TargetFramework, + CapabilitiesText = string.Join(I18n.T("PluginsEnumSeparator"), module.Capabilities), + DependenciesText = local?.DependenciesText ?? "", + InstallPath = local?.InstallPath ?? "", + Sha256Text = string.IsNullOrWhiteSpace(module.Sha256) ? (local?.Sha256Text ?? "") : module.Sha256, + Features = features, + }; + } +} + +internal static class PluginFeatureReader +{ + public static List Read(PluginInstance instance) + { + try + { + string manifestPath = Path.Combine(instance.Directory, PluginPaths.ManifestFileName); + if (!File.Exists(manifestPath)) return new List(); + + using JsonDocument document = JsonDocument.Parse(File.ReadAllText(manifestPath), new JsonDocumentOptions + { + CommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + }); + if (!document.RootElement.TryGetProperty("features", out JsonElement features) || features.ValueKind != JsonValueKind.Array) + return new List(); + + var result = new List(); + foreach (JsonElement item in features.EnumerateArray()) + { + if (item.ValueKind != JsonValueKind.Object) continue; + string id = ReadString(item, "id"); + string name = ReadString(item, "name"); + if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(name)) continue; + result.Add(new PluginFeatureItem + { + Id = id, + Name = name, + Description = ReadString(item, "description"), + Category = ReadString(item, "category"), + IconKey = ReadNullableString(item, "iconKey"), + }); + } + return result; + } + catch (Exception ex) + { + AppLogger.LogWarn($"[plugin] 读取插件功能清单失败 {instance.PluginId}:{ex.Message}"); + return new List(); + } + } + + private static string ReadString(JsonElement element, string name) => + element.TryGetProperty(name, out JsonElement value) && value.ValueKind == JsonValueKind.String ? value.GetString() ?? "" : ""; + + private static string? ReadNullableString(JsonElement element, string name) + { + string value = ReadString(element, name); + return string.IsNullOrWhiteSpace(value) ? null : value; + } +} diff --git a/WinPieGestures/Plugin/PluginListItem.cs b/WinPieGestures/Plugin/PluginListItem.cs index 52f80ba4..90ef9198 100644 --- a/WinPieGestures/Plugin/PluginListItem.cs +++ b/WinPieGestures/Plugin/PluginListItem.cs @@ -56,6 +56,9 @@ internal sealed class PluginListItem /// 卡片上「设置」按钮的文字。同 的绑定理由。 public string SettingsText { get; init; } = ""; + /// 卡片上「详情」按钮的文字。 + public string DetailsText { get; init; } = ""; + /// /// 该插件是否声明了插件级参数页(SDK 1.6)。为假时卡片上不出现「设置」按钮。 /// @@ -146,6 +149,7 @@ public static PluginListItem Build(PluginInstance instance) UninstallText = I18n.T("PluginsCardUninstallButton"), PreloadText = I18n.T("PluginsCardPreloadCheckBox"), SettingsText = I18n.T("PluginsCardSettingsButton"), + DetailsText = I18n.T("PluginsDetailButton"), HasSettingsPage = PluginSettingsPageService.HasPage(instance.PluginId), ErrorText = errorText, IsEnabled = entry.Enabled, diff --git a/WinPieGestures/Plugin/PluginPaths.cs b/WinPieGestures/Plugin/PluginPaths.cs index 8a0f866b..d0fa1412 100644 --- a/WinPieGestures/Plugin/PluginPaths.cs +++ b/WinPieGestures/Plugin/PluginPaths.cs @@ -78,6 +78,9 @@ public static bool ScanRootExists /// 宿主运维数据:失败计数 / 安全模式标记。 public static string HealthFile => Path.Combine(Root, "health.json"); + /// 官方插件 catalog 的本地缓存。网络只在用户主动刷新或安装引导时访问。 + public static string OfficialCatalogCacheFile => Path.Combine(Root, "module-catalog.json"); + /// 插件日志目录(与主日志同根,按插件分文件)。 public static string LogDirectory => Path.Combine(AppLogger.GetLogFolderPath(), "plugins"); diff --git a/WinPieGestures/PluginDetailWindow.xaml b/WinPieGestures/PluginDetailWindow.xaml new file mode 100644 index 00000000..7e2ed8f2 --- /dev/null +++ b/WinPieGestures/PluginDetailWindow.xaml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -4009,6 +4012,7 @@ +