From b9578edc3af24fd4f189bf88d1d4b87b4854017a Mon Sep 17 00:00:00 2001 From: QWQLwToo <2467013926@qq.com> Date: Mon, 6 Jul 2026 23:08:53 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A3=B3=E5=AD=90=E6=9B=BF=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/YMhut.Box.Core/Tools/ToolResultBuilder.cs | 107 ++++++++++++++++++ src/YMhut.Box.Tests/ReferenceToolsTests.cs | 32 ++++++ src/YMhut.Box.Tests/StartupCheckTests.cs | 13 ++- src/box-winUI/Assets/tool-results/result.css | 9 ++ src/box-winUI/Assets/tool-results/result.js | 32 +++++- .../vendor/local-tools-runtime.js | 80 +++++++++++++ .../Assets/tool-results/vendor/manifest.json | 66 ++++++++++- .../Services/LocalToolVendorAssetService.cs | 24 ++-- .../Services/StartupInitializationService.cs | 20 ++-- src/box-winUI/Services/ToolResultWebBridge.cs | 36 +++++- src/box-winUI/Views/HomePage.cs | 62 ++++++++-- src/box-winUI/YMhut.Box.WinUI.csproj | 3 + 12 files changed, 450 insertions(+), 34 deletions(-) create mode 100644 src/box-winUI/Assets/tool-results/vendor/local-tools-runtime.js diff --git a/src/YMhut.Box.Core/Tools/ToolResultBuilder.cs b/src/YMhut.Box.Core/Tools/ToolResultBuilder.cs index 242b047..e0b5ed9 100644 --- a/src/YMhut.Box.Core/Tools/ToolResultBuilder.cs +++ b/src/YMhut.Box.Core/Tools/ToolResultBuilder.cs @@ -938,9 +938,116 @@ public static class ToolResultBuilder metadata["defaultExpandedBlocks"] = string.Join(",", experience.DefaultExpandedBlocks ?? []); } + var localJsProfile = LocalJsProfileFor(toolId); + if (!string.IsNullOrWhiteSpace(localJsProfile)) + { + metadata["localJsProfile"] = localJsProfile; + } + + var relatedActions = RelatedActionsFor(toolId, language); + if (!string.IsNullOrWhiteSpace(relatedActions)) + { + metadata["relatedActions"] = relatedActions; + } + return metadata; } + private static string LocalJsProfileFor(string toolId) + => toolId switch + { + "json_formatter" => "json", + "yaml_json_converter" => "yaml", + "xml_formatter" => "xml", + "html_minifier" or "js_obfuscator" => "markup", + "markdown_preview" or "markdown_table_normalizer" => "markdown", + "html_entity" => "html-entity", + "punycode_codec" => "idn", + "timestamp_converter" or "date_time_calculator" => "time", + "base64_codec" or "url_codec" or "unicode_codec" => "codec", + "hash_generator" or "hmac_generator" => "hash", + "qr_generator" => "qrcode", + _ => string.Empty + }; + + private static string RelatedActionsFor(string toolId, string language) + { + var english = language.StartsWith("en", StringComparison.OrdinalIgnoreCase); + object Action(string kind, string labelZh, string labelEn, string target, string toolId = "", string routeTag = "") + => new + { + kind, + label = english ? labelEn : labelZh, + target, + toolId, + routeTag + }; + + var actions = toolId switch + { + "json_formatter" => new[] + { + Action("navigateTool", "转到 JSONPath", "Open JSONPath", "json_path_helper", "json_path_helper"), + Action("navigateTool", "转 YAML/JSON", "Open YAML/JSON", "yaml_json_converter", "yaml_json_converter") + }, + "yaml_json_converter" => new[] + { + Action("navigateTool", "转到 JSON 格式化", "Open JSON formatter", "json_formatter", "json_formatter") + }, + "markdown_preview" => new[] + { + Action("navigateTool", "整理 Markdown 表格", "Normalize markdown table", "markdown_table_normalizer", "markdown_table_normalizer") + }, + "markdown_table_normalizer" => new[] + { + Action("navigateTool", "预览 Markdown", "Preview markdown", "markdown_preview", "markdown_preview") + }, + "base64_codec" or "url_codec" or "html_entity" or "unicode_codec" or "punycode_codec" => new[] + { + Action("navigateTool", "转到 URL 编解码", "Open URL codec", "url_codec", "url_codec"), + Action("navigateTool", "转到 Base64", "Open Base64", "base64_codec", "base64_codec") + }, + "url_inspector" or "url_redirect_trace" => new[] + { + Action("navigateTool", "转到 URL 编解码", "Open URL codec", "url_codec", "url_codec"), + Action("navigateTool", "转到域名 RDAP", "Open domain RDAP", "rdap_domain_lookup", "rdap_domain_lookup") + }, + "dns_query" or "ip_lookup" or "ip_info" => new[] + { + Action("navigateTool", "转到 IP 信息", "Open IP info", "ip_info", "ip_info"), + Action("navigateTool", "转到 RDAP 查询", "Open RDAP lookup", "rdap_ip_lookup", "rdap_ip_lookup") + }, + "hash_generator" or "hmac_generator" => new[] + { + Action("navigateTool", "生成哈希清单", "Build hash manifest", "hash_manifest_builder", "hash_manifest_builder"), + Action("navigateTool", "校验哈希清单", "Verify hash manifest", "hash_manifest_verify", "hash_manifest_verify") + }, + "hash_manifest_builder" => new[] + { + Action("navigateTool", "校验哈希清单", "Verify hash manifest", "hash_manifest_verify", "hash_manifest_verify") + }, + "hash_manifest_verify" => new[] + { + Action("navigateTool", "生成哈希清单", "Build hash manifest", "hash_manifest_builder", "hash_manifest_builder") + }, + "qr_generator" => new[] + { + Action("navigateTool", "扫描二维码", "Scan QR code", "qrcode_scanner", "qrcode_scanner") + }, + "qrcode_scanner" => new[] + { + Action("navigateTool", "生成二维码", "Generate QR code", "qr_generator", "qr_generator") + }, + "sanguosha_skin" => new[] + { + Action("navigateRoute", "查看服务状态", "Open service status", "serviceStatus", routeTag: "serviceStatus") + }, + _ => [] + }; + + return actions.Length == 0 ? string.Empty : JsonSerializer.Serialize(actions); + } + private static Dictionary RemoteMetadata(ApiEndpoint endpoint, ToolResultKind kind, string language) { var metadata = Metadata(endpoint.Id, kind, language, "remote-dashboard"); diff --git a/src/YMhut.Box.Tests/ReferenceToolsTests.cs b/src/YMhut.Box.Tests/ReferenceToolsTests.cs index fe5b69f..38d8e83 100644 --- a/src/YMhut.Box.Tests/ReferenceToolsTests.cs +++ b/src/YMhut.Box.Tests/ReferenceToolsTests.cs @@ -208,6 +208,38 @@ public sealed class ReferenceToolsTests Assert.HasCount(17, modules); } + [TestMethod] + public void WifiPasswordParsersReadEnglishAndChineseNetshOutput() + { + const string englishInterfaces = """ + There is 1 interface on the system: + + State : connected + SSID : Office WiFi + """; + const string chineseInterfaces = """ + 系统上有 1 个接口: + + 状态 : 已连接 + SSID : 家庭网络 + """; + const string englishProfile = """ + Security settings + ----------------- + Key Content : english-secret + """; + const string chineseProfile = """ + 安全设置 + -------- + 关键内容 : chinese-secret + """; + + Assert.AreEqual("Office WiFi", BuiltinReferenceToolService.ParseConnectedSsid(englishInterfaces)); + Assert.AreEqual("家庭网络", BuiltinReferenceToolService.ParseConnectedSsid(chineseInterfaces)); + Assert.AreEqual("english-secret", BuiltinReferenceToolService.ParseWifiKeyContent(englishProfile)); + Assert.AreEqual("chinese-secret", BuiltinReferenceToolService.ParseWifiKeyContent(chineseProfile)); + } + [TestMethod] public async Task ResilientLogServiceFallsBackToJsonlWhenPrimaryFails() { diff --git a/src/YMhut.Box.Tests/StartupCheckTests.cs b/src/YMhut.Box.Tests/StartupCheckTests.cs index c463458..e7e3a0b 100644 --- a/src/YMhut.Box.Tests/StartupCheckTests.cs +++ b/src/YMhut.Box.Tests/StartupCheckTests.cs @@ -190,7 +190,7 @@ public sealed class StartupCheckTests } [TestMethod] - public async Task IntegrityReportsMissingExternalToolLaunchableAsWarning() + public async Task IntegrityTreatsLegacyExternalToolManifestEntriesAsOptional() { using var workspace = TestWorkspace.Create("ymhut-startup-tool-missing"); var installRoot = CreateHealthyInstall(workspace); @@ -211,8 +211,15 @@ public sealed class StartupCheckTests var missing = report.Items.FirstOrDefault(item => item.Id == "file:Tools/Sample/missing.exe"); Assert.IsNotNull(missing); - Assert.AreEqual(StartupCheckStatus.Missing, missing.Status); - Assert.AreEqual(StartupCheckSeverity.Warning, missing.Severity); + Assert.IsFalse(missing.IsIssue); + Assert.AreEqual(StartupCheckStatus.Skipped, missing.Status); + Assert.AreEqual(StartupCheckSeverity.Info, missing.Severity); + Assert.IsFalse(report.Items.Any(item => + item.IsIssue && + item.Id.StartsWith("file:Tools/", StringComparison.OrdinalIgnoreCase))); + Assert.IsFalse(report.Items.Any(item => + item.IsIssue && + item.Id.StartsWith("file:Metadata/", StringComparison.OrdinalIgnoreCase))); } [TestMethod] diff --git a/src/box-winUI/Assets/tool-results/result.css b/src/box-winUI/Assets/tool-results/result.css index f118c53..bc5e8d7 100644 --- a/src/box-winUI/Assets/tool-results/result.css +++ b/src/box-winUI/Assets/tool-results/result.css @@ -108,6 +108,7 @@ p { .table-wrap, .code-wrap, .summary-item, +.related-actions, .kv, .list-item, .file-card, @@ -173,6 +174,7 @@ p { } .actions, +.related-actions, .link-actions, .raw-actions { display: flex; @@ -181,6 +183,13 @@ p { justify-content: flex-end; } +.related-actions { + grid-column: 1 / -1; + justify-content: flex-start; + padding: 10px; + background: color-mix(in srgb, var(--panel) 76%, var(--soft)); +} + .summary-strip { grid-column: 1 / -1; display: grid; diff --git a/src/box-winUI/Assets/tool-results/result.js b/src/box-winUI/Assets/tool-results/result.js index c1d140d..6ce4f4e 100644 --- a/src/box-winUI/Assets/tool-results/result.js +++ b/src/box-winUI/Assets/tool-results/result.js @@ -128,8 +128,8 @@ function el(tag, className, content) { return node; } -function post(action, value) { - window.chrome?.webview?.postMessage({ action, value }); +function post(action, value, extra) { + window.chrome?.webview?.postMessage({ action, value, payload: extra || null }); } function metadata(data) { @@ -196,10 +196,38 @@ function hero(data) { actions.append(button(t('copyRaw'), () => post('copy', data.resultDocument?.rawText || ''))); actions.append(button(t('copyJson'), () => post('copy', JSON.stringify(data.resultDocument || {}, null, 2)), 'ghost')); + const related = relatedActions(data); root.append(identity, actions, summaryMetrics(data)); + if (related.childElementCount) root.append(related); return root; } +function relatedActions(data) { + const actions = parseRelatedActions(data); + const root = el('div', 'related-actions'); + for (const action of actions) { + const label = action.label || action.title || action.target || action.toolId || action.routeTag; + if (!label) continue; + const kind = action.kind || action.action || 'navigateTool'; + const value = action.toolId || action.routeTag || action.url || action.target || ''; + if (!value) continue; + root.append(button(label, () => post(kind, value, action), 'ghost compact')); + } + return root; +} + +function parseRelatedActions(data) { + const raw = metadata(data).relatedActions; + if (!raw) return []; + if (Array.isArray(raw)) return raw; + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + function summaryMetrics(data) { const doc = data.resultDocument || {}; const raw = doc.rawText || ''; diff --git a/src/box-winUI/Assets/tool-results/vendor/local-tools-runtime.js b/src/box-winUI/Assets/tool-results/vendor/local-tools-runtime.js new file mode 100644 index 0000000..a039fef --- /dev/null +++ b/src/box-winUI/Assets/tool-results/vendor/local-tools-runtime.js @@ -0,0 +1,80 @@ +(function (global) { + 'use strict'; + + const asText = value => value == null ? '' : String(value); + + function formatJson(input, space = 2) { + return JSON.stringify(JSON.parse(asText(input)), null, space); + } + + function compactJson(input) { + return JSON.stringify(JSON.parse(asText(input))); + } + + function yamlToJson(input, space = 2) { + if (!global.jsyaml) throw new Error('js-yaml is not available'); + return JSON.stringify(global.jsyaml.load(asText(input)), null, space); + } + + function jsonToYaml(input) { + if (!global.jsyaml) throw new Error('js-yaml is not available'); + return global.jsyaml.dump(JSON.parse(asText(input)), { lineWidth: 120 }); + } + + function beautify(input, mode = 'js') { + const text = asText(input); + if (mode === 'html' && typeof global.html_beautify === 'function') return global.html_beautify(text); + if (mode === 'css' && typeof global.css_beautify === 'function') return global.css_beautify(text); + if (typeof global.js_beautify === 'function') return global.js_beautify(text); + return text; + } + + function markdownToHtml(input) { + if (!global.marked?.parse) throw new Error('marked is not available'); + return global.marked.parse(asText(input)); + } + + function htmlEncode(input) { + if (!global.he?.encode) throw new Error('he is not available'); + return global.he.encode(asText(input), { useNamedReferences: true }); + } + + function htmlDecode(input) { + if (!global.he?.decode) throw new Error('he is not available'); + return global.he.decode(asText(input)); + } + + function toAsciiDomain(input) { + if (!global.punycode?.toASCII) throw new Error('punycode is not available'); + return asText(input).split('.').map(part => global.punycode.toASCII(part)).join('.'); + } + + function toUnicodeDomain(input) { + if (!global.punycode?.toUnicode) throw new Error('punycode is not available'); + return asText(input).split('.').map(part => global.punycode.toUnicode(part)).join('.'); + } + + function formatTimestamp(input) { + if (!global.dayjs) throw new Error('dayjs is not available'); + const raw = asText(input).trim(); + const number = Number(raw); + const millis = Number.isFinite(number) && raw.length <= 10 ? number * 1000 : number; + const date = Number.isFinite(millis) ? global.dayjs(millis) : global.dayjs(raw); + if (!date.isValid()) throw new Error('Invalid timestamp'); + return date.format('YYYY-MM-DD HH:mm:ss'); + } + + global.YMhutLocalTools = { + formatJson, + compactJson, + yamlToJson, + jsonToYaml, + beautify, + markdownToHtml, + htmlEncode, + htmlDecode, + toAsciiDomain, + toUnicodeDomain, + formatTimestamp + }; +})(window); diff --git a/src/box-winUI/Assets/tool-results/vendor/manifest.json b/src/box-winUI/Assets/tool-results/vendor/manifest.json index 1b25d63..3abee86 100644 --- a/src/box-winUI/Assets/tool-results/vendor/manifest.json +++ b/src/box-winUI/Assets/tool-results/vendor/manifest.json @@ -1,5 +1,67 @@ { "policy": "local-only", - "libraries": [], - "fallback": "first-party renderer" + "runtime": { + "file": "local-tools-runtime.js", + "sha256": "ddc8c5fd3cf854fcd87b3bb38c6bedc6b527f52b32e096dc6025f4b2e7c78ede", + "purpose": "First-party wrappers for offline formatter, codec, markdown, timestamp and IDN tools" + }, + "libraries": [ + { + "name": "js-beautify", + "version": "2.0.3", + "file": "js-beautify.min.js", + "license": "MIT", + "licenseFile": "LICENSE-js-beautify.txt", + "sha256": "921fbb92e4042e4cf07985d402c26e1f577c109f9b19ccf7e98005cc3d461212" + }, + { + "name": "js-yaml", + "version": "5.2.1", + "file": "js-yaml.umd.min.js", + "license": "MIT", + "licenseFile": "LICENSE-js-yaml.txt", + "sha256": "7569c47393b3c48e4a01163552517e98ccf13ad5de688b200efabcc471583582" + }, + { + "name": "marked", + "version": "18.0.5", + "file": "marked.umd.js", + "license": "MIT", + "licenseFile": "LICENSE-marked.txt", + "sha256": "2dc4769dfde29f51c7aca1a539c6407c789c8ea644cf8b7d01ded28a9c1d800b" + }, + { + "name": "dayjs", + "version": "1.11.21", + "file": "dayjs.min.js", + "license": "MIT", + "licenseFile": "LICENSE-dayjs.txt", + "sha256": "0198dd0b1f760cded169c7e7ff7eaf56bc36c4c22c7c9b7c683e59437ed8700e" + }, + { + "name": "he", + "version": "1.2.0", + "file": "he.js", + "license": "MIT", + "licenseFile": "LICENSE-he.txt", + "sha256": "76c554d5bbfd032fe620595076a50abea5124b9cbd4e9ffe6ac94a4f855aeceb" + }, + { + "name": "punycode", + "version": "2.3.1", + "file": "punycode.js", + "license": "MIT", + "licenseFile": "LICENSE-punycode.txt", + "sha256": "6052a80eac47e46bd4de17ae0095e0192c336d3d7c387d292ad2176ebfc53d04" + } + ], + "csharpFallback": [ + "base64_codec", + "url_codec", + "hash_generator", + "hmac_generator", + "qr_generator", + "qrcode_scanner" + ], + "fallback": "first-party renderer and C# tool executor" } diff --git a/src/box-winUI/Services/LocalToolVendorAssetService.cs b/src/box-winUI/Services/LocalToolVendorAssetService.cs index b539d59..878b41e 100644 --- a/src/box-winUI/Services/LocalToolVendorAssetService.cs +++ b/src/box-winUI/Services/LocalToolVendorAssetService.cs @@ -110,12 +110,15 @@ public sealed class LocalToolVendorAssetService( Path.Combine(cacheRoot, "manifest.json"), manifestHash, extracted); - await logService.WriteAsync( - "Information", - "tool-vendor", - "Local JS vendor assets extracted", - $"files={extracted.Count}; hash={manifestHash}", - cancellationToken).ConfigureAwait(false); + if (logService is not null) + { + await logService.WriteAsync( + "Information", + "tool-vendor", + "Local JS vendor assets extracted", + $"files={extracted.Count}; hash={manifestHash}", + cancellationToken).ConfigureAwait(false); + } return _snapshot; } finally @@ -131,9 +134,12 @@ public sealed class LocalToolVendorAssetService( ? Directory.EnumerateFiles(sourceRoot, "*", SearchOption.AllDirectories).OrderBy(path => path, StringComparer.OrdinalIgnoreCase).ToArray() : []; var manifest = Path.Combine(sourceRoot, "manifest.json"); - var hash = File.Exists(manifest) - ? Convert.ToHexString(await SHA256.HashDataAsync(File.OpenRead(manifest), cancellationToken).ConfigureAwait(false)).ToLowerInvariant() - : HashText(string.Join("|", files.Select(Path.GetFileName))); + var hash = HashText(string.Join("|", files.Select(Path.GetFileName))); + if (File.Exists(manifest)) + { + await using var manifestStream = File.OpenRead(manifest); + hash = Convert.ToHexString(await SHA256.HashDataAsync(manifestStream, cancellationToken).ConfigureAwait(false)).ToLowerInvariant(); + } return new LocalToolVendorAssetSnapshot(sourceRoot, manifest, hash, files); } diff --git a/src/box-winUI/Services/StartupInitializationService.cs b/src/box-winUI/Services/StartupInitializationService.cs index f8d771a..ac16ac3 100644 --- a/src/box-winUI/Services/StartupInitializationService.cs +++ b/src/box-winUI/Services/StartupInitializationService.cs @@ -1,4 +1,4 @@ -using YMhut.Box.Core.App; +using YMhut.Box.Core.App; using YMhut.Box.Core.Data; using YMhut.Box.Core.Downloads; using YMhut.Box.Core.Logging; @@ -160,12 +160,12 @@ public sealed class StartupInitializationService( context.Report(1, T("本地设置已载入。", "Local settings loaded.")); }); - yield return Stage("resources", T("验证随包资源", "Validating bundled resources"), 10, false, (context, token) => + yield return Stage("resources", T("验证随包资源", "Validating bundled resources"), 10, false, async (context, token) => { if (!getSettings().ValidateResourcesOnStartup) { context.Report(1, T("已跳过随包资源完整性检查。", "Bundled resource validation skipped.")); - return Task.CompletedTask; + return; } context.Report(0.2, T("正在检查三国杀、本地参考数据和工具结果前端资源...", "Checking Sanguosha, local reference data, and tool result frontend assets...")); @@ -173,15 +173,16 @@ public sealed class StartupInitializationService( "data/sanguosha/skin_config.json", "data/sanguosha/GeneralSkinInfoConfig.json", "data/sanguosha/十周年皮肤配置对应关系.xlsx"); + context.Report(0.72, T("正在准备本地 JS 工具库缓存...", "Preparing local JS tool vendor cache...")); + var vendor = await localToolVendorAssets.EnsureExtractedAsync(token).ConfigureAwait(false); + var vendorHealthy = vendor.Files.Count > 0 && File.Exists(vendor.ManifestPath); context.Report( 1, - validation.IsHealthy - ? T("三国杀随包资源已就绪。", "Sanguosha bundled resources are ready.") - : T($"缺少 {validation.MissingPaths.Count} 个随包资源,相关工具会显示修复提示。", $"{validation.MissingPaths.Count} bundled resources are missing; affected tools will show repair guidance."), - validation.IsHealthy ? StartupCheckSeverity.Info : StartupCheckSeverity.Warning); - return Task.CompletedTask; + validation.IsHealthy && vendorHealthy + ? T("三国杀随包资源和本地 JS 工具库已就绪。", "Sanguosha resources and local JS vendor assets are ready.") + : T($"缺少 {validation.MissingPaths.Count} 个随包资源,本地 JS 文件 {vendor.Files.Count} 个。", $"{validation.MissingPaths.Count} bundled resources are missing; {vendor.Files.Count} local JS files are visible."), + validation.IsHealthy && vendorHealthy ? StartupCheckSeverity.Info : StartupCheckSeverity.Warning); }); - yield return Stage("database", T("初始化主 SQLite", "Initializing main SQLite"), 10, true, async (context, token) => { context.Report(0.25, T("正在初始化日志与主数据库...", "Initializing logs and main database...")); @@ -295,3 +296,4 @@ public sealed class StartupInitializationService( private static string T(string zh, string en) => AppLocalizer.T(zh, en); } + diff --git a/src/box-winUI/Services/ToolResultWebBridge.cs b/src/box-winUI/Services/ToolResultWebBridge.cs index d3f324b..2d7294c 100644 --- a/src/box-winUI/Services/ToolResultWebBridge.cs +++ b/src/box-winUI/Services/ToolResultWebBridge.cs @@ -1,5 +1,6 @@ using System.Text.Json; using System.Text.Json.Serialization; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Web.WebView2.Core; using Windows.ApplicationModel.DataTransfer; using Windows.System; @@ -11,7 +12,8 @@ namespace YMhut.Box.WinUI.Services; public sealed class ToolResultWebBridge( IToolResultExperienceCatalog experienceCatalog, ISettingsService settingsService, - IToolLinkNavigationService linkNavigationService) + IToolLinkNavigationService linkNavigationService, + IServiceProvider services) { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) { @@ -97,6 +99,38 @@ public sealed class ToolResultWebBridge( case "openSystemBrowser": await linkNavigationService.OpenAsync(value, ToolLinkTarget.SystemBrowser); break; + case "openExternal": + await linkNavigationService.OpenAsync(value, ToolLinkTarget.SystemBrowser); + break; + case "navigateTool": + if (!string.IsNullOrWhiteSpace(value) && + services.GetService()?.NavigateToTool(value) == true) + { + ToastService.Show(AppLocalizer.T("已跳转到相关工具", "Opened related tool"), ToastKind.Success); + } + else + { + ToastService.Show(AppLocalizer.T("暂时无法跳转到该工具", "Unable to open related tool"), ToastKind.Warning); + } + break; + case "navigateRoute": + if (!string.IsNullOrWhiteSpace(value) && + services.GetService()?.Navigate(value) == true) + { + ToastService.Show(AppLocalizer.T("已打开相关页面", "Opened related page"), ToastKind.Success); + } + else + { + ToastService.Show(AppLocalizer.T("暂时无法打开相关页面", "Unable to open related page"), ToastKind.Warning); + } + break; + case "rerunWithInput": + if (!string.IsNullOrWhiteSpace(value) && + services.GetService()?.NavigateToTool(value) == true) + { + ToastService.Show(AppLocalizer.T("已打开工具,请确认输入后重新运行", "Tool opened; review the input and run again"), ToastKind.Information); + } + break; } } diff --git a/src/box-winUI/Views/HomePage.cs b/src/box-winUI/Views/HomePage.cs index 4b2948f..8c49db5 100644 --- a/src/box-winUI/Views/HomePage.cs +++ b/src/box-winUI/Views/HomePage.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Linq; using System.Text.Json; @@ -280,9 +280,9 @@ public sealed class HomePage : Page var current = _versionService.GetCurrent().Version; var relation = info.CompareToCurrent(current); - _announcementTitle.Text = AnnouncementTitle(info, relation); - _announcementText.Text = AnnouncementSummary(info, current, relation); - _announcementMeta.Text = AnnouncementMeta(info, current, relation, includeTime: false); + _announcementTitle.Text = VersionAnnouncementTitle(info, relation); + _announcementText.Text = VersionAnnouncementSummary(info, current, relation); + _announcementMeta.Text = VersionAnnouncementMeta(info, current, relation, includeTime: false); } private async Task ShowAnnouncementDialogAsync() @@ -293,13 +293,13 @@ public sealed class HomePage : Page var relation = info is null ? 0 : info.CompareToCurrent(current); var title = info is null ? AppLocalizer.T("完整公告", "Full announcement") - : AnnouncementTitle(info, relation); + : VersionAnnouncementTitle(info, relation); var content = info is null ? AppLocalizer.T("欢迎使用 YMhut Box。新版 WinUI 首页已对齐旧版的公告、搜索和工具工作台体验。", "Welcome to YMhut Box. The WinUI home page now aligns with the classic announcement, search, and dashboard experience.") : CombineAnnouncement(info, current, relation); var meta = info is null ? AppLocalizer.T("本地公告", "Local announcement") - : AnnouncementMeta(info, current, relation, includeTime: true); + : VersionAnnouncementMeta(info, current, relation, includeTime: true); var dialog = new ContentDialog { @@ -323,7 +323,7 @@ public sealed class HomePage : Page Content = ModernUi.GutterScroll( MarkdownRenderHelper.Render( notes, - _announcementInfo is null ? null : AnnouncementMeta( + _announcementInfo is null ? null : VersionAnnouncementMeta( _announcementInfo, _versionService.GetCurrent().Version, _announcementInfo.CompareToCurrent(_versionService.GetCurrent().Version), @@ -349,11 +349,56 @@ public sealed class HomePage : Page return value.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()?.Trim() ?? value.Trim(); } + + private static string VersionAnnouncementTitle(RemoteUpdateInfo info, int relation) + { + if (relation > 0) + { + return string.IsNullOrWhiteSpace(info.Title) + ? AppLocalizer.T("发现新版本", "New version available") + : info.Title; + } + + return relation == 0 + ? AppLocalizer.T("当前版本公告", "Current version announcement") + : AppLocalizer.T("测试版公告", "Preview build announcement"); + } + + private static string VersionAnnouncementSummary(RemoteUpdateInfo info, string currentVersion, int relation) + { + return relation switch + { + > 0 => FirstAnnouncementLine(info), + 0 => AppLocalizer.T( + $"当前客户端版本 {currentVersion} 已经和服务端公告版本 {info.DisplayVersion} 对齐,稳稳的。", + $"Current client version {currentVersion} matches server version {info.DisplayVersion}. You are all set."), + _ => AppLocalizer.T( + $"你正在使用测试/抢跑版本:本地 {currentVersion} 已经跑在公告版本 {info.DisplayVersion} 前面啦,未来感有点足。", + $"You are using a preview build: local {currentVersion} is ahead of server announcement version {info.DisplayVersion}.") + }; + } + + private static string VersionAnnouncementMeta(RemoteUpdateInfo info, string currentVersion, int relation, bool includeTime) + { + var status = relation switch + { + > 0 => AppLocalizer.T("可更新", "Update available"), + 0 => AppLocalizer.T("已是最新", "Up to date"), + _ => AppLocalizer.T("抢跑测试版", "Preview build") + }; + var version = AppLocalizer.T($"公告 {info.DisplayVersion} / 本地 {currentVersion}", $"Server {info.DisplayVersion} / Local {currentVersion}"); + var published = info.PublishedAt is null + ? string.Empty + : includeTime + ? $" · {info.PublishedAt.Value:yyyy-MM-dd HH:mm}" + : $" · {info.PublishedAt.Value:yyyy-MM-dd}"; + return $"{status} · {version}{published}"; + } private static string CombineAnnouncement(RemoteUpdateInfo info, string currentVersion, int relation) { return string.Join(Environment.NewLine + Environment.NewLine, new[] { - relation > 0 ? null : AnnouncementSummary(info, currentVersion, relation), + relation > 0 ? null : VersionAnnouncementSummary(info, currentVersion, relation), info.DisplayMessage, info.DisplayReleaseNotes, info.PublishedAt is null ? null : AppLocalizer.T($"发布时间:{info.PublishedAt.Value:yyyy-MM-dd HH:mm}", $"Published: {info.PublishedAt.Value:yyyy-MM-dd HH:mm}") @@ -1158,3 +1203,4 @@ public sealed class HomePage : Page }; } } + diff --git a/src/box-winUI/YMhut.Box.WinUI.csproj b/src/box-winUI/YMhut.Box.WinUI.csproj index e63f6e1..c823546 100644 --- a/src/box-winUI/YMhut.Box.WinUI.csproj +++ b/src/box-winUI/YMhut.Box.WinUI.csproj @@ -53,6 +53,9 @@ + + YMhut.Box.WinUI.Assets.tool-results.vendor.%(Filename)%(Extension) + YMhut.Box.WinUI.version.json