diff --git a/PCL.Core/App/Configuration/Storage/FileConfigStorage.cs b/PCL.Core/App/Configuration/Storage/FileConfigStorage.cs index 38877e4ad..cee09ef32 100644 --- a/PCL.Core/App/Configuration/Storage/FileConfigStorage.cs +++ b/PCL.Core/App/Configuration/Storage/FileConfigStorage.cs @@ -1,3 +1,6 @@ +using PCL.Core.Logging; +using PCL.Core.UI; +using PCL.Core.UI.MsgBox; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -5,8 +8,6 @@ using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; -using PCL.Core.Logging; -using PCL.Core.UI; namespace PCL.Core.App.Configuration.Storage; @@ -70,8 +71,13 @@ void Sync() catch (Exception ex) { LogWrapper.Error(ex, "Config", "配置文件保存失败"); - var hint = $"保存配置文件时出现问题,若该问题能够稳定复现,请尽快提交反馈。" + - $"\n\n错误信息:\n{ex.GetType().FullName}: {ex.Message}"; + var hint = $""" + 保存配置文件时出现问题,若该问题能够稳定复现,请尽快提交反馈。 + + + 错误信息: + {ex.GetType().FullName}: {ex.Message} + """; MsgBoxWrapper.Show(hint, "配置文件保存失败", MsgBoxTheme.Error); } } diff --git a/PCL.Core/App/Essentials/MsgBoxService.cs b/PCL.Core/App/Essentials/MsgBoxService.cs new file mode 100644 index 000000000..9ea288ea2 --- /dev/null +++ b/PCL.Core/App/Essentials/MsgBoxService.cs @@ -0,0 +1,129 @@ +using PCL.Core.App.IoC; +using PCL.Core.UI.MsgBox; +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using System.Windows.Threading; + +namespace PCL.Core.App.Essentials; + +[LifecycleService(LifecycleState.Loading)] +[LifecycleScope("msgbox", "消息弹窗", true)] +public partial class MsgBoxService +{ + private static readonly Channel _Channel = Channel.CreateUnbounded( + new UnboundedChannelOptions + { + SingleReader = true, + SingleWriter = false + }); + + private static readonly ConcurrentDictionary> _Pending = []; + public static ChannelReader Reader => _Channel.Reader; + + [LifecycleStop] + private static async Task _StopAsync() + { + _Channel.Writer.Complete(); + foreach (var (id, tcs) in _Pending) + { + tcs.TrySetCanceled(); + } + _Pending.Clear(); + } + + public static async Task ShowAsync(MsgBoxRequest request, CancellationToken ct = default) + { + using var timeoutCts = request.Timeout is not null + ? new CancellationTokenSource((TimeSpan)request.Timeout) + : null; + using var linkedCts = timeoutCts is not null + ? CancellationTokenSource.CreateLinkedTokenSource(ct, request.CancellationToken, timeoutCts.Token) + : CancellationTokenSource.CreateLinkedTokenSource(ct, request.CancellationToken); + + var effectiveCt = linkedCts.Token; + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + if (!_Pending.TryAdd(request.RequestId, tcs)) + { + throw new InvalidOperationException($"Duplicate request ID: {request.RequestId}"); + } + + await using var _ = effectiveCt.Register(() => + { + if (_Pending.TryRemove(request.RequestId, out var value)) + { + value.TrySetCanceled(effectiveCt); + } + }); + + try + { + await _Channel.Writer.WriteAsync(request, effectiveCt).ConfigureAwait(false); + return await tcs.Task.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + if (_Pending.TryRemove(request.RequestId, out var _)) + { + // already handled by Register above + } + + throw; + } + } + + public static MsgBoxResponse Show(MsgBoxRequest request) + { + if (_IsOnUiThread()) + { + return _ShowOnUiThread(request); + } + + return Task.Run(() => ShowAsync(request)).GetAwaiter().GetResult(); + } + + /// + /// Used by UI layer to complete the request + /// + public static void Complete(Guid requestId, MsgBoxResponse response) + { + if (!_IsOnUiThread()) + { + throw new InvalidOperationException("Complete must be called on the UI thread."); + } + + if (_Pending.TryRemove(requestId, out var tcs)) + { + tcs.TrySetResult(response); + } + } + + private static bool _IsOnUiThread() => System.Windows.Application.Current?.Dispatcher?.CheckAccess() == true; + + private static MsgBoxResponse _ShowOnUiThread(MsgBoxRequest request) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + if (!_Pending.TryAdd(request.RequestId, tcs)) + { + throw new InvalidOperationException($"Duplicate request ID: {request.RequestId}"); + } + + _Channel.Writer.TryWrite(request); + + var frame = new DispatcherFrame(); + tcs.Task.ContinueWith(_ => frame.Continue = false, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + + Dispatcher.PushFrame(frame); + + // already completed, will not be blocked + return tcs.Task.GetAwaiter().GetResult(); + } +} \ No newline at end of file diff --git a/PCL.Core/Logging/LogService.cs b/PCL.Core/Logging/LogService.cs index 430651ea2..9826416ef 100644 --- a/PCL.Core/Logging/LogService.cs +++ b/PCL.Core/Logging/LogService.cs @@ -1,12 +1,13 @@ +using PCL.Core.App; +using PCL.Core.App.Essentials; +using PCL.Core.App.IoC; +using PCL.Core.UI; +using PCL.Core.UI.MsgBox; using System; using System.IO; using System.Threading; using System.Threading.Tasks; using System.Windows; -using PCL.Core.App; -using PCL.Core.App.Essentials; -using PCL.Core.App.IoC; -using PCL.Core.UI; namespace PCL.Core.Logging; @@ -46,10 +47,11 @@ public async Task StopAsync() private static void _LogAction(LogLevel level, ActionLevel actionLevel, string formatted, string plain, Exception? ex) { - if (ex is not null) { + if (ex is not null) + { TelemetryService.ReportException(ex, plain, level); } - + // log #if !TRACE if (actionLevel != ActionLevel.TraceLog) diff --git a/PCL.Core/UI/MsgBox/IMsgBoxControl.cs b/PCL.Core/UI/MsgBox/IMsgBoxControl.cs new file mode 100644 index 000000000..13aee0c5f --- /dev/null +++ b/PCL.Core/UI/MsgBox/IMsgBoxControl.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; + +namespace PCL.Core.UI.MsgBox; + +/// +/// MsgBox UI Control interface +/// Used by MsgBoxActor
+/// MVVM Control will implement this interface in the future +///
+public interface IMsgBoxControl +{ + MsgBoxRequest Request { get; } + event EventHandler? Completed; + void InvokeShowAnimation(); + Task InvokeCloseAnimationAsync(MsgBoxResponse response); +} \ No newline at end of file diff --git a/PCL.Core/UI/MsgBox/MsgBoxButtonInfo.cs b/PCL.Core/UI/MsgBox/MsgBoxButtonInfo.cs new file mode 100644 index 000000000..a705e064b --- /dev/null +++ b/PCL.Core/UI/MsgBox/MsgBoxButtonInfo.cs @@ -0,0 +1,9 @@ +using System; + +namespace PCL.Core.UI.MsgBox; + +public record MsgBoxButtonInfo( + string Text, + int Value = 0, + Action? OnClick = null + ); \ No newline at end of file diff --git a/PCL.Core/UI/MsgBox/MsgBoxRequest.cs b/PCL.Core/UI/MsgBox/MsgBoxRequest.cs new file mode 100644 index 000000000..3b8d00441 --- /dev/null +++ b/PCL.Core/UI/MsgBox/MsgBoxRequest.cs @@ -0,0 +1,38 @@ +using FluentValidation; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading; + +namespace PCL.Core.UI.MsgBox; + +public record MsgBoxRequest +{ + public Guid RequestId { get; init; } = Guid.NewGuid(); + public string Title { get; init; } = string.Empty; + public string Message { get; init; } = string.Empty; + public string? Hint { get; init; } = null; + public string Caption { get; init; } = string.Empty; + public object? Content { get; init; } = null; + public MsgBoxRequestType RequestType { get; init; } + public MsgBoxTheme Theme { get; init; } + public Collection>? ValidateRules { get; init; } = null; + public IReadOnlyList Buttons { get; init; } = []; + public bool IsBlocking { get; init; } = true; + public CancellationToken CancellationToken { get; init; } + + /// + /// Gets or sets the timeout duration for the message box. If set, the message box will automatically close after the specified time has elapsed.
+ /// means infinite. + ///
+ public TimeSpan? Timeout { get; init; } = null; +} + +public enum MsgBoxRequestType +{ + Text, + Select, + Input, + Login, + Markdown +} \ No newline at end of file diff --git a/PCL.Core/UI/MsgBox/MsgBoxResponse.cs b/PCL.Core/UI/MsgBox/MsgBoxResponse.cs new file mode 100644 index 000000000..20ea64f9e --- /dev/null +++ b/PCL.Core/UI/MsgBox/MsgBoxResponse.cs @@ -0,0 +1,17 @@ +using System; + +namespace PCL.Core.UI.MsgBox; + +public record MsgBoxResponse +{ + public Guid RequestId { get; init; } + public int? ButtonValue { get; init; } + public MsgBoxButtonInfo? Button { get; init; } + + public static MsgBoxResponse Cancelled(Guid requestId) => new() + { + RequestId = requestId, + ButtonValue = null, + Button = null + }; +} \ No newline at end of file diff --git a/PCL.Core/UI/MsgBox/MsgBoxTheme.cs b/PCL.Core/UI/MsgBox/MsgBoxTheme.cs new file mode 100644 index 000000000..e8b591187 --- /dev/null +++ b/PCL.Core/UI/MsgBox/MsgBoxTheme.cs @@ -0,0 +1,8 @@ +namespace PCL.Core.UI.MsgBox; + +public enum MsgBoxTheme +{ + Info, + Warning, + Error +} \ No newline at end of file diff --git a/PCL.Core/UI/MsgBoxWrapper.cs b/PCL.Core/UI/MsgBoxWrapper.cs index 2f1d9306a..60aa1f754 100644 --- a/PCL.Core/UI/MsgBoxWrapper.cs +++ b/PCL.Core/UI/MsgBoxWrapper.cs @@ -1,22 +1,11 @@ -using System; +using PCL.Core.App.Localization; +using PCL.Core.UI.MsgBox; using System.Collections.Generic; using System.Linq; -using PCL.Core.App.Localization; namespace PCL.Core.UI; -public record MsgBoxButtonInfo( - string Context, - int Value = 0, - Action? OnClick = null -); -public enum MsgBoxTheme -{ - Info, - Warning, - Error -} public delegate void MsgBoxHandler( string message, diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MsgBoxAnimationProfile.cs b/Plain Craft Launcher 2/Controls/MyMsg/MsgBoxAnimationProfile.cs new file mode 100644 index 000000000..0b43aab4a --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyMsg/MsgBoxAnimationProfile.cs @@ -0,0 +1,50 @@ +using PCL.Core.UI.MsgBox; + +namespace PCL.Controls.MyMsg; + +/// +/// 弹窗动画参数配置。由工厂方法 生成不同配置。 +/// 控件只管按参数执行,不需要判断 isWarn / 按钮数 等分支。 +/// +public class MsgBoxAnimationProfile +{ + // ── Show 动画 ── + public double ShowFadeMs { get; init; } = 120; + public double ShowSlideMs { get; init; } = 300; + public double ShowRotateMs { get; init; } = 300; + public double ShowDelayMs { get; init; } = 60; + public ModAnimation.AniEase ShowSlideEase { get; init; } = + new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak); + + public ModAnimation.AniEase ShowRotateEase { get; init; } = + new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak); + + // ── Close 动画 ── + public double CloseFadeMs { get; init; } = 80; + public double CloseFadeDelayMs { get; init; } = 20; + public double CloseSlideMs { get; init; } = 150; + public double CloseSlideDistance { get; init; } = 20; + public double CloseAngle { get; init; } = 6; + public double CloseDelayMs { get; init; } = 30; + public ModAnimation.AniEase CloseSlideEase { get; init; } = new ModAnimation.AniEaseOutFluent(); + + public ModAnimation.AniEase CloseRotateEase { get; init; } = + new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak); + + // ── 按钮样式 ── + public bool HighlightPrimaryButton { get; private init; } = true; + + // ── 工厂 ── + public static MsgBoxAnimationProfile ForTheme(MsgBoxTheme theme) => theme switch + { + MsgBoxTheme.Warning or MsgBoxTheme.Error => WarningProfile, + _ => DefaultProfile + }; + + private static readonly MsgBoxAnimationProfile DefaultProfile = new(); + + private static readonly MsgBoxAnimationProfile WarningProfile = new() + { + HighlightPrimaryButton = false + }; +} diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MsgBoxAnimations.cs b/Plain Craft Launcher 2/Controls/MyMsg/MsgBoxAnimations.cs new file mode 100644 index 000000000..f076368b8 --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyMsg/MsgBoxAnimations.cs @@ -0,0 +1,53 @@ +using PCL.Core.UI; +using System.Windows; +using System.Windows.Media; + +namespace PCL.Controls.MyMsg; + +/// +/// MsgBox 控件的 Show / Close 动画静态实现。 +/// 所有 MyMsg* 控件通过此类执行统一动画,避免在每个控件中重复 30+ 行动画代码。 +/// +public static class MsgBoxAnimations +{ + public static void AnimateShow( + UIElement element, + TranslateTransform pos, + RotateTransform rot, + MsgBoxAnimationProfile profile, + string animationGroup) + { + element.Opacity = 0d; + ModAnimation.AniStart((ModAnimation.AniData[]) + [ + ModAnimation.AaOpacity(element, 1d, (int)profile.ShowFadeMs, (int)profile.ShowDelayMs), + ModAnimation.AaDouble(i => pos.Y += (double)i, + -pos.Y, (int)profile.ShowSlideMs, (int)profile.ShowDelayMs, profile.ShowSlideEase), + ModAnimation.AaDouble(i => rot.Angle += (double)i, + -rot.Angle, (int)profile.ShowRotateMs, (int)profile.ShowDelayMs, profile.ShowRotateEase) + ], animationGroup); + } + + public static Task AnimateCloseAsync( + UIElement element, + TranslateTransform pos, + RotateTransform rot, + MsgBoxAnimationProfile profile, + string animationGroup) + { + var tcs = new TaskCompletionSource(); + ModAnimation.AniStart((ModAnimation.AniData[]) + [ + ModAnimation.AaOpacity(element, -element.Opacity, + (int)profile.CloseFadeMs, (int)profile.CloseFadeDelayMs), + ModAnimation.AaDouble(i => pos.Y += (double)i, + profile.CloseSlideDistance - pos.Y, + (int)profile.CloseSlideMs, 0, profile.CloseSlideEase), + ModAnimation.AaDouble(i => rot.Angle += (double)i, + profile.CloseAngle - rot.Angle, + (int)profile.CloseSlideMs, 0, profile.CloseRotateEase), + ModAnimation.AaCode(tcs.SetResult, after: true) + ], animationGroup); + return tcs.Task; + } +} diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml.cs b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml.cs index b8dc7c782..881b88c3f 100644 --- a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml.cs +++ b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml.cs @@ -1,46 +1,104 @@ +using PCL.Controls.MyMsg; +using PCL.Core.UI.MsgBox; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Interop; -using PCL.Core.UI.Controls; namespace PCL; -public partial class MyMsgInput +public partial class MyMsgInput : IMsgBoxControl { - private readonly ModMain.MyMsgBoxConverter myConverter; - private readonly int uuid = ModBase.GetUuid(); + public MsgBoxRequest Request { get; } + public event EventHandler? Completed; + + private readonly MsgBoxAnimationProfile _anim; + private bool _isExited; + private readonly string _animGroup; + + public MyMsgInput(MsgBoxRequest request) + { + Request = request; + _anim = MsgBoxAnimationProfile.ForTheme(request.Theme); + _animGroup = "MyMsgInput " + Request.RequestId; + InitFromRequest(request.Content as string ?? "", request.Hint ?? "", request.ValidateRules); + } public MyMsgInput(ModMain.MyMsgBoxConverter converter) { - try + var isWarn = converter.IsWarn; + var buttons = new List { - InitializeComponent(); - AppendUniqueNameSuffix(Btn1); - AppendUniqueNameSuffix(Btn2); - myConverter = converter; - LabTitle.Text = converter.Title; - LabText.Text = converter.Text; - PanText.Visibility = string.IsNullOrEmpty(converter.Text) ? Visibility.Collapsed : Visibility.Visible; - TextArea.Text = (string)converter.Content; - TextArea.HintText = converter.HintText; - TextArea.ValidateRules = converter.ValidateRules; - ConfigurePrimaryButton(converter.Button1, converter.IsWarn); - ConfigureSecondaryButton(converter.Button2); - ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d); - } + new(converter.Button1, 1), + new(converter.Button2, 2) + }; - catch (Exception ex) + var content = (string?)converter.Content ?? ""; + var hint = converter.HintText; + var rules = converter.ValidateRules; + + var request = new MsgBoxRequest { - ModBase.Log(ex, "输入弹窗初始化失败", ModBase.LogLevel.Hint); - } + Caption = converter.Title, + Message = converter.Text, + Theme = isWarn ? MsgBoxTheme.Warning : MsgBoxTheme.Info, + Buttons = buttons, + IsBlocking = true, + Content = content, + Hint = hint, + ValidateRules = rules + }; + Request = request; + _anim = MsgBoxAnimationProfile.ForTheme(request.Theme); + _animGroup = "MyMsgBox " + ModBase.GetUuid(); + + Completed += async (_, response) => + { + converter.IsExited = true; + converter.Result = response.ButtonValue == 1 ? TextArea.Text : null; + converter.WaitFrame.Continue = false; + ComponentDispatcher.PopModal(); + await InvokeCloseAnimationAsync(response).ConfigureAwait(true); + }; - Loaded += Load; + InitFromRequest(content, hint, rules); } - private void AppendUniqueNameSuffix(FrameworkElement element) + private void InitFromRequest(string content, string hint, + System.Collections.ObjectModel.Collection>? rules) { - element.Name += ModBase.GetUuid(); + var isWarn = Request.Theme is MsgBoxTheme.Warning or MsgBoxTheme.Error; + var btn1 = Request.Buttons.ElementAtOrDefault(0); + var btn2 = Request.Buttons.ElementAtOrDefault(1); + + InitializeComponent(); + LabTitle.Text = Request.Caption; + LabText.Text = Request.Message; + PanText.Visibility = string.IsNullOrEmpty(Request.Message) ? Visibility.Collapsed : Visibility.Visible; + TextArea.Text = content; + TextArea.HintText = hint; + if (rules is not null) TextArea.ValidateRules = rules; + ConfigurePrimaryButton(btn1?.Text ?? "确定", isWarn); + ConfigureSecondaryButton(btn2?.Text ?? ""); + ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d); + + if (_anim.HighlightPrimaryButton && Btn2.IsVisible && Btn1.ColorType != MyButton.ColorState.Red) + Btn1.ColorType = MyButton.ColorState.Highlight; + + Loaded += (_, _) => + { + try + { + TextArea.Focus(); + TextArea.SelectionStart = TextArea.Text.Length; + InvokeShowAnimation(); + ModBase.Log("[Control] 输入弹窗:" + LabTitle.Text); + } + catch (Exception ex) + { + ModBase.Log(ex, "输入弹窗加载失败", ModBase.LogLevel.Hint); + } + }; } private void ConfigurePrimaryButton(string text, bool isWarn) @@ -59,85 +117,41 @@ private void ConfigureSecondaryButton(string text) Btn2.Visibility = string.IsNullOrEmpty(text) ? Visibility.Collapsed : Visibility.Visible; } - private void Load(object sender, EventArgs e) + public void InvokeShowAnimation() { - try - { - // UI 初始化 - if (Btn2.IsVisible && !(Btn1.ColorType == MyButton.ColorState.Red)) - Btn1.ColorType = MyButton.ColorState.Highlight; - TextArea.Focus(); - TextArea.SelectionStart = TextArea.Text.Length; - // 动画 - Opacity = 0d; - ModAnimation.AniStart( - ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, BlurBorder.BackgroundProperty, - (myConverter.IsWarn - ? new ModBase.MyColor(140d, 80d, 0d, 0d) - : new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.frmMain.PanMsgBackground.Background, 200), - "PanMsgBackground Background"); - ModAnimation.AniStart( - new[] - { - ModAnimation.AaOpacity(this, 1d, 120, 60), - ModAnimation.AaDouble(i => TransformPos.Y += (double)i, - -TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), - ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, - -TransformRotate.Angle, 300, 60, - new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)) - }, "MyMsgBox " + uuid); - // 记录日志 - ModBase.Log("[Control] 输入弹窗:" + LabTitle.Text); - } - - catch (Exception ex) - { - ModBase.Log(ex, "输入弹窗加载失败", ModBase.LogLevel.Hint); - } + Opacity = 0d; + MsgBoxAnimations.AnimateShow(this, TransformPos, TransformRotate, _anim, _animGroup); } - private void Close() + public async Task InvokeCloseAnimationAsync(MsgBoxResponse response) { - // 结束线程阻塞 - myConverter.WaitFrame.Continue = false; - ComponentDispatcher.PopModal(); - // 动画 - ModAnimation.AniStart(new[] - { - ModAnimation.AaCode(() => - { - if (!ModMain.WaitingMyMsgBox.Any()) - ModAnimation.AniStart(ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, - BlurBorder.BackgroundProperty, - new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.frmMain.PanMsgBackground.Background, 200, - ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))); - }, 30), - ModAnimation.AaOpacity(this, -Opacity, 80, 20), - ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y, - 150, 0, new ModAnimation.AniEaseOutFluent()), - ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, - 6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)), - ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), after: true) - }, "MyMsgBox " + uuid); + await MsgBoxAnimations.AnimateCloseAsync(this, TransformPos, TransformRotate, _anim, _animGroup).ConfigureAwait(true); + if (Parent is Grid g) g.Children.Remove(this); } public void Btn1_Click(object sender, MouseButtonEventArgs e) { - TextArea.Validate(); // #5773 - if (myConverter.IsExited || !TextArea.IsValidated) - return; - myConverter.IsExited = true; - myConverter.Result = TextArea.Text; - Close(); + TextArea.Validate(); + if (_isExited || !TextArea.IsValidated) return; + _isExited = true; + Completed?.Invoke(this, new MsgBoxResponse + { + RequestId = Request.RequestId, + ButtonValue = Request.Buttons.ElementAtOrDefault(0)?.Value ?? 1, + Button = Request.Buttons.ElementAtOrDefault(0) + }); } public void Btn2_Click(object sender, MouseButtonEventArgs e) { - if (myConverter.IsExited) - return; - myConverter.IsExited = true; - myConverter.Result = null; - Close(); + if (_isExited) return; + _isExited = true; + Completed?.Invoke(this, new MsgBoxResponse + { + RequestId = Request.RequestId, + ButtonValue = Request.Buttons.ElementAtOrDefault(1)?.Value ?? 2, + Button = Request.Buttons.ElementAtOrDefault(1) + }); } private void TextCaption_ValidateChanged(object sender, EventArgs e) diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml.cs b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml.cs index b6bc2faaf..4fcc75297 100644 --- a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml.cs +++ b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml.cs @@ -1,45 +1,97 @@ +using PCL.Controls.MyMsg; +using PCL.Core.UI; +using PCL.Core.UI.MsgBox; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Interop; -using PCL.Core.UI.Controls; namespace PCL; -public partial class MyMsgMarkdown +public partial class MyMsgMarkdown : IMsgBoxControl { - private readonly ModMain.MyMsgBoxConverter myConverter; - private readonly int uuid = ModBase.GetUuid(); + public MsgBoxRequest Request { get; } + public event EventHandler? Completed; + + private readonly MsgBoxAnimationProfile _anim; + private bool _isExited; + private readonly string _animGroup; + + public MyMsgMarkdown(MsgBoxRequest request) + { + Request = request; + _anim = MsgBoxAnimationProfile.ForTheme(request.Theme); + _animGroup = "MyMsgMarkdown " + Request.RequestId; + InitFromRequest(); + } public MyMsgMarkdown(ModMain.MyMsgBoxConverter converter) { - try + var isWarn = converter.IsWarn; + var buttons = new List(); + buttons.Add(new MsgBoxButtonInfo(converter.Button1, 1, converter.Button1Action)); + if (!string.IsNullOrEmpty(converter.Button2)) + buttons.Add(new MsgBoxButtonInfo(converter.Button2, 2, converter.Button2Action)); + if (!string.IsNullOrEmpty(converter.Button3)) + buttons.Add(new MsgBoxButtonInfo(converter.Button3, 3, converter.Button3Action)); + + var request = new MsgBoxRequest { - InitializeComponent(); - AppendUniqueNameSuffix(Btn1); - AppendUniqueNameSuffix(Btn2); - AppendUniqueNameSuffix(Btn3); - myConverter = converter; - LabTitle.Text = converter.Title; - LabCaption.Markdown = converter.Text; - DataContext = this; - ConfigurePrimaryButton(converter.Button1, converter.IsWarn); - ConfigureSecondaryButton(Btn2, converter.Button2); - ConfigureSecondaryButton(Btn3, converter.Button3); - ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d); - } + Caption = converter.Title, + Message = converter.Text, + Theme = isWarn ? MsgBoxTheme.Warning : MsgBoxTheme.Info, + Buttons = buttons, + IsBlocking = converter.ForceWait || !string.IsNullOrEmpty(converter.Button2) + }; + Request = request; + _anim = MsgBoxAnimationProfile.ForTheme(request.Theme); + _animGroup = "MyMsgBox " + ModBase.GetUuid(); - catch (Exception ex) + Completed += async (_, response) => { - ModBase.Log(ex, "普通弹窗初始化失败", ModBase.LogLevel.Hint); - } + converter.IsExited = true; + converter.Result = response.ButtonValue; + if (converter.ForceWait || !string.IsNullOrEmpty(converter.Button2)) + converter.WaitFrame.Continue = false; + ComponentDispatcher.PopModal(); + await InvokeCloseAnimationAsync(response).ConfigureAwait(true); + }; - Loaded += Load; + InitFromRequest(); } - private void AppendUniqueNameSuffix(FrameworkElement element) + private void InitFromRequest() { - element.Name += ModBase.GetUuid(); + var isWarn = Request.Theme is MsgBoxTheme.Warning or MsgBoxTheme.Error; + var btn1 = Request.Buttons.ElementAtOrDefault(0); + var btn2 = Request.Buttons.ElementAtOrDefault(1); + var btn3 = Request.Buttons.ElementAtOrDefault(2); + + InitializeComponent(); + LabTitle.Text = Request.Caption; + LabCaption.Markdown = Request.Message; + DataContext = this; + ConfigurePrimaryButton(btn1?.Text ?? "确定", isWarn); + ConfigureSecondaryButton(Btn2, btn2?.Text ?? ""); + ConfigureSecondaryButton(Btn3, btn3?.Text ?? ""); + ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d); + + if (_anim.HighlightPrimaryButton && Btn2.IsVisible && Btn1.ColorType != MyButton.ColorState.Red) + Btn1.ColorType = MyButton.ColorState.Highlight; + + Loaded += (_, _) => + { + try + { + Btn1.Focus(); + InvokeShowAnimation(); + ModBase.Log("[Control] Markdown 弹窗:" + LabTitle.Text); + } + catch (Exception ex) + { + ModBase.Log(ex, "普通弹窗加载失败", ModBase.LogLevel.Hint); + } + }; } private void ConfigurePrimaryButton(string text, bool isWarn) @@ -58,121 +110,74 @@ private static void ConfigureSecondaryButton(MyButton button, string text) button.Visibility = string.IsNullOrEmpty(text) ? Visibility.Collapsed : Visibility.Visible; } - private void Load(object sender, EventArgs e) + public void InvokeShowAnimation() { - try - { - // UI 初始化 - if (Btn2.IsVisible && !(Btn1.ColorType == MyButton.ColorState.Red)) - Btn1.ColorType = MyButton.ColorState.Highlight; - Btn1.Focus(); - // 动画 - Opacity = 0d; - ModAnimation.AniStart( - ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, BlurBorder.BackgroundProperty, - (myConverter.IsWarn - ? new ModBase.MyColor(140d, 80d, 0d, 0d) - : new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.frmMain.PanMsgBackground.Background, 200), - "PanMsgBackground Background"); - ModAnimation.AniStart( - new[] - { - ModAnimation.AaOpacity(this, 1d, 120, 60), - ModAnimation.AaDouble(i => TransformPos.Y += (double)i, - -TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), - ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, - -TransformRotate.Angle, 300, 60, - new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)) - }, "MyMsgBox " + uuid); - // 记录日志 - ModBase.Log("[Control] 普通弹窗:" + LabTitle.Text + "\r\n" + LabCaption.Markdown); - } - - catch (Exception ex) - { - ModBase.Log(ex, "普通弹窗加载失败", ModBase.LogLevel.Hint); - } + Opacity = 0d; + MsgBoxAnimations.AnimateShow(this, TransformPos, TransformRotate, _anim, _animGroup); } - private void Close() + public async Task InvokeCloseAnimationAsync(MsgBoxResponse response) { - // 结束线程阻塞 - if (myConverter.ForceWait || !string.IsNullOrEmpty(myConverter.Button2)) - myConverter.WaitFrame.Continue = false; - ComponentDispatcher.PopModal(); - // 动画 - ModAnimation.AniStart(new[] - { - ModAnimation.AaCode(() => - { - if (!ModMain.WaitingMyMsgBox.Any()) - ModAnimation.AniStart(ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, - BlurBorder.BackgroundProperty, - new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.frmMain.PanMsgBackground.Background, 200, - ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))); - }, 30), - ModAnimation.AaOpacity(this, -Opacity, 80, 20), - ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y, - 150, 0, new ModAnimation.AniEaseOutFluent()), - ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, - 6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)), - ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), after: true) - }, "MyMsgBox " + uuid); + await MsgBoxAnimations.AnimateCloseAsync(this, TransformPos, TransformRotate, _anim, _animGroup).ConfigureAwait(true); + if (Parent is Grid g) g.Children.Remove(this); } public void Btn1_Click(object sender, MouseButtonEventArgs e) { - if (myConverter.IsExited) - return; - if (myConverter.Button1Action is not null) + if (_isExited) return; + if (Request.Buttons.ElementAtOrDefault(0)?.OnClick is { } action) { - myConverter.Button1Action(); + action(); + return; } - else + _isExited = true; + Completed?.Invoke(this, new MsgBoxResponse { - myConverter.IsExited = true; - myConverter.Result = 1; - Close(); - } + RequestId = Request.RequestId, + ButtonValue = Request.Buttons.ElementAtOrDefault(0)?.Value ?? 1, + Button = Request.Buttons.ElementAtOrDefault(0) + }); } public void Btn2_Click(object sender, MouseButtonEventArgs e) { - if (myConverter.IsExited) - return; - if (myConverter.Button2Action is not null) + if (_isExited) return; + if (Request.Buttons.ElementAtOrDefault(1)?.OnClick is { } action) { - myConverter.Button2Action(); + action(); + return; } - else + _isExited = true; + Completed?.Invoke(this, new MsgBoxResponse { - myConverter.IsExited = true; - myConverter.Result = 2; - Close(); - } + RequestId = Request.RequestId, + ButtonValue = Request.Buttons.ElementAtOrDefault(1)?.Value ?? 2, + Button = Request.Buttons.ElementAtOrDefault(1) + }); } public void Btn3_Click(object sender, MouseButtonEventArgs e) { - if (myConverter.IsExited) - return; - if (myConverter.Button3Action is not null) + if (_isExited) return; + if (Request.Buttons.ElementAtOrDefault(2)?.OnClick is { } action) { - myConverter.Button3Action(); + action(); + return; } - else + _isExited = true; + Completed?.Invoke(this, new MsgBoxResponse { - myConverter.IsExited = true; - myConverter.Result = 3; - Close(); - } + RequestId = Request.RequestId, + ButtonValue = Request.Buttons.ElementAtOrDefault(2)?.Value ?? 3, + Button = Request.Buttons.ElementAtOrDefault(2) + }); } private void Drag(object? sender = null, MouseButtonEventArgs? e = null) { try { - if (e.LeftButton == MouseButtonState.Pressed) + if (e?.LeftButton == MouseButtonState.Pressed) if (e.GetPosition(ShapeLine).Y <= 2d) ModMain.frmMain.DragMove(); } diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml.cs b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml.cs index e33749f8f..0b86f516a 100644 --- a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml.cs +++ b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml.cs @@ -1,51 +1,101 @@ +using PCL.Controls.MyMsg; +using PCL.Core.UI.MsgBox; using System.Collections; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Interop; -using PCL.Core.UI.Controls; namespace PCL; -public partial class MyMsgSelect +public partial class MyMsgSelect : IMsgBoxControl { - private readonly ModMain.MyMsgBoxConverter myConverter; - private readonly int uuid = ModBase.GetUuid(); + public MsgBoxRequest Request { get; } + public event EventHandler? Completed; - private int selectedIndex = -1; + private readonly MsgBoxAnimationProfile _anim; + private bool _isExited; + private int _selectedIndex = -1; + private readonly string _animGroup; + + public MyMsgSelect(MsgBoxRequest request) + { + Request = request; + _anim = MsgBoxAnimationProfile.ForTheme(request.Theme); + _animGroup = "MyMsgSelect " + Request.RequestId; + InitFromRequest(request.Content as IEnumerable); + } public MyMsgSelect(ModMain.MyMsgBoxConverter converter) { - try + var isWarn = converter.IsWarn; + var buttons = new List { - InitializeComponent(); - AppendUniqueNameSuffix(Btn1); - AppendUniqueNameSuffix(Btn2); - myConverter = converter; - LabTitle.Text = converter.Title; - ConfigurePrimaryButton(converter.Button1, converter.IsWarn); - ConfigureSecondaryButton(converter.Button2); - ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d); - InitializeSelectionList(converter.Content); - } + new(converter.Button1, 1), + new(converter.Button2, 2) + }; - catch (Exception ex) + var content = converter.Content as IEnumerable; + + var request = new MsgBoxRequest { - ModBase.Log(ex, "选择弹窗初始化失败", ModBase.LogLevel.Hint); - } + Caption = converter.Title, + Theme = isWarn ? MsgBoxTheme.Warning : MsgBoxTheme.Info, + Buttons = buttons, + IsBlocking = true, + Content = converter.Content + }; + Request = request; + _anim = MsgBoxAnimationProfile.ForTheme(request.Theme); + _animGroup = "MyMsgBox " + ModBase.GetUuid(); + + Completed += async (_, response) => + { + converter.IsExited = true; + converter.Result = response.ButtonValue == 1 ? _selectedIndex : null; + converter.WaitFrame.Continue = false; + ComponentDispatcher.PopModal(); + await InvokeCloseAnimationAsync(response).ConfigureAwait(true); + }; + + InitFromRequest(content); + } + + private void InitFromRequest(IEnumerable? selections) + { + var isWarn = Request.Theme is MsgBoxTheme.Warning or MsgBoxTheme.Error; + var btn1 = Request.Buttons.ElementAtOrDefault(0); + var btn2 = Request.Buttons.ElementAtOrDefault(1); + + InitializeComponent(); + LabTitle.Text = Request.Caption; + ConfigurePrimaryButton(btn1?.Text ?? "确定", isWarn); + ConfigureSecondaryButton(btn2?.Text ?? ""); + ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d); + InitializeSelectionList(selections); + + if (_anim.HighlightPrimaryButton && Btn2.IsVisible && Btn1.ColorType != MyButton.ColorState.Red) + Btn1.ColorType = MyButton.ColorState.Highlight; + + Loaded += (_, _) => + { + try + { + InvokeShowAnimation(); + ModBase.Log("[Control] 选择弹窗:" + LabTitle.Text); + } + catch (Exception ex) + { + ModBase.Log(ex, "选择弹窗加载失败", ModBase.LogLevel.Hint); + } + }; - Loaded += Load; Btn1.Click += Btn1_Click; Btn2.Click += Btn2_Click; LabTitle.MouseLeftButtonDown += Drag; PanBorder.MouseLeftButtonDown += Drag; } - private void AppendUniqueNameSuffix(FrameworkElement element) - { - element.Name += ModBase.GetUuid(); - } - private void ConfigurePrimaryButton(string text, bool isWarn) { Btn1.Text = text; @@ -62,23 +112,19 @@ private void ConfigureSecondaryButton(string text) Btn2.Visibility = string.IsNullOrEmpty(text) ? Visibility.Collapsed : Visibility.Visible; } - private void InitializeSelectionList(object content) + private void InitializeSelectionList(IEnumerable? rawList) { - // 添加选择控件 Btn1.IsEnabled = false; - foreach (var rawContent in (IEnumerable)content) + if (rawList is null) return; + + foreach (var rawContent in rawList) { - // 1. Initialize and get the actual element - // Note: We use a new variable because 'foreach' variables are read-only var selectionContent = MyVirtualizingElement.TryInit((FrameworkElement)rawContent); - - // 2. Interface casting and event subscription if (selectionContent is IMyRadio selection) { PanSelection.Children.Add((UIElement)selection); - selection.Check += (sender, e) => OnChecked((IMyRadio)sender, e); + selection.Check += (_, _2) => OnChecked(selection, _2); - // 3. Property configuration based on specific type if (selection is MyListItem listItem) { listItem.Type = MyListItem.CheckType.RadioBox; @@ -92,88 +138,46 @@ private void InitializeSelectionList(object content) } } - private void Load(object sender, EventArgs e) + public void InvokeShowAnimation() { - try - { - // UI 初始化 - if (Btn2.IsVisible && !(Btn1.ColorType == MyButton.ColorState.Red)) - Btn1.ColorType = MyButton.ColorState.Highlight; - // 动画 - Opacity = 0d; - ModAnimation.AniStart( - ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, BlurBorder.BackgroundProperty, - (myConverter.IsWarn - ? new ModBase.MyColor(140d, 80d, 0d, 0d) - : new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.frmMain.PanMsgBackground.Background, 200), - "PanMsgBackground Background"); - ModAnimation.AniStart( - new[] - { - ModAnimation.AaOpacity(this, 1d, 120, 60), - ModAnimation.AaDouble(i => TransformPos.Y += (double)i, - -TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), - ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, - -TransformRotate.Angle, 300, 60, - new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)) - }, "MyMsgBox " + uuid); - // 记录日志 - ModBase.Log("[Control] 选择弹窗:" + LabTitle.Text); - } - - catch (Exception ex) - { - ModBase.Log(ex, "选择弹窗加载失败", ModBase.LogLevel.Hint); - } + Opacity = 0d; + MsgBoxAnimations.AnimateShow(this, TransformPos, TransformRotate, _anim, _animGroup); } - private void Close() + public async Task InvokeCloseAnimationAsync(MsgBoxResponse response) { - // 结束线程阻塞 - myConverter.WaitFrame.Continue = false; - ComponentDispatcher.PopModal(); - // 动画 - ModAnimation.AniStart(new[] - { - ModAnimation.AaCode(() => - { - if (!ModMain.WaitingMyMsgBox.Any()) - ModAnimation.AniStart(ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, - BlurBorder.BackgroundProperty, - new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.frmMain.PanMsgBackground.Background, 200, - ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))); - }, 30), - ModAnimation.AaOpacity(this, -Opacity, 80, 20), - ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y, - 150, 0, new ModAnimation.AniEaseOutFluent()), - ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, - 6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)), - ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), after: true) - }, "MyMsgBox " + uuid); + await MsgBoxAnimations.AnimateCloseAsync(this, TransformPos, TransformRotate, _anim, _animGroup).ConfigureAwait(true); + if (Parent is Grid g) g.Children.Remove(this); } public void Btn1_Click(object sender, MouseButtonEventArgs e) { - if (myConverter.IsExited || selectedIndex == -1) - return; - myConverter.IsExited = true; - myConverter.Result = selectedIndex; - Close(); + if (_isExited || _selectedIndex == -1) return; + _isExited = true; + Completed?.Invoke(this, new MsgBoxResponse + { + RequestId = Request.RequestId, + ButtonValue = 1, + Button = Request.Buttons.ElementAtOrDefault(0) + }); } public void Btn2_Click(object sender, MouseButtonEventArgs e) { - if (myConverter.IsExited) - return; - myConverter.IsExited = true; - myConverter.Result = null; - Close(); + if (_isExited) return; + _isExited = true; + Completed?.Invoke(this, new MsgBoxResponse + { + RequestId = Request.RequestId, + ButtonValue = 2, + Button = Request.Buttons.ElementAtOrDefault(1) + }); } private void OnChecked(IMyRadio sender, EventArgs e) { Btn1.IsEnabled = true; - selectedIndex = PanSelection.Children.IndexOf((UIElement)sender); + _selectedIndex = PanSelection.Children.IndexOf((UIElement)sender); } private void Drag(object sender, MouseButtonEventArgs e) diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml.cs b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml.cs index ece7fe122..386adc39d 100644 --- a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml.cs +++ b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml.cs @@ -1,44 +1,96 @@ +using PCL.Controls.MyMsg; +using PCL.Core.UI.MsgBox; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Interop; -using PCL.Core.UI.Controls; namespace PCL; -public partial class MyMsgText +public partial class MyMsgText : IMsgBoxControl { - private readonly ModMain.MyMsgBoxConverter myConverter; - private readonly int uuid = ModBase.GetUuid(); + public MsgBoxRequest Request { get; } + public event EventHandler? Completed; + + private readonly MsgBoxAnimationProfile _anim; + private bool _isExited; + private readonly string _animGroup; + + public MyMsgText(MsgBoxRequest request) + { + Request = request; + _anim = MsgBoxAnimationProfile.ForTheme(request.Theme); + _animGroup = $"MyMsgText {Request.RequestId}"; + InitFromRequest(); + } public MyMsgText(ModMain.MyMsgBoxConverter converter) { - try + var isWarn = converter.IsWarn; + var buttons = new List(); + buttons.Add(new MsgBoxButtonInfo(converter.Button1, 1, converter.Button1Action)); + if (!string.IsNullOrEmpty(converter.Button2)) + buttons.Add(new MsgBoxButtonInfo(converter.Button2, 2, converter.Button2Action)); + if (!string.IsNullOrEmpty(converter.Button3)) + buttons.Add(new MsgBoxButtonInfo(converter.Button3, 3, converter.Button3Action)); + + var request = new MsgBoxRequest { - InitializeComponent(); - AppendUniqueNameSuffix(Btn1); - AppendUniqueNameSuffix(Btn2); - AppendUniqueNameSuffix(Btn3); - myConverter = converter; - LabTitle.Text = converter.Title; - LabCaption.Text = converter.Text; - ConfigurePrimaryButton(converter.Button1, converter.IsWarn); - ConfigureSecondaryButton(Btn2, converter.Button2); - ConfigureSecondaryButton(Btn3, converter.Button3); - ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d); - } + Caption = converter.Title ?? "", + Message = converter.Text ?? "", + Theme = isWarn ? MsgBoxTheme.Warning : MsgBoxTheme.Info, + Buttons = buttons, + IsBlocking = converter.ForceWait || !string.IsNullOrEmpty(converter.Button2) + }; + Request = request; + _anim = MsgBoxAnimationProfile.ForTheme(request.Theme); + _animGroup = "MyMsgBox " + ModBase.GetUuid(); - catch (Exception ex) + Completed += async (_, response) => { - ModBase.Log(ex, "普通弹窗初始化失败", ModBase.LogLevel.Hint); - } + converter.IsExited = true; + converter.Result = response.ButtonValue; + // 结束线程阻塞 + if (converter.ForceWait || !string.IsNullOrEmpty(converter.Button2)) + converter.WaitFrame.Continue = false; + ComponentDispatcher.PopModal(); + await InvokeCloseAnimationAsync(response).ConfigureAwait(true); + }; - Loaded += Load; + InitFromRequest(); } - private void AppendUniqueNameSuffix(FrameworkElement element) + private void InitFromRequest() { - element.Name += ModBase.GetUuid(); + var isWarn = Request.Theme is MsgBoxTheme.Warning or MsgBoxTheme.Error; + var btn1 = Request.Buttons.ElementAtOrDefault(0); + var btn2 = Request.Buttons.ElementAtOrDefault(1); + var btn3 = Request.Buttons.ElementAtOrDefault(2); + + InitializeComponent(); + LabTitle.Text = Request.Caption; + LabCaption.Text = Request.Message; + ConfigurePrimaryButton(btn1?.Text ?? "确定", isWarn); + ConfigureSecondaryButton(Btn2, btn2?.Text ?? ""); + ConfigureSecondaryButton(Btn3, btn3?.Text ?? ""); + ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d); + + if (_anim.HighlightPrimaryButton && Btn2.IsVisible && Btn1.ColorType != MyButton.ColorState.Red) + Btn1.ColorType = MyButton.ColorState.Highlight; + + Loaded += (_, _) => + { + try + { + Btn1.Focus(); + InvokeShowAnimation(); + ModBase.Log("[Control] 普通弹窗:" + LabTitle.Text + "\r\n" + LabCaption.Text); + } + catch (Exception ex) + { + ModBase.Log(ex, "普通弹窗加载失败", ModBase.LogLevel.Hint); + } + }; } private void ConfigurePrimaryButton(string text, bool isWarn) @@ -57,114 +109,67 @@ private static void ConfigureSecondaryButton(MyButton button, string text) button.Visibility = string.IsNullOrEmpty(text) ? Visibility.Collapsed : Visibility.Visible; } - private void Load(object sender, RoutedEventArgs e) + public void InvokeShowAnimation() { - try - { - // UI 初始化 - if (Btn2.IsVisible && !(Btn1.ColorType == MyButton.ColorState.Red)) - Btn1.ColorType = MyButton.ColorState.Highlight; - Btn1.Focus(); - // 动画 - Opacity = 0d; - ModAnimation.AniStart( - ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, BlurBorder.BackgroundProperty, - (myConverter.IsWarn - ? new ModBase.MyColor(140d, 80d, 0d, 0d) - : new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.frmMain.PanMsgBackground.Background, 200), - "PanMsgBackground Background"); - ModAnimation.AniStart( - new[] - { - ModAnimation.AaOpacity(this, 1d, 120, 60), - ModAnimation.AaDouble(i => TransformPos.Y += (double)i, - -TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), - ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, - -TransformRotate.Angle, 300, 60, - new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)) - }, "MyMsgBox " + uuid); - // 记录日志 - ModBase.Log("[Control] 普通弹窗:" + LabTitle.Text + "\r\n" + LabCaption.Text); - } - - catch (Exception ex) - { - ModBase.Log(ex, "普通弹窗加载失败", ModBase.LogLevel.Hint); - } + Opacity = 0d; + MsgBoxAnimations.AnimateShow(this, TransformPos, TransformRotate, _anim, _animGroup); } - private void Close() + public async Task InvokeCloseAnimationAsync(MsgBoxResponse response) { - // 结束线程阻塞 - if (myConverter.ForceWait || !string.IsNullOrEmpty(myConverter.Button2)) - myConverter.WaitFrame.Continue = false; - ComponentDispatcher.PopModal(); - // 动画 - ModAnimation.AniStart(new[] - { - ModAnimation.AaCode(() => - { - if (!ModMain.WaitingMyMsgBox.Any()) - ModAnimation.AniStart(ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, - BlurBorder.BackgroundProperty, - new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.frmMain.PanMsgBackground.Background, 200, - ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))); - }, 30), - ModAnimation.AaOpacity(this, -Opacity, 80, 20), - ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y, - 150, 0, new ModAnimation.AniEaseOutFluent()), - ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, - 6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)), - ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), after: true) - }, "MyMsgBox " + uuid); + await MsgBoxAnimations.AnimateCloseAsync(this, TransformPos, TransformRotate, _anim, _animGroup).ConfigureAwait(true); + if (Parent is Grid g) g.Children.Remove(this); } public void Btn1_Click(object? sender = null, MouseButtonEventArgs? e = null) { - if (myConverter.IsExited) - return; - if (myConverter.Button1Action is not null) + if (_isExited) return; + if (Request.Buttons.ElementAtOrDefault(0)?.OnClick is { } action) { - myConverter.Button1Action(); + action(); + return; } - else + _isExited = true; + Completed?.Invoke(this, new MsgBoxResponse { - myConverter.IsExited = true; - myConverter.Result = 1; - Close(); - } + RequestId = Request.RequestId, + ButtonValue = Request.Buttons.ElementAtOrDefault(0)?.Value ?? 1, + Button = Request.Buttons.ElementAtOrDefault(0) + }); } public void Btn2_Click(object sender, MouseButtonEventArgs e) { - if (myConverter.IsExited) - return; - if (myConverter.Button2Action is not null) + if (_isExited) return; + if (Request.Buttons.ElementAtOrDefault(1)?.OnClick is { } action) { - myConverter.Button2Action(); + action(); + return; } - else + _isExited = true; + Completed?.Invoke(this, new MsgBoxResponse { - myConverter.IsExited = true; - myConverter.Result = 2; - Close(); - } + RequestId = Request.RequestId, + ButtonValue = Request.Buttons.ElementAtOrDefault(1)?.Value ?? 2, + Button = Request.Buttons.ElementAtOrDefault(1) + }); } public void Btn3_Click(object sender, MouseButtonEventArgs e) { - if (myConverter.IsExited) - return; - if (myConverter.Button3Action is not null) + if (_isExited) return; + if (Request.Buttons.ElementAtOrDefault(2)?.OnClick is { } action) { - myConverter.Button3Action(); + action(); + return; } - else + _isExited = true; + Completed?.Invoke(this, new MsgBoxResponse { - myConverter.IsExited = true; - myConverter.Result = 3; - Close(); - } + RequestId = Request.RequestId, + ButtonValue = Request.Buttons.ElementAtOrDefault(2)?.Value ?? 3, + Button = Request.Buttons.ElementAtOrDefault(2) + }); } private void Drag(object sender, MouseButtonEventArgs e) diff --git a/Plain Craft Launcher 2/Controls/ViewModelBase.cs b/Plain Craft Launcher 2/Controls/ViewModelBase.cs new file mode 100644 index 000000000..1814837b5 --- /dev/null +++ b/Plain Craft Launcher 2/Controls/ViewModelBase.cs @@ -0,0 +1,15 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace PCL.Controls; + +public class ViewModelBase: INotifyPropertyChanged +{ + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void SetProperty(T newValue, ref T value, [CallerMemberName] string propertyName = "") + { + value = newValue; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} diff --git a/Plain Craft Launcher 2/FormMain.xaml.cs b/Plain Craft Launcher 2/FormMain.xaml.cs index 3eeced9c7..0698483f8 100644 --- a/Plain Craft Launcher 2/FormMain.xaml.cs +++ b/Plain Craft Launcher 2/FormMain.xaml.cs @@ -1,13 +1,3 @@ -using System.ComponentModel; -using System.IO; -using System.Net; -using System.Runtime.InteropServices; -using System.Windows; -using System.Windows.Controls; -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; @@ -19,6 +9,15 @@ using PCL.Core.Utils.OS; using PCL.Core.Utils.Validate; using PCL.Network; +using System.ComponentModel; +using System.IO; +using System.Net; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Media; namespace PCL; @@ -92,7 +91,8 @@ public FormMain() AddHandler(DragDrop.DragEnterEvent, new DragEventHandler(HandleDrag), true); AddHandler(DragDrop.DragOverEvent, new DragEventHandler(HandleDrag), true); // 注册 MsgBox 事件 - MsgBoxWrapper.OnShow += ModMain.MsgBoxWrapper_OnShow; + ModMain.MsgBoxActor = new MsgBoxActor(PanMsg, PanMsgBackground); + ModMain.MsgBoxActor.Start(); // 注册 Hint 事件 HintWrapper.OnShow += HintService.HintWrapper_OnShow; // 加载 UI @@ -517,7 +517,7 @@ public void EndProgram(bool sendWarning, bool isUpdating = false) transformScale.CenterX = Width / 2d; transformScale.CenterY = Height / 2d; RenderTransform = new TransformGroup - { Children = new TransformCollection([transformRotate, transformPos, transformScale]) }; + { Children = new TransformCollection([transformRotate, transformPos, transformScale]) }; ModAnimation.AniStart(new[] { ModAnimation.AaOpacity(this, -Opacity, 140, 40, @@ -672,55 +672,11 @@ private void FormMain_KeyDown(object sender, KeyEventArgs e) { if (e.IsRepeat) return; - // 调用弹窗:回车选择第一个,Esc 选择最后一个 - if (PanMsg.Children.Count > 0) - { - if (e.Key == Key.Enter) - { - var msg = PanMsg.Children[0]; - Action? enterAction = msg switch - { - MyMsgInput input => () => input.Btn1_Click(sender, null), - MyMsgSelect select => () => select.Btn1_Click(sender, null), - MyMsgText text => () => text.Btn1_Click(sender, null), - MyMsgMarkdown markdown => () => markdown.Btn1_Click(sender, null), - MyMsgLogin login => () => login.Btn1_Click(sender, null), - _ => null - }; - enterAction?.Invoke(); - return; - } - if (e.Key == Key.Escape) - { - var msg = PanMsg.Children[0]; - Action? escapeAction = msg switch - { - MyMsgInput input => input.Btn2.Visibility == Visibility.Visible - ? () => input.Btn2_Click(sender, null) - : () => input.Btn1_Click(sender, null), - MyMsgSelect select => select.Btn2.Visibility == Visibility.Visible - ? () => select.Btn2_Click(sender, null) - : () => select.Btn1_Click(sender, null), - MyMsgText text => text.Btn3.Visibility == Visibility.Visible - ? () => text.Btn3_Click(sender, null) - : text.Btn2.Visibility == Visibility.Visible - ? () => text.Btn2_Click(sender, null) - : () => text.Btn1_Click(sender, null), - MyMsgMarkdown markdown => markdown.Btn3.Visibility == Visibility.Visible - ? () => markdown.Btn3_Click(sender, null) - : markdown.Btn2.Visibility == Visibility.Visible - ? () => markdown.Btn2_Click(sender, null) - : () => markdown.Btn1_Click(sender, null), - MyMsgLogin login => login.Btn3.Visibility == Visibility.Visible - ? () => login.Btn3_Click(sender, null) - : () => login.Btn1_Click(sender, null), - _ => null - }; - escapeAction?.Invoke(); - return; - } - } + // 调用弹窗:通过 MsgBoxActor 处理回车和 Esc + ModMain.MsgBoxActor?.HandleKeyEvent(sender, e); + if (e.Handled) + return; // 按 ESC 返回上一级 if (e.Key == Key.Escape) @@ -1037,87 +993,87 @@ ModMain.frmInstanceSchematic is not null && switch (PageCurrentSub) { case PageSubType.VersionWorld: - { - var destFolder = PageInstanceLeft.McInstance.PathIndie + @"saves\" + - ModBase.GetFileNameWithoutExtentionFromPath(filePath); - var destLevelDat = Path.Combine(destFolder, "level.dat"); - if (Directory.Exists(destFolder)) { - HintService.Hint(Lang.Text("Main.FileDrag.SameFolderExists", destFolder), HintType.Error); - return; - } - - var extractFolder = Path.Combine(ModBase.pathTemp, "Cache", "WorldImport", ModBase.GetUuid().ToString()); - try - { - ModBase.ExtractFile(filePath, extractFolder); - var saveRoot = SaveImportHelper.GetSaveRootDirectory(extractFolder); - if (saveRoot is null) + var destFolder = PageInstanceLeft.McInstance.PathIndie + @"saves\" + + ModBase.GetFileNameWithoutExtentionFromPath(filePath); + var destLevelDat = Path.Combine(destFolder, "level.dat"); + if (Directory.Exists(destFolder)) { - HintService.Hint(Lang.Text("Main.FileDrag.SaveNotFound"), HintType.Error); + HintService.Hint(Lang.Text("Main.FileDrag.SameFolderExists", destFolder), HintType.Error); return; } - ModBase.CopyDirectory(saveRoot, destFolder); - if (!File.Exists(destLevelDat)) + var extractFolder = Path.Combine(ModBase.pathTemp, "Cache", "WorldImport", ModBase.GetUuid().ToString()); + try + { + ModBase.ExtractFile(filePath, extractFolder); + var saveRoot = SaveImportHelper.GetSaveRootDirectory(extractFolder); + if (saveRoot is null) + { + HintService.Hint(Lang.Text("Main.FileDrag.SaveNotFound"), HintType.Error); + return; + } + + ModBase.CopyDirectory(saveRoot, destFolder); + if (!File.Exists(destLevelDat)) + { + if (Directory.Exists(destFolder)) + ModBase.DeleteDirectory(destFolder, true); + HintService.Hint(Lang.Text("Main.FileDrag.SaveInvalid"), HintType.Error); + return; + } + } + catch (Exception ex) { if (Directory.Exists(destFolder)) ModBase.DeleteDirectory(destFolder, true); - HintService.Hint(Lang.Text("Main.FileDrag.SaveInvalid"), HintType.Error); + ModBase.Log(ex, Lang.Text("Main.FileDrag.SaveImportFailed"), ModBase.LogLevel.Hint); return; } - } - catch (Exception ex) - { - if (Directory.Exists(destFolder)) - ModBase.DeleteDirectory(destFolder, true); - ModBase.Log(ex, Lang.Text("Main.FileDrag.SaveImportFailed"), ModBase.LogLevel.Hint); + finally + { + if (Directory.Exists(extractFolder)) + ModBase.DeleteDirectory(extractFolder, true); + } + + HintService.Hint(Lang.Text("Main.FileDrag.Imported", ModBase.GetFileNameWithoutExtentionFromPath(filePath)), + HintType.Success); + if (ModMain.frmInstanceSaves is not null) + ModBase.RunInUi(() => ModMain.frmInstanceSaves.Reload()); return; } - finally - { - if (Directory.Exists(extractFolder)) - ModBase.DeleteDirectory(extractFolder, true); - } - - HintService.Hint(Lang.Text("Main.FileDrag.Imported", ModBase.GetFileNameWithoutExtentionFromPath(filePath)), - HintType.Success); - if (ModMain.frmInstanceSaves is not null) - ModBase.RunInUi(() => ModMain.frmInstanceSaves.Reload()); - return; - } case PageSubType.VersionResourcePack: - { - var destFile = PageInstanceLeft.McInstance.PathIndie + @"resourcepacks\" + - ModBase.GetFileNameFromPath(filePath); - if (File.Exists(destFile)) { - HintService.Hint(Lang.Text("Main.FileDrag.SameFileExists", destFile), HintType.Error); + var destFile = PageInstanceLeft.McInstance.PathIndie + @"resourcepacks\" + + ModBase.GetFileNameFromPath(filePath); + if (File.Exists(destFile)) + { + HintService.Hint(Lang.Text("Main.FileDrag.SameFileExists", destFile), HintType.Error); + return; + } + + ModBase.CopyFile(filePath, destFile); + HintService.Hint(Lang.Text("Main.FileDrag.Imported", ModBase.GetFileNameFromPath(filePath)), HintType.Success); + if (ModMain.frmInstanceResourcePack is not null) + ModBase.RunInUi(() => ModMain.frmInstanceResourcePack.ReloadCompFileList()); return; } - - ModBase.CopyFile(filePath, destFile); - HintService.Hint(Lang.Text("Main.FileDrag.Imported", ModBase.GetFileNameFromPath(filePath)), HintType.Success); - if (ModMain.frmInstanceResourcePack is not null) - ModBase.RunInUi(() => ModMain.frmInstanceResourcePack.ReloadCompFileList()); - return; - } case PageSubType.VersionShader: - { - var destFile = PageInstanceLeft.McInstance.PathIndie + @"shaderpacks\" + - ModBase.GetFileNameFromPath(filePath); - if (File.Exists(destFile)) { - HintService.Hint(Lang.Text("Main.FileDrag.SameFileExists", destFile), HintType.Error); + var destFile = PageInstanceLeft.McInstance.PathIndie + @"shaderpacks\" + + ModBase.GetFileNameFromPath(filePath); + if (File.Exists(destFile)) + { + HintService.Hint(Lang.Text("Main.FileDrag.SameFileExists", destFile), HintType.Error); + return; + } + + ModBase.CopyFile(filePath, destFile); + HintService.Hint(Lang.Text("Main.FileDrag.Imported", ModBase.GetFileNameFromPath(filePath)), HintType.Success); + if (ModMain.frmInstanceShader is not null) + ModBase.RunInUi(() => ModMain.frmInstanceShader.ReloadCompFileList()); return; } - - ModBase.CopyFile(filePath, destFile); - HintService.Hint(Lang.Text("Main.FileDrag.Imported", ModBase.GetFileNameFromPath(filePath)), HintType.Success); - if (ModMain.frmInstanceShader is not null) - ModBase.RunInUi(() => ModMain.frmInstanceShader.ReloadCompFileList()); - return; - } } // 处理投影文件 @@ -1298,17 +1254,17 @@ private void WindowStateChanged(object sender, EventArgs e) switch (WindowState) { case WindowState.Minimized: - { - ModVideoBack.isMinimized = true; - ModVideoBack.VideoPause(); - break; - } + { + ModVideoBack.isMinimized = true; + ModVideoBack.VideoPause(); + break; + } case WindowState.Normal: - { - ModVideoBack.isMinimized = false; - ModVideoBack.VideoPlay(); - break; - } + { + ModVideoBack.isMinimized = false; + ModVideoBack.VideoPlay(); + break; + } } } @@ -1439,34 +1395,34 @@ private string PageNameGet(PageStackData stack) switch (stack.page) { case PageType.InstanceSelect: - { - return Lang.Text("Main.Title.InstanceSelect"); - } + { + return Lang.Text("Main.Title.InstanceSelect"); + } case PageType.TaskManager: - { - return Lang.Text("Main.Title.TaskManager"); - } + { + return Lang.Text("Main.Title.TaskManager"); + } case PageType.GameLog: - { - return Lang.Text("Main.Title.GameLog"); - } + { + return Lang.Text("Main.Title.GameLog"); + } case PageType.InstanceSetup: - { - return Lang.Text("Main.Title.InstanceSetup", PageInstanceLeft.McInstance is null ? Lang.Text("Common.State.Unknown") : PageInstanceLeft.McInstance.Name); - } + { + return Lang.Text("Main.Title.InstanceSetup", PageInstanceLeft.McInstance is null ? Lang.Text("Common.State.Unknown") : PageInstanceLeft.McInstance.Name); + } case PageType.CompDetail: - { - return Lang.Text("Main.Title.ResourceDownload", stack.additional.Value.CompProject.TranslatedName); - } + { + return Lang.Text("Main.Title.ResourceDownload", stack.additional.Value.CompProject.TranslatedName); + } case PageType.VersionSaves: - { - return Lang.Text("Main.Title.SaveManagement", ModBase.GetFolderNameFromPath(stack.additional.Value.SavePath)); - } + { + return Lang.Text("Main.Title.SaveManagement", ModBase.GetFolderNameFromPath(stack.additional.Value.SavePath)); + } default: - { - return ""; - } + { + return ""; + } } } @@ -1507,30 +1463,30 @@ public PageSubType PageCurrentSub switch (pageCurrent.page) { case PageType.Download: - { - if (ModMain.frmDownloadLeft is null) - ModMain.frmDownloadLeft = new PageDownloadLeft(); - return ModMain.frmDownloadLeft.pageID; - } + { + if (ModMain.frmDownloadLeft is null) + ModMain.frmDownloadLeft = new PageDownloadLeft(); + return ModMain.frmDownloadLeft.pageID; + } case PageType.Setup: - { - if (ModMain.frmSetupLeft is null) - ModMain.frmSetupLeft = new PageSetupLeft(); - return ModMain.frmSetupLeft.pageID; - } + { + if (ModMain.frmSetupLeft is null) + ModMain.frmSetupLeft = new PageSetupLeft(); + return ModMain.frmSetupLeft.pageID; + } case PageType.InstanceSetup: - { - if (ModMain.frmInstanceLeft is null) - ModMain.frmInstanceLeft = new PageInstanceLeft(); - return ModMain.frmInstanceLeft.pageID; - } + { + if (ModMain.frmInstanceLeft is null) + ModMain.frmInstanceLeft = new PageInstanceLeft(); + return ModMain.frmInstanceLeft.pageID; + } default: - { - return 0; // 没有子页面 - } + { + return 0; // 没有子页面 + } } } } @@ -1626,33 +1582,33 @@ public void PageChange(PageStackData stack, PageSubType subType = PageSubType.De switch (stack.page) { case PageType.Download: - { - if (ModMain.frmDownloadLeft is null) - ModMain.frmDownloadLeft = new PageDownloadLeft(); - foreach (var item in ModMain.frmDownloadLeft.PanItem.Children) - if (item is MyListItem listItem && - ModBase.Val(listItem.Tag) == (double)subType) - { - listItem.SetChecked(true, true, stack == pageCurrent); - break; - } + { + if (ModMain.frmDownloadLeft is null) + ModMain.frmDownloadLeft = new PageDownloadLeft(); + foreach (var item in ModMain.frmDownloadLeft.PanItem.Children) + if (item is MyListItem listItem && + ModBase.Val(listItem.Tag) == (double)subType) + { + listItem.SetChecked(true, true, stack == pageCurrent); + break; + } - break; - } + break; + } case PageType.Setup: - { - if (ModMain.frmSetupLeft is null) - ModMain.frmSetupLeft = new PageSetupLeft(); - foreach (var item in ModMain.frmSetupLeft.PanItem.Children) - if (item is MyListItem listItem && - ModBase.Val(listItem.Tag) == (double)subType) - { - listItem.SetChecked(true, true, stack == pageCurrent); - break; - } + { + if (ModMain.frmSetupLeft is null) + ModMain.frmSetupLeft = new PageSetupLeft(); + foreach (var item in ModMain.frmSetupLeft.PanItem.Children) + if (item is MyListItem listItem && + ModBase.Val(listItem.Tag) == (double)subType) + { + listItem.SetChecked(true, true, stack == pageCurrent); + break; + } - break; - } + break; + } } PageChangeActual(stack, subType); @@ -1663,33 +1619,33 @@ public void PageChange(PageStackData stack, PageSubType subType = PageSubType.De switch (stack.page) { case PageType.InstanceSetup: - { - if (ModMain.frmInstanceLeft is null) - ModMain.frmInstanceLeft = new PageInstanceLeft(); - foreach (var item in ModMain.frmInstanceLeft.PanItem.Children) - if (item is MyListItem listItem && - ModBase.Val(listItem.Tag) == (double)subType) - { - listItem.SetChecked(true, true, stack == pageCurrent); - break; - } + { + if (ModMain.frmInstanceLeft is null) + ModMain.frmInstanceLeft = new PageInstanceLeft(); + foreach (var item in ModMain.frmInstanceLeft.PanItem.Children) + if (item is MyListItem listItem && + ModBase.Val(listItem.Tag) == (double)subType) + { + listItem.SetChecked(true, true, stack == pageCurrent); + break; + } - break; - } + break; + } case PageType.VersionSaves: - { - if (ModMain.frmInstanceSavesLeft is null) - ModMain.frmInstanceSavesLeft = new PageInstanceSavesLeft(); - foreach (var item in ModMain.frmInstanceSavesLeft.PanItem.Children) - if (item is MyListItem listItem && - ModBase.Val(listItem.Tag) == (double)subType) - { - listItem.SetChecked(true, true, stack == pageCurrent); - break; - } + { + if (ModMain.frmInstanceSavesLeft is null) + ModMain.frmInstanceSavesLeft = new PageInstanceSavesLeft(); + foreach (var item in ModMain.frmInstanceSavesLeft.PanItem.Children) + if (item is MyListItem listItem && + ModBase.Val(listItem.Tag) == (double)subType) + { + listItem.SetChecked(true, true, stack == pageCurrent); + break; + } - break; - } + break; + } } PageChangeActual(stack, subType); @@ -1705,7 +1661,7 @@ private void BtnTitleSelect_Click(MyRadioButton sender, bool raiseByMouse) return; var pageType = (PageType)int.Parse(sender.Tag.ToString()); PageChangeActual(pageType, PageSubType.Default); - } + } private void BtnTitleInner_Click(object sender, EventArgs e) { @@ -2039,7 +1995,7 @@ public void DragDoing() { if (ModMain.dragControl is null) return; - if (Mouse.LeftButton == MouseButtonState.Pressed) + if (Mouse.LeftButton == MouseButtonState.Pressed) { ModMain.dragControl.DragDoing(); } diff --git a/Plain Craft Launcher 2/Modules/Minecraft/CrashAnalysis/Presentation/CrashDialogPresenter.cs b/Plain Craft Launcher 2/Modules/Minecraft/CrashAnalysis/Presentation/CrashDialogPresenter.cs index 42bc7c9cb..3ef3566ae 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/CrashAnalysis/Presentation/CrashDialogPresenter.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/CrashAnalysis/Presentation/CrashDialogPresenter.cs @@ -1,9 +1,10 @@ -using System.Globalization; -using System.IO; using PCL.Core.App; using PCL.Core.App.Localization; using PCL.Core.Logging; using PCL.Core.UI; +using PCL.Core.UI.MsgBox; +using System.Globalization; +using System.IO; namespace PCL; diff --git a/Plain Craft Launcher 2/Modules/ModMain.cs b/Plain Craft Launcher 2/Modules/ModMain.cs index 1a653f3e3..2fea41b5d 100644 --- a/Plain Craft Launcher 2/Modules/ModMain.cs +++ b/Plain Craft Launcher 2/Modules/ModMain.cs @@ -1,23 +1,19 @@ -using System.Collections; -using System.Collections.ObjectModel; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Interop; -using System.Windows.Media; -using System.Windows.Threading; using FluentValidation; using Microsoft.VisualBasic; using Microsoft.Win32; using PCL.Core.App; using PCL.Core.App.Configuration; using PCL.Core.App.Localization; -using PCL.Core.UI; +using PCL.Core.UI.MsgBox; using PCL.Core.Utils; using PCL.Core.Utils.OS; using PCL.Core.Utils.Secret; +using System.Collections.ObjectModel; +using System.IO; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Interop; +using System.Windows.Threading; namespace PCL; @@ -100,6 +96,8 @@ public static class ModMain /// public static List WaitingMyMsgBox { get; } = []; + public static MsgBoxActor? MsgBoxActor { get; internal set; } + private static void TimerMain() { try @@ -310,9 +308,17 @@ public enum MyMsgBoxType /// 点击第二个按钮将执行该方法,不关闭弹窗。 /// 点击第三个按钮将执行该方法,不关闭弹窗。 /// 是否为警告弹窗,若为 True,弹窗配色和背景会变为红色。 - public static int MyMsgBox(string caption, string? title = null, string? button1 = null, string? button2 = "", - string? button3 = "", bool isWarn = false, bool highLight = true, bool forceWait = false, - Action button1Action = null, Action button2Action = null, Action button3Action = null) + public static int MyMsgBox(string caption, + string? title = null, + string? button1 = null, + string? button2 = null, + string? button3 = null, + bool isWarn = false, + bool highLight = true, + bool forceWait = false, + Action? button1Action = null, + Action? button2Action = null, + Action? button3Action = null) { title ??= GetDefaultDialogTitle(); button1 ??= GetDefaultConfirmText(); @@ -321,9 +327,18 @@ public static int MyMsgBox(string caption, string? title = null, string? button1 // 将弹窗列入队列 var converter = new MyMsgBoxConverter { - Type = MyMsgBoxType.Text, Button1 = button1, Button2 = button2, Button3 = button3, Text = caption, - IsWarn = isWarn, Title = title, HighLight = highLight, ForceWait = true, Button1Action = button1Action, - Button2Action = button2Action, Button3Action = button3Action + Type = MyMsgBoxType.Text, + Button1 = button1, + Button2 = button2, + Button3 = button3, + Text = caption, + IsWarn = isWarn, + Title = title, + HighLight = highLight, + ForceWait = true, + Button1Action = button1Action, + Button2Action = button2Action, + Button3Action = button3Action }; WaitingMyMsgBox.Add(converter); if (ModBase.RunInUi()) @@ -344,20 +359,20 @@ public static int MyMsgBox(string caption, string? title = null, string? button1 switch (rawResult) { case MsgBoxResult.Yes: - { - converter.Result = 1; - break; - } + { + converter.Result = 1; + break; + } case MsgBoxResult.No: - { - converter.Result = 2; - break; - } + { + converter.Result = 2; + break; + } case MsgBoxResult.Cancel: - { - converter.Result = 3; - break; - } + { + converter.Result = 3; + break; + } } } else @@ -416,9 +431,18 @@ public static int MyMsgBoxMarkdown(string caption, string? title = null, string? // 将弹窗列入队列 var converter = new MyMsgBoxConverter { - Type = MyMsgBoxType.Markdown, Button1 = button1, Button2 = button2, Button3 = button3, Text = caption, - IsWarn = isWarn, Title = title, HighLight = highLight, ForceWait = true, Button1Action = button1Action, - Button2Action = button2Action, Button3Action = button3Action + Type = MyMsgBoxType.Markdown, + Button1 = button1, + Button2 = button2, + Button3 = button3, + Text = caption, + IsWarn = isWarn, + Title = title, + HighLight = highLight, + ForceWait = true, + Button1Action = button1Action, + Button2Action = button2Action, + Button3Action = button3Action }; WaitingMyMsgBox.Add(converter); if (ModBase.RunInUi()) @@ -439,20 +463,20 @@ public static int MyMsgBoxMarkdown(string caption, string? title = null, string? switch (rawResult) { case MsgBoxResult.Yes: - { - converter.Result = 1; - break; - } + { + converter.Result = 1; + break; + } case MsgBoxResult.No: - { - converter.Result = 2; - break; - } + { + converter.Result = 2; + break; + } case MsgBoxResult.Cancel: - { - converter.Result = 3; - break; - } + { + converter.Result = 3; + break; + } } } else @@ -508,12 +532,19 @@ public static string MyMsgBoxInput(string title, string text = "", string defaul // 将弹窗列入队列 var converter = new MyMsgBoxConverter { - Text = text, HintText = hintText, Type = MyMsgBoxType.Input, - ValidateRules = validateRules ?? [], Button1 = button1, Button2 = button2, - Content = defaultInput, IsWarn = isWarn, Title = title + Text = text, + HintText = hintText, + Type = MyMsgBoxType.Input, + ValidateRules = validateRules ?? [], + Button1 = button1, + Button2 = button2, + Content = defaultInput, + IsWarn = isWarn, + Title = title }; WaitingMyMsgBox.Add(converter); // 虽然我也不知道这是啥但是能用就成了 :) + // whitecat346: 这是模态窗口 try { frmMain?.DragStop(); @@ -545,15 +576,19 @@ public static string MyMsgBoxInput(string title, string text = "", string defaul // 将弹窗列入队列 var converter = new MyMsgBoxConverter { - Type = MyMsgBoxType.Select, Button1 = button1, Button2 = button2, Content = selections, IsWarn = isWarn, + Type = MyMsgBoxType.Select, + Button1 = button1, + Button2 = button2, + Content = selections, + IsWarn = isWarn, Title = title }; WaitingMyMsgBox.Add(converter); // 虽然我也不知道这是啥但是能用就成了 :) + // whitecat346: 这是模态窗口 try { - if (frmMain is not null) - frmMain.DragStop(); + frmMain?.DragStop(); ComponentDispatcher.PushModal(); Dispatcher.PushFrame(converter.WaitFrame); } @@ -585,36 +620,36 @@ public static void MyMsgBoxTick() switch (WaitingMyMsgBox[0].Type) { case MyMsgBoxType.Input: - { - frmMain.PanMsg.Children.Add(new MyMsgInput(WaitingMyMsgBox[0])); - break; - } + { + frmMain.PanMsg.Children.Add(new MyMsgInput(WaitingMyMsgBox[0])); + break; + } case MyMsgBoxType.Select: - { - frmMain.PanMsg.Children.Add(new MyMsgSelect(WaitingMyMsgBox[0])); - break; - } + { + frmMain.PanMsg.Children.Add(new MyMsgSelect(WaitingMyMsgBox[0])); + break; + } case MyMsgBoxType.Text: - { - frmMain.PanMsg.Children.Add(new MyMsgText(WaitingMyMsgBox[0])); - break; - } + { + frmMain.PanMsg.Children.Add(new MyMsgText(WaitingMyMsgBox[0])); + break; + } case MyMsgBoxType.Login: - { - frmMain.PanMsg.Children.Add(new MyMsgLogin(WaitingMyMsgBox[0])); - break; - } + { + frmMain.PanMsg.Children.Add(new MyMsgLogin(WaitingMyMsgBox[0])); + break; + } case MyMsgBoxType.Markdown: - { - frmMain.PanMsg.Children.Add(new MyMsgMarkdown(WaitingMyMsgBox[0])); - break; - } + { + frmMain.PanMsg.Children.Add(new MyMsgMarkdown(WaitingMyMsgBox[0])); + break; + } } WaitingMyMsgBox.RemoveAt(0); } // 没有弹窗,没有等待的弹窗 - else if (!(frmMain.PanMsgBackground.Visibility == Visibility.Collapsed)) + else if (frmMain.PanMsgBackground.Visibility != Visibility.Collapsed) { frmMain.PanMsgBackground.Visibility = Visibility.Collapsed; } @@ -628,11 +663,11 @@ public static void MyMsgBoxTick() public static void MsgBoxWrapper_OnShow(string message, string caption, ICollection buttons, MsgBoxTheme theme, bool block, ref int result) { - var btnText1 = buttons.Count < 1 ? GetDefaultConfirmText() : buttons.ElementAt(0).Context; + var btnText1 = buttons.Count < 1 ? GetDefaultConfirmText() : buttons.ElementAt(0).Text; var btnAct1 = (Action)(buttons.Count < 1 ? (object)null : buttons.ElementAt(0).OnClick); - var btnText2 = buttons.Count < 2 ? GetDefaultCancelText() : buttons.ElementAt(1).Context; + var btnText2 = buttons.Count < 2 ? GetDefaultCancelText() : buttons.ElementAt(1).Text; var btnAct2 = (Action)(buttons.Count < 2 ? (object)null : buttons.ElementAt(1).OnClick); - var btnText3 = buttons.Count < 3 ? "" : buttons.ElementAt(2).Context; + var btnText3 = buttons.Count < 3 ? "" : buttons.ElementAt(2).Text; var btnAct3 = (Action)(buttons.Count < 3 ? (object)null : buttons.ElementAt(2).OnClick); var isWarn = theme == MsgBoxTheme.Warning || theme == MsgBoxTheme.Error; @@ -765,25 +800,25 @@ private static void TimerFool() switch (RandomUtils.NextInt(0, 3)) { case 0: - { - HintService.Hint("放弃吧!只需要点一下右下角的小白旗……"); - break; - } + { + HintService.Hint("放弃吧!只需要点一下右下角的小白旗……"); + break; + } case 1: - { - HintService.Hint("看到右下角的那面小白旗了吗?"); - break; - } + { + HintService.Hint("看到右下角的那面小白旗了吗?"); + break; + } case 2: - { - HintService.Hint("这里建议点一下右下角的小白旗投降呢.jpg"); - break; - } + { + HintService.Hint("这里建议点一下右下角的小白旗投降呢.jpg"); + break; + } case 3: - { - HintService.Hint("右下角的小白旗永远等着你……"); - break; - } + { + HintService.Hint("右下角的小白旗永远等着你……"); + break; + } } } } @@ -866,107 +901,107 @@ public static void SetGPUPreference(string executeable, bool wantHighPerformance /// /// 对替换标记进行处理。会对替换内容使用 EscapeHandler 进行转义。 /// /// - public static string ArgumentReplace(string text, Func escapeHandler = null, bool replaceTime = true) + public static string ArgumentReplace(string text, Func escapeHandler = null, bool replaceTime = true) { - // 预处理 - if (text is null) return null; - - Func replacer = (s) => - { - if (s is null) return ""; - if (escapeHandler is null) return s; - if (s.Contains(":\\")) s = ModBase.ShortenPath(s); - return escapeHandler(s); - }; - - // 基础 - text = text.Replace("{pcl_version}", replacer(ModBase.versionBaseName)); - text = text.Replace("{pcl_version_code}", replacer(ModBase.versionCode.ToString())); - text = text.Replace("{pcl_version_branch}", replacer(ModBase.versionBranchName)); - text = text.Replace("{pcl_branch}", replacer(ModBase.versionBranchName)); - text = text.Replace("{identify}", replacer(Identify.LauncherId)); - text = text.Replace("{path}", replacer(Basics.ExecutableDirectory)); - text = text.Replace("{path_with_name}", replacer(Basics.ExecutableName)); - text = text.Replace("{path_temp}", replacer(ModBase.pathTemp)); - - // 时间 - if (replaceTime) // 在窗口标题中,时间会被后续动态替换,所以此时不应该替换 - { - text = text.Replace("{date}", replacer(Lang.Date(DateTime.Now, "d"))); - text = text.Replace("{time}", replacer(Lang.Date(DateTime.Now, "T"))); - } - - // Minecraft - text = text.Replace("{java}", replacer(ModLaunch.mcLaunchJavaSelected?.Installation.JavaFolder)); - text = text.Replace("{minecraft}", replacer(ModFolder.mcFolderSelected)); - - if (ModInstanceList.McMcInstanceSelected is not null) - { - text = text.Replace("{version_path}", replacer(ModInstanceList.McMcInstanceSelected.PathInstance)); - text = text.Replace("{verpath}", replacer(ModInstanceList.McMcInstanceSelected.PathInstance)); - text = text.Replace("{version_indie}", replacer(ModInstanceList.McMcInstanceSelected.PathIndie)); - text = text.Replace("{verindie}", replacer(ModInstanceList.McMcInstanceSelected.PathIndie)); - text = text.Replace("{name}", replacer(ModInstanceList.McMcInstanceSelected.Name)); - - if (new[] { "unknown", "old", "pending" }.Contains(ModInstanceList.McMcInstanceSelected.Info.VanillaName)) + // 预处理 + if (text is null) return null; + + Func replacer = (s) => + { + if (s is null) return ""; + if (escapeHandler is null) return s; + if (s.Contains(":\\")) s = ModBase.ShortenPath(s); + return escapeHandler(s); + }; + + // 基础 + text = text.Replace("{pcl_version}", replacer(ModBase.versionBaseName)); + text = text.Replace("{pcl_version_code}", replacer(ModBase.versionCode.ToString())); + text = text.Replace("{pcl_version_branch}", replacer(ModBase.versionBranchName)); + text = text.Replace("{pcl_branch}", replacer(ModBase.versionBranchName)); + text = text.Replace("{identify}", replacer(Identify.LauncherId)); + text = text.Replace("{path}", replacer(Basics.ExecutableDirectory)); + text = text.Replace("{path_with_name}", replacer(Basics.ExecutableName)); + text = text.Replace("{path_temp}", replacer(ModBase.pathTemp)); + + // 时间 + if (replaceTime) // 在窗口标题中,时间会被后续动态替换,所以此时不应该替换 { - text = text.Replace("{version}", replacer(ModInstanceList.McMcInstanceSelected.Name)); + text = text.Replace("{date}", replacer(Lang.Date(DateTime.Now, "d"))); + text = text.Replace("{time}", replacer(Lang.Date(DateTime.Now, "T"))); + } + + // Minecraft + text = text.Replace("{java}", replacer(ModLaunch.mcLaunchJavaSelected?.Installation.JavaFolder)); + text = text.Replace("{minecraft}", replacer(ModFolder.mcFolderSelected)); + + if (ModInstanceList.McMcInstanceSelected is not null) + { + text = text.Replace("{version_path}", replacer(ModInstanceList.McMcInstanceSelected.PathInstance)); + text = text.Replace("{verpath}", replacer(ModInstanceList.McMcInstanceSelected.PathInstance)); + text = text.Replace("{version_indie}", replacer(ModInstanceList.McMcInstanceSelected.PathIndie)); + text = text.Replace("{verindie}", replacer(ModInstanceList.McMcInstanceSelected.PathIndie)); + text = text.Replace("{name}", replacer(ModInstanceList.McMcInstanceSelected.Name)); + + if (new[] { "unknown", "old", "pending" }.Contains(ModInstanceList.McMcInstanceSelected.Info.VanillaName)) + { + text = text.Replace("{version}", replacer(ModInstanceList.McMcInstanceSelected.Name)); + } + else + { + text = text.Replace("{version}", replacer(ModInstanceList.McMcInstanceSelected.Info.VanillaName)); + } } else { - text = text.Replace("{version}", replacer(ModInstanceList.McMcInstanceSelected.Info.VanillaName)); + text = text.Replace("{version_path}", replacer(null)); + text = text.Replace("{verpath}", replacer(null)); + text = text.Replace("{version_indie}", replacer(null)); + text = text.Replace("{verindie}", replacer(null)); + text = text.Replace("{name}", replacer(null)); + text = text.Replace("{version}", replacer(null)); } - } - else - { - text = text.Replace("{version_path}", replacer(null)); - text = text.Replace("{verpath}", replacer(null)); - text = text.Replace("{version_indie}", replacer(null)); - text = text.Replace("{verindie}", replacer(null)); - text = text.Replace("{name}", replacer(null)); - text = text.Replace("{version}", replacer(null)); - } - - // 验证信息 - if (ModLaunch.mcLoginLoader.State == ModBase.LoadState.Finished) - { - text = text.Replace("{user}", replacer(ModLaunch.mcLoginLoader.output.Name)); - text = text.Replace("{uuid}", replacer(ModLaunch.mcLoginLoader.output.Uuid.ToLower())); - - switch (ModLaunch.mcLoginLoader.input.LoginType) + + // 验证信息 + if (ModLaunch.mcLoginLoader.State == ModBase.LoadState.Finished) { - case ModLaunch.McLoginType.Legacy: - text = text.Replace("{login}", replacer("离线")); - break; - case ModLaunch.McLoginType.Ms: - text = text.Replace("{login}", replacer("正版")); - break; - case ModLaunch.McLoginType.Auth: - text = text.Replace("{login}", replacer("Authlib-Injector")); - break; + text = text.Replace("{user}", replacer(ModLaunch.mcLoginLoader.output.Name)); + text = text.Replace("{uuid}", replacer(ModLaunch.mcLoginLoader.output.Uuid.ToLower())); + + switch (ModLaunch.mcLoginLoader.input.LoginType) + { + case ModLaunch.McLoginType.Legacy: + text = text.Replace("{login}", replacer("离线")); + break; + case ModLaunch.McLoginType.Ms: + text = text.Replace("{login}", replacer("正版")); + break; + case ModLaunch.McLoginType.Auth: + text = text.Replace("{login}", replacer("Authlib-Injector")); + break; + } } + else + { + text = text.Replace("{user}", replacer(null)); + text = text.Replace("{uuid}", replacer(null)); + text = text.Replace("{login}", replacer(null)); + } + + // 高级 + text = ModBase.RegexReplaceEach(text, @"\{hint\}", m => replacer(PageToolsTest.GetRandomHint())); + text = ModBase.RegexReplaceEach(text, @"\{cave\}", m => replacer(PageToolsTest.GetRandomCave())); + text = ModBase.RegexReplaceEach(text, @"\{setup:([a-zA-Z0-9]+)\}", m => + { + if (ConfigService.TryGetConfigItemNoType(m.Groups[1].Value, out var item) && item.Source != ConfigSource.SharedEncrypt) + return replacer(item.GetValueNoType(ModInstanceList.McMcInstanceSelected?.PathInstance)?.ToString() ?? ""); + return replacer(""); + }); + text = ModBase.RegexReplaceEach(text, @"\{varible:([^:\}]+)(?::([^\}]+))?\}", m => replacer(CustomEvent.GetCustomVariable(m.Groups[1].Value, m.Groups[2].Value))); + text = ModBase.RegexReplaceEach(text, @"\{variable:([^:\}]+)(?::([^\}]+))?\}", m => replacer(CustomEvent.GetCustomVariable(m.Groups[1].Value, m.Groups[2].Value))); + + return text; } - else - { - text = text.Replace("{user}", replacer(null)); - text = text.Replace("{uuid}", replacer(null)); - text = text.Replace("{login}", replacer(null)); - } - - // 高级 - text = ModBase.RegexReplaceEach(text, @"\{hint\}", m => replacer(PageToolsTest.GetRandomHint())); - text = ModBase.RegexReplaceEach(text, @"\{cave\}", m => replacer(PageToolsTest.GetRandomCave())); - text = ModBase.RegexReplaceEach(text, @"\{setup:([a-zA-Z0-9]+)\}", m => - { - if (ConfigService.TryGetConfigItemNoType(m.Groups[1].Value, out var item) && item.Source != ConfigSource.SharedEncrypt) - return replacer(item.GetValueNoType(ModInstanceList.McMcInstanceSelected?.PathInstance)?.ToString() ?? ""); - return replacer(""); - }); - text = ModBase.RegexReplaceEach(text, @"\{varible:([^:\}]+)(?::([^\}]+))?\}", m => replacer(CustomEvent.GetCustomVariable(m.Groups[1].Value, m.Groups[2].Value))); - text = ModBase.RegexReplaceEach(text, @"\{variable:([^:\}]+)(?::([^\}]+))?\}", m => replacer(CustomEvent.GetCustomVariable(m.Groups[1].Value, m.Groups[2].Value))); - - return text; -} #endregion #region 任务缓存 @@ -1042,7 +1077,7 @@ public static string RequestTaskTempFolder(bool requireNonSpace = false) } #endregion - + public static void RaiseCustomEvent(DependencyObject control) { // 收集事件列表 diff --git a/Plain Craft Launcher 2/Modules/UI/MsgBoxActor.cs b/Plain Craft Launcher 2/Modules/UI/MsgBoxActor.cs new file mode 100644 index 000000000..1079200e8 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/UI/MsgBoxActor.cs @@ -0,0 +1,204 @@ +using PCL.Core.App.Essentials; +using PCL.Core.UI.MsgBox; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Threading; + +namespace PCL; + +public sealed class MsgBoxActor(Grid panMsg, FrameworkElement background) : IDisposable +{ + private readonly CancellationTokenSource _cts = new(); + + private readonly Dictionary _cancellations = []; + + public void Start() + { + _ = RunAsync(_cts.Token); + } + + private async Task RunAsync(CancellationToken ct) + { + var reader = MsgBoxService.Reader; + try + { + await foreach (var request in reader.ReadAllAsync(ct)) + { + await System.Windows.Application.Current.Dispatcher.InvokeAsync( + () => ShowOnUi(request), + DispatcherPriority.Normal, + ct); + + if (request.Timeout is not null || request.CancellationToken.CanBeCanceled) + { + var combinedCt = request.CancellationToken; + if (request.Timeout is not null) + { + var timeoutCts = new CancellationTokenSource((TimeSpan)request.Timeout); + combinedCt = + CancellationTokenSource.CreateLinkedTokenSource(request.CancellationToken, + timeoutCts.Token).Token; + } + + var reg = combinedCt.Register(() => + { + System.Windows.Application.Current.Dispatcher.InvokeAsync(() => + { + CancelRequest(request.RequestId); + }); + }); + + lock (_cancellations) + { + _cancellations[request.RequestId] = reg; + } + } + } + } + catch (OperationCanceledException) + { + // normally exit, ignore + } + } + + private void ShowOnUi(MsgBoxRequest request) + { + background.Visibility = Visibility.Visible; + + IMsgBoxControl control = CreateControl(request); + control.Completed += OnControlCOmpleted; + + panMsg.Children.Add((UIElement)control); + control.InvokeShowAnimation(); + } + + private void OnControlCOmpleted(object? sender, MsgBoxResponse response) + { + if (sender is not IMsgBoxControl control) + { + return; + } + + control.Completed -= OnControlCOmpleted; + + response.Button?.OnClick?.Invoke(); + + lock (_cancellations) + { + if (_cancellations.TryGetValue(response.RequestId, out var reg)) + { + reg.Dispose(); + _cancellations.Remove(response.RequestId); + } + } + + _ = DoCloseAsync(control, response); + } + + private async Task DoCloseAsync(IMsgBoxControl control, MsgBoxResponse response) + { + await control.InvokeCloseAnimationAsync(response).ConfigureAwait(true); + panMsg.Children.Remove((UIElement)control); + + if (panMsg.Children.Count == 0) + { + background.Visibility = Visibility.Collapsed; + } + + MsgBoxService.Complete(response.RequestId, response); + + } + + private void CancelRequest(Guid requestId) + { + IMsgBoxControl? target = null; + foreach (var child in panMsg.Children) + { + if (child is IMsgBoxControl c && + c.Request.RequestId == requestId) + { + target = c; + break; + } + } + + if (target is not null) + { + DoCloseAsync(target, MsgBoxResponse.Cancelled(requestId)).GetAwaiter().GetResult(); // idk what should i do to handle this async method + } + } + + private IMsgBoxControl CreateControl(MsgBoxRequest request) => + request.RequestType switch + { + // add MVVM control type at here + MsgBoxRequestType.Text => new MyMsgText(request), + MsgBoxRequestType.Select => new MyMsgSelect(request), + MsgBoxRequestType.Input => new MyMsgInput(request), + MsgBoxRequestType.Login => new MyMsgLogin(request, request.Content as JsonObject), + MsgBoxRequestType.Markdown => new MyMsgMarkdown(request), + _ => throw new ArgumentOutOfRangeException(nameof(request), request, null) + }; + + /// 处理键盘事件(由 FormMain_KeyDown 调用) + public void HandleKeyEvent(object sender, KeyEventArgs e) + { + if (e.IsRepeat || panMsg.Children.Count == 0) + return; + + var msg = panMsg.Children[0]; + + if (e.Key == Key.Enter) + { + Action? enterAction = msg switch + { + MyMsgInput input => () => input.Btn1_Click(sender, null), + MyMsgSelect select => () => select.Btn1_Click(sender, null), + MyMsgText text => () => text.Btn1_Click(sender, null), + MyMsgMarkdown markdown => () => markdown.Btn1_Click(sender, null), + MyMsgLogin login => () => login.Btn1_Click(sender, null), + _ => null + }; + enterAction?.Invoke(); + e.Handled = true; + return; + } + + if (e.Key == Key.Escape) + { + Action? escapeAction = msg switch + { + MyMsgInput input => input.Btn2.Visibility == Visibility.Visible + ? () => input.Btn2_Click(sender, null) + : () => input.Btn1_Click(sender, null), + MyMsgSelect select => select.Btn2.Visibility == Visibility.Visible + ? () => select.Btn2_Click(sender, null) + : () => select.Btn1_Click(sender, null), + MyMsgText text => text.Btn3.Visibility == Visibility.Visible + ? () => text.Btn3_Click(sender, null) + : text.Btn2.Visibility == Visibility.Visible + ? () => text.Btn2_Click(sender, null) + : () => text.Btn1_Click(sender, null), + MyMsgMarkdown markdown => markdown.Btn3.Visibility == Visibility.Visible + ? () => markdown.Btn3_Click(sender, null) + : markdown.Btn2.Visibility == Visibility.Visible + ? () => markdown.Btn2_Click(sender, null) + : () => markdown.Btn1_Click(sender, null), + MyMsgLogin login => login.Btn3.Visibility == Visibility.Visible + ? () => login.Btn3_Click(sender, null) + : () => login.Btn1_Click(sender, null), + _ => null + }; + escapeAction?.Invoke(); + e.Handled = true; + return; + } + } + + /// + public void Dispose() + { + _cts.Dispose(); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml.cs b/Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml.cs index f14775232..f2944e0a2 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml.cs +++ b/Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml.cs @@ -1,67 +1,151 @@ -using System.Windows.Controls; -using System.Windows.Input; +using PCL.Controls.MyMsg; using PCL.Core.App; using PCL.Core.App.Localization; -using PCL.Core.UI.Controls; -using PCL.Core.Utils; using PCL.Core.IO.Net.Http; +using PCL.Core.UI.MsgBox; +using PCL.Core.Utils; using System.Text.Json.Serialization; +using System.Windows.Controls; +using System.Windows.Input; namespace PCL; -public partial class MyMsgLogin +public partial class MyMsgLogin : IMsgBoxControl { - private readonly JsonObject data; - private string deviceCode; // 用于轮询的设备代码 - private string oAuthUrl = ""; // OAuth 轮询验证地址 - private string userCode; // 需要用户在网页上输入的设备代码 - private string website; // 验证网页的网址 - private Task? workingThread; + private readonly JsonObject? _data; + private string _deviceCode = ""; + private string _oAuthUrl = ""; + private string _userCode = ""; + private string _website = ""; + private Task? _workingThread; + + public MsgBoxRequest Request { get; } + public event EventHandler? Completed; + + private readonly MsgBoxAnimationProfile _anim; + private bool _isExited; + private readonly string _animGroup; public MyMsgLogin() { InitializeComponent(); - // Handles Loaded += Load; Btn1.Click += Btn1_Click; Btn3.Click += Btn3_Click; PanBorder.MouseLeftButtonDown += Drag; LabTitle.MouseLeftButtonDown += Drag; + Request = new MsgBoxRequest(); + _anim = MsgBoxAnimationProfile.ForTheme(MsgBoxTheme.Info); + _animGroup = "MyMsgLogin designer"; + _data = null; } - private void Finished(object result) + public MyMsgLogin(MsgBoxRequest request, JsonObject data) { - if (myConverter.IsExited) - return; - myConverter.IsExited = true; - myConverter.Result = result; - ModBase.RunInUi(Close); - Thread.Sleep(200); - ModMain.frmMain.ShowWindowToTop(); + Request = request; + _anim = MsgBoxAnimationProfile.ForTheme(request.Theme); + _animGroup = $"MyMsgLogin {Request.RequestId}"; + _data = data; + InitCommon(); + Init(); + } + + public MyMsgLogin(ModMain.MyMsgBoxConverter converter) + { + var isWarn = converter.IsWarn; + var data = (JsonObject)converter.Content; + + var request = new MsgBoxRequest + { + Caption = "", + Theme = isWarn ? MsgBoxTheme.Warning : MsgBoxTheme.Info, + Buttons = [new("", 1), new("", 2), new("", 3)], + IsBlocking = true, + Content = converter.Content + }; + Request = request; + _anim = MsgBoxAnimationProfile.ForTheme(request.Theme); + _animGroup = $"MyMsgBox {ModBase.GetUuid()}"; + _data = data; + _oAuthUrl = converter.AuthUrl.ToString() ?? ""; + _legacyConverter = converter; + + InitCommon(); + Init(); + } + + private readonly ModMain.MyMsgBoxConverter? _legacyConverter; + + private void LegacyComplete(object result) + { + if (_legacyConverter is null || _isExited) return; + _isExited = true; + _legacyConverter.IsExited = true; + _legacyConverter.Result = result; + _legacyConverter.WaitFrame.Continue = false; + } + + private void InitCommon() + { + InitializeComponent(); + Btn1.Name += ModBase.GetUuid(); + Btn2.Name += ModBase.GetUuid(); + Btn3.Name += ModBase.GetUuid(); + ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d); + Loaded += Load; } private void Init() { - userCode = (string)data["user_code"]; - deviceCode = (string)data["device_code"]; - ModBase.ClipboardSet(deviceCode); - if (data["verification_uri_complete"] is not null) + if (_data is null) return; + _userCode = (string)_data["user_code"]!; + _deviceCode = (string)_data["device_code"]!; + ModBase.ClipboardSet(_deviceCode); + if (_data["verification_uri_complete"] is not null) { - website = (string)data["verification_uri_complete"]; - LabCaption.Text = Lang.Text("Launch.Account.LoginDialog.MicrosoftInstructions.WithAutoFill", userCode, website); + _website = (string)_data["verification_uri_complete"]!; + LabCaption.Text = Lang.Text("Launch.Account.LoginDialog.MicrosoftInstructions.WithAutoFill", _userCode, _website); } else { - website = (string)data["verification_uri"]; - LabCaption.Text = Lang.Text("Launch.Account.LoginDialog.MicrosoftInstructions", userCode, website); + _website = (string)_data["verification_uri"]!; + LabCaption.Text = Lang.Text("Launch.Account.LoginDialog.MicrosoftInstructions", _userCode, _website); } - // 设置 UI LabTitle.Text = Lang.Text("Launch.Account.LoginDialog.MinecraftLogin"); - CustomEventService.SetEventData(Btn1, website); - CustomEventService.SetEventData(Btn2, userCode); - // 启动工作线程 - workingThread = WorkThreadAsync(); + CustomEventService.SetEventData(Btn1, _website); + CustomEventService.SetEventData(Btn2, _userCode); + _workingThread = WorkThreadAsync(); + } + + private void Finished(object result) + { + if (_isExited) return; + _isExited = true; + + if (_legacyConverter is not null) + { + // 旧路径:直接写 Converter + LegacyComplete(result); + ModBase.RunInUi(() => _ = InvokeCloseAnimationAsync(MsgBoxResponse.Cancelled(Request.RequestId))); + } + else + { + // 新路径:触发 Completed 事件 + var response = result switch + { + string[] tokens => new MsgBoxResponse + { + RequestId = Request.RequestId, + ButtonValue = 1, + Button = new MsgBoxButtonInfo("", 1) + }, + _ => MsgBoxResponse.Cancelled(Request.RequestId) + }; + ModBase.RunInUi(() => Completed?.Invoke(this, response)); + } + Thread.Sleep(200); + ModMain.frmMain.ShowWindowToTop(); } private record ErrorBody( @@ -71,18 +155,17 @@ private record ErrorBody( private async Task WorkThreadAsync() { await Task.Delay(2000).ConfigureAwait(false); - if (myConverter.IsExited) - return; - ModBase.OpenWebsite(website); - ModBase.ClipboardSet(userCode); - var delayTime = (data["interval"].ToObject() - 1) * 1000; - // 轮询 + if (_isExited) return; + ModBase.OpenWebsite(_website); + ModBase.ClipboardSet(_userCode); + var delayTime = (_data!["interval"]!.ToObject() - 1) * 1000; + var unknownFailureCount = 0; - while (!myConverter.IsExited) + while (!_isExited) { try { - var bodyData = $"grant_type=urn:ietf:params:oauth:grant-type:device_code&client_id={Secrets.MSOAuthClientId}&device_code={deviceCode}&scope=XboxLive.signin%20offline_access"; + var bodyData = $"grant_type=urn:ietf:params:oauth:grant-type:device_code&client_id={Secrets.MSOAuthClientId}&device_code={_deviceCode}&scope=XboxLive.signin%20offline_access"; using var result = await HttpRequest .Create("https://login.microsoftonline.com/consumers/oauth2/v2.0/token") .WithFormContent(bodyData) @@ -90,28 +173,22 @@ private async Task WorkThreadAsync() .ConfigureAwait(false); if (!result.IsSuccess) { - var error = await result.AsJsonAsync() - .ConfigureAwait(false); - switch(error?.Error) + var error = await result.AsJsonAsync().ConfigureAwait(false); + switch (error?.Error) { case "authorization_pending": - { - await Task.Delay(delayTime) - .ConfigureAwait(false); - continue; - } + await Task.Delay(delayTime).ConfigureAwait(false); + continue; default: - { - throw new Exception(error?.Error ?? "Unable to get body"); - } + throw new Exception(error?.Error ?? "Unable to get body"); } } - // 获取结果 + var ctx = await result.AsStringAsync().ConfigureAwait(false); var resultJson = (JsonObject)ModBase.GetJson(ctx); ModProfile.ProfileLog($"令牌过期时间:{resultJson["expires_in"]} 秒"); HintService.Hint(Lang.Text("Launch.Account.LoginDialog.Success"), HintType.Success); - Finished(new[] { resultJson["access_token"].ToString(), resultJson["refresh_token"].ToString() }); + Finished(new[] { resultJson["access_token"]!.ToString(), resultJson["refresh_token"]!.ToString() }); return; } catch (Exception ex) @@ -132,57 +209,15 @@ await Task.Delay(delayTime) } } - - #region 弹窗 - - private readonly ModMain.MyMsgBoxConverter myConverter; - private readonly int uuid = ModBase.GetUuid(); - - public MyMsgLogin(ModMain.MyMsgBoxConverter converter) - { - try - { - InitializeComponent(); - Btn1.Name += ModBase.GetUuid(); - Btn2.Name += ModBase.GetUuid(); - Btn3.Name += ModBase.GetUuid(); - myConverter = converter; - ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d); - data = (JsonObject)converter.Content; - oAuthUrl = converter.AuthUrl?.ToString() ?? ""; - Init(); - } - catch (Exception ex) - { - ModBase.Log(ex, Lang.Text("Launch.Account.LoginDialog.Error.Init"), ModBase.LogLevel.Hint); - } - - Loaded += Load; - } - private void Load(object sender, EventArgs e) { try { - // 动画 - Opacity = 0d; + Btn3.IsEnabled = false; ModAnimation.AniStart( - ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, BlurBorder.BackgroundProperty, - (myConverter.IsWarn - ? new ModBase.MyColor(140d, 80d, 0d, 0d) - : new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.frmMain.PanMsgBackground.Background, 200), - "PanMsgBackground Background"); - ModAnimation.AniStart( - new[] - { - ModAnimation.AaOpacity(this, 1d, 120, 60), - ModAnimation.AaDouble(i => TransformPos.Y += (double)i, - -TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), - ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, - -TransformRotate.Angle, 300, 60, - new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)) - }, "MyMsgBox " + uuid); - // 记录日志 + ModAnimation.AaCode(() => Btn3.IsEnabled = true, 120000), + "MyMsgBox " + (Request.RequestId)); + InvokeShowAnimation(); ModBase.Log($"[Control] 正版验证弹窗:{LabTitle.Text}\r\n{LabCaption.Text}"); } catch (Exception ex) @@ -191,44 +226,32 @@ private void Load(object sender, EventArgs e) } } - private void Close() + public void InvokeShowAnimation() { - // 动画 - ModAnimation.AniStart(new[] - { - ModAnimation.AaCode(() => - { - if (!ModMain.WaitingMyMsgBox.Any()) - ModAnimation.AniStart(ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, - BlurBorder.BackgroundProperty, - new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.frmMain.PanMsgBackground.Background, 200, - ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))); - }, 30), - ModAnimation.AaOpacity(this, -Opacity, 80, 20), - ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y, - 150, 0, new ModAnimation.AniEaseOutFluent()), - ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, - 6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)), - ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), after: true) - }, "MyMsgBox " + uuid); + Opacity = 0d; + MsgBoxAnimations.AnimateShow(this, TransformPos, TransformRotate, _anim, _animGroup); + } + + public async Task InvokeCloseAnimationAsync(MsgBoxResponse response) + { + await MsgBoxAnimations.AnimateCloseAsync(this, TransformPos, TransformRotate, _anim, _animGroup).ConfigureAwait(true); + if (Parent is Grid g) g.Children.Remove(this); } - // 实现回车和 Esc 的接口(#4857) public void Btn1_Click(object sender, MouseButtonEventArgs e) { + // Btn1 负责打开浏览器(由 CustomEventService.SetEventData 处理) } public void Btn3_Click(object sender, MouseButtonEventArgs e) { - Finished(new ThreadInterruptedException()); + if (!_isExited) + Finished(new ThreadInterruptedException()); } private void Drag(object sender, MouseButtonEventArgs e) { - // On Error Resume Next if (e.GetPosition(ShapeLine).Y <= 2d) ModMain.frmMain.DragMove(); } - - #endregion -} \ No newline at end of file +}