壳子替换

This commit is contained in:
QWQLwToo
2026-07-06 23:08:53 +08:00
parent 31d778710b
commit b9578edc3a
12 changed files with 450 additions and 34 deletions
@@ -938,9 +938,116 @@ public static class ToolResultBuilder
metadata["defaultExpandedBlocks"] = string.Join(",", experience.DefaultExpandedBlocks ?? []); 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; 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<string, string> RemoteMetadata(ApiEndpoint endpoint, ToolResultKind kind, string language) private static Dictionary<string, string> RemoteMetadata(ApiEndpoint endpoint, ToolResultKind kind, string language)
{ {
var metadata = Metadata(endpoint.Id, kind, language, "remote-dashboard"); var metadata = Metadata(endpoint.Id, kind, language, "remote-dashboard");
@@ -208,6 +208,38 @@ public sealed class ReferenceToolsTests
Assert.HasCount(17, modules); 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] [TestMethod]
public async Task ResilientLogServiceFallsBackToJsonlWhenPrimaryFails() public async Task ResilientLogServiceFallsBackToJsonlWhenPrimaryFails()
{ {
+10 -3
View File
@@ -190,7 +190,7 @@ public sealed class StartupCheckTests
} }
[TestMethod] [TestMethod]
public async Task IntegrityReportsMissingExternalToolLaunchableAsWarning() public async Task IntegrityTreatsLegacyExternalToolManifestEntriesAsOptional()
{ {
using var workspace = TestWorkspace.Create("ymhut-startup-tool-missing"); using var workspace = TestWorkspace.Create("ymhut-startup-tool-missing");
var installRoot = CreateHealthyInstall(workspace); 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"); var missing = report.Items.FirstOrDefault(item => item.Id == "file:Tools/Sample/missing.exe");
Assert.IsNotNull(missing); Assert.IsNotNull(missing);
Assert.AreEqual(StartupCheckStatus.Missing, missing.Status); Assert.IsFalse(missing.IsIssue);
Assert.AreEqual(StartupCheckSeverity.Warning, missing.Severity); 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] [TestMethod]
@@ -108,6 +108,7 @@ p {
.table-wrap, .table-wrap,
.code-wrap, .code-wrap,
.summary-item, .summary-item,
.related-actions,
.kv, .kv,
.list-item, .list-item,
.file-card, .file-card,
@@ -173,6 +174,7 @@ p {
} }
.actions, .actions,
.related-actions,
.link-actions, .link-actions,
.raw-actions { .raw-actions {
display: flex; display: flex;
@@ -181,6 +183,13 @@ p {
justify-content: flex-end; 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 { .summary-strip {
grid-column: 1 / -1; grid-column: 1 / -1;
display: grid; display: grid;
+30 -2
View File
@@ -128,8 +128,8 @@ function el(tag, className, content) {
return node; return node;
} }
function post(action, value) { function post(action, value, extra) {
window.chrome?.webview?.postMessage({ action, value }); window.chrome?.webview?.postMessage({ action, value, payload: extra || null });
} }
function metadata(data) { function metadata(data) {
@@ -196,10 +196,38 @@ function hero(data) {
actions.append(button(t('copyRaw'), () => post('copy', data.resultDocument?.rawText || ''))); actions.append(button(t('copyRaw'), () => post('copy', data.resultDocument?.rawText || '')));
actions.append(button(t('copyJson'), () => post('copy', JSON.stringify(data.resultDocument || {}, null, 2)), 'ghost')); actions.append(button(t('copyJson'), () => post('copy', JSON.stringify(data.resultDocument || {}, null, 2)), 'ghost'));
const related = relatedActions(data);
root.append(identity, actions, summaryMetrics(data)); root.append(identity, actions, summaryMetrics(data));
if (related.childElementCount) root.append(related);
return root; 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) { function summaryMetrics(data) {
const doc = data.resultDocument || {}; const doc = data.resultDocument || {};
const raw = doc.rawText || ''; const raw = doc.rawText || '';
@@ -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);
+64 -2
View File
@@ -1,5 +1,67 @@
{ {
"policy": "local-only", "policy": "local-only",
"libraries": [], "runtime": {
"fallback": "first-party renderer" "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"
} }
@@ -110,12 +110,15 @@ public sealed class LocalToolVendorAssetService(
Path.Combine(cacheRoot, "manifest.json"), Path.Combine(cacheRoot, "manifest.json"),
manifestHash, manifestHash,
extracted); extracted);
await logService.WriteAsync( if (logService is not null)
"Information", {
"tool-vendor", await logService.WriteAsync(
"Local JS vendor assets extracted", "Information",
$"files={extracted.Count}; hash={manifestHash}", "tool-vendor",
cancellationToken).ConfigureAwait(false); "Local JS vendor assets extracted",
$"files={extracted.Count}; hash={manifestHash}",
cancellationToken).ConfigureAwait(false);
}
return _snapshot; return _snapshot;
} }
finally finally
@@ -131,9 +134,12 @@ public sealed class LocalToolVendorAssetService(
? Directory.EnumerateFiles(sourceRoot, "*", SearchOption.AllDirectories).OrderBy(path => path, StringComparer.OrdinalIgnoreCase).ToArray() ? Directory.EnumerateFiles(sourceRoot, "*", SearchOption.AllDirectories).OrderBy(path => path, StringComparer.OrdinalIgnoreCase).ToArray()
: []; : [];
var manifest = Path.Combine(sourceRoot, "manifest.json"); var manifest = Path.Combine(sourceRoot, "manifest.json");
var hash = File.Exists(manifest) var hash = HashText(string.Join("|", files.Select(Path.GetFileName)));
? Convert.ToHexString(await SHA256.HashDataAsync(File.OpenRead(manifest), cancellationToken).ConfigureAwait(false)).ToLowerInvariant() if (File.Exists(manifest))
: HashText(string.Join("|", files.Select(Path.GetFileName))); {
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); return new LocalToolVendorAssetSnapshot(sourceRoot, manifest, hash, files);
} }
@@ -1,4 +1,4 @@
using YMhut.Box.Core.App; using YMhut.Box.Core.App;
using YMhut.Box.Core.Data; using YMhut.Box.Core.Data;
using YMhut.Box.Core.Downloads; using YMhut.Box.Core.Downloads;
using YMhut.Box.Core.Logging; using YMhut.Box.Core.Logging;
@@ -160,12 +160,12 @@ public sealed class StartupInitializationService(
context.Report(1, T("本地设置已载入。", "Local settings loaded.")); 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) if (!getSettings().ValidateResourcesOnStartup)
{ {
context.Report(1, T("已跳过随包资源完整性检查。", "Bundled resource validation skipped.")); 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...")); 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/skin_config.json",
"data/sanguosha/GeneralSkinInfoConfig.json", "data/sanguosha/GeneralSkinInfoConfig.json",
"data/sanguosha/十周年皮肤配置对应关系.xlsx"); "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( context.Report(
1, 1,
validation.IsHealthy validation.IsHealthy && vendorHealthy
? T("三国杀随包资源已就绪。", "Sanguosha bundled resources are ready.") ? T("三国杀随包资源和本地 JS 工具库已就绪。", "Sanguosha resources and local JS vendor assets are ready.")
: T($"缺少 {validation.MissingPaths.Count} 个随包资源,相关工具会显示修复提示。", $"{validation.MissingPaths.Count} bundled resources are missing; affected tools will show repair guidance."), : 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 ? StartupCheckSeverity.Info : StartupCheckSeverity.Warning); validation.IsHealthy && vendorHealthy ? StartupCheckSeverity.Info : StartupCheckSeverity.Warning);
return Task.CompletedTask;
}); });
yield return Stage("database", T("初始化主 SQLite", "Initializing main SQLite"), 10, true, async (context, token) => yield return Stage("database", T("初始化主 SQLite", "Initializing main SQLite"), 10, true, async (context, token) =>
{ {
context.Report(0.25, T("正在初始化日志与主数据库...", "Initializing logs and main database...")); 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); private static string T(string zh, string en) => AppLocalizer.T(zh, en);
} }
+35 -1
View File
@@ -1,5 +1,6 @@
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Core;
using Windows.ApplicationModel.DataTransfer; using Windows.ApplicationModel.DataTransfer;
using Windows.System; using Windows.System;
@@ -11,7 +12,8 @@ namespace YMhut.Box.WinUI.Services;
public sealed class ToolResultWebBridge( public sealed class ToolResultWebBridge(
IToolResultExperienceCatalog experienceCatalog, IToolResultExperienceCatalog experienceCatalog,
ISettingsService settingsService, ISettingsService settingsService,
IToolLinkNavigationService linkNavigationService) IToolLinkNavigationService linkNavigationService,
IServiceProvider services)
{ {
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
{ {
@@ -97,6 +99,38 @@ public sealed class ToolResultWebBridge(
case "openSystemBrowser": case "openSystemBrowser":
await linkNavigationService.OpenAsync(value, ToolLinkTarget.SystemBrowser); await linkNavigationService.OpenAsync(value, ToolLinkTarget.SystemBrowser);
break; break;
case "openExternal":
await linkNavigationService.OpenAsync(value, ToolLinkTarget.SystemBrowser);
break;
case "navigateTool":
if (!string.IsNullOrWhiteSpace(value) &&
services.GetService<IShellNavigationService>()?.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<IShellNavigationService>()?.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<IShellNavigationService>()?.NavigateToTool(value) == true)
{
ToastService.Show(AppLocalizer.T("已打开工具,请确认输入后重新运行", "Tool opened; review the input and run again"), ToastKind.Information);
}
break;
} }
} }
+54 -8
View File
@@ -1,4 +1,4 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
@@ -280,9 +280,9 @@ public sealed class HomePage : Page
var current = _versionService.GetCurrent().Version; var current = _versionService.GetCurrent().Version;
var relation = info.CompareToCurrent(current); var relation = info.CompareToCurrent(current);
_announcementTitle.Text = AnnouncementTitle(info, relation); _announcementTitle.Text = VersionAnnouncementTitle(info, relation);
_announcementText.Text = AnnouncementSummary(info, current, relation); _announcementText.Text = VersionAnnouncementSummary(info, current, relation);
_announcementMeta.Text = AnnouncementMeta(info, current, relation, includeTime: false); _announcementMeta.Text = VersionAnnouncementMeta(info, current, relation, includeTime: false);
} }
private async Task ShowAnnouncementDialogAsync() private async Task ShowAnnouncementDialogAsync()
@@ -293,13 +293,13 @@ public sealed class HomePage : Page
var relation = info is null ? 0 : info.CompareToCurrent(current); var relation = info is null ? 0 : info.CompareToCurrent(current);
var title = info is null var title = info is null
? AppLocalizer.T("完整公告", "Full announcement") ? AppLocalizer.T("完整公告", "Full announcement")
: AnnouncementTitle(info, relation); : VersionAnnouncementTitle(info, relation);
var content = info is null 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.") ? 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); : CombineAnnouncement(info, current, relation);
var meta = info is null var meta = info is null
? AppLocalizer.T("本地公告", "Local announcement") ? AppLocalizer.T("本地公告", "Local announcement")
: AnnouncementMeta(info, current, relation, includeTime: true); : VersionAnnouncementMeta(info, current, relation, includeTime: true);
var dialog = new ContentDialog var dialog = new ContentDialog
{ {
@@ -323,7 +323,7 @@ public sealed class HomePage : Page
Content = ModernUi.GutterScroll( Content = ModernUi.GutterScroll(
MarkdownRenderHelper.Render( MarkdownRenderHelper.Render(
notes, notes,
_announcementInfo is null ? null : AnnouncementMeta( _announcementInfo is null ? null : VersionAnnouncementMeta(
_announcementInfo, _announcementInfo,
_versionService.GetCurrent().Version, _versionService.GetCurrent().Version,
_announcementInfo.CompareToCurrent(_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(); 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) private static string CombineAnnouncement(RemoteUpdateInfo info, string currentVersion, int relation)
{ {
return string.Join(Environment.NewLine + Environment.NewLine, new[] 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.DisplayMessage,
info.DisplayReleaseNotes, 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}") 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
}; };
} }
} }
+3
View File
@@ -53,6 +53,9 @@
<Content Include="Assets\home-globe\**\*.*" CopyToOutputDirectory="PreserveNewest" /> <Content Include="Assets\home-globe\**\*.*" CopyToOutputDirectory="PreserveNewest" />
<Content Include="Assets\tool-pages\**\*.*" CopyToOutputDirectory="PreserveNewest" /> <Content Include="Assets\tool-pages\**\*.*" CopyToOutputDirectory="PreserveNewest" />
<Content Include="Assets\tool-results\**\*.*" CopyToOutputDirectory="PreserveNewest" /> <Content Include="Assets\tool-results\**\*.*" CopyToOutputDirectory="PreserveNewest" />
<EmbeddedResource Include="Assets\tool-results\vendor\**\*.*">
<LogicalName>YMhut.Box.WinUI.Assets.tool-results.vendor.%(Filename)%(Extension)</LogicalName>
</EmbeddedResource>
<Content Remove="Assets\home-globe\textures\solar\solar-texture-manifest.json" /> <Content Remove="Assets\home-globe\textures\solar\solar-texture-manifest.json" />
<EmbeddedResource Include="..\..\version.json"> <EmbeddedResource Include="..\..\version.json">
<LogicalName>YMhut.Box.WinUI.version.json</LogicalName> <LogicalName>YMhut.Box.WinUI.version.json</LogicalName>