龙腾猫跃 2 лет назад
Родитель
Сommit
0811b4f382
3 измененных файлов с 878 добавлено и 3 удалено
  1. 0 2
      .gitignore
  2. 1 1
      Plain Craft Launcher 2/FormMain.xaml.vb
  3. 877 0
      Plain Craft Launcher 2/Modules/ModMain.vb

+ 0 - 2
.gitignore

@@ -351,5 +351,3 @@ PageOtherTest.*
 PageSetupUI.*
 PageSetupUI.*
 # 开发者工具
 # 开发者工具
 ModDevelop.vb
 ModDevelop.vb
-# 不公开底层函数库,以免抄袭
-ModMain.vb

+ 1 - 1
Plain Craft Launcher 2/FormMain.xaml.vb

@@ -336,7 +336,7 @@ Public Class FormMain
         Setup.Load("UiBackgroundBlur")
         Setup.Load("UiBackgroundBlur")
         Setup.Load("UiLogoType")
         Setup.Load("UiLogoType")
         Setup.Load("UiHiddenPageDownload")
         Setup.Load("UiHiddenPageDownload")
-        BackgroundRefresh(False, True)
+        PageSetupUI.BackgroundRefresh(False, True)
         MusicRefreshPlay(False, True)
         MusicRefreshPlay(False, True)
         '扩展按钮
         '扩展按钮
         BtnExtraDownload.ShowCheck = AddressOf BtnExtraDownload_ShowCheck
         BtnExtraDownload.ShowCheck = AddressOf BtnExtraDownload_ShowCheck

+ 877 - 0
Plain Craft Launcher 2/Modules/ModMain.vb

@@ -0,0 +1,877 @@
+Imports System.Windows.Interop
+Imports System.Windows.Threading
+
+Public Module ModMain
+
+#Region "弹出提示"
+
+    ''' <summary>
+    ''' 提示信息的种类。
+    ''' </summary>
+    Public Enum HintType
+        ''' <summary>
+        ''' 信息,通常是蓝色的“i”。
+        ''' </summary>
+        ''' <remarks></remarks>
+        Info
+        ''' <summary>
+        ''' 已完成,通常是绿色的“√”。
+        ''' </summary>
+        ''' <remarks></remarks>
+        Finish
+        ''' <summary>
+        ''' 错误,通常是红色的“×”。
+        ''' </summary>
+        ''' <remarks></remarks>
+        Critical
+    End Enum
+    Private Structure HintMessage
+        Public Text As String
+        Public Type As HintType
+        Public Log As Boolean
+    End Structure
+
+    ''' <summary>
+    ''' 等待弹出的提示列表。以 {String, HintType, Log As Boolean} 形式存储为数组。
+    ''' </summary>
+    Private HintWaiting As List(Of HintMessage) = If(HintWaiting, New List(Of HintMessage))
+    ''' <summary>
+    ''' 在窗口左下角弹出提示文本。
+    ''' </summary>
+    Public Sub Hint(Text As String, Optional Type As HintType = HintType.Info, Optional Log As Boolean = True)
+        If HintWaiting Is Nothing Then HintWaiting = New List(Of HintMessage)
+        HintWaiting.Add(New HintMessage With {.Text = If(Text, ""), .Type = Type, .Log = Log})
+    End Sub
+
+    Private Sub HintTick()
+        Try
+
+            'Tag 存储了:{ 是否可以重用, Uuid }
+            If Not HintWaiting.Any() Then Exit Sub
+            Do While HintWaiting.Any
+                ''清除空提示
+                'If IsNothing(HintWaiting(0)) OrElse IsNothing(HintWaiting(0)(0)) Then
+                '    HintWaiting.RemoveAt(0)
+                '    Continue Do
+                'End If
+                Dim CurrentHint = HintWaiting(0)
+                '去回车
+                CurrentHint.Text = CurrentHint.Text.Replace(vbCrLf, " ").Replace(vbCr, " ").Replace(vbLf, " ")
+                '超量提示直接忽略
+                If FrmMain.PanHint.Children.Count >= 20 Then GoTo EndHint
+                '检查是否有重复提示
+                Dim DoubleStack As Border = Nothing
+                For Each stack As Border In FrmMain.PanHint.Children
+                    If stack.Tag(0) AndAlso CType(stack.Child, TextBlock).Text = CurrentHint.Text Then DoubleStack = stack
+                Next
+                '获取渐变颜色
+                Dim TargetColor0, TargetColor1 As MyColor
+                Dim Percent As Double = 0.3
+                Select Case CurrentHint.Type
+                    Case HintType.Info
+                        TargetColor0 = New MyColor(37, 155, 252)
+                        TargetColor1 = New MyColor(10, 142, 252)
+                    Case HintType.Finish
+                        TargetColor0 = New MyColor(33, 177, 33)
+                        TargetColor1 = New MyColor(29, 160, 29)
+                    Case Else 'HintType.Critical
+                        TargetColor0 = New MyColor(255, 53, 11)
+                        TargetColor1 = New MyColor(255, 43, 0)
+                End Select
+                If Not IsNothing(DoubleStack) Then
+                    '有重复提示,且该提示的进入动画已播放
+                    If Not AniIsRun("Hint Show " & DoubleStack.Tag(1)) Then
+                        AniStop("Hint Hide " & DoubleStack.Tag(1))
+                        Dim Delay As Double = (800 + MathClamp(CurrentHint.Text.Length, 5, 23) * 180) * AniSpeed
+                        AniStart({
+                            AaX(DoubleStack, -12 - DoubleStack.Margin.Left, 50,, New AniEaseOutFluent),
+                            AaX(DoubleStack, -8, 50, 50, New AniEaseInFluent),
+                            AaX(DoubleStack, 8, 50, 100, New AniEaseOutFluent),
+                            AaX(DoubleStack, -8, 50, 150, New AniEaseInFluent),
+                            AaDouble(Sub(i)
+                                         Percent += i
+                                         Dim Gradient As LinearGradientBrush = DoubleStack.Background
+                                         Gradient.GradientStops(0).Color = TargetColor0 * Percent + New MyColor(255, 255, 255) * (1 - Percent)
+                                         Gradient.GradientStops(1).Color = TargetColor1 * Percent + New MyColor(255, 255, 255) * (1 - Percent)
+                                     End Sub, 0.7, 250),
+                            AaX(DoubleStack, -50, 200, Delay, New AniEaseInFluent),
+                            AaOpacity(DoubleStack, -1, 150, Delay),
+                            AaCode(Sub() DoubleStack.Tag(0) = False, Delay),
+                            AaHeight(DoubleStack, -26, 100,, New AniEaseOutFluent, True),
+                            AaCode(Sub() FrmMain.PanHint.Children.Remove(DoubleStack), , True)
+                      }, "Hint Hide " & DoubleStack.Tag(1))
+                    End If
+                Else
+                    '准备控件
+                    Dim NewHintControl As New Border With {.Tag = {True, GetUuid()}, .Margin = New Thickness(-70, 0, 20, 0), .Opacity = 0, .Height = 0, .HorizontalAlignment = HorizontalAlignment.Left, .CornerRadius = New CornerRadius(0, 6, 6, 0)}
+                    NewHintControl.Background = New LinearGradientBrush(New GradientStopCollection(New List(Of GradientStop) From {
+                        New GradientStop(TargetColor0 * Percent + New MyColor(255, 255, 255) * (1 - Percent), 0),
+                        New GradientStop(TargetColor1 * Percent + New MyColor(255, 255, 255) * (1 - Percent), 1)}), 90)
+                    NewHintControl.Child = New TextBlock With {.TextTrimming = TextTrimming.CharacterEllipsis, .FontSize = 13, .Text = CurrentHint.Text, .Foreground = New MyColor(255, 255, 255), .Margin = New Thickness(33, 5, 8, 5)}
+                    'AddHandler NewHintControl.MouseLeftButtonDown, AddressOf HideAllHint
+                    FrmMain.PanHint.Children.Add(NewHintControl)
+                    '控件动画
+                    Dim Animations As New List(Of AniData)
+                    If FrmMain.PanHint.Children.Count > 1 Then
+                        '已有提示
+                        Animations.Add(AaHeight(NewHintControl, 26, 150, , New AniEaseOutFluent))
+                    Else
+                        '是唯一提示
+                        NewHintControl.Height = 26
+                    End If
+                    '开始动画
+                    Animations.AddRange({
+                        AaX(NewHintControl, 30, 400, , New AniEaseOutElastic(AniEasePower.Weak)),
+                        AaX(NewHintControl, 20, 200, , New AniEaseOutFluent),
+                        AaOpacity(NewHintControl, 1, 100),
+                        AaDouble(Sub(i)
+                                     Percent += i
+                                     Dim Gradient As LinearGradientBrush = NewHintControl.Background
+                                     Gradient.GradientStops(0).Color = TargetColor0 * Percent + New MyColor(255, 255, 255) * (1 - Percent)
+                                     Gradient.GradientStops(1).Color = TargetColor1 * Percent + New MyColor(255, 255, 255) * (1 - Percent)
+                                 End Sub, 0.7, 250, 100)
+                    })
+                    AniStart(Animations, "Hint Show " & NewHintControl.Tag(1))
+                    '结束动画
+                    Dim Delay As Double = (800 + MathClamp(CurrentHint.Text.Length, 5, 23) * 180) * AniSpeed
+                    AniStart({
+                        AaX(NewHintControl, -50, 200, Delay, New AniEaseInFluent),
+                        AaOpacity(NewHintControl, -1, 150, Delay),
+                        AaCode(Sub() NewHintControl.Tag(0) = False, Delay),
+                        AaHeight(NewHintControl, -26, 100,, New AniEaseOutFluent, True),
+                        AaCode(Sub() FrmMain.PanHint.Children.Remove(NewHintControl), , True)
+                    }, "Hint Hide " & NewHintControl.Tag(1))
+                End If
+                '结束处理
+EndHint:
+                If CurrentHint.Log Then Log("[UI] 弹出提示:" & CurrentHint.Text)
+                HintWaiting.RemoveAt(0)
+            Loop
+        Catch ex As Exception
+            Log(ex, "显示弹出提示失败", LogLevel.Normal)
+        End Try
+    End Sub
+    Private Sub HideAllHint()
+        For Each Control As Border In FrmMain.PanHint.Children
+            Control.IsHitTestVisible = False
+            AniStart({
+                AaX(Control, -50, 200, , New AniEaseInFluent),
+                AaOpacity(Control, -1, 150, , New AniEaseInFluent),
+                AaCode(Sub() Control.Tag(0) = False),
+                AaHeight(Control, -26, 100,, New AniEaseOutFluent, True),
+                AaCode(Sub() FrmMain.PanHint.Children.Remove(Control), , True)
+            }, "Hint Hide " & Control.Tag(1))
+        Next
+    End Sub
+
+#End Region
+
+#Region "弹窗"
+
+    ''' <summary>
+    ''' 存储弹窗信息的转换器。
+    ''' </summary>
+    Public Class MyMsgBoxConverter
+        Public Type As MyMsgBoxType
+        Public Title As String
+        Public Text As String
+        ''' <summary>
+        ''' 输入框模式:文本框的文本;选择模式:需要放进去的 List(Of MyListItem)。
+        ''' </summary>
+        Public Content As Object
+        ''' <summary>
+        ''' 仅输入框模式:输入验证规则。
+        ''' </summary>
+        Public ValidateRules As ObjectModel.Collection(Of Validate)
+        ''' <summary>
+        ''' 仅输入框模式:提示文本。
+        ''' </summary>
+        Public HintText As String = ""
+        ''' <summary>
+        ''' 有多个按钮时,是否给第一个按钮加高亮。
+        ''' </summary>
+        Public HighLight As Boolean
+        Public Button1 As String = "确定"
+        Public Button2 As String = ""
+        Public Button3 As String = ""
+        ''' <summary>
+        ''' 点击第一个按钮将执行该方法,不关闭弹窗。
+        ''' </summary>
+        Public Button1Action As Action = Nothing
+        ''' <summary>
+        ''' 点击第二个按钮将执行该方法,不关闭弹窗。
+        ''' </summary>
+        Public Button2Action As Action = Nothing
+        ''' <summary>
+        ''' 点击第三个按钮将执行该方法,不关闭弹窗。
+        ''' </summary>
+        Public Button3Action As Action = Nothing
+        Public IsWarn As Boolean = False
+        Public ForceWait As Boolean = False
+        Public WaitFrame As New DispatcherFrame(True)
+        ''' <summary>
+        ''' 弹窗是否已经关闭。
+        ''' </summary>
+        Public IsExited As Boolean = False
+        ''' <summary>
+        ''' 点击的按钮编号或输入的文本。若输入弹窗点击了非第一个按钮,则为 Nothing。
+        ''' </summary>
+        Public Result As Object
+    End Class
+    Public Enum MyMsgBoxType
+        Text
+        [Select]
+        Input
+    End Enum
+
+    ''' <summary>
+    ''' 显示弹窗,返回点击按钮的编号(从 1 开始)。
+    ''' </summary>
+    ''' <param name="Title">弹窗的标题。</param>
+    ''' <param name="Caption">弹窗的内容。</param>
+    ''' <param name="Button1">显示的第一个按钮,默认为“确定”。</param>
+    ''' <param name="Button2">显示的第二个按钮,默认为空。</param>
+    ''' <param name="Button3">显示的第三个按钮,默认为空。</param>
+    ''' <param name="Button1Action">点击第一个按钮将执行该方法,不关闭弹窗。</param>
+    ''' <param name="Button2Action">点击第二个按钮将执行该方法,不关闭弹窗。</param>
+    ''' <param name="Button3Action">点击第三个按钮将执行该方法,不关闭弹窗。</param>
+    ''' <param name="IsWarn">是否为警告弹窗,若为 True,弹窗配色和背景会变为红色。</param>
+    Public Function MyMsgBox(Caption As String, Optional Title As String = "提示",
+                             Optional Button1 As String = "确定", Optional Button2 As String = "", Optional Button3 As String = "",
+                             Optional IsWarn As Boolean = False, Optional HighLight As Boolean = True, Optional ForceWait As Boolean = False,
+                             Optional Button1Action As Action = Nothing, Optional Button2Action As Action = Nothing, Optional Button3Action As Action = Nothing) As Integer
+        '将弹窗列入队列
+        Dim Converter As New MyMsgBoxConverter With {.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 RunInUi() Then
+            '若为 UI 线程,立即执行弹窗刻, 避免快速(连点器)点击时多次弹窗
+            MyMsgBoxTick()
+        End If
+        If Button2.Length > 0 OrElse ForceWait Then
+            '若有多个按钮则开始等待
+            If FrmMain Is Nothing OrElse FrmMain.PanMsg Is Nothing AndAlso RunInUi() Then
+                '主窗体尚未加载,用老土的弹窗来替代
+                WaitingMyMsgBox.Remove(Converter)
+                If Button2.Length > 0 Then
+                    Dim RawResult As MsgBoxResult = MsgBox(Caption, If(Button3.Length > 0, MsgBoxStyle.YesNoCancel, MsgBoxStyle.YesNo) + If(IsWarn, MsgBoxStyle.Critical, MsgBoxStyle.Question), Title)
+                    Select Case RawResult
+                        Case MsgBoxResult.Yes
+                            Converter.Result = 1
+                        Case MsgBoxResult.No
+                            Converter.Result = 2
+                        Case MsgBoxResult.Cancel
+                            Converter.Result = 3
+                    End Select
+                Else
+                    MsgBox(Caption, MsgBoxStyle.OkOnly + If(IsWarn, MsgBoxStyle.Critical, MsgBoxStyle.Question), Title)
+                    Converter.Result = 1
+                End If
+                Log("[Control] 主窗体加载完成前出现意料外的等待弹窗:" & Button1 & "," & Button2 & "," & Button3, LogLevel.Debug)
+            Else
+                Try
+                    FrmMain.DragStop()
+                    ComponentDispatcher.PushModal()
+                    Dispatcher.PushFrame(Converter.WaitFrame)
+                Finally
+                    ComponentDispatcher.PopModal()
+                End Try
+            End If
+            Log("[Control] 普通弹框返回:" & If(Converter.Result, "null"))
+            Return Converter.Result
+        Else
+            '不进行等待,直接返回
+            Return 1
+        End If
+    End Function
+    ''' <summary>
+    ''' 显示输入框并返回输入的文本。若点击第二个按钮,则返回 Nothing。
+    ''' </summary>
+    ''' <param name="Title">弹窗的标题。</param>
+    ''' <param name="ValidateRules">文本框的输入检测。</param>
+    ''' <param name="Text">弹窗的介绍文本。</param>
+    ''' <param name="DefaultInput">文本框的默认内容。</param>
+    ''' <param name="HintText">文本框的提示内容。</param>
+    ''' <param name="Button1">显示的第一个按钮,默认为“确定”。</param>
+    ''' <param name="Button2">显示的第二个按钮,默认为“取消”。</param>
+    ''' <param name="IsWarn">是否为警告弹窗,若为 True,弹窗配色和背景会变为红色。</param>
+    Public Function MyMsgBoxInput(Title As String, Optional Text As String = "", Optional DefaultInput As String = "", Optional ValidateRules As ObjectModel.Collection(Of Validate) = Nothing, Optional HintText As String = "", Optional Button1 As String = "确定", Optional Button2 As String = "取消", Optional IsWarn As Boolean = False) As String
+        '将弹窗列入队列
+        Dim Converter As New MyMsgBoxConverter With {.Text = Text, .HintText = HintText, .Type = MyMsgBoxType.Input, .ValidateRules = If(ValidateRules, New ObjectModel.Collection(Of Validate)), .Button1 = Button1, .Button2 = Button2, .Content = DefaultInput, .IsWarn = IsWarn, .Title = Title}
+        WaitingMyMsgBox.Add(Converter)
+        '虽然我也不知道这是啥但是能用就成了 :)
+        Try
+            If FrmMain IsNot Nothing Then FrmMain.DragStop()
+            ComponentDispatcher.PushModal()
+            Dispatcher.PushFrame(Converter.WaitFrame)
+        Finally
+            ComponentDispatcher.PopModal()
+        End Try
+        Log("[Control] 输入弹框返回:" & If(Converter.Result, "null"))
+        Return Converter.Result
+    End Function
+    ''' <summary>
+    ''' 显示选择框并返回选择的第几项(从 0 开始)。若点击第二个按钮,则返回 Nothing。
+    ''' </summary>
+    ''' <param name="Title">弹窗的标题。</param>
+    ''' <param name="Button1">显示的第一个按钮,默认为 “确定”。</param>
+    ''' <param name="Button2">显示的第二个按钮,默认为空。</param>
+    ''' <param name="IsWarn">是否为警告弹窗,若为 True,弹窗配色和背景会变为红色。</param>
+    Public Function MyMsgBoxSelect(Selections As List(Of IMyRadio), Optional Title As String = "提示", Optional Button1 As String = "确定", Optional Button2 As String = "", Optional IsWarn As Boolean = False) As Integer?
+        '将弹窗列入队列
+        Dim Converter As New MyMsgBoxConverter With {.Type = MyMsgBoxType.Select, .Button1 = Button1, .Button2 = Button2, .Content = Selections, .IsWarn = IsWarn, .Title = Title}
+        WaitingMyMsgBox.Add(Converter)
+        '虽然我也不知道这是啥但是能用就成了 :)
+        Try
+            If FrmMain IsNot Nothing Then FrmMain.DragStop()
+            ComponentDispatcher.PushModal()
+            Dispatcher.PushFrame(Converter.WaitFrame)
+        Finally
+            ComponentDispatcher.PopModal()
+        End Try
+        Log("[Control] 选择弹框返回:" & If(Converter.Result, "null"))
+        Return Converter.Result
+    End Function
+
+    ''' <summary>
+    ''' 等待显示的弹窗。
+    ''' </summary>
+    Public WaitingMyMsgBox As List(Of MyMsgBoxConverter) = If(WaitingMyMsgBox, New List(Of MyMsgBoxConverter))
+    Public Sub MyMsgBoxTick()
+        Try
+            If FrmMain Is Nothing OrElse FrmMain.PanMsg Is Nothing OrElse FrmMain.WindowState = WindowState.Minimized Then Exit Sub
+            If FrmMain.PanMsg.Children.Count > 0 Then
+                '弹窗中
+                FrmMain.PanMsg.Visibility = Visibility.Visible
+            ElseIf WaitingMyMsgBox.Any Then
+                '没有弹窗,显示一个等待的弹窗
+                FrmMain.PanMsg.Visibility = Visibility.Visible
+                Select Case CType(WaitingMyMsgBox(0), MyMsgBoxConverter).Type
+                    Case MyMsgBoxType.Input
+                        FrmMain.PanMsg.Children.Add(New MyMsgInput(WaitingMyMsgBox(0)))
+                    Case MyMsgBoxType.Select
+                        FrmMain.PanMsg.Children.Add(New MyMsgSelect(WaitingMyMsgBox(0)))
+                    Case MyMsgBoxType.Text
+                        FrmMain.PanMsg.Children.Add(New MyMsgText(WaitingMyMsgBox(0)))
+                End Select
+                WaitingMyMsgBox.RemoveAt(0)
+            Else
+                '没有弹窗,没有等待的弹窗
+                If Not FrmMain.PanMsg.Visibility = Visibility.Collapsed Then FrmMain.PanMsg.Visibility = Visibility.Collapsed
+            End If
+        Catch ex As Exception
+            Log(ex, "处理等待中的弹窗失败", LogLevel.Feedback)
+        End Try
+    End Sub
+
+#End Region
+
+#Region "页面声明"
+    '在最后进行页面声明,避免颜色尚未加载完毕
+
+    '窗体声明
+    Public FrmMain As FormMain
+    Public FrmStart As SplashScreen
+
+    '页面声明(出于单元测试考虑,初始化页面已转入 FormMain 中)
+    Public FrmLaunchLeft As PageLaunchLeft
+    Public FrmLaunchRight As PageLaunchRight
+    Public FrmSelectLeft As PageSelectLeft
+    Public FrmSelectRight As PageSelectRight
+    Public FrmSpeedLeft As PageSpeedLeft
+    Public FrmSpeedRight As PageSpeedRight
+
+    '联机页面声明
+    Public FrmLinkLeft As PageLinkLeft
+    Public FrmLinkIoi As PageLinkIoi
+    Public FrmLinkHiper As PageLinkHiper
+    Public FrmLinkHelp As PageOtherHelpDetail
+    Public FrmLinkFeedback As PageLinkFeedback
+
+    '下载页面声明
+    Public FrmDownloadLeft As PageDownloadLeft
+    Public FrmDownloadInstall As PageDownloadInstall
+    Public FrmDownloadClient As PageDownloadClient
+    Public FrmDownloadOptiFine As PageDownloadOptiFine
+    Public FrmDownloadLiteLoader As PageDownloadLiteLoader
+    Public FrmDownloadForge As PageDownloadForge
+    Public FrmDownloadFabric As PageDownloadFabric
+    Public FrmDownloadMod As PageDownloadMod
+    Public FrmDownloadPack As PageDownloadPack
+
+    '设置页面声明
+    Public FrmSetupLeft As PageSetupLeft
+    Public FrmSetupLaunch As PageSetupLaunch
+    Public FrmSetupUI As PageSetupUI
+    Public FrmSetupSystem As PageSetupSystem
+    Public FrmSetupLink As PageSetupLink
+
+    '其他页面声明
+    Public FrmOtherLeft As PageOtherLeft
+    Public FrmOtherHelp As PageOtherHelp
+    Public FrmOtherAbout As PageOtherAbout
+    Public FrmOtherTest As PageOtherTest
+
+    '登录页面声明
+    Public FrmLoginLegacy As PageLoginLegacy
+    Public FrmLoginNide As PageLoginNide
+    Public FrmLoginNideSkin As PageLoginNideSkin
+    Public FrmLoginAuth As PageLoginAuth
+    Public FrmLoginAuthSkin As PageLoginAuthSkin
+    Public FrmLoginMs As PageLoginMs
+    Public FrmLoginMsSkin As PageLoginMsSkin
+
+    '版本设置页面声明
+    Public FrmVersionLeft As PageVersionLeft
+    Public FrmVersionOverall As PageVersionOverall
+    Public FrmVersionMod As PageVersionMod
+    Public FrmVersionModDisabled As PageVersionModDisabled
+    Public FrmVersionSetup As PageVersionSetup
+
+    '资源信息分页声明
+    Public FrmDownloadCompDetail As PageDownloadCompDetail
+
+#End Region
+
+#Region "帮助"
+
+    Public Class HelpEntry
+
+        '基础
+
+        ''' <summary>
+        ''' 显示标题。
+        ''' </summary>
+        Public Title As String
+        ''' <summary>
+        ''' 显示描述。
+        ''' </summary>
+        Public Desc As String
+        ''' <summary>
+        ''' 检索关键字。
+        ''' </summary>
+        Public Search As String
+        ''' <summary>
+        ''' 用于分类的标签列表。
+        ''' </summary>
+        Public Types As List(Of String)
+
+        '显示(可选)
+
+        ''' <summary>
+        ''' 帮助项的自定义图标。可能为 Nothing。
+        ''' </summary>
+        Public Logo As String = Nothing
+        ''' <summary>
+        ''' 是否显示在搜索结果。默认为 True。
+        ''' </summary>
+        Public ShowInSearch As Boolean = True
+        ''' <summary>
+        ''' 是否在公开版的 PCL 中显示(这会影响主页与搜索)。默认为 True。
+        ''' </summary>
+        Public ShowInPublic As Boolean = True
+        ''' <summary>
+        ''' 是否在快照版的 PCL 中显示(这会影响主页与搜索)。默认为 True。
+        ''' </summary>
+        Public ShowInSnapshot As Boolean = True
+
+        '动作
+
+        ''' <summary>
+        ''' 是否为 “执行事件”。
+        ''' </summary>
+        Public IsEvent As Boolean
+        Public EventType As String
+        Public EventData As String
+        ''' <summary>
+        ''' 若非执行事件,其对应的 .xaml 本地文件内容。
+        ''' </summary>
+        Public XamlContent As String
+
+        '转换
+
+        ''' <summary>
+        ''' 从文件初始化 HelpEntry 对象,失败会抛出异常。
+        ''' </summary>
+        Public Sub New(FilePath As String)
+            Dim JsonData As JObject = GetJson(HelpArgumentReplace(ReadFile(FilePath)))
+            If JsonData Is Nothing Then Throw New FileNotFoundException("未找到帮助文件:" & FilePath, FilePath)
+            '加载常规信息
+            If JsonData("Title") IsNot Nothing Then
+                Title = JsonData("Title")
+            Else
+                Throw New ArgumentException("未找到 Title 项")
+            End If
+            Desc = If(JsonData("Description"), "")
+            Search = If(JsonData("Keywords"), "")
+            Logo = JsonData("Logo") '为保持 Nothing,不要加 If
+            ShowInSearch = If(JsonData("ShowInSearch"), ShowInSearch)
+            ShowInPublic = If(JsonData("ShowInPublic"), ShowInPublic)
+            ShowInSnapshot = If(JsonData("ShowInSnapshot"), ShowInSnapshot)
+            Types = New List(Of String)
+            For Each NameOfType In If(JsonData("Types"), GetJson("[]"))
+                Types.Add(NameOfType)
+            Next
+            '加载事件信息
+            If If(JsonData("IsEvent"), False) Then
+                EventType = JsonData("EventType")
+                If EventType Is Nothing Then Throw New ArgumentException("未找到 EventType 项")
+                EventData = If(JsonData("EventData"), "")
+                IsEvent = True
+            Else
+                Dim XamlAddress As String = FilePath.ToLower.Replace(".json", ".xaml")
+                If File.Exists(XamlAddress) Then
+                    XamlContent = ReadFile(XamlAddress)
+                    IsEvent = False
+                Else
+                    Throw New FileNotFoundException("未找到帮助条目 .json 对应的 .xaml 文件(" & XamlAddress & ")")
+                End If
+            End If
+        End Sub
+        ''' <summary>
+        ''' 获取该 HelpEntry 对应的 MyListItem。
+        ''' </summary>
+        Public Function ToListItem() As MyListItem
+            Return SetToListItem(New MyListItem)
+        End Function
+        ''' <summary>
+        ''' 将属性设置入一个现有的 ListItem。
+        ''' </summary>
+        Public Function SetToListItem(Item As MyListItem) As MyListItem
+            Dim Logo As String
+            If IsEvent Then
+                If EventType = "弹出窗口" Then
+                    Logo = PathImage & "Blocks/GrassPath.png"
+                Else
+                    Logo = PathImage & "Blocks/CommandBlock.png"
+                End If
+            Else
+                Logo = PathImage & "Blocks/Grass.png"
+            End If
+            '设置属性
+            With Item
+                .SnapsToDevicePixels = True
+                .Title = Title
+                .Info = Desc
+                .Logo = If(Me.Logo, Logo)
+                .Height = 42
+                .Type = MyListItem.CheckType.Clickable
+                .Tag = Me
+                .EventType = Nothing
+                .EventData = Nothing
+            End With
+            '项目的点击事件
+            AddHandler Item.Click, Sub(sender, e) PageOtherHelp.OnItemClick(sender.Tag)
+            Return Item
+        End Function
+
+    End Class
+
+    Public HelpLoader As New LoaderTask(Of Integer, List(Of HelpEntry))("Help Page", AddressOf HelpLoad,, ThreadPriority.BelowNormal)
+    Private ReadOnly HelpLoadLock As New Object
+    ''' <summary>
+    ''' 初始化帮助列表对象。
+    ''' </summary>
+    Private Sub HelpLoad(Loader As LoaderTask(Of Integer, List(Of HelpEntry)))
+        SyncLock HelpLoadLock '避免重复解压文件导致出错
+            Try
+
+                '解压内置文件
+                HelpTryExtract()
+
+                '遍历文件
+                Dim FileList As New List(Of String)
+                Try
+                    Dim IgnoreList As New List(Of String)
+                    '读取自定义文件
+                    If Directory.Exists(Path & "PCL\Help\") Then
+                        For Each File In EnumerateFiles(Path & "PCL\Help\")
+                            Select Case File.Extension.ToLower
+                                Case ".helpignore"
+                                    '加载忽略列表
+                                    Log("[Help] 发现 .helpignore 文件:" & File.FullName)
+                                    For Each Line In ReadFile(File.FullName).Split(vbCrLf.ToCharArray)
+                                        Dim RealString As String = Line.Before("#").Trim
+                                        If String.IsNullOrWhiteSpace(RealString) Then Continue For
+                                        IgnoreList.Add(RealString)
+                                        If ModeDebug Then Log("[Help]  > " & RealString)
+                                    Next
+                                Case ".json"
+                                    FileList.Add(File.FullName)
+                            End Select
+                        Next
+                    End If
+                    Log("[Help] 已扫描 PCL 文件夹下的帮助文件,目前总计 " & FileList.Count & " 条")
+                    '读取自带文件
+                    For Each File In EnumerateFiles(PathTemp & "Help")
+                        '跳过非 json 文件与以 . 开头的文件夹
+                        If File.Extension.ToLower <> ".json" OrElse File.Directory.FullName.Replace(PathTemp & "Help", "").Contains("\.") Then Continue For
+                        '检查忽略列表
+                        Dim RealPath As String = File.FullName.Replace(PathTemp & "Help\", "")
+                        For Each Ignore In IgnoreList
+                            If RegexCheck(RealPath, Ignore) Then
+                                If ModeDebug Then Log("[Help] 已忽略 " & RealPath & ":" & Ignore)
+                                GoTo NextFile
+                            End If
+                        Next
+                        FileList.Add(File.FullName)
+NextFile:
+                    Next
+                    Log("[Help] 已扫描缓存文件夹下的帮助文件,目前总计 " & FileList.Count & " 条")
+                Catch ex As Exception
+                    Log(ex, "检查帮助文件夹失败", LogLevel.Msgbox)
+                End Try
+                If Loader.IsAborted Then Exit Sub
+
+                '将文件实例化
+                Dim Dict As New List(Of HelpEntry)
+                For Each FilePath As String In FileList
+                    Try
+                        Dim Entry As New HelpEntry(FilePath)
+                        Dict.Add(Entry)
+                        If ModeDebug Then Log("[Help] 已加载的帮助条目:" & Entry.Title & " ← " & FilePath)
+                    Catch ex As Exception
+                        Log(ex, "初始化帮助条目失败(" & FilePath & ")", LogLevel.Msgbox)
+                    End Try
+                Next
+
+                '回设
+                If Not Dict.Any() Then Throw New Exception("未找到可用的帮助;若不需要帮助页面,可以在 设置 → 个性化 → 功能隐藏 中将其隐藏")
+                If Loader.IsAborted Then Exit Sub
+                Loader.Output = Dict
+
+            Catch ex As Exception
+                Log(ex, "帮助列表初始化失败")
+                Throw
+            End Try
+        End SyncLock
+    End Sub
+    ''' <summary>
+    ''' 尝试解压内置帮助文件。
+    ''' </summary>
+    Public Sub HelpTryExtract()
+        If Setup.Get("SystemHelpVersion") <> VersionCode OrElse Not File.Exists(PathTemp & "Help\启动器\备份设置.xaml") Then
+            DeleteDirectory(PathTemp & "Help")
+            Directory.CreateDirectory(PathTemp & "Help")
+            WriteFile(PathTemp & "Cache\Help.zip", GetResources("Help"))
+            ExtractFile(PathTemp & "Cache\Help.zip", PathTemp & "Help", Encoding.UTF8)
+            Setup.Set("SystemHelpVersion", VersionCode)
+            Log("[Help] 已解压内置帮助文件,目前状态:" & File.Exists(PathTemp & "Help\启动器\备份设置.xaml"), LogLevel.Debug)
+        End If
+    End Sub
+    ''' <summary>
+    ''' 对帮助文件约定的替换标记进行处理,如果遇到需要转义的字符会进行转义。
+    ''' </summary>
+    Public Function HelpArgumentReplace(Xaml As String) As String
+        Dim Result = Xaml.Replace("{path}", EscapeXML(Path))
+        Result = RegexReplaceEach(Result, Function() EscapeXML(PageOtherTest.GetRandomHint()), "\{hint\}")
+        Result = RegexReplaceEach(Result, Function() EscapeXML(PageOtherTest.GetRandomCave()), "\{cave\}")
+        Return Result
+    End Function
+
+#End Region
+
+#Region "愚人节"
+
+    Public IsAprilEnabled As Boolean = Date.Now.Month = 4 AndAlso Date.Now.Day = 1
+    Public IsAprilGiveup As Boolean = False
+    Private AprilSpeed As New Vector(0, 0)
+    Private AprilIdieCount As Integer = 0, AprilMousePosLast As New Point(0, 0)
+    Private AprilDistance As Integer = 0
+    Private Sub TimerFool()
+        Try
+            If FrmLaunchLeft Is Nothing OrElse FrmLaunchLeft.AprilPosTrans Is Nothing OrElse FrmMain.lastMouseArg Is Nothing Then Exit Sub
+            If IsAprilGiveup OrElse FrmMain.PageCurrent <> FormMain.PageType.Launch OrElse AniControlEnabled <> 0 OrElse Not FrmLaunchLeft.BtnLaunch.IsLoaded Then Exit Sub
+
+            '计算是否空闲
+            Dim MousePos = FrmMain.lastMouseArg.GetPosition(FrmMain)
+            If MousePos = AprilMousePosLast Then
+                AprilIdieCount += 1
+            Else
+                AprilMousePosLast = MousePos
+                AprilIdieCount = 0
+            End If
+            '计算躲避移动
+            Dim Direction As Vector
+            Dim Distance As Double
+            Dim ButtonWidth = FrmLaunchLeft.BtnLaunch.ActualWidth / 2, ButtonHeight = FrmLaunchLeft.BtnLaunch.ActualHeight / 2
+            Dim Vec As Vector = FrmMain.lastMouseArg.GetPosition(FrmLaunchLeft.BtnLaunch) - New Vector(ButtonWidth, ButtonHeight)
+            Dim Dir As New Vector(Vec.X, Vec.Y)
+            Dir.Normalize()
+            Direction = -Dir
+            Distance = New Vector(Math.Max(0, Math.Abs(Vec.X) - ButtonWidth), Math.Max(0, Math.Abs(Vec.Y) - ButtonHeight)).Length
+            Dim BreathScale = Math.Sin(Timer150Count / 37.5 * Math.PI)
+            Dim Acc = Math.Max(0, BreathScale * 0.25 - 0.65 - Math.Log((Distance + 0.4) / 200)) * Direction '加速度
+            '计算回归移动
+            If AprilIdieCount >= 64 * 5 Then
+                Dim SafeDist As Vector = FrmMain.lastMouseArg.GetPosition(FrmMain.PanMain) - New Vector(ButtonWidth, FrmMain.PanMain.ActualHeight - ButtonHeight * 3)
+                Dim Back As New Vector(FrmLaunchLeft.AprilPosTrans.X, FrmLaunchLeft.AprilPosTrans.Y)
+                If SafeDist.Length > 250 AndAlso Back.Length > 0.4 Then
+                    Acc -= Back * 0.0005
+                    Back.Normalize()
+                    Acc -= Back * 0.15
+                End If
+            End If
+            '回到边界
+            Dim Relative As Point = FrmLaunchLeft.BtnLaunch.TranslatePoint(New Point(0, 0), FrmMain.PanForm)
+            If Relative.X < -ButtonWidth * 2 Then
+                FrmLaunchLeft.AprilPosTrans.X += FrmMain.PanForm.ActualWidth + ButtonWidth * 2 '离开左边界
+                AprilSpeed.X -= 80
+                If Relative.Y < 0 Then
+                    FrmLaunchLeft.AprilPosTrans.Y += ButtonHeight * 2.5
+                ElseIf Relative.Y > FrmMain.PanForm.ActualHeight - ButtonHeight * 2 Then
+                    FrmLaunchLeft.AprilPosTrans.Y -= ButtonHeight * 2.5
+                End If
+            ElseIf Relative.X > FrmMain.PanForm.ActualWidth Then
+                FrmLaunchLeft.AprilPosTrans.X -= FrmMain.PanForm.ActualWidth + ButtonWidth * 2 '离开右边界
+                AprilSpeed.X += 80
+                If Relative.Y < 0 Then
+                    FrmLaunchLeft.AprilPosTrans.Y += ButtonHeight * 2.5
+                ElseIf Relative.Y > FrmMain.PanForm.ActualHeight - ButtonHeight * 2 Then
+                    FrmLaunchLeft.AprilPosTrans.Y -= ButtonHeight * 2.5
+                End If
+            ElseIf Relative.Y < -ButtonHeight * 2 Then
+                FrmLaunchLeft.AprilPosTrans.Y += FrmMain.PanForm.ActualHeight + ButtonHeight * 2 '离开上边界
+                AprilSpeed.Y -= 25
+                If Relative.X < 0 Then
+                    FrmLaunchLeft.AprilPosTrans.X += ButtonWidth * 2
+                ElseIf Relative.X > FrmMain.PanForm.ActualWidth - ButtonWidth * 2 Then
+                    FrmLaunchLeft.AprilPosTrans.X -= ButtonWidth * 2
+                End If
+            ElseIf Relative.Y > FrmMain.PanForm.ActualHeight Then
+                FrmLaunchLeft.AprilPosTrans.Y -= FrmMain.PanForm.ActualHeight + ButtonHeight * 2 '离开下边界
+                AprilSpeed.Y += 25
+                If Relative.X < 0 Then
+                    FrmLaunchLeft.AprilPosTrans.X += ButtonWidth * 2
+                ElseIf Relative.X > FrmMain.PanForm.ActualWidth - ButtonWidth * 2 Then
+                    FrmLaunchLeft.AprilPosTrans.X -= ButtonWidth * 2
+                End If
+            End If
+            '移动
+            AprilSpeed = AprilSpeed * 0.8 + Acc
+            Dim SpeedValue = Math.Min(60, AprilSpeed.Length)
+            If SpeedValue < 0.01 Then Exit Sub
+            AprilSpeed.Normalize()
+            AprilSpeed *= SpeedValue
+            AprilDistance += SpeedValue
+            FrmLaunchLeft.AprilPosTrans.X += AprilSpeed.X
+            FrmLaunchLeft.AprilPosTrans.Y += AprilSpeed.Y
+            '大小改变
+            FrmLaunchLeft.AprilScaleTrans.ScaleX = MathClamp(1 - (Math.Abs(Direction.X) - Math.Abs(Direction.Y)) * (SpeedValue / 160), 0.2, 1.8)
+            FrmLaunchLeft.AprilScaleTrans.ScaleY = MathClamp(1 - (Math.Abs(Direction.Y) - Math.Abs(Direction.X)) * (SpeedValue / 100), 0.2, 1.8)
+            '放弃提示
+            If AprilDistance > 4000 Then
+                AprilDistance = -4000
+                Select Case RandomInteger(0, 3)
+                    Case 0
+                        Hint("放弃吧!只需要点一下右下角的小白旗……")
+                    Case 1
+                        Hint("看到右下角的那面小白旗了吗?")
+                    Case 2
+                        Hint("这里建议点一下右下角的小白旗投降呢.jpg")
+                    Case 3
+                        Hint("右下角的小白旗永远等着你……")
+                End Select
+            End If
+
+        Catch ex As Exception
+            Log(ex, "愚人节移动出错", LogLevel.Feedback)
+        End Try
+    End Sub
+
+#End Region
+
+#Region "系统"
+
+    ''' <summary>
+    ''' 把某个 PCL 窗口拖到最前面。
+    ''' </summary>
+    Public Sub ShowWindowToTop(Handle As IntPtr)
+        Try
+            PostMessage(Handle, 400 * 16 + 2, 0, 0)
+            SetForegroundWindow(Handle) '不在这里放不行,神秘 WinAPI,建议别动
+        Catch ex As Exception
+            Log(ex, "设置窗口置顶失败", LogLevel.Hint)
+        End Try
+    End Sub
+    Public Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ClassName As String, WindowName As String) As IntPtr
+    Public Declare Function SetForegroundWindow Lib "user32" (hWnd As IntPtr) As Integer
+    Private Declare Function PostMessage Lib "user32" Alias "PostMessageA" (hWnd As IntPtr, msg As UInteger, wParam As Long, lParam As Long) As Boolean
+
+#End Region
+
+    Public DragControl = Nothing
+    Private Timer4Count As Integer = 0
+    Private Timer150Count As Integer = 0
+    Private Sub TimerMain()
+        Try
+#Region "每 50ms 执行一次的代码"
+            HintTick()
+            MyMsgBoxTick()
+            FrmMain.DragTick()
+            LoaderTaskbarProgressRefresh()
+            If ThemeDontClick = 2 Then ThemeRefresh()
+#End Region
+        Catch ex As Exception
+            Log(ex, "短程主时钟执行异常", LogLevel.Assert)
+        End Try
+        Timer4Count += 1
+        If Timer4Count = 4 Then
+            Timer4Count = 0
+            Try
+#Region "每 250ms 执行一次的代码"
+                If ThemeNow = 12 Then ThemeRefresh()
+#End Region
+            Catch ex As Exception
+                Log(ex, "中程主时钟执行异常", LogLevel.Debug)
+            End Try
+        End If
+        Timer150Count += 1
+        If Timer150Count = 150 Then
+            Timer150Count = 0
+            Try
+#Region "每 7.5s 执行一次的代码"
+                If FrmMain.BtnExtraApril_ShowCheck AndAlso AprilDistance <> 0 Then FrmMain.BtnExtraApril.Ribble()
+                '以未知原因窗口被丢到一边去的修复(Top、Left = -25600),还有 #745
+                RunInUi(Sub()
+                            If Not FrmMain.Hidden Then
+                                If FrmMain.Top < -9000 Then FrmMain.Top = 100
+                                If FrmMain.Left < -9000 Then FrmMain.Left = 100 '窗口拉至最大时 Left = -18.8
+                            End If
+                        End Sub)
+#End Region
+            Catch ex As Exception
+                Log(ex, "长程主时钟执行异常", LogLevel.Assert)
+            End Try
+        End If
+    End Sub
+    Public Sub TimerMainStart()
+        RunInNewThread(
+        Sub()
+            Try
+                Do While True
+                    RunInUiWait(AddressOf TimerMain)
+                    Thread.Sleep(50 * 0.98)
+                Loop
+            Catch ex As Exception
+                Log(ex, "程序主时钟出错", LogLevel.Feedback)
+            End Try
+        End Sub, "Timer Main")
+        If Not IsAprilEnabled Then Exit Sub
+        RunInNewThread(
+        Sub()
+            Try
+                Dim LastTime = My.Computer.Clock.TickCount
+                Do While True
+                    If LastTime <> My.Computer.Clock.TickCount Then
+                        LastTime = My.Computer.Clock.TickCount
+                        RunInUiWait(AddressOf TimerFool)
+                    End If
+                    Thread.Sleep(1)
+                Loop
+            Catch ex As Exception
+                Log(ex, "愚人节主时钟出错", LogLevel.Feedback)
+            End Try
+        End Sub, "Timer Main Fool")
+    End Sub
+
+End Module