Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions PCL.Core/App/Configuration/Storage/FileConfigStorage.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
using PCL.Core.Logging;
using PCL.Core.UI;
using PCL.Core.UI.MsgBox;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
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;

Expand Down Expand Up @@ -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);
}
}
Expand Down
129 changes: 129 additions & 0 deletions PCL.Core/App/Essentials/MsgBoxService.cs
Original file line number Diff line number Diff line change
@@ -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<MsgBoxRequest> _Channel = Channel.CreateUnbounded<MsgBoxRequest>(
new UnboundedChannelOptions
{
SingleReader = true,
SingleWriter = false
});

private static readonly ConcurrentDictionary<Guid, TaskCompletionSource<MsgBoxResponse>> _Pending = [];
public static ChannelReader<MsgBoxRequest> 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<MsgBoxResponse> 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<MsgBoxResponse>(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();
}

/// <summary>
/// Used by UI layer to complete the request
/// </summary>
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<MsgBoxResponse>(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();
}
}
14 changes: 8 additions & 6 deletions PCL.Core/Logging/LogService.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions PCL.Core/UI/MsgBox/IMsgBoxControl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using System.Threading.Tasks;

namespace PCL.Core.UI.MsgBox;

/// <summary>
/// MsgBox UI Control interface
/// Used by MsgBoxActor<br/>
/// MVVM Control will implement this interface in the future
/// </summary>
public interface IMsgBoxControl
{
MsgBoxRequest Request { get; }
event EventHandler<MsgBoxResponse>? Completed;
void InvokeShowAnimation();
Task InvokeCloseAnimationAsync(MsgBoxResponse response);
}
9 changes: 9 additions & 0 deletions PCL.Core/UI/MsgBox/MsgBoxButtonInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;

namespace PCL.Core.UI.MsgBox;

public record MsgBoxButtonInfo(
string Text,
int Value = 0,
Action? OnClick = null
);
38 changes: 38 additions & 0 deletions PCL.Core/UI/MsgBox/MsgBoxRequest.cs
Original file line number Diff line number Diff line change
@@ -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<IValidator<string>>? ValidateRules { get; init; } = null;
public IReadOnlyList<MsgBoxButtonInfo> Buttons { get; init; } = [];
public bool IsBlocking { get; init; } = true;
public CancellationToken CancellationToken { get; init; }

/// <summary>
/// Gets or sets the timeout duration for the message box. If set, the message box will automatically close after the specified time has elapsed. <br/>
/// <see langword="null"/> means infinite.
/// </summary>
public TimeSpan? Timeout { get; init; } = null;
}

public enum MsgBoxRequestType
{
Text,
Select,
Input,
Login,
Markdown
}
17 changes: 17 additions & 0 deletions PCL.Core/UI/MsgBox/MsgBoxResponse.cs
Original file line number Diff line number Diff line change
@@ -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
};
}
8 changes: 8 additions & 0 deletions PCL.Core/UI/MsgBox/MsgBoxTheme.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace PCL.Core.UI.MsgBox;

public enum MsgBoxTheme
{
Info,
Warning,
Error
}
15 changes: 2 additions & 13 deletions PCL.Core/UI/MsgBoxWrapper.cs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
50 changes: 50 additions & 0 deletions Plain Craft Launcher 2/Controls/MyMsg/MsgBoxAnimationProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using PCL.Core.UI.MsgBox;

namespace PCL.Controls.MyMsg;

/// <summary>
/// 弹窗动画参数配置。由工厂方法 <see cref="ForTheme" /> 按 <see cref="MsgBoxTheme" /> 生成不同配置。
/// 控件只管按参数执行,不需要判断 isWarn / 按钮数 等分支。
/// </summary>
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
};
}
Loading