diff --git a/PCL.Core/App/Config.cs b/PCL.Core/App/Config.cs index 1ff4c17ef..cc6aa536f 100644 --- a/PCL.Core/App/Config.cs +++ b/PCL.Core/App/Config.cs @@ -37,6 +37,11 @@ public static partial class Config /// 动画帧率上限。 /// [ConfigItem("UiAniFPS", 59)] public partial int AnimationFpsLimit { get; set; } + + /// + /// 隐藏的公告 + /// + [ConfigItem("HiddenAnnouncementList", "[]")] public partial string HiddenAnnouncement { get; set; } } /// diff --git a/PCL.Core/App/Essentials/Announcement/AnnouncementService.cs b/PCL.Core/App/Essentials/Announcement/AnnouncementService.cs new file mode 100644 index 000000000..f8572dd48 --- /dev/null +++ b/PCL.Core/App/Essentials/Announcement/AnnouncementService.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using PCL.Core.App.Essentials.Announcement.Models; +using PCL.Core.App.IoC; +using PCL.Core.IO.Net.Http; +using PCL.Core.Logging; +using PCL.Core.UI; + +namespace PCL.Core.App.Essentials.Announcement; + +[LifecycleScope("announcement","公告")] +[LifecycleService(LifecycleState.Running)] +public partial class AnnouncementService +{ + + private static readonly string[] _AllowScheme = ["http", "https", "minecraft" ]; + private static readonly string[] _AnnouncementServerList = Secrets.AnnouncementServerList; + + private static List _ignored = + JsonSerializer.Deserialize(Config.System.HiddenAnnouncement)?.ToList() ?? []; + + [LifecycleStart] + private static async Task _Start() + { + // 可能会出现公告服务比配置服务晚关闭的情况 + Lifecycle.StateChanged += state => + { + if (state == LifecycleState.Closing) Config.System.HiddenAnnouncement = JsonSerializer.Serialize(_ignored); + }; + try + { + foreach (var source in _AnnouncementServerList) + { + var response = await HttpRequest.GetJsonAsync>(source) + .ConfigureAwait(false); + if (response is null) continue; + + // 对忽略的公告进行检查1,以确保仍然处于公告列表内 + + var invalid = _ignored.Except(response.Select(a => a.Id)).ToList(); + _ignored.RemoveAll(invalid.Contains); + + var announcements = response.OrderByDescending(a => a.Priority).Where(a => + { + var isNotAfterValid = DateTimeOffset.TryParse(a.SkipOn.NotAfter, out var notAfter); + var isNotBeforeValid = DateTimeOffset.TryParse(a.SkipOn.NotBefore, out var notBefore); + var localTime = DateTimeOffset.Now; + if (isNotAfterValid && localTime > notAfter) return false; + if (isNotBeforeValid && localTime < notBefore) return false; + var currentVersion = new Version(Basics.VersionName.Split("-")[0]); + var max = new Version(a.SkipOn.MaxVersion ?? "999.999.999"); + var min = new Version(a.SkipOn.MinVersion ?? "0.0.0"); + + // [min,max] + return currentVersion >= min && currentVersion <= max; + + }); + foreach (var detail in announcements) + { + Context.Debug(MsgBoxWrapper.ShowWithCustomButtons( + detail.Details, $"{detail.Title} ({detail.ReleaseDate})", _GetSelectTheme(detail.Level), + false, + detail.Buttons.Select(operation => new MsgBoxButtonInfo(operation.ButtonText, + OnClick: _GetSelectCallback(operation.Operation, operation.Argument))).ToArray()).ToString()); + } + } + } + catch (HttpRequestException ex) + { + Context.Error("加载公告失败", ex, ActionLevel.HintErr); + } + } + + private static Action _GetSelectCallback(string operation, string arguments) => operation switch + { + "OpenWebSite" => () => + { + if (arguments.Length == 0) throw new ArgumentException("Uri is missing"); + if (_AllowScheme.All(s => new Uri(arguments).Scheme != s)) + throw new InvalidOperationException("This uri contains a unsupported scheme."); + Process.Start(new ProcessStartInfo(arguments){ UseShellExecute = true }); + + }, + "StopShow" => () => + { + _ignored.Add(arguments); + }, + _ => static () => { } + }; + + private static MsgBoxTheme _GetSelectTheme(AnnouncementLevel level) => level switch + { + AnnouncementLevel.Medium => MsgBoxTheme.Warning, + AnnouncementLevel.Highest => MsgBoxTheme.Error, + _ => MsgBoxTheme.Info + }; +} diff --git a/PCL.Core/App/Essentials/Announcement/Models/AnnouncementDetails.cs b/PCL.Core/App/Essentials/Announcement/Models/AnnouncementDetails.cs new file mode 100644 index 000000000..01ad6463b --- /dev/null +++ b/PCL.Core/App/Essentials/Announcement/Models/AnnouncementDetails.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace PCL.Core.App.Essentials.Announcement.Models; + +public record AnnouncementDetails +{ + /// + /// 公告标题 + /// + [JsonPropertyName("title")] + public required string Title { get; init; } + + /// + /// 公告内容 + /// + [JsonPropertyName("details")] + public required string Details { get; init; } + + /// + /// 该公告的优先级,值越高优先级越高 + /// + [JsonPropertyName("priority")] + public int Priority { get; init; } + + /// + /// 公告 ID + /// + [JsonPropertyName("id")] + public required string Id { get; init; } + + /// + /// 该公告的等级,决定弹窗应该用什么样式 + /// + [JsonPropertyName("level")] + public required AnnouncementLevel Level { get; init; } + + /// + /// 该公告的发布日期 + /// + [JsonPropertyName("date")] + public required string ReleaseDate { get; init; } + + /// + /// 显示条件 + /// + [JsonPropertyName("skip")] + public required AnnouncementSkipCondition SkipOn { get; init; } + + /// + /// 弹窗按钮信息 + /// + [JsonPropertyName("buttons")] + public required IEnumerable Buttons { get; init; } +} \ No newline at end of file diff --git a/PCL.Core/App/Essentials/Announcement/Models/AnnouncementLevel.cs b/PCL.Core/App/Essentials/Announcement/Models/AnnouncementLevel.cs new file mode 100644 index 000000000..91ee301d1 --- /dev/null +++ b/PCL.Core/App/Essentials/Announcement/Models/AnnouncementLevel.cs @@ -0,0 +1,17 @@ +namespace PCL.Core.App.Essentials.Announcement.Models; + +public enum AnnouncementLevel +{ + /// + /// 最低的等级,属于可看可不看的那种 + /// + Lowest, + /// + /// 用户应该稍微有点了解的公告 + /// + Medium, + /// + /// 必须让用户知道并理解的公告内容 + /// + Highest +} \ No newline at end of file diff --git a/PCL.Core/App/Essentials/Announcement/Models/AnnouncementOperation.cs b/PCL.Core/App/Essentials/Announcement/Models/AnnouncementOperation.cs new file mode 100644 index 000000000..20358be4c --- /dev/null +++ b/PCL.Core/App/Essentials/Announcement/Models/AnnouncementOperation.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace PCL.Core.App.Essentials.Announcement.Models; + +public record AnnouncementOperation +{ + /// + /// 按钮文本 + /// + [JsonPropertyName("text")] + public required string ButtonText { get; init; } + + /// + /// 按下后的操作 + /// + [JsonPropertyName("exec")] + public required string Operation { get; init; } + + /// + /// 参数列表 + /// + [JsonPropertyName("argument")] + public required string Argument { get; init; } +} \ No newline at end of file diff --git a/PCL.Core/App/Essentials/Announcement/Models/AnnouncementSkipCondition.cs b/PCL.Core/App/Essentials/Announcement/Models/AnnouncementSkipCondition.cs new file mode 100644 index 000000000..e30758b3b --- /dev/null +++ b/PCL.Core/App/Essentials/Announcement/Models/AnnouncementSkipCondition.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace PCL.Core.App.Essentials.Announcement.Models; + +public class AnnouncementSkipCondition +{ + [JsonPropertyName("min")] + public string? MinVersion { get; init; } + [JsonPropertyName("max")] + public string? MaxVersion { get; init; } + [JsonPropertyName("notAfter")] + public string? NotAfter { get; init; } + [JsonPropertyName("notBefore")] + public string? NotBefore { get; init; } + +} diff --git a/PCL.Core/App/Secrets.cs b/PCL.Core/App/Secrets.cs index 8c0b91edf..f9a70cfb4 100644 --- a/PCL.Core/App/Secrets.cs +++ b/PCL.Core/App/Secrets.cs @@ -40,4 +40,10 @@ public static class Secrets /// 当前版本的 Git 提交 SHA /// public static string CommitHash { get; } = EnvironmentInterop.GetSecret("GITHUB_SHA", readEnvDebugOnly: true).ReplaceNullOrEmpty(); + + /// + /// 公告服务器地址 + /// + public static string[] AnnouncementServerList { get; } = EnvironmentInterop + .GetSecret("ANNOUNCEMENT_SERVER", readEnvDebugOnly: true).ReplaceNullOrEmpty().Split("|"); }