diff --git a/PCL.Core/App/Localization/Languages/en-US.xaml b/PCL.Core/App/Localization/Languages/en-US.xaml index 43250bafb3..4eab8f49b2 100644 --- a/PCL.Core/App/Localization/Languages/en-US.xaml +++ b/PCL.Core/App/Localization/Languages/en-US.xaml @@ -66,6 +66,8 @@ Cancel Close OK + Toast details + Click to view full content Continue Copy Decline diff --git a/PCL.Core/App/Localization/Languages/zh-CN.xaml b/PCL.Core/App/Localization/Languages/zh-CN.xaml index 4834027e96..237fd8a6ff 100644 --- a/PCL.Core/App/Localization/Languages/zh-CN.xaml +++ b/PCL.Core/App/Localization/Languages/zh-CN.xaml @@ -66,6 +66,8 @@ 取消 关闭 确定 + 提示详情 + 点击查看完整内容 继续 复制 拒绝 diff --git a/Plain Craft Launcher 2/Controls/MyToast.xaml b/Plain Craft Launcher 2/Controls/MyToast.xaml index ac47e33575..649f11c235 100644 --- a/Plain Craft Launcher 2/Controls/MyToast.xaml +++ b/Plain Craft Launcher 2/Controls/MyToast.xaml @@ -8,7 +8,7 @@ CornerRadius="8" BorderThickness="0" UseLayoutRounding="True" SnapsToDevicePixels="True"> - + @@ -28,18 +28,23 @@ - - - - - - + + + + + + + + + diff --git a/Plain Craft Launcher 2/Controls/MyToast.xaml.cs b/Plain Craft Launcher 2/Controls/MyToast.xaml.cs index 92d4efc7ee..2f66be30af 100644 --- a/Plain Craft Launcher 2/Controls/MyToast.xaml.cs +++ b/Plain Craft Launcher 2/Controls/MyToast.xaml.cs @@ -4,7 +4,9 @@ using System.Windows.Media; using System.Windows.Media.Animation; using PCL.Core.App; +using PCL.Core.App.Localization; using PCL.Core.UI.Theme; +using PCL.Core.Utils; namespace PCL; @@ -40,6 +42,14 @@ public partial class MyToast // 进度条状态 private double _progressStartWidth; private double _progressTotalMs; + private bool _pausedByHover; + private double _hoverRemainingMs; + + /// 悬停暂停开始时刻(毫秒),配合 _hoverRemainingMs 判断暂停期间倒计时是否已走完。 + private long _pauseStartedAtTick; + + /// 当前隐藏动画倒计时结束、实际开始滑出/淡出的起始 tick(0 表示无待滑出的隐藏)。 + private long _hideStartsAtTick; public MyToast() { @@ -49,6 +59,8 @@ public MyToast() PreviewMouseMove += Toast_PreviewMouseMove; PreviewMouseLeftButtonUp += Toast_PreviewMouseLeftButtonUp; LostMouseCapture += Toast_LostMouseCapture; + Root.MouseEnter += Root_MouseEnter; + Root.MouseLeave += Root_MouseLeave; Loaded += (_, _) => { UpdateColors(); @@ -64,7 +76,9 @@ public MyToast() ModAnimation.AniStop($"Toast Dismiss {Uuid}"); ModAnimation.AniStop($"Toast Emphasize {Uuid}"); ModAnimation.AniStop($"Toast Drag Return {Uuid}"); + ModAnimation.AniStop($"Toast StackSettle {Uuid}"); ProgressBar.BeginAnimation(WidthProperty, null); + ResetHoverPause(); }; } @@ -77,6 +91,13 @@ public string Context set => TitleText.Text = value; } + // 点击展开,展示完整内容 + private void ShowDetail() + { + if (IsDismissing) return; + ModMain.MyMsgBox(Context, Lang.Text("Main.Toast.Detail.Title"), Lang.Text("Common.Action.Confirm")); + } + public string Icon { get; set; } = "lucide/info"; public HintType ToastType { get; set; } = HintType.Info; @@ -85,14 +106,15 @@ public string Context public bool IsDismissing { get; private set; } - private double _targetHeight; + /// 弹窗完全展开后的目标高度,用于纵向排布的定位计算。 + internal double _targetHeight; public void Show() { if (Parent is not Panel) return; if (System.Windows.Application.Current.MainWindow is not null) - MaxWidth = System.Windows.Application.Current.MainWindow.ActualWidth * 0.9; + MaxWidth = Math.Min(System.Windows.Application.Current.MainWindow.ActualWidth * 0.9, 360d); Margin = new Thickness(0, 0, 16, 4); Opacity = 0; @@ -103,12 +125,18 @@ public void Show() RenderTransform = new TranslateTransform(60, 0); + // 图标“按入”微动效:0.8 → 1,略带过头回弹,呼应启动器按钮的按压语言 + ToastIcon.RenderTransformOrigin = new Point(0.5, 0.5); + ToastIcon.RenderTransform = new ScaleTransform(0.8, 0.8); + ModAnimation.AniStop($"Toast Drag Return {Uuid}"); var enterAnimations = new List { - ModAnimation.AaTranslateX(this, -60, 400, ease: new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaTranslateX(this, -60, 380, ease: new ModAnimation.AniEaseOutCar()), ModAnimation.AaHeight(this, _targetHeight, 150, ease: new ModAnimation.AniEaseOutFluent()), - ModAnimation.AaOpacity(this, 1, 100) + ModAnimation.AaOpacity(this, 1, 100), + ModAnimation.AaScaleTransform(ToastIcon, 0.2, 260, 120, + ease: new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)) }; ModAnimation.AniStart(enterAnimations, $"Toast Show {Uuid}"); @@ -121,16 +149,19 @@ public void Emphasize() ModAnimation.AniStop($"Toast Hide {Uuid}"); ModAnimation.AniStop($"Toast Emphasize {Uuid}"); ModAnimation.AniStop($"Toast Drag Return {Uuid}"); + _hideStartsAtTick = 0; // 隐藏倒计时被取消,复位标志避免 IsHiding 误判 ProgressBar.BeginAnimation(WidthProperty, null); + ResetHoverPause(); if (RenderTransform is TranslateTransform tt) tt.X = 0; Opacity = 1; Height = _targetHeight; + ToastIcon.RenderTransform = new ScaleTransform(1, 1); // 若入场动效尚未结束,先复位图标缩放 ModAnimation.AniStart(new List { - ModAnimation.AaTranslateX(this, -8, 70, ease: new ModAnimation.AniEaseOutFluent()), - ModAnimation.AaTranslateX(this, 16, 70, after: true), - ModAnimation.AaTranslateX(this, -8, 60, after: true, ease: new ModAnimation.AniEaseOutFluent()), - ModAnimation.AaCode(RestartHideAnimation, after: true), + ModAnimation.AaTranslateX(this, -14, 90, ease: new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaTranslateX(this, 18, 100, after: true, ease: new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaTranslateX(this, -4, 110, after: true, ease: new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaCode(RestartHideAnimation, after: true) }, $"Toast Emphasize {Uuid}"); } @@ -143,15 +174,19 @@ private void RestartHideAnimation() private void StartHideAnimation(double delayMs) { var delay = (int)Math.Round(delayMs); + _hideStartsAtTick = TimeUtils.GetTimeTick() + delay; ModAnimation.AniStart(new List { - ModAnimation.AaTranslateX(this, 60, 200, delay, new ModAnimation.AniEaseInFluent()), - ModAnimation.AaOpacity(this, -1, 150, delay), + ModAnimation.AaTranslateX(this, 60, 150, delay, new ModAnimation.AniEaseInFluent()), + ModAnimation.AaOpacity(this, -1, 110, delay), ModAnimation.AaHeight(this, -_targetHeight, 100, ease: new ModAnimation.AniEaseOutFluent(), after: true), ModAnimation.AaCode(() => { if (Parent is Panel p) + { p.Children.Remove(this); + HintService.OnToastRemoved(this); + } }, after: true) }, $"Toast Hide {Uuid}"); } @@ -167,7 +202,10 @@ public void Dismiss() ModAnimation.AniStop($"Toast Hide {Uuid}"); ModAnimation.AniStop($"Toast Emphasize {Uuid}"); ModAnimation.AniStop($"Toast Drag Return {Uuid}"); + ModAnimation.AniStop($"Toast StackSettle {Uuid}"); + _hideStartsAtTick = 0; // 隐藏动画被取消(改为滑动关闭),复位标志避免 IsHiding 误判 ProgressBar.BeginAnimation(WidthProperty, null); + ResetHoverPause(); ModAnimation.AniStart(new List { ModAnimation.AaTranslateX(this, 60, 150, ease: new ModAnimation.AniEaseInFluent()), @@ -175,7 +213,10 @@ public void Dismiss() ModAnimation.AaCode(() => { if (Parent is Panel p) + { p.Children.Remove(this); + HintService.OnToastRemoved(this); + } }, after: true) }, $"Toast Dismiss {Uuid}"); } @@ -202,25 +243,31 @@ private void RootGrid_SizeChanged(object sender, SizeChangedEventArgs e) private void UpdateColors() { - var baseHue = ToastType switch - { - HintType.Success => 145d, - HintType.Error => 355d, - HintType.Warning => 40d, - _ => 210d - }; + var isInfo = ToastType == HintType.Info; var res = System.Windows.Application.Current.Resources; - var accent = new ModBase.MyColor().FromHSL2(baseHue, 75, 60); + // 信息类型整体用主题色,其余类型用饱和状态色 + var accentBrush = isInfo + ? (Brush)res["ColorBrush2"] + : new SolidColorBrush(ToastType switch + { + HintType.Success => new ModBase.MyColor().FromHSL2(145d, 75d, 60d), + HintType.Error => new ModBase.MyColor().FromHSL2(355d, 75d, 60d), + HintType.Warning => new ModBase.MyColor().FromHSL2(40d, 75d, 60d), + _ => new ModBase.MyColor().FromHSL2(210d, 75d, 60d) + }); var bg = ThemeService.IsDarkMode ? new SolidColorBrush(LabColor.FromLch(0.35)) : (Brush)res["ColorBrushBackground"]; var text = (SolidColorBrush)res["ColorBrushGray1"]; - var accentBrush = new SolidColorBrush(accent); + var track = new SolidColorBrush(ThemeService.IsDarkMode + ? Color.FromArgb(70, 255, 255, 255) // 暗色卡片上略亮的内嵌暗槽 + : Color.FromArgb(60, 0, 0, 0)); // 亮色卡片上略暗的内嵌暗槽 - Root.Background = bg; - Root.BorderBrush = bg; + // 卡片背景移到 RootGrid:模糊 RootGrid 时背景一起模糊,并被 8px 圆角裁剪;阴影仍在 Root 上不受影响 + RootGrid.Background = bg; TitleText.Foreground = text; - ProgressBar.Fill = accentBrush; + ProgressTrack.Fill = track; + ProgressBar.Fill = accentBrush; // Info 为主题色,其余类型为状态色 BtnClose.Foreground = text; ToastIcon.Icon = Icon; ToastIcon.IconBrush = accentBrush; @@ -284,6 +331,7 @@ private void Toast_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs if (_dragPending && !_isDragging) { _dragPending = false; + ShowDetail(); return; } @@ -325,6 +373,7 @@ private void BeginDrag(double initialDelta) ModAnimation.AniStop($"Toast Hide {Uuid}"); ModAnimation.AniStop($"Toast Emphasize {Uuid}"); ModAnimation.AniStop($"Toast Drag Return {Uuid}"); + _hideStartsAtTick = 0; // 拖拽取消隐藏倒计时,复位标志避免 IsHiding 误判 PauseProgress(); @@ -364,7 +413,11 @@ private void ReturnFromDrag() { ModAnimation.AaTranslateX(this, -currentX, ReturnAnimationMs, ease: new ModAnimation.AniEaseOutFluent()), ModAnimation.AaOpacity(this, 1d - currentOpacity, ReturnAnimationMs), - ModAnimation.AaCode(() => StartHideAnimation(GetProgressRemainingMs()), after: true) + ModAnimation.AaCode(() => + { + HintService.RearrangeToasts(); // 复位后重排,让位/补位的旧弹窗回到正确位置 + StartHideAnimation(GetProgressRemainingMs()); + }, after: true) }, $"Toast Drag Return {Uuid}"); } @@ -427,5 +480,56 @@ private double GetProgressRemainingMs() return _progressTotalMs * (currentWidth / _progressStartWidth); } + private void Root_MouseEnter(object sender, MouseEventArgs e) + { + if (_isDragging || _pausedByHover || IsDismissing) + return; + // 隐藏动画已进入滑出/淡出阶段:不悬停暂停,避免停掉半途的隐藏导致卡半透明、永不消失 + if (_hideStartsAtTick > 0 && TimeUtils.GetTimeTick() >= _hideStartsAtTick) + return; + // 悬停暂停:停住隐藏倒计时并冻结进度条,移出后按剩余时间恢复 + ModAnimation.AniStop($"Toast Hide {Uuid}"); + var currentWidth = ProgressBar.Width; + ProgressBar.BeginAnimation(WidthProperty, null); + ProgressBar.Width = currentWidth; + _hoverRemainingMs = GetProgressRemainingMs(); + _pauseStartedAtTick = TimeUtils.GetTimeTick(); + _hideStartsAtTick = 0; // 悬停暂停即取消隐藏倒计时,结束判定改由 _pauseStartedAtTick + 剩余时长得出 + _pausedByHover = true; + ProgressBar.Opacity = 0.4; + } + + private void Root_MouseLeave(object sender, MouseEventArgs e) + { + if (_isDragging) + return; // 拖拽接管进度条,悬停恢复交由 ReturnFromDrag 处理 + if (!_pausedByHover) + return; + _pausedByHover = false; + ProgressBar.Opacity = 1; + if (_hoverRemainingMs <= 0 || ProgressBar.Width <= 0) + return; // 已结束或未启动,无需恢复动画 + // 异常路径:暂停期间倒计时已走完,不再恢复倒计时,直接滑出消失 + if (_pauseStartedAtTick > 0 && TimeUtils.GetTimeTick() - _pauseStartedAtTick >= _hoverRemainingMs) + { + StartHideAnimation(0); + return; + } + StartHideAnimation(_hoverRemainingMs); + var currentWidth = ProgressBar.Width; + var anim = new DoubleAnimation(currentWidth, 0d, TimeSpan.FromMilliseconds(_hoverRemainingMs)); + ProgressBar.BeginAnimation(WidthProperty, anim); + } + + private void ResetHoverPause() + { + if (!_pausedByHover) + return; + _pausedByHover = false; + _hoverRemainingMs = 0; + _pauseStartedAtTick = 0; + ProgressBar.Opacity = 1; + } + #endregion } diff --git a/Plain Craft Launcher 2/FormMain.xaml b/Plain Craft Launcher 2/FormMain.xaml index eeef4f4325..c94578ec2f 100644 --- a/Plain Craft Launcher 2/FormMain.xaml +++ b/Plain Craft Launcher 2/FormMain.xaml @@ -219,10 +219,10 @@ UseLayoutRounding="False" SnapsToDevicePixels="False" /> - + 相邻弹窗之间的间距(像素)。 + private const double ToastGap = 4d; + private struct HintMessage { public string Text; @@ -42,7 +47,7 @@ internal static void Tick() try { ModMain.frmMain!.PanHint.HorizontalAlignment = HorizontalAlignment.Right; - ModMain.frmMain.PanHint.VerticalAlignment = VerticalAlignment.Bottom; + ModMain.frmMain.PanHint.VerticalAlignment = VerticalAlignment.Stretch; var extraHeight = ModMain.frmMain.PanExtraButtons.ActualHeight; ModMain.frmMain.PanHint.Margin = new Thickness(0, 0, 0, extraHeight > 0 ? extraHeight + 20 : 20); @@ -64,7 +69,7 @@ internal static void Tick() var activeCount = ModMain.frmMain.PanHint.Children.OfType().Count(t => !t.IsDismissing); if (activeCount >= 5) { - var oldest = ModMain.frmMain.PanHint.Children.OfType().FirstOrDefault(t => !t.IsDismissing); + var oldest = ModMain.frmMain.PanHint.Children.OfType().LastOrDefault(t => !t.IsDismissing); oldest?.Dismiss(); return; } @@ -83,8 +88,9 @@ internal static void Tick() DisplayDuration = (800d + ModBase.MathClamp(currentHint.Text.Length, 5d, 23d) * 180d) * ModAnimation.aniSpeed }; - ModMain.frmMain.PanHint.Children.Add(toast); + ModMain.frmMain.PanHint.Children.Insert(0, toast); toast.Show(); + RearrangeToasts(); if (currentHint.Log) ModBase.Log("[UI] 弹出提示:" + currentHint.Text); @@ -96,9 +102,53 @@ internal static void Tick() } } + // 非叠置布局:弹窗自底向上纵向排开,最新在最下,旧弹窗依次上移,彼此不重叠 + internal static void RearrangeToasts() + { + var toasts = ModMain.frmMain.PanHint.Children.OfType().Where(t => !t.IsDismissing).ToList(); + if (toasts.Count == 0) + return; + var available = ModMain.frmMain.PanHint.ActualHeight; + if (available > 0) + { + var required = toasts.Sum(t => t._targetHeight + ToastGap) + ToastGap; + if (required > available) + { + toasts[^1].Dismiss(); // 超出可用高度时收掉最旧的弹窗,保证其余完整可见 + return; + } + } + var bottom = ToastGap; + for (var i = 0; i < toasts.Count; i++) + { + var t = toasts[i]; // Children 顺序,索引 0 = 最新 + var oldBottom = t.Margin.Bottom; + var shift = oldBottom - bottom; // >0 下落补位,<0 上抬让位 + t.Margin = new Thickness(0, 0, 16, bottom); + t.VerticalAlignment = VerticalAlignment.Bottom; + Panel.SetZIndex(t, toasts.Count - 1 - i); + if (Math.Abs(shift) > 0.5) + { + var tt = t.RenderTransform as TranslateTransform ?? new TranslateTransform(); + t.RenderTransform = tt; + tt.Y = -shift; // 视觉先停在旧位置,再动画归零,形成整体层动 + ModAnimation.AniStart( + ModAnimation.AaTranslateY(t, shift, 200, ease: new ModAnimation.AniEaseOutFluent()), + $"Toast StackSettle {t.Uuid}"); + } + bottom += t._targetHeight + ToastGap; + } + } + public static void HideAll() { foreach (MyToast toast in ModMain.frmMain!.PanHint.Children.OfType().ToList()) toast.Dismiss(); } + + // 弹窗移除后回填错位(由 MyToast 移除自身后调用) + internal static void OnToastRemoved(MyToast toast) + { + RearrangeToasts(); + } }