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 46f3f8e62c..768e4ec777 100644 --- a/Plain Craft Launcher 2/Modules/Event/CustomEvent.cs +++ b/Plain Craft Launcher 2/Modules/Event/CustomEvent.cs @@ -3,6 +3,7 @@ using PCL.Core.App.Configuration; using PCL.Core.App.Localization; using PCL.Core.Utils.OS; +using PCL.Network; namespace PCL { @@ -94,7 +95,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 +319,111 @@ 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 (!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; + } + + 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) { - relativeUrl = relativeUrl.Replace('/', '\\').ToLower().TrimStart('\\'); + 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 = 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) + { + throw new Exception("联网帮助页面须指向一个帮助 JSON 文件,并在同路径下包含相应 XAML 文件!\n" + + "例如:\n" + + " - https://www.baidu.com/test.json(填写这个路径)\n" + + " - https://www.baidu.com/test.xaml(同时也需要包含这个文件)", e); + } + + // 下载文件 + 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 + { + Task.WhenAll( + ModNet.NetDownloadByClient(remoteUri.AbsoluteUri, jsonPath), + ModNet.NetDownloadByClient(xamlUri, xamlPath) + ).GetAwaiter().GetResult(); + } + 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 = jsonPath; + } + + // 确认路径 + 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]; } - 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 +432,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]; 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)); + } + } +}