From 6c2a72f4ecff59cf5f981522c3532a9f6151b88f Mon Sep 17 00:00:00 2001 From: Pigeon0v0 <60414767+Pigeon0v0@users.noreply.github.com> Date: Sun, 9 Aug 2026 13:36:32 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(custom-page):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E4=B8=BB=E9=A1=B5=E4=B8=8D=E8=83=BD?= =?UTF-8?q?=E6=AD=A3=E5=B8=B8=E8=B7=B3=E8=BD=AC=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=20(WIP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modules/Event/CustomEvent.cs | 68 +++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/Plain Craft Launcher 2/Modules/Event/CustomEvent.cs b/Plain Craft Launcher 2/Modules/Event/CustomEvent.cs index 46f3f8e62c..193eabf4d3 100644 --- a/Plain Craft Launcher 2/Modules/Event/CustomEvent.cs +++ b/Plain Craft Launcher 2/Modules/Event/CustomEvent.cs @@ -1,8 +1,10 @@ using System.IO; +using System.Runtime.InteropServices; using PCL.Core.App; using PCL.Core.App.Configuration; using PCL.Core.App.Localization; using PCL.Core.Utils.OS; +using PCL.Network; namespace PCL { @@ -94,7 +96,7 @@ public static string GetCustomVariable(string name, string defaultValue = "") => [EventType.WriteSetting] = _WriteSetting, [EventType.ModifyVariable] = _WriteVariable, [EventType.WriteVariable] = _WriteVariable, - [EventType.OpenHelp] = (_, __) => ModBase.OpenWebsite("https://docs.pclc.cc/ce"), + [EventType.OpenHelp] = _OpenHelp, }; /// @@ -318,17 +320,75 @@ private static void _WriteVariable(string arg, EventType type) HintService.Hint(Lang.Text("Event.Variable.Written", args[0], args[1]), HintType.Success); } + /// + /// 打开帮助或自定义主页页面 + /// + private static void _OpenHelp(string arg, EventType type) + { + var args = SplitArgs(arg); + if (!args[0].StartsWithF("http", true)) + { + ModBase.OpenWebsite("https://docs.pclc.cc/ce"); + return; + } + var actualPaths = GetAbsoluteUrls(args[0], type); + var location = actualPaths.FirstOrDefault(); + + + } + public static string[] GetAbsoluteUrls(string relativeUrl, EventType type) { + if (relativeUrl.StartsWithF("http", true)) + { + if (ModBase.RunInUi()) throw new Exception("能打开联网帮助页面的 MyListItem 必须手动设置 Title、Info 属性!"); + + // 获取文件名 + string rawFileName; + try + { + rawFileName = relativeUrl.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).BeforeFirst("#").BeforeFirst("?"); + if (!rawFileName.EndsWithF(".json", true)) throw new Exception("未指向 .json 后缀的文件"); + } + catch (Exception e) + { + throw new Exception("联网帮助页面须指向一个帮助 JSON 文件,并在同路径下包含相应 XAML 文件!\n" + + "例如:\n" + + " - https://www.baidu.com/test.json(填写这个路径)\n" + + " - https://www.baidu.com/test.xaml(同时也需要包含这个文件)", e); + } + + // 下载文件 + string temp = ModMain.RequestTaskTempFolder() + rawFileName; + ModBase.Log($"转换网络资源: {relativeUrl} -> {temp}"); + try + { + ModNet.NetDownloadByClient(relativeUrl, temp); + ModNet.NetDownloadByClient(relativeUrl.Replace(".json", ".xaml"), temp.Replace(".json", ".xaml")); + } + catch (Exception e) + { + throw new Exception("下载指定的文件失败!\n" + + "注意,联网帮助页面须指向一个帮助 JSON 文件,并在同路径下包含相应 XAML 文件!\n" + + "例如:\n" + + " - https://www.baidu.com/test.json(填写这个路径)\n" + + " - https://www.baidu.com/test.xaml(同时也需要包含这个文件)", e); + } + + relativeUrl = temp; + } + relativeUrl = relativeUrl.Replace('/', '\\').ToLower().TrimStart('\\'); + + // 确认路径 relativeUrl = relativeUrl.Replace('/', '\\').ToLower().TrimStart('\\'); var pclDir = Path.Combine(Basics.ExecutableDirectory, "PCL"); - if (relativeUrl.Contains(":\\")) + if (relativeUrl.Contains(":\\")) // 绝对路径 { ModBase.Log($"[Control] 自定义事件中由绝对路径 {type}: {relativeUrl}"); return [relativeUrl, pclDir]; } - if (File.Exists(Path.Combine(pclDir, relativeUrl))) + if (File.Exists(Path.Combine(pclDir, relativeUrl))) // 相对 PCL 文件夹的路径 { var fullPath = Path.Combine(pclDir, relativeUrl); var resolved = Path.GetFullPath(fullPath); @@ -337,7 +397,7 @@ public static string[] GetAbsoluteUrls(string relativeUrl, EventType type) ModBase.Log($"[Control] 自定义事件中由相对 PCL 文件夹的路径 {type}: {fullPath}"); return [fullPath, pclDir]; } - if (type is EventType.OpenFile or EventType.ExecuteCommand) + if (type is EventType.OpenFile or EventType.ExecuteCommand) // 直接使用原有路径启动程序 { ModBase.Log($"[Control] 自定义事件中直接 {type}: {relativeUrl}"); return [relativeUrl, pclDir]; From a0ce396763a12f6973f74efebe5c109397b1a5c7 Mon Sep 17 00:00:00 2001 From: Pigeon0v0 <60414767+Pigeon0v0@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:09:30 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat(custom-page):=20=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E4=B8=BB=E9=A1=B5=E8=B7=B3=E8=BD=AC?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Plain Craft Launcher 2/FormMain.xaml.cs | 24 +++++- .../Modules/Event/CustomEvent.cs | 73 +++++++++++----- .../Pages/PageHelpDetail.xaml | 24 ++++++ .../Pages/PageHelpDetail.xaml.cs | 86 +++++++++++++++++++ 4 files changed, 186 insertions(+), 21 deletions(-) create mode 100644 Plain Craft Launcher 2/Pages/PageHelpDetail.xaml create mode 100644 Plain Craft Launcher 2/Pages/PageHelpDetail.xaml.cs diff --git a/Plain Craft Launcher 2/FormMain.xaml.cs b/Plain Craft Launcher 2/FormMain.xaml.cs index 3826112f31..184445596f 100644 --- a/Plain Craft Launcher 2/FormMain.xaml.cs +++ b/Plain Craft Launcher 2/FormMain.xaml.cs @@ -7,7 +7,6 @@ using System.Windows.Input; using System.Windows.Interop; using System.Windows.Media; -using System.Windows.Media.Effects; using PCL.Core.App; using PCL.Core.App.IoC; using PCL.Core.App.Localization; @@ -1402,6 +1401,11 @@ public enum PageType /// CompDetail = 8, + /// + /// 帮助详情。这是一个副页面。 + /// + HelpDetail = 9, + /// /// 游戏实时日志。这是一个副页面。 /// @@ -1496,6 +1500,10 @@ private string PageNameGet(PageStackData stack) { return Lang.Text("Main.Title.ResourceDownload", stack.additional.Value.CompProject.TranslatedName); } + case PageType.HelpDetail: + { + return stack.helpPage?.Title ?? ""; + } case PageType.VersionSaves: { return Lang.Text("Main.Title.SaveManagement", ModBase.GetFolderNameFromPath(stack.additional.Value.SavePath)); @@ -1595,6 +1603,11 @@ public class PageStackData string SavePath )? additional; + /// + /// 帮助详情页实例。仅在 中使用。 + /// + public PageHelpDetail? helpPage; + public PageType page; public override bool Equals(object other) @@ -1606,6 +1619,8 @@ public override bool Equals(object other) var pageOther = (PageStackData)other; if (page != pageOther.page) return false; + if (helpPage is not null || pageOther.helpPage is not null) + return ReferenceEquals(helpPage, pageOther.helpPage); if (additional is null) return pageOther.additional is null; return pageOther.additional is not null && additional.Equals(pageOther.additional); @@ -1899,6 +1914,13 @@ private void PageChangeActual(PageStackData stack, PageSubType subType) PageChangeAnim(new MyPageLeft(), ModMain.frmDownloadCompDetail); break; } + case PageType.HelpDetail: // 帮助详情 + { + if (stack.helpPage is null) + throw new InvalidOperationException("帮助详情页面未初始化"); + PageChangeAnim(new MyPageLeft(), stack.helpPage); + break; + } case PageType.VersionSaves: // 存档管理 { if (ModMain.frmInstanceSavesLeft is null) diff --git a/Plain Craft Launcher 2/Modules/Event/CustomEvent.cs b/Plain Craft Launcher 2/Modules/Event/CustomEvent.cs index 193eabf4d3..03fa4b4e51 100644 --- a/Plain Craft Launcher 2/Modules/Event/CustomEvent.cs +++ b/Plain Craft Launcher 2/Modules/Event/CustomEvent.cs @@ -1,5 +1,4 @@ using System.IO; -using System.Runtime.InteropServices; using PCL.Core.App; using PCL.Core.App.Configuration; using PCL.Core.App.Localization; @@ -326,29 +325,58 @@ private static void _WriteVariable(string arg, EventType type) private static void _OpenHelp(string arg, EventType type) { var args = SplitArgs(arg); - if (!args[0].StartsWithF("http", true)) + if (!Uri.TryCreate(args[0], UriKind.Absolute, out var uri) || + (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)) { ModBase.OpenWebsite("https://docs.pclc.cc/ce"); return; } - var actualPaths = GetAbsoluteUrls(args[0], type); - var location = actualPaths.FirstOrDefault(); - - + + ModBase.RunInThread(() => + { + try + { + var actualPaths = GetAbsoluteUrls(args[0], type); + var location = actualPaths[0]; + var content = PageHelpDetail.LoadContent(location); + ModBase.RunInUiWait(() => + { + var page = new PageHelpDetail(content); + ModMain.frmMain.PageChange(new FormMain.PageStackData + { + page = FormMain.PageType.HelpDetail, + helpPage = page + }); + }); + } + catch (Exception ex) + { + ModBase.Log( + ex, + Lang.Text("Event.Error.ExecutionFailed", type, arg), + ModBase.LogLevel.Msgbox, + userSummary: Lang.Text("Event.Error.ExecutionFailed", type, arg)); + } + }); } public static string[] GetAbsoluteUrls(string relativeUrl, EventType type) { - if (relativeUrl.StartsWithF("http", true)) + if (type == EventType.OpenHelp && + Uri.TryCreate(relativeUrl, UriKind.Absolute, out var remoteUri) && + (remoteUri.Scheme == Uri.UriSchemeHttp || remoteUri.Scheme == Uri.UriSchemeHttps)) { if (ModBase.RunInUi()) throw new Exception("能打开联网帮助页面的 MyListItem 必须手动设置 Title、Info 属性!"); - + // 获取文件名 string rawFileName; try { - rawFileName = relativeUrl.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).BeforeFirst("#").BeforeFirst("?"); - if (!rawFileName.EndsWithF(".json", true)) throw new Exception("未指向 .json 后缀的文件"); + rawFileName = Uri.UnescapeDataString(Path.GetFileName(remoteUri.AbsolutePath)); + if (!Path.GetExtension(rawFileName).Equals(".json", StringComparison.OrdinalIgnoreCase)) + throw new Exception("未指向 .json 后缀的文件"); + if (rawFileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) + throw new Exception("文件名包含非法字符"); } catch (Exception e) { @@ -357,14 +385,20 @@ public static string[] GetAbsoluteUrls(string relativeUrl, EventType type) " - https://www.baidu.com/test.json(填写这个路径)\n" + " - https://www.baidu.com/test.xaml(同时也需要包含这个文件)", e); } - + // 下载文件 - string temp = ModMain.RequestTaskTempFolder() + rawFileName; - ModBase.Log($"转换网络资源: {relativeUrl} -> {temp}"); + var tempFolder = ModMain.RequestTaskTempFolder(); + var jsonPath = Path.Combine(tempFolder, rawFileName); + var xamlPath = Path.ChangeExtension(jsonPath, ".xaml"); + var xamlUri = new UriBuilder(remoteUri) + { + Path = Path.ChangeExtension(remoteUri.AbsolutePath, ".xaml").Replace('\\', '/') + }.Uri.AbsoluteUri; + ModBase.Log($"[Control] 转换网络帮助资源:{relativeUrl} -> {jsonPath}"); try { - ModNet.NetDownloadByClient(relativeUrl, temp); - ModNet.NetDownloadByClient(relativeUrl.Replace(".json", ".xaml"), temp.Replace(".json", ".xaml")); + ModNet.NetDownloadByClient(remoteUri.AbsoluteUri, jsonPath); + ModNet.NetDownloadByClient(xamlUri, xamlPath); } catch (Exception e) { @@ -375,15 +409,14 @@ public static string[] GetAbsoluteUrls(string relativeUrl, EventType type) " - https://www.baidu.com/test.xaml(同时也需要包含这个文件)", e); } - relativeUrl = temp; + relativeUrl = jsonPath; } - relativeUrl = relativeUrl.Replace('/', '\\').ToLower().TrimStart('\\'); - + // 确认路径 - relativeUrl = relativeUrl.Replace('/', '\\').ToLower().TrimStart('\\'); + relativeUrl = relativeUrl.Replace('/', Path.DirectorySeparatorChar); var pclDir = Path.Combine(Basics.ExecutableDirectory, "PCL"); - if (relativeUrl.Contains(":\\")) // 绝对路径 + if (Path.IsPathFullyQualified(relativeUrl)) // 绝对路径 { ModBase.Log($"[Control] 自定义事件中由绝对路径 {type}: {relativeUrl}"); return [relativeUrl, pclDir]; diff --git a/Plain Craft Launcher 2/Pages/PageHelpDetail.xaml b/Plain Craft Launcher 2/Pages/PageHelpDetail.xaml new file mode 100644 index 0000000000..a33254ad4a --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageHelpDetail.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + diff --git a/Plain Craft Launcher 2/Pages/PageHelpDetail.xaml.cs b/Plain Craft Launcher 2/Pages/PageHelpDetail.xaml.cs new file mode 100644 index 0000000000..d7f01f2c44 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageHelpDetail.xaml.cs @@ -0,0 +1,86 @@ +using System.IO; +using System.Security; +using System.Windows; +using PCL.Core.App.Localization; + +namespace PCL; + +public partial class PageHelpDetail : IRefreshable +{ + internal sealed record HelpContent(string SourcePath, string Title, string Xaml); + + private HelpContent _content = null!; + + public string Title => _content.Title; + + internal PageHelpDetail(HelpContent content) + { + InitializeComponent(); + Loaded += (_, _) => PanBack.ScrollToHome(); + ApplyContent(content); + } + + /// + /// 从帮助 JSON 及同名 XAML 文件读取详情页数据。失败会抛出异常。 + /// + internal static HelpContent LoadContent(string jsonPath) + { + if (!File.Exists(jsonPath)) + throw new FileNotFoundException("未找到帮助 JSON 文件", jsonPath); + + var json = ModMain.ArgumentReplace(File.ReadAllText(jsonPath), SecurityElement.Escape); + using var document = JsonDocument.Parse(json); + if (!document.RootElement.TryGetProperty("Title", out var titleElement) || + titleElement.ValueKind != JsonValueKind.String || + string.IsNullOrWhiteSpace(titleElement.GetString())) + throw new ArgumentException("帮助 JSON 中未找到有效的 Title 项", nameof(jsonPath)); + + var xamlPath = Path.ChangeExtension(jsonPath, ".xaml"); + if (!File.Exists(xamlPath)) + throw new FileNotFoundException("未找到帮助 JSON 对应的 XAML 文件", xamlPath); + + var xaml = File.ReadAllText(xamlPath); + if (string.IsNullOrWhiteSpace(xaml)) + throw new InvalidDataException("帮助 XAML 文件为空"); + + return new HelpContent(jsonPath, titleElement.GetString()!, xaml); + } + + private void ApplyContent(HelpContent content) + { + // 修改时应同时修改 PageLaunchRight.LoadContent。 + var xaml = ModMain.ArgumentReplace(content.Xaml); + while (xaml.Contains("xmlns")) + xaml = xaml.RegexReplace("xmlns[^\"']*(\"|')[^\"']*(\"|')", "").Replace("xmlns", ""); + xaml = + $"{xaml}"; + + var element = (UIElement)ModBase.GetObjectFromXML(xaml, out var sanitizeResult); + foreach (var unsupported in sanitizeResult.UnsupportedTypesFound) + HintService.Hint(Lang.Text("Event.Sanitize.UnsupportedTypeHint", unsupported), HintType.Error); + foreach (var unknown in sanitizeResult.UnrecognizedTypes) + HintService.Hint(Lang.Text("Event.Sanitize.UnknownTypeHint", unknown), HintType.Error); + + _content = content; + PanCustom.Children.Clear(); + PanCustom.Children.Add(element); + if (ModMain.frmMain?.pageCurrent.page == FormMain.PageType.HelpDetail) + ModMain.frmMain.PageNameRefresh(); + } + + public void Refresh() + { + try + { + ApplyContent(LoadContent(_content.SourcePath)); + } + catch (Exception ex) + { + ModBase.Log( + ex, + "刷新帮助详情页失败", + ModBase.LogLevel.Msgbox, + userSummary: Lang.Text("Event.Error.ExecutionFailed", EventType.OpenHelp, _content.SourcePath)); + } + } +} From d8f47f5c4445cfb3a8f0005215395a7797d328a0 Mon Sep 17 00:00:00 2001 From: Pigeon0v0 <60414767+Pigeon0v0@users.noreply.github.com> Date: Tue, 11 Aug 2026 22:22:34 +0800 Subject: [PATCH 3/3] fix --- Plain Craft Launcher 2/Modules/Event/CustomEvent.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Plain Craft Launcher 2/Modules/Event/CustomEvent.cs b/Plain Craft Launcher 2/Modules/Event/CustomEvent.cs index 03fa4b4e51..768e4ec777 100644 --- a/Plain Craft Launcher 2/Modules/Event/CustomEvent.cs +++ b/Plain Craft Launcher 2/Modules/Event/CustomEvent.cs @@ -397,8 +397,10 @@ public static string[] GetAbsoluteUrls(string relativeUrl, EventType type) ModBase.Log($"[Control] 转换网络帮助资源:{relativeUrl} -> {jsonPath}"); try { - ModNet.NetDownloadByClient(remoteUri.AbsoluteUri, jsonPath); - ModNet.NetDownloadByClient(xamlUri, xamlPath); + Task.WhenAll( + ModNet.NetDownloadByClient(remoteUri.AbsoluteUri, jsonPath), + ModNet.NetDownloadByClient(xamlUri, xamlPath) + ).GetAwaiter().GetResult(); } catch (Exception e) {