diff --git a/Plain Craft Launcher 2/Modules/ModEvent.cs b/Plain Craft Launcher 2/Modules/ModEvent.cs index e60916e23..8d25bc567 100644 --- a/Plain Craft Launcher 2/Modules/ModEvent.cs +++ b/Plain Craft Launcher 2/Modules/ModEvent.cs @@ -428,7 +428,10 @@ public static string[] GetAbsoluteUrls(string relativeUrl, EventType type) private static bool EventSafetyConfirm(string message) { - if (ModBase.Setup.Get("HintCustomCommand") == "True") + var skipConfirm = ModBase.Setup.Get("HintCustomCommand"); + if (skipConfirm is bool skipConfirmBool && skipConfirmBool) + return true; + if (skipConfirm is string skipConfirmString && string.Equals(skipConfirmString, "True", StringComparison.OrdinalIgnoreCase)) return true; switch (ModMain.MyMsgBox( @@ -441,11 +444,11 @@ private static bool EventSafetyConfirm(string message) case 1: return true; case 2: - ModBase.Setup.Set("HintCustomCommand", "True"); + ModBase.Setup.Set("HintCustomCommand", true); return true; default: return false; } } } -} \ No newline at end of file +} diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.cs b/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.cs index 83c43c738..3d33c3614 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.cs +++ b/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.cs @@ -1,5 +1,12 @@ using System.IO; +using System.Globalization; +using System.Reflection; +using System.Threading; using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Threading; +using Newtonsoft.Json.Linq; using PCL.Core.App; using PCL.Core.Logging; using PCL.Core.UI; @@ -16,6 +23,7 @@ public PageLaunchRight() { ReloadTimeout = 10 * 60 * 1000 }; Loaded += (_, _) => Init(); Loaded += (_, _) => Refresh(); + Unloaded += (_, _) => _DisposeHomepageLiveWatcher(); } private void Init() @@ -30,6 +38,7 @@ private void Init() LabHint1.Text = $"你正在使用 PCL 社区版!此版本为独立开发和维护,与官方版本维护路线不同,体验有所出入。{"\r\n"}{"\r\n"}如果你是意外下载到了社区版,我们十分建议您下载 PCL 官方版长期使用,此发行版本对新手用户体验可能不友好。{"\r\n"}此外,社区版的问题请向社区版的仓库提交 Issue,不要向官方仓库反馈社区版的问题哦!{"\r\n"}"; LabHint2.Text = "若要永久隐藏此提示,请输入正确的 PCL CE 开发组织名称。"; + _EnsureHomepageLiveWatcher(); } // 暂时关闭快照版提示 @@ -400,7 +409,10 @@ private void LoadContent(string Content) // 如果加载目标内容一致则不加载 var Hash = Content.GetHashCode(); if (Hash == LoadedContentHash) + { + _ApplyHomepageLivePatchesFromFile(); return; + } LoadedContentHash = Hash; // 实际加载内容 PanCustom.Children.Clear(); @@ -421,6 +433,7 @@ private void LoadContent(string Content) $"{Content}"; ModBase.Log($"[Page] 实例化:加载主页 UI 开始,最终内容长度:{Content.Count()}"); PanCustom.Children.Add((UIElement)ModBase.GetObjectFromXML(Content)); + _ApplyHomepageLivePatchesFromFile(); } catch (Exception ex) { @@ -455,6 +468,332 @@ ex is UnauthorizedAccessException private int LoadedContentHash = -1; private readonly object LoadContentLock = new(); + private const string HomepageLivePatchFileName = "CustomLive.json"; + private const string HomepageLiveSupportFileName = "CustomLive.supported.json"; + private FileSystemWatcher? _homepageLiveWatcher; + private DispatcherTimer? _homepageLivePatchTimer; + + private void _EnsureHomepageLiveWatcher() + { + if (_homepageLiveWatcher != null) return; + if ((int)Config.Preference.Homepage.Type != 1) return; + + try + { + var directory = _GetHomepageLiveDirectory(); + Directory.CreateDirectory(directory); + _WriteHomepageLiveSupportMarker(directory); + + _homepageLiveWatcher = new FileSystemWatcher(directory, HomepageLivePatchFileName) + { + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.FileName + }; + _homepageLiveWatcher.Changed += (_, _) => _QueueHomepageLivePatchApply(); + _homepageLiveWatcher.Created += (_, _) => _QueueHomepageLivePatchApply(); + _homepageLiveWatcher.Renamed += (_, _) => _QueueHomepageLivePatchApply(); + _homepageLiveWatcher.EnableRaisingEvents = true; + _QueueHomepageLivePatchApply(); + } + catch (Exception ex) + { + ModBase.Log(ex, "[Page] Failed to start custom homepage live patch watcher", ModBase.LogLevel.Developer); + } + } + + private void _DisposeHomepageLiveWatcher() + { + try + { + _homepageLiveWatcher?.Dispose(); + } + catch (Exception ex) + { + ModBase.Log(ex, "[Page] Failed to dispose custom homepage live patch watcher", ModBase.LogLevel.Developer); + } + + _homepageLiveWatcher = null; + + try + { + if (_homepageLivePatchTimer != null) + { + _homepageLivePatchTimer.Stop(); + _homepageLivePatchTimer.Tick -= _HomepageLivePatchTimerTick; + _homepageLivePatchTimer = null; + } + } + catch (Exception ex) + { + ModBase.Log(ex, "[Page] Failed to dispose custom homepage live patch debounce timer", ModBase.LogLevel.Developer); + } + + _DeleteHomepageLiveSupportMarker(); + } + + private void _QueueHomepageLivePatchApply() + { + ModBase.RunInUi(() => + { + _homepageLivePatchTimer ??= new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(120) + }; + _homepageLivePatchTimer.Tick -= _HomepageLivePatchTimerTick; + _homepageLivePatchTimer.Tick += _HomepageLivePatchTimerTick; + _homepageLivePatchTimer.Stop(); + _homepageLivePatchTimer.Start(); + }); + } + + private void _HomepageLivePatchTimerTick(object? sender, EventArgs e) + { + _homepageLivePatchTimer?.Stop(); + _ApplyHomepageLivePatchesFromFile(); + } + + private void _ApplyHomepageLivePatchesFromFile() + { + if (PanCustom.Children.Count == 0) return; + if ((int)Config.Preference.Homepage.Type != 1) return; + + var file = Path.Combine(_GetHomepageLiveDirectory(), HomepageLivePatchFileName); + if (!File.Exists(file)) return; + + try + { + var token = JToken.Parse(_ReadHomepageLivePatchFile(file)); + foreach (var patch in _EnumerateHomepageLivePatches(token)) + _ApplyHomepageLivePatch(patch); + } + catch (Exception ex) + { + ModBase.Log(ex, "[Page] Failed to apply custom homepage live patches", ModBase.LogLevel.Developer); + } + } + + private static string _ReadHomepageLivePatchFile(string file) + { + Exception? lastException = null; + for (var i = 0; i < 3; i++) + { + try + { + using var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + catch (Exception ex) + { + lastException = ex; + Thread.Sleep(50); + } + } + + throw lastException ?? new IOException("Unable to read custom homepage live patch file."); + } + + private static string _GetHomepageLiveDirectory() + { + return Path.Combine(ModBase.ExePath, "PCL"); + } + + private static void _WriteHomepageLiveSupportMarker(string directory) + { + try + { + var marker = new JObject + { + ["processId"] = Environment.ProcessId, + ["processPath"] = Environment.ProcessPath ?? "", + ["patchFile"] = HomepageLivePatchFileName, + ["startedAt"] = DateTime.Now.ToString("O", CultureInfo.InvariantCulture) + }; + File.WriteAllText(Path.Combine(directory, HomepageLiveSupportFileName), marker.ToString(Newtonsoft.Json.Formatting.None)); + } + catch (Exception ex) + { + ModBase.Log(ex, "[Page] Failed to write custom homepage live patch support marker", ModBase.LogLevel.Developer); + } + } + + private static void _DeleteHomepageLiveSupportMarker() + { + try + { + var file = Path.Combine(_GetHomepageLiveDirectory(), HomepageLiveSupportFileName); + if (!File.Exists(file)) return; + + var marker = JObject.Parse(_ReadHomepageLivePatchFile(file)); + if (marker["processId"]?.Value() == Environment.ProcessId) + File.Delete(file); + } + catch (Exception ex) + { + ModBase.Log(ex, "[Page] Failed to delete custom homepage live patch support marker", ModBase.LogLevel.Developer); + } + } + + private static IEnumerable _EnumerateHomepageLivePatches(JToken token) + { + if (token is JObject obj) + { + if (obj["patches"] is JArray patches) + { + foreach (var patch in patches.OfType()) + yield return patch; + yield break; + } + + if (_TryGetString(obj, "target", "tag", "name") != null) + { + yield return obj; + yield break; + } + + foreach (var property in obj.Properties()) + { + if (property.Value is not JObject patch) continue; + patch = (JObject)patch.DeepClone(); + patch["target"] ??= property.Name; + yield return patch; + } + } + else if (token is JArray array) + { + foreach (var patch in array.OfType()) + yield return patch; + } + } + + private void _ApplyHomepageLivePatch(JObject patch) + { + var target = _TryGetString(patch, "target", "tag", "name"); + if (string.IsNullOrWhiteSpace(target)) return; + + foreach (var element in _FindElementsByTag(PanCustom, target)) + _ApplyHomepageLivePatchToElement(element, patch); + } + + private void _ApplyHomepageLivePatchToElement(FrameworkElement element, JObject patch) + { + _SetPropertyIfPresent(element, patch, "text", "Text"); + _SetPropertyIfPresent(element, patch, "title", "Title"); + _SetPropertyIfPresent(element, patch, "info", "Info"); + _SetPropertyIfPresent(element, patch, "tooltip", "ToolTip"); + _SetPropertyIfPresent(element, patch, "toolTip", "ToolTip"); + _SetPropertyIfPresent(element, patch, "visibility", "Visibility"); + _SetPropertyIfPresent(element, patch, "isEnabled", "IsEnabled"); + _SetPropertyIfPresent(element, patch, "opacity", "Opacity"); + + if (patch["properties"] is JObject properties) + { + foreach (var property in properties.Properties()) + _TrySetElementProperty(element, property.Name, property.Value?.ToString() ?? ""); + } + + var childrenXaml = _TryGetString(patch, "childrenXaml", "ChildrenXaml"); + if (!string.IsNullOrEmpty(childrenXaml) && element is Panel panel) + _ReplacePanelChildren(panel, childrenXaml); + } + + private static void _SetPropertyIfPresent(FrameworkElement element, JObject patch, string jsonName, string propertyName) + { + if (patch.TryGetValue(jsonName, StringComparison.OrdinalIgnoreCase, out var value)) + _TrySetElementProperty(element, propertyName, value?.ToString() ?? ""); + } + + private static bool _TrySetElementProperty(FrameworkElement element, string propertyName, string value) + { + var property = element.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public); + if (property == null || !property.CanWrite) return false; + + try + { + var propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; + var trimmedValue = value.Trim(); + object convertedValue; + if (propertyType == typeof(string)) + convertedValue = value; + else if (propertyType == typeof(object)) + convertedValue = value; + else if (propertyType == typeof(bool) && bool.TryParse(trimmedValue, out var boolValue)) + convertedValue = boolValue; + else if (propertyType == typeof(int) && int.TryParse(trimmedValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) + convertedValue = intValue; + else if (propertyType == typeof(double) && double.TryParse(trimmedValue, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var doubleValue)) + convertedValue = doubleValue; + else if (propertyType == typeof(Visibility)) + { + if (!Enum.TryParse(trimmedValue, true, out Visibility visibilityValue)) + return false; + convertedValue = visibilityValue; + } + else if (propertyType.IsEnum && Enum.TryParse(propertyType, trimmedValue, true, out var enumValue)) + convertedValue = enumValue; + else + return false; + + property.SetValue(element, convertedValue); + return true; + } + catch (Exception ex) + { + ModBase.Log(ex, $"[Page] Failed to set live patch property {propertyName}", ModBase.LogLevel.Developer); + return false; + } + } + + private static void _ReplacePanelChildren(Panel panel, string childrenXaml) + { + var content = ModMain.ArgumentReplace(childrenXaml); + while (content.Contains("xmlns")) + content = content.RegexReplace("xmlns[^\"']*(\"|')[^\"']*(\"|')", "").Replace("xmlns", ""); + + var wrapped = + $"{content}"; + + if (ModBase.GetObjectFromXML(wrapped) is not Panel parsedPanel) return; + + var children = parsedPanel.Children.OfType().ToList(); + parsedPanel.Children.Clear(); + panel.Children.Clear(); + foreach (var child in children) + panel.Children.Add(child); + } + + private static IEnumerable _FindElementsByTag(DependencyObject root, string tag) + { + if (root is FrameworkElement element && + string.Equals(element.Tag?.ToString(), tag, StringComparison.OrdinalIgnoreCase)) + yield return element; + + int count; + try + { + count = VisualTreeHelper.GetChildrenCount(root); + } + catch + { + yield break; + } + + for (var i = 0; i < count; i++) + { + foreach (var child in _FindElementsByTag(VisualTreeHelper.GetChild(root, i), tag)) + yield return child; + } + } + + private static string? _TryGetString(JObject obj, params string[] names) + { + foreach (var name in names) + { + if (obj.TryGetValue(name, StringComparison.OrdinalIgnoreCase, out var value)) + return value?.ToString(); + } + + return null; + } #endregion }