壳子替换
This commit is contained in:
@@ -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<string, string> RemoteMetadata(ApiEndpoint endpoint, ToolResultKind kind, string language)
|
||||
{
|
||||
var metadata = Metadata(endpoint.Id, kind, language, "remote-dashboard");
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 || '';
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -110,12 +110,15 @@ public sealed class LocalToolVendorAssetService(
|
||||
Path.Combine(cacheRoot, "manifest.json"),
|
||||
manifestHash,
|
||||
extracted);
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,9 @@
|
||||
<Content Include="Assets\home-globe\**\*.*" CopyToOutputDirectory="PreserveNewest" />
|
||||
<Content Include="Assets\tool-pages\**\*.*" 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" />
|
||||
<EmbeddedResource Include="..\..\version.json">
|
||||
<LogicalName>YMhut.Box.WinUI.version.json</LogicalName>
|
||||
|
||||
Reference in New Issue
Block a user