龙腾猫跃 2 лет назад
Родитель
Сommit
007b51b314

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

@@ -124,6 +124,15 @@ Public Class FormMain
         '3:BUG+ IMP* FEAT-
         '2:BUG* IMP-
         '1:BUG-
+        If LastVersion < 334 Then 'Snapshot 2.8.5
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "Mod 管理页面允许筛选可更新/启用/禁用的 Mod"))
+            If LastVersion = 333 Then
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复无法安装愚人节和预发布版本的 Bug"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复无法导出错误报告的 Bug"))
+            End If
+            FeatureCount += 6
+            BugCount += 7
+        End If
         If LastVersion < 333 Then 'Snapshot 2.8.4
             FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "下载 Mod 时会使用 MCIM 国内镜像源"))
             FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "打开 PCL 时会自动安装同目录下的 modpack.zip"))
@@ -744,7 +753,7 @@ Public Class FormMain
         Try
             If PageCurrent = PageType.VersionSetup AndAlso PageCurrentSub = PageSubType.VersionMod Then
                 'Mod 管理自动刷新
-                FrmVersionMod.RefreshList()
+                FrmVersionMod.ReloadModList()
             ElseIf PageCurrent = PageType.VersionSelect Then
                 '版本选择自动刷新
                 LoaderFolderRun(McVersionListLoader, PathMcFolder, LoaderFolderRunType.RunOnUpdated, MaxDepth:=1, ExtraPath:="versions\")

+ 54 - 62
Plain Craft Launcher 2/Modules/Base/ModBase.vb

@@ -11,12 +11,12 @@ Public Module ModBase
 #Region "声明"
 
     '下列版本信息由更新器自动修改
-    Public Const VersionBaseName As String = "2.8.4" '不含分支前缀的显示用版本名
-    Public Const VersionStandardCode As String = "2.8.4." & VersionBranchCode '标准格式的四段式版本号
+    Public Const VersionBaseName As String = "2.8.5" '不含分支前缀的显示用版本名
+    Public Const VersionStandardCode As String = "2.8.5." & VersionBranchCode '标准格式的四段式版本号
 #If BETA Then
     Public Const VersionCode As Integer = 332 'Release
 #Else
-    Public Const VersionCode As Integer = 333 'Snapshot
+    Public Const VersionCode As Integer = 334 'Snapshot
 #End If
     '自动生成的版本信息
     Public Const VersionDisplayName As String = VersionBranchName & " " & VersionBaseName
@@ -696,7 +696,7 @@ Public Module ModBase
             If Not FileName.Contains(":\") Then FileName = $"{Path}PCL\{FileName}.ini"
             WriteFile(FileName, FileContent.ToString)
         Catch ex As Exception
-            Log(ex, $"写入文件失败({FileName} -> {Key}:{Value})")
+            Log(ex, $"写入文件失败({FileName} → {Key}:{Value})", LogLevel.Hint)
         End Try
     End Sub
 
@@ -770,10 +770,10 @@ Public Module ModBase
         End Try
     End Sub
     ''' <summary>
-    ''' 读取文件,如果失败则返回空字符串。
+    ''' 读取文件,如果失败则返回空数组。
     ''' </summary>
     ''' <param name="FilePath">文件完整或相对路径。</param>
-    Public Function ReadFile(FilePath As String, Optional Encoding As Encoding = Nothing) As String
+    Public Function ReadFileBytes(FilePath As String, Optional Encoding As Encoding = Nothing) As Byte()
         Try
             '还原文件路径
             If Not FilePath.Contains(":\") Then FilePath = Path & FilePath
@@ -783,17 +783,25 @@ Public Module ModBase
                     ReDim FileBytes(ReadStream.Length - 1)
                     ReadStream.Read(FileBytes, 0, ReadStream.Length)
                 End Using
-                ReadFile = If(Encoding Is Nothing, DecodeBytes(FileBytes), Encoding.GetString(FileBytes))
+                Return FileBytes
             Else
-                Log("[System] 欲读取的文件不存在,已返回空字符串:" & FilePath)
-                Return ""
+                Log("[System] 欲读取的文件不存在,已返回空内容:" & FilePath)
+                Return {}
             End If
         Catch ex As Exception
             Log(ex, "读取文件出错:" & FilePath)
-            Return ""
+            Return {}
         End Try
     End Function
     ''' <summary>
+    ''' 读取文件,如果失败则返回空字符串。
+    ''' </summary>
+    ''' <param name="FilePath">文件完整或相对路径。</param>
+    Public Function ReadFile(FilePath As String, Optional Encoding As Encoding = Nothing) As String
+        Dim FileBytes = ReadFileBytes(FilePath)
+        ReadFile = If(Encoding Is Nothing, DecodeBytes(FileBytes), Encoding.GetString(FileBytes))
+    End Function
+    ''' <summary>
     ''' 读取流中的所有文本。
     ''' </summary>
     Public Function ReadFile(Stream As Stream, Optional Encoding As Encoding = Nothing) As String
@@ -818,50 +826,39 @@ Public Module ModBase
     ''' <param name="FilePath">文件完整或相对路径。</param>
     ''' <param name="Text">文件内容。</param>
     ''' <param name="Append">是否将文件内容追加到当前文件,而不是覆盖它。</param>
-    Public Function WriteFile(FilePath As String, Text As String, Optional Append As Boolean = False, Optional Encoding As Encoding = Nothing) As Boolean
-        Try
-            '还原文件路径
-            If Not FilePath.Contains(":\") Then FilePath = Path & FilePath
-            '确保目录存在
-            Directory.CreateDirectory(GetPathFromFullPath(FilePath))
-            '写入文件
-            If File.Exists(FilePath) Then
-                '如果文件存在,刷新目前文件
-                Using writer As New StreamWriter(FilePath, Append, If(Encoding, GetEncoding(FilePath)))
-                    writer.Write(Text)
-                    writer.Flush()
-                    writer.Close()
-                End Using
-            Else
-                '如果文件不存在,则新建并写入
-                File.WriteAllText(FilePath, Text, If(Encoding, New UTF8Encoding(False)))
-            End If
-            Return True
-        Catch ex As Exception
-            Log(ex, "写入文件时出错:" & FilePath)
-            Return False
-        End Try
-    End Function
+    Public Sub WriteFile(FilePath As String, Text As String, Optional Append As Boolean = False, Optional Encoding As Encoding = Nothing)
+        '还原文件路径
+        If Not FilePath.Contains(":\") Then FilePath = Path & FilePath
+        '确保目录存在
+        Directory.CreateDirectory(GetPathFromFullPath(FilePath))
+        '写入文件
+        If File.Exists(FilePath) Then
+            '如果文件存在,刷新目前文件
+            Using writer As New StreamWriter(FilePath, Append, If(Encoding, GetEncoding(ReadFileBytes(FilePath))))
+                writer.Write(Text)
+                writer.Flush()
+                writer.Close()
+            End Using
+        Else
+            '如果文件不存在,则新建并写入
+            File.WriteAllText(FilePath, Text, If(Encoding, New UTF8Encoding(False)))
+        End If
+    End Sub
     ''' <summary>
     ''' 写入文件。
+    ''' 如果 CanThrow 设置为 False,返回是否写入成功。
     ''' </summary>
     ''' <param name="FilePath">文件完整或相对路径。</param>
     ''' <param name="Content">文件内容。</param>
     ''' <param name="Append">是否将文件内容追加到当前文件,而不是覆盖它。</param>
-    Public Function WriteFile(FilePath As String, Content As Byte(), Optional Append As Boolean = False) As Boolean
-        Try
-            '还原文件路径
-            If Not FilePath.Contains(":\") Then FilePath = Path & FilePath
-            '确保目录存在
-            Directory.CreateDirectory(GetPathFromFullPath(FilePath))
-            '写入文件
-            File.WriteAllBytes(FilePath, Content)
-            Return True
-        Catch ex As Exception
-            Log(ex, "写入文件时出错:" & FilePath)
-            Return False
-        End Try
-    End Function
+    Public Sub WriteFile(FilePath As String, Content As Byte(), Optional Append As Boolean = False)
+        '还原文件路径
+        If Not FilePath.Contains(":\") Then FilePath = Path & FilePath
+        '确保目录存在
+        Directory.CreateDirectory(GetPathFromFullPath(FilePath))
+        '写入文件
+        File.WriteAllBytes(FilePath, Content)
+    End Sub
     ''' <summary>
     ''' 将流写入文件。
     ''' </summary>
@@ -891,21 +888,11 @@ Public Module ModBase
 
     '文件编码
     ''' <summary>
-    ''' 获取文件编码。
-    ''' </summary>
-    ''' <param name="FilePath">文件完整或相对路径。</param>
-    Public Function GetEncoding(FilePath As String) As Encoding
-        '还原文件路径
-        If Not FilePath.Contains(":\") Then FilePath = Path & FilePath
-        '获取编码
-        GetEncoding = GetEncoding(File.ReadAllBytes(FilePath))
-    End Function
-    ''' <summary>
-    ''' 获取 Bytes 的编码。
+    ''' 根据字节数组分析其编码。
     ''' </summary>
     Public Function GetEncoding(Bytes As Byte()) As Encoding
         Dim Length As Integer = Bytes.Count
-        If Length <= 2 Then Return New UTF8Encoding(False) '不带 BOM 的 UTF8
+        If Length < 3 Then Return New UTF8Encoding(False) '不带 BOM 的 UTF8
         '根据 BOM 判断编码
         If Bytes(0) >= &HEF Then
             '有 BOM 类型
@@ -1649,10 +1636,15 @@ RetryDir:
     End Function
 
     ''' <summary>
-    ''' 输入 And 字符不会报错的 Val。
+    ''' 不会报错的 Val。
+    ''' 如果输入有误,返回 0。
     ''' </summary>
     Public Function Val(Str As Object) As Double
-        Return If(TypeOf Str Is String AndAlso Str = "&", 0, Conversion.Val(Str))
+        Try
+            Return If(TypeOf Str Is String AndAlso Str = "&", 0, Conversion.Val(Str))
+        Catch
+            Return 0
+        End Try
     End Function
 
     '转义

+ 18 - 8
Plain Craft Launcher 2/Modules/Base/ModNet.vb

@@ -268,12 +268,7 @@ RequestFinished:
         '下载
         Using Client As New WebClient
             Try
-                If UseBrowserUserAgent Then
-                    Client.Headers(HttpRequestHeader.UserAgent) = "PCL2/" & VersionStandardCode & " Mozilla/5.0 AppleWebKit/537.36 Chrome/63.0.3239.132 Safari/537.36"
-                Else
-                    Client.Headers(HttpRequestHeader.UserAgent) = "PCL2/" & VersionStandardCode
-                End If
-                Client.Headers(HttpRequestHeader.Referer) = "http://" & VersionCode & ".pcl2.server/"
+                SecretHeadersSign(Url, Client, UseBrowserUserAgent)
                 Client.DownloadFile(Url, LocalFile)
             Catch ex As Exception
                 File.Delete(LocalFile)
@@ -315,7 +310,7 @@ Retry:
         Catch ex As ThreadInterruptedException
             Throw
         Catch ex As Exception
-            If ex.InnerException IsNot Nothing AndAlso ex.InnerException.Message.Contains("(40") AndAlso DontRetryOnRefused Then RetryCount = 999
+            If ex.InnerException IsNot Nothing AndAlso ex.InnerException.Message.Contains("(40") AndAlso DontRetryOnRefused Then Throw
             Select Case RetryCount
                 Case 0
                     If ModeDebug Then Log(ex, "[Net] 网络请求第一次失败(" & Url & ")")
@@ -432,7 +427,11 @@ RequestFinished:
                     End Using
                 Catch
                 End Try
-                ex = New WebException($"网络请求失败({ex.Status},{ex.Message},{Url}){If(String.IsNullOrEmpty(Res), "", vbCrLf & Res)}", ex)
+                If Res = "" Then
+                    ex = New WebException($"网络请求失败({ex.Status},{ex.Message},{Url})", ex)
+                Else
+                    ex = New ResponsedWebException($"服务器返回错误({ex.Status},{ex.Message},{Url}){vbCrLf}{Res}", Res, ex)
+                End If
             End If
             If MakeLog Then Log(ex, "NetRequestOnce 失败", LogLevel.Developer)
             Throw ex
@@ -445,6 +444,17 @@ RequestFinished:
             If Resp IsNot Nothing Then Resp.Dispose()
         End Try
     End Function
+    Public Class ResponsedWebException
+        Inherits WebException
+        ''' <summary>
+        ''' 远程服务器给予的回复。
+        ''' </summary>
+        Public Overloads Property Response As String
+        Public Sub New(Message As String, Response As String, InnerException As Exception)
+            MyBase.New(Message, InnerException)
+            Me.Response = Response
+        End Sub
+    End Class
 
     ''' <summary>
     ''' 最大线程数。

+ 2 - 2
Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb

@@ -1077,7 +1077,7 @@ Retry:
         If String.IsNullOrEmpty(Task.Input.SearchText) Then
             '如果没有搜索文本,按下载量将结果排序
             For Each Result As CompProject In RealResults
-                Scores.Add(Result, Result.DownloadCount * If(Result.FromCurseForge, 1, 30))
+                Scores.Add(Result, Result.DownloadCount * If(Result.FromCurseForge, 1, 10))
             Next
         Else
             '如果有搜索文本,按关联度将结果排序
@@ -1085,7 +1085,7 @@ Retry:
             Dim Entry As New List(Of SearchEntry(Of CompProject))
             For Each Result As CompProject In RealResults
                 Scores.Add(Result, If(Result.WikiId > 0, 0.2, 0) +
-                           Math.Log10(Math.Max(Result.DownloadCount, 1) * If(Result.FromCurseForge, 1, 30)) / 9)
+                           Math.Log10(Math.Max(Result.DownloadCount, 1) * If(Result.FromCurseForge, 1, 10)) / 9)
                 Entry.Add(New SearchEntry(Of CompProject) With {.Item = Result, .SearchSource = New List(Of KeyValuePair(Of String, Double)) From {
                           New KeyValuePair(Of String, Double)(If(IsChineseSearch, Result.TranslatedName, Result.RawName), 1),
                           New KeyValuePair(Of String, Double)(Result.Description, 0.05)}})

+ 7 - 6
Plain Craft Launcher 2/Modules/Minecraft/ModCrash.vb

@@ -844,8 +844,11 @@ NextStack:
     Public Sub Output(IsHandAnalyze As Boolean, Optional ExtraFiles As List(Of String) = Nothing)
         '弹窗提示
         FrmMain.ShowWindowToTop()
-        Dim ShowLog As Action =
+        Select Case MyMsgBox(GetAnalyzeResult(IsHandAnalyze), If(IsHandAnalyze, "错误报告分析结果", "Minecraft 出现错误"),
+            "确定", If(IsHandAnalyze OrElse DirectFile Is Nothing, "", "查看日志"), If(IsHandAnalyze, "", "导出错误报告"),
+            Button2Action:=If(IsHandAnalyze OrElse DirectFile Is Nothing, Nothing,
             Sub()
+                '弹窗选择:查看日志
                 If File.Exists(DirectFile.Value.Key) Then
                     ShellOnly("notepad", DirectFile.Value.Key)
                 Else
@@ -853,11 +856,9 @@ NextStack:
                     WriteFile(FilePath, Join(DirectFile.Value.Value, vbCrLf))
                     ShellOnly(FilePath)
                 End If
-            End Sub
-        Select Case MyMsgBox(GetAnalyzeResult(IsHandAnalyze), If(IsHandAnalyze, "错误报告分析结果", "Minecraft 出现错误"),
-                             "确定", If(IsHandAnalyze OrElse DirectFile Is Nothing, "", "查看日志"), If(IsHandAnalyze, "", "导出错误报告"),
-                             Button2Action:=If(IsHandAnalyze OrElse DirectFile Is Nothing, Nothing, ShowLog))
+            End Sub))
             Case 3
+                '弹窗选择:导出错误报告
                 Dim FileAddress As String = Nothing
                 Try
                     '获取文件路径
@@ -881,7 +882,7 @@ NextStack:
                                 FileName = "游戏崩溃前的输出.txt"
                         End Select
                         If File.Exists(OutputFile) Then
-                            Dim FileEncoding As Encoding = GetEncoding(OutputFile)
+                            Dim FileEncoding As Encoding = GetEncoding(ReadFileBytes(OutputFile))
                             WriteFile(TempFolder & "Report\" & FileName,
                                       SecretFilter(ReadFile(OutputFile, FileEncoding), If(FileName = "启动脚本.bat", "F", "*")),
                                       Encoding:=FileEncoding)

+ 2 - 2
Plain Craft Launcher 2/Modules/Minecraft/ModJava.vb

@@ -216,8 +216,8 @@
 
             '添加特定的 Java
             Dim JavaPreList As New Dictionary(Of String, Boolean)
-            If PathMcFolder.Split("\").Count > 3 Then
-                JavaSearchFolder(GetPathFromFullPath(PathMcFolder), JavaPreList, False, True) 'Minecraft 文件夹的父文件夹(如果不是根目录的话)
+            If PathMcFolder.Split("\").Count > 3 AndAlso Not PathMcFolder.Contains("AppData\Roaming") Then
+                JavaSearchFolder(GetPathFromFullPath(PathMcFolder), JavaPreList, False, True) 'Minecraft 文件夹的父文件夹(如果不是根目录或 %APPDATA% 的话)
             End If
             JavaSearchFolder(PathMcFolder, JavaPreList, False, True) 'Minecraft 文件夹
             JavaPreList = JavaPreList.Where(Function(j) Not j.Key.Contains(".minecraft\runtime")).

+ 35 - 32
Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb

@@ -1,5 +1,4 @@
 Imports System.IO.Compression
-Imports Newtonsoft.Json
 
 Public Module ModLaunch
 
@@ -671,6 +670,7 @@ LoginFinish:
             Url:=Data.Input.BaseUrl & "/validate",
             Method:="POST",
             Data:=RequestData.ToString(0),
+            Headers:=New Dictionary(Of String, String) From {{"Accept-Language", "zh_CN"}},
             ContentType:="application/json; charset=utf-8") '没有返回值的
         '将登录结果输出
         Data.Output.AccessToken = AccessToken
@@ -694,6 +694,7 @@ LoginFinish:
                    ""name"":""" & Setup.Get("Cache" & Data.Input.Token & "Name") & """},", "") & "
                ""accessToken"":""" & Setup.Get("Cache" & Data.Input.Token & "Access") & """,
                ""clientToken"":""" & Setup.Get("Cache" & Data.Input.Token & "Client") & """}",
+               Headers:=New Dictionary(Of String, String) From {{"Accept-Language", "zh_CN"}},
                ContentType:="application/json; charset=utf-8"))
         '将登录结果输出
         If LoginJson("selectedProfile") Is Nothing Then Throw New Exception("选择的角色 " & Setup.Get("Cache" & Data.Input.Token & "Name") & " 无效!")
@@ -711,16 +712,6 @@ LoginFinish:
         Setup.Set("Cache" & Data.Input.Token & "Pass", Data.Input.Password)
         McLaunchLog("刷新登录成功(Refresh, " & Data.Input.Token & ")")
     End Sub
-
-    Public Class AuthErrorResponse
-        Public Property [error] As String
-        Public Property errorMessage As String
-    End Class
-    Private Function ParseErrorResponse(ex As Exception) As AuthErrorResponse
-        Dim SplitEx As String = ex.ToString().Split(vbCrLf)(1).Split("-")(0).ToString().Trim()
-        Dim JsonEx As AuthErrorResponse = JsonConvert.DeserializeObject(Of AuthErrorResponse)(SplitEx)
-        Return JsonEx
-    End Function
     Private Function McLoginRequestLogin(ByRef Data As LoaderTask(Of McLoginServer, McLoginResult)) As Boolean
         Try
             Dim NeedRefresh As Boolean = False
@@ -730,13 +721,11 @@ LoginFinish:
                 New JProperty("username", Data.Input.UserName),
                 New JProperty("password", Data.Input.Password),
                 New JProperty("requestUser", True))
-            Dim LHeaders As New Dictionary(Of String, String)
-            LHeaders.Add("Accept-Language", "zh_CN")
             Dim LoginJson As JObject = GetJson(NetRequestRetry(
                 Url:=Data.Input.BaseUrl & "/authenticate",
                 Method:="POST",
                 Data:=RequestData.ToString(0),
-                Headers:=LHeaders,
+                Headers:=New Dictionary(Of String, String) From {{"Accept-Language", "zh_CN"}},
                 ContentType:="application/json; charset=utf-8"))
             '检查登录结果
             If LoginJson("availableProfiles").Count = 0 Then
@@ -796,13 +785,19 @@ LoginFinish:
         Catch ex As Exception
             Dim AllMessage As String = GetExceptionSummary(ex)
             Log(ex, "登录失败原始错误信息", LogLevel.Normal)
+            '读取服务器返回的错误
+            If TypeOf ex Is ResponsedWebException Then
+                Dim ErrorMessage As String = Nothing
+                Try
+                    ErrorMessage = GetJson(DirectCast(ex, ResponsedWebException).Response)("errorMessage")
+                Catch
+                End Try
+                If Not String.IsNullOrWhiteSpace(ErrorMessage) Then Throw New Exception("$登录失败:" & ErrorMessage)
+            End If
+            '通用关键字检测
             If AllMessage.Contains("403") Then
                 Select Case Data.Input.Type
                     Case McLoginType.Auth
-                        Dim ErrorResponse As AuthErrorResponse = ParseErrorResponse(ex)
-                        If (ErrorResponse.errorMessage <> "") Then
-                            Throw New Exception("$登录失败,以下为认证服务器提供的信息:" & vbCrLf & $" - {ErrorResponse.errorMessage}")
-                        End If
                         Throw New Exception("$登录失败,以下为可能的原因:" & vbCrLf &
                                             " - 输入的账号或密码错误。" & vbCrLf &
                                             " - 登录尝试过于频繁,导致被暂时屏蔽。请不要操作,等待 10 分钟后再试。" & vbCrLf &
@@ -1349,20 +1344,28 @@ LoginFinish:
         Dim WrapperPath As String = BaseDir & "\JavaWrapper.jar"
         Log("[Java] 选定的 Java Wrapper 路径:" & WrapperPath)
         SyncLock ExtractJavaWrapperLock '避免 OptiFine 和 Forge 安装时同时释放 Java Wrapper 导致冲突
-            Dim IsWrapperWritten As Boolean = WriteFile(WrapperPath, GetResources("JavaWrapper"))
-            If Not IsWrapperWritten AndAlso File.Exists(WrapperPath) Then
-                '以下为 #4243 的修复,因为未知原因 Java Wrapper 可能变为只读文件
-                Log("[Java] Java Wrapper 文件释放失败,但文件已存在,将在删除后尝试重新生成", LogLevel.Debug)
-                Try
-                    File.Delete(WrapperPath)
-                    IsWrapperWritten = WriteFile(WrapperPath, GetResources("JavaWrapper"))
-                Catch ex As Exception
-                    Log(ex, "Java Wrapper 文件重新释放失败,将尝试更换文件名重新生成")
-                    WrapperPath = BaseDir & "\JavaWrapper2.jar"
-                    IsWrapperWritten = WriteFile(WrapperPath, GetResources("JavaWrapper"))
-                End Try
-            End If
-            If Not IsWrapperWritten Then Throw New FileNotFoundException("释放 Java Wrapper 失败,请查看 PCL 日志查找详细信息")
+            Try
+                WriteFile(WrapperPath, GetResources("JavaWrapper"))
+            Catch ex As Exception
+                If File.Exists(WrapperPath) Then
+                    '因为未知原因 Java Wrapper 可能变为只读文件(#4243)
+                    Log(ex, "Java Wrapper 文件释放失败,但文件已存在,将在删除后尝试重新生成", LogLevel.Developer)
+                    Try
+                        File.Delete(WrapperPath)
+                        WriteFile(WrapperPath, GetResources("JavaWrapper"))
+                    Catch ex2 As Exception
+                        Log(ex2, "Java Wrapper 文件重新释放失败,将尝试更换文件名重新生成", LogLevel.Developer)
+                        WrapperPath = BaseDir & "\JavaWrapper2.jar"
+                        Try
+                            WriteFile(WrapperPath, GetResources("JavaWrapper"))
+                        Catch ex3 As Exception
+                            Throw New FileNotFoundException("释放 Java Wrapper 最终尝试失败", ex3)
+                        End Try
+                    End Try
+                Else
+                    Throw New FileNotFoundException("释放 Java Wrapper 失败", ex)
+                End If
+            End Try
         End SyncLock
         Return WrapperPath
     End Function

+ 4 - 4
Plain Craft Launcher 2/Modules/Minecraft/ModMod.vb

@@ -707,7 +707,7 @@ Finished:
                     End If
                     '读取文件
                     Dim data As New List(Of Byte)
-                    For Each b As Byte In File.ReadAllBytes(Path)
+                    For Each b As Byte In ReadFileBytes(Path)
                         If b = 9 OrElse b = 10 OrElse b = 13 OrElse b = 32 Then Continue For
                         data.Add(b)
                     Next
@@ -1130,13 +1130,13 @@ Finished:
         Mods = Mods.Where(Function(m) m.Comp IsNot Nothing).ToList()
         Log($"[Mod] 联网获取本地 Mod 信息完成,为 {Mods.Count} 个 Mod 更新缓存")
         If Not Mods.Any() Then Exit Sub
-        For Each Entry In Mods 'TODO: Bookshelf 的 Logo 会在两个网站间横跳
+        For Each Entry In Mods
             Entry.CompLoaded = SucceedThreadCount = 2
             Cache(Entry.ModrinthHash & McVersion & ModLoaders.Join("")) = Entry.ToJson()
         Next
         WriteFile(PathTemp & "Cache\LocalMod.json", Cache.ToString(If(ModeDebug, Newtonsoft.Json.Formatting.Indented, Newtonsoft.Json.Formatting.None)))
-        '刷新下边栏(#4377)
-        RunInUi(Sub() FrmVersionMod?.RefreshBottomBar())
+        '刷新边栏
+        RunInUi(Sub() FrmVersionMod?.RefreshBars())
     End Sub
     Private Function GetTargetModLoaders() As List(Of CompModLoaderType)
         Dim ModLoaders As New List(Of CompModLoaderType)

+ 7 - 3
Plain Craft Launcher 2/Modules/ModSecret.vb

@@ -98,8 +98,10 @@ Friend Module ModSecret
     ''' <summary>
     ''' 设置 Headers 的 UA、Referer。
     ''' </summary>
-    Friend Sub SecretHeadersSign(Url As String, ByRef Client As CookieWebClient, Optional UseBrowserUserAgent As Boolean = False)
-        If UseBrowserUserAgent Then
+    Friend Sub SecretHeadersSign(Url As String, ByRef Client As WebClient, Optional UseBrowserUserAgent As Boolean = False)
+        If Url.Contains("modrinth.com") Then '根据 #4334,不添加 PCL 的 UA 反而能正常访问
+            Client.Headers("User-Agent") = "Mozilla/5.0 AppleWebKit/537.36 Chrome/63.0.3239.132 Safari/537.36"
+        ElseIf UseBrowserUserAgent Then
             Client.Headers("User-Agent") = "PCL2/" & VersionStandardCode & " Mozilla/5.0 AppleWebKit/537.36 Chrome/63.0.3239.132 Safari/537.36"
         Else
             Client.Headers("User-Agent") = "PCL2/" & VersionStandardCode
@@ -111,7 +113,9 @@ Friend Module ModSecret
     ''' 设置 Headers 的 UA、Referer。
     ''' </summary>
     Friend Sub SecretHeadersSign(Url As String, ByRef Request As HttpWebRequest, Optional UseBrowserUserAgent As Boolean = False)
-        If UseBrowserUserAgent Then
+        If Url.Contains("modrinth.com") Then '根据 #4334,不添加 PCL 的 UA 反而能正常访问
+            Request.UserAgent = "Mozilla/5.0 AppleWebKit/537.36 Chrome/63.0.3239.132 Safari/537.36"
+        ElseIf UseBrowserUserAgent Then
             Request.UserAgent = "PCL2/" & VersionStandardCode & " Mozilla/5.0 AppleWebKit/537.36 Chrome/63.0.3239.132 Safari/537.36"
         Else
             Request.UserAgent = "PCL2/" & VersionStandardCode

+ 2 - 2
Plain Craft Launcher 2/My Project/AssemblyInfo.vb

@@ -51,6 +51,6 @@ Imports System.Runtime.InteropServices
 ' 可以指定所有值,也可以使用以下所示的 "*" 预置版本号和修订号
 ' 方法是按如下所示使用“*”
 
-<Assembly: AssemblyVersion("2.8.4.0")>
-<Assembly: AssemblyFileVersion("2.8.4.0")>
+<Assembly: AssemblyVersion("2.8.5.0")>
+<Assembly: AssemblyFileVersion("2.8.5.0")>
 <Assembly: NeutralResourcesLanguage("")>

+ 1 - 1
Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb

@@ -2021,7 +2021,7 @@ Sub(Task As LoaderTask(Of List(Of NetFile), Boolean))
         If Request.LiteLoaderEntry IsNot Nothing Then LiteLoaderFolder = TempMcFolder & "versions\" & Request.MinecraftName & "-LiteLoader"
 
         '判断 OptiFine 是否作为 Mod 进行下载
-        Dim MinecraftCode As Integer = If(Request.MinecraftName.Contains("."), Request.MinecraftName.Split(".")(1), 99)
+        Dim MinecraftCode As Integer = If(Request.MinecraftName.Contains("."), Val(Request.MinecraftName.Split(".")(1)), 0)
         Dim OptiFineAsMod As Boolean = Request.OptiFineEntry IsNot Nothing AndAlso '1. 选择了 OptiFine
             (Request.FabricVersion IsNot Nothing OrElse '2. 选择了 Fabric...
             (Request.ForgeEntry IsNot Nothing AndAlso MinecraftCode >= 14 AndAlso MinecraftCode <= 15)) '...或者 Forge 1.14~15(#4134)

+ 1 - 1
Plain Craft Launcher 2/Pages/PageDownload/PageDownloadNeoForge.xaml

@@ -8,7 +8,7 @@
         <StackPanel Orientation="Vertical" Margin="5">
             <local:MyCard Margin="20,20,20,0" Grid.IsSharedSizeScope="True" Title="NeoForge 简介" x:Name="CardTip">
                 <StackPanel Margin="25,40,15,20">
-                    <TextBlock Margin="0,0,0,11" Text="NeoForge 是支持 Minecraft 1.20.1 及以上的 Mod 加载器,你需要先安装它才能安装各种 NeoForge 模组。&#xa;本页面提供 NeoForge 安装器下载,在下载后你需要手动打开安装器进行安装。" TextWrapping="Wrap" />
+                    <TextBlock Margin="0,0,0,11" Text="NeoForge 是 Minecraft 1.20.1+ 的 Mod 加载器,你需要先安装它才能安装各种 NeoForge 模组,它也兼容一些 Forge 模组。&#xa;本页面提供 NeoForge 安装器下载,在下载后你需要手动打开安装器进行安装。" TextWrapping="Wrap" />
                     <Grid Height="35">
                         <Grid.ColumnDefinitions>
                             <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />

+ 4 - 0
Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.vb

@@ -102,6 +102,10 @@ Download:
                         Log("[Page] 主页预设:Minecraft 皮肤推荐")
                         Url = "https://forgepixel.com/pcl_sub_file"
                         GoTo Download
+                    Case 6
+                        Log("[Page] 主页预设:OpenBMCLAPI 仪表盘 Lite")
+                        Url = "https://pcl-bmcl.milulu.xyz/"
+                        GoTo Download
                 End Select
         End Select
         RunInUi(Sub() LoadContent(Content))

+ 1 - 1
Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml.vb

@@ -99,7 +99,7 @@ Retry:
                 Client.DefaultRequestHeaders.UserAgent.Add(New Net.Http.Headers.ProductInfoHeaderValue("MojangSharp", "0.1"))
                 Dim Contents As New Net.Http.MultipartFormDataContent From {
                     {New Net.Http.StringContent(If(SkinInfo.IsSlim, "slim", "classic")), "variant"},
-                    {New Net.Http.ByteArrayContent(File.ReadAllBytes(SkinInfo.LocalFile)), "file", GetFileNameFromPath(SkinInfo.LocalFile)}
+                    {New Net.Http.ByteArrayContent(ReadFileBytes(SkinInfo.LocalFile)), "file", GetFileNameFromPath(SkinInfo.LocalFile)}
                 }
                 Dim Result As String = Await (Await Client.PostAsync(New Uri("https://api.minecraftservices.com/minecraft/profile/skins"), Contents)).Content.ReadAsStringAsync
                 If Result.Contains("request requires user authentication") Then

+ 1 - 1
Plain Craft Launcher 2/Pages/PageSetup/ModSetup.vb

@@ -107,7 +107,7 @@
         {"ToolDownloadTranslate", New SetupEntry(0, Source:=SetupSource.Registry)},
         {"ToolDownloadKeepModpack", New SetupEntry(False, Source:=SetupSource.Registry)},
         {"ToolDownloadIgnoreQuilt", New SetupEntry(True, Source:=SetupSource.Registry)},
-        {"ToolDownloadCert", New SetupEntry(True, Source:=SetupSource.Registry)},
+        {"ToolDownloadCert", New SetupEntry(False, Source:=SetupSource.Registry)},
         {"ToolDownloadMod", New SetupEntry(1, Source:=SetupSource.Registry)},
         {"ToolUpdateAlpha", New SetupEntry(0, Source:=SetupSource.Registry, Encoded:=True)},
         {"ToolUpdateRelease", New SetupEntry(False, Source:=SetupSource.Registry)},

+ 1 - 1
Plain Craft Launcher 2/Pages/PageSetup/PageSetupSystem.xaml

@@ -66,7 +66,7 @@
                                           ToolTip="下载 Mod 时,若该 Mod 支持 Forge 和 Fabric,则显示为支持任意 Mod 加载器。&#xa;例如,开启时 JEI 显示支持全版本,关闭后 JEI 就会显示仅支持 Forge / Fabric 全版本。" />
                         <local:MyCheckBox Margin="0,2,0,4" Text="验证 SSL 证书" Grid.Row="12" Height="22" Grid.ColumnSpan="2"
                                           x:Name="CheckDownloadCert" Tag="ToolDownloadCert" 
-                                          ToolTip="验证证书会提高安全性,但疑似有概率导致正版登录失败(#3018)。&#xa;如果在正版登录时因为 [根据验证过程,远程证书无效] 失败,可以尝试关闭此项。" />
+                                          ToolTip="开启验证会提高安全性、降低盗号风险(见 #2767),但也可能导致正版登录失败(见 #3018)。" />
                     </Grid>
                 </StackPanel>
             </local:MyCard>

+ 1 - 0
Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml

@@ -242,6 +242,7 @@
                             <local:MyComboBoxItem Content="简单主页(作者:MFn233)" />
                             <local:MyComboBoxItem Content="每日整合包推荐(作者:wkea)" />
                             <local:MyComboBoxItem Content="Minecraft 皮肤推荐(作者:wkea)" />
+                            <local:MyComboBoxItem Content="OpenBMCLAPI 仪表盘 Lite(作者:Silverteal、Mxmilu666)" />
                         </local:MyComboBox>
                     </Grid>
                 </StackPanel>

+ 1 - 1
Plain Craft Launcher 2/Pages/PageVersion/PageVersionLeft.xaml.vb

@@ -96,7 +96,7 @@
         Catch ex As Exception
             Log(ex, "强制刷新时清理本地 Mod 信息缓存失败")
         End Try
-        If FrmVersionMod IsNot Nothing Then FrmVersionMod.RefreshList(True) '无需 Else,还没加载刷个鬼的新
+        If FrmVersionMod IsNot Nothing Then FrmVersionMod.ReloadModList(True) '无需 Else,还没加载刷个鬼的新
         ItemMod.Checked = True
         Hint("正在刷新……", Log:=False)
     End Sub

+ 9 - 14
Plain Craft Launcher 2/Pages/PageVersion/PageVersionMod.xaml

@@ -7,8 +7,7 @@
     <Grid>
         <Grid x:Name="PanAllBack">
             <local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
-                <StackPanel x:Name="PanMain" Margin="25,10,25,10" Grid.IsSharedSizeScope="True">
-                    <!-- 若显示多选模块则为 92 -->
+                <StackPanel x:Name="PanMain" Margin="25,10,25,10" Grid.IsSharedSizeScope="True"> <!-- 若显示多选模块则为 92 -->
                     <local:MySearchBox Margin="0,15" HintText="搜索 Mod 名称 / 描述 / 标签" x:Name="SearchBox" />
                     <local:MyCard Grid.Row="0" Margin="0,0,0,15" x:Name="PanManage">
                         <Grid Margin="15,15,0,15" Height="35">
@@ -24,19 +23,15 @@
                             <local:MyButton x:Name="BtnManageCheck" Grid.Column="3" MinWidth="140" Text="检查 Mod" Padding="13,0" Margin="0,0,20,0" HorizontalAlignment="Left" ToolTip="快速检查 Mod 的版本要求、重复、前置缺失等问题。&#xa;结果仅供参考,由于部分 Mod 标注的信息有误,可能并不准确。" Visibility="Collapsed" />
                         </Grid>
                     </local:MyCard>
-                    <local:MyCard x:Name="PanListBack" VerticalAlignment="Top" Opacity="0" Margin="0,0,0,15" Title=" " MinHeight="40">
-                        <StackPanel Margin="0,8,9,0" Orientation="Horizontal" VerticalAlignment="Top" HorizontalAlignment="Right" Height="24">
-                            <local:MyRadioButton VerticalAlignment="Center" Height="24" Margin="2,0" Text="全部" x:Name="BtnTypeAll" ColorType="Highlight" Checked="True"/>
-                            <TextBlock Text="|" Foreground="{StaticResource ColorBrushGray1}" VerticalAlignment="Center"/>
-                            <local:MyRadioButton VerticalAlignment="Center" Height="24" Margin="2,0" Text="已启用" x:Name="BtnTypeEnabled" ColorType="Highlight"/>
-                            <TextBlock Text="|" Foreground="{StaticResource ColorBrushGray1}" VerticalAlignment="Center"/>
-                            <local:MyRadioButton VerticalAlignment="Center" Height="24" Margin="2,0" Text="已禁用" x:Name="BtnTypeDisabled" ColorType="Highlight"/>
-                            <TextBlock Text="|" Foreground="{StaticResource ColorBrushGray1}" VerticalAlignment="Center"/>
-                            <local:MyRadioButton VerticalAlignment="Center" Height="24" Margin="2,0" Text="可更新" x:Name="BtnTypeCanUpdate" ColorType="Highlight"/>
-                            <TextBlock Text="|" Foreground="{StaticResource ColorBrushGray1}" VerticalAlignment="Center"/>
-                            <local:MyRadioButton VerticalAlignment="Center" Height="24" Margin="2,0" Text="错误" x:Name="BtnTypeError" ColorType="Highlight"/>
+                    <local:MyCard x:Name="PanListBack" VerticalAlignment="Top" Opacity="0" Margin="0,0,0,14" Title=" " MinHeight="55">
+                        <StackPanel x:Name="PanFilter" Margin="15,13,0,0" Orientation="Horizontal" VerticalAlignment="Top" HorizontalAlignment="Left" Height="28">
+                            <local:MyRadioButton Tag="0" ColorType="Highlight" VerticalAlignment="Center" Margin="2,0" Text="全部" x:Name="BtnFilterAll" Checked="True" />
+                            <local:MyRadioButton Tag="1" ColorType="Highlight" VerticalAlignment="Center" Margin="2,0" Text="已启用" x:Name="BtnFilterEnabled" />
+                            <local:MyRadioButton Tag="2" ColorType="Highlight" VerticalAlignment="Center" Margin="2,0" Text="已禁用" x:Name="BtnFilterDisabled" />
+                            <local:MyRadioButton Tag="3" ColorType="Highlight" VerticalAlignment="Center" Margin="2,0" Text="可更新" x:Name="BtnFilterCanUpdate" />
+                            <local:MyRadioButton Tag="4" ColorType="Highlight" VerticalAlignment="Center" Margin="2,0" Text="错误" x:Name="BtnFilterError" />
                         </StackPanel>
-                        <StackPanel Margin="20,38,18,18" Name="PanList" VerticalAlignment="Top" />
+                        <StackPanel Margin="20,48,18,22" Name="PanList" VerticalAlignment="Top" />
                     </local:MyCard>
                 </StackPanel>
             </local:MyScrollViewer>

+ 222 - 223
Plain Craft Launcher 2/Pages/PageVersion/PageVersionMod.xaml.vb

@@ -4,16 +4,11 @@
 
     Private IsLoad As Boolean = False
     Public Sub PageOther_Loaded() Handles Me.Loaded
-        BtnTypeAll.Tag = ViewType.All
-        BtnTypeEnabled.Tag = ViewType.Enabled
-        BtnTypeDisabled.Tag = ViewType.Disabled
-        BtnTypeCanUpdate.Tag = ViewType.CanUpdate
-        BtnTypeError.Tag = ViewType.InError
 
         If FrmMain.PageLast.Page <> FormMain.PageType.CompDetail Then PanBack.ScrollToHome()
         AniControlEnabled += 1
         SelectedMods.Clear()
-        RefreshList()
+        ReloadModList()
         ChangeAllSelected(False)
         AniControlEnabled -= 1
 
@@ -21,6 +16,11 @@
         If IsLoad Then Exit Sub
         IsLoad = True
 
+        '调整按钮边距(这玩意儿没法从 XAML 改)
+        For Each Btn As MyRadioButton In PanFilter.Children
+            Btn.LabText.Margin = New Thickness(-2, 0, 8, 0)
+        Next
+
 #If DEBUG Then
         BtnManageCheck.Visibility = Visibility.Visible
 #End If
@@ -29,11 +29,10 @@
     ''' <summary>
     ''' 刷新 Mod 列表。
     ''' </summary>
-    Public Sub RefreshList(Optional ForceReload As Boolean = False)
+    Public Sub ReloadModList(Optional ForceReload As Boolean = False)
         If LoaderFolderRun(McModLoader, PageVersionLeft.Version.PathIndie & "mods\", If(ForceReload, LoaderFolderRunType.ForceRun, LoaderFolderRunType.RunOnUpdated)) Then
             Log("[System] 已刷新 Mod 列表")
-            ViewModType = ViewType.All
-            BtnTypeAll.Checked = True
+            Filter = FilterType.All
             PanBack.ScrollToHome()
             SearchBox.Text = ""
         End If
@@ -60,10 +59,9 @@
     ''' 将加载器结果的 Mod 列表加载为 UI。
     ''' </summary>
     Private Sub LoadUIFromLoaderOutput()
-        Dim Mods As List(Of McMod) = McModLoader.Output
         Try
             '判断应该显示哪一个页面
-            If Mods.Any() Then
+            If McModLoader.Output.Any() Then
                 PanBack.Visibility = Visibility.Visible
                 PanEmpty.Visibility = Visibility.Collapsed
             Else
@@ -71,13 +69,15 @@
                 PanBack.Visibility = Visibility.Collapsed
                 Exit Sub
             End If
-            '输出结果
+            '修改缓存
             ModItems.Clear()
-            For Each ModEntity As McMod In Mods
+            For Each ModEntity As McMod In McModLoader.Output
                 ModItems(ModEntity.RawFileName) = McModListItem(ModEntity)
             Next
+            '显示结果
+            Filter = FilterType.All
             SearchBox.Text = "" '这会触发结果刷新,所以需要在 ModItems 更新之后,详见 #3124 的视频
-            RefreshResult(Mods)
+            RefreshUI()
         Catch ex As Exception
             Log(ex, "加载 Mod 列表 UI 失败", LogLevel.Feedback)
         End Try
@@ -129,85 +129,137 @@
     End Sub
 
     ''' <summary>
-    ''' 刷新结果显示。
+    ''' 刷新整个 UI。
     ''' </summary>
-    Private Sub RefreshResult(Mods As List(Of McMod))
+    Public Sub RefreshUI()
         If PanList Is Nothing Then Exit Sub
-        Dim ShowMods As List(Of McMod) = New List(Of McMod)
-        Select Case ViewModType
-            Case ViewType.All
-                ShowMods = Mods
-            Case ViewType.Enabled
-                For Each Item In Mods
-                    If Item.State.Equals(McMod.McModState.Fine) Then ShowMods.Add(Item)
-                Next
-            Case ViewType.Disabled
-                For Each Item In Mods
-                    If Item.State.Equals(McMod.McModState.Disabled) Then ShowMods.Add(Item)
-                Next
-            Case ViewType.CanUpdate
-                For Each Item In Mods
-                    If Item.CanUpdate Then ShowMods.Add(Item)
-                Next
-            Case ViewType.InError
-                For Each Item In Mods
-                    If Item.State.Equals(McMod.McModState.Unavaliable) Then ShowMods.Add(Item)
-                Next
-        End Select
-        PanList.Children.Clear()
-        For Each TargetMod In ShowMods
-            PanList.Children.Add(ModItems(TargetMod.RawFileName))
-        Next
-        Dim ModEnabled As Integer = 0
-        Dim ModDisabled As Integer = 0
-        Dim ModCanUpdate As Integer = 0
-        Dim ModError As Integer = 0
-        For Each ModItem In ModItems
-            If ModItem.Value.Entry.CanUpdate Then
-                ModCanUpdate += 1
-            End If
-            If ModItem.Value.Entry.State.Equals(McMod.McModState.Fine) Then
-                ModEnabled += 1
-            End If
-            If ModItem.Value.Entry.State.Equals(McMod.McModState.Disabled) Then
-                ModDisabled += 1
-            End If
-            If ModItem.Value.Entry.State.Equals(McMod.McModState.Unavaliable) Then
-                ModError += 1
-            End If
-        Next
-        BtnTypeAll.Text = $"全部 ({ModEnabled + ModDisabled + ModError}) "
-        BtnTypeCanUpdate.Text = $"可更新 ({ModCanUpdate}) "
-        BtnTypeEnabled.Text = $"已启用 ({ModEnabled}) "
-        BtnTypeDisabled.Text = $"已禁用 ({ModDisabled}) "
-        BtnTypeError.Text = $"错误 ({ModError}) "
-        RefreshTitle()
+        Dim ShowMods = GetShowingMods(True).ToList()
+        '重新列出列表
+        AniControlEnabled += 1
+        If ShowMods.Any() Then
+            PanList.Visibility = Visibility.Visible
+            PanList.Children.Clear()
+            For Each TargetMod In ShowMods
+                Dim Item As MyLocalModItem = ModItems(TargetMod.RawFileName)
+                Item.Checked = SelectedMods.Contains(TargetMod.RawFileName) '更新选中状态
+                PanList.Children.Add(Item)
+            Next
+        Else
+            PanList.Visibility = Visibility.Collapsed
+        End If
+        AniControlEnabled -= 1
+        SelectedMods = SelectedMods.Where(Function(m) ShowMods.Any(Function(s) s.RawFileName = m)).ToList '取消选中已经不显示的 Mod
+        RefreshBars()
     End Sub
+
     ''' <summary>
-    ''' 刷新卡片标题。
+    ''' 刷新顶栏和底栏显示。
     ''' </summary>
-    Private Sub RefreshTitle()
-        If Not IsSearching Then
-            PanListBack.Title = "Mod 列表 - "
-        ElseIf PanList.Children.Count > 0 Then
-            PanListBack.Title = "搜索结果 - "
+    Public Sub RefreshBars()
+        '-----------------
+        ' 顶部栏
+        '-----------------
+
+        '计数
+        Dim AnyCount As Integer = 0
+        Dim EnabledCount As Integer = 0
+        Dim DisabledCount As Integer = 0
+        Dim UpdateCount As Integer = 0
+        Dim UnavalialeCount As Integer = 0
+        For Each ModItem In GetShowingMods(False)
+            AnyCount += 1
+            If ModItem.CanUpdate Then UpdateCount += 1
+            If ModItem.State.Equals(McMod.McModState.Fine) Then EnabledCount += 1
+            If ModItem.State.Equals(McMod.McModState.Disabled) Then DisabledCount += 1
+            If ModItem.State.Equals(McMod.McModState.Unavaliable) Then UnavalialeCount += 1
+        Next
+        '显示
+        BtnFilterAll.Text = If(IsSearching, "搜索结果", "全部") & $" ({AnyCount})"
+        BtnFilterCanUpdate.Text = $"可更新 ({UpdateCount})"
+        BtnFilterCanUpdate.Visibility = If(Filter = FilterType.CanUpdate OrElse
+            UpdateCount > 0, Visibility.Visible, Visibility.Collapsed)
+        BtnFilterEnabled.Text = $"启用 ({EnabledCount})"
+        BtnFilterEnabled.Visibility = If(Filter = FilterType.Enabled OrElse Filter = FilterType.Disabled OrElse
+            EnabledCount > 0 AndAlso EnabledCount <> AnyCount, Visibility.Visible, Visibility.Collapsed)
+        BtnFilterDisabled.Text = $"禁用 ({DisabledCount})"
+        BtnFilterDisabled.Visibility = If(Filter = FilterType.Enabled OrElse Filter = FilterType.Disabled OrElse
+            DisabledCount > 0, Visibility.Visible, Visibility.Collapsed)
+        BtnFilterError.Text = $"错误 ({UnavalialeCount})"
+        BtnFilterError.Visibility = If(Filter = FilterType.Unavaliable OrElse
+            UnavalialeCount > 0, Visibility.Visible, Visibility.Collapsed)
+
+        '-----------------
+        ' 底部栏
+        '-----------------
+
+        '计数
+        Dim NewCount As Integer = SelectedMods.Count
+        Dim Selected = NewCount > 0
+        If Selected Then LabSelect.Text = $"已选择 {NewCount} 个文件" '取消所有选择时不更新数字
+        '按钮可用性
+        If Selected Then
+            Dim HasUpdate As Boolean = False
+            Dim HasEnabled As Boolean = False
+            Dim HasDisabled As Boolean = False
+            For Each ModEntity In McModLoader.Output
+                If SelectedMods.Contains(ModEntity.RawFileName) Then
+                    If ModEntity.CanUpdate Then HasUpdate = True
+                    If ModEntity.State = McMod.McModState.Fine Then
+                        HasEnabled = True
+                    ElseIf ModEntity.State = McMod.McModState.Disabled Then
+                        HasDisabled = True
+                    End If
+                End If
+            Next
+            BtnSelectDisable.IsEnabled = HasEnabled
+            BtnSelectEnable.IsEnabled = HasDisabled
+            BtnSelectUpdate.IsEnabled = HasUpdate
+        End If
+        '更新显示状态
+        If AniControlEnabled = 0 Then
+            PanListBack.Margin = New Thickness(0, 0, 0, If(Selected, 95, 15))
+            If Selected Then
+                '仅在数量增加时播放出现/跳跃动画
+                If BottomBarShownCount >= NewCount Then
+                    BottomBarShownCount = NewCount
+                    Return
+                Else
+                    BottomBarShownCount = NewCount
+                End If
+                '出现/跳跃动画
+                CardSelect.Visibility = Visibility.Visible
+                AniStart({
+                    AaOpacity(CardSelect, 1 - CardSelect.Opacity, 60),
+                    AaTranslateY(CardSelect, -27 - TransSelect.Y, 120, Ease:=New AniEaseOutFluent(AniEasePower.Weak)),
+                    AaTranslateY(CardSelect, 3, 150, 120, Ease:=New AniEaseInoutFluent(AniEasePower.Weak)),
+                    AaTranslateY(CardSelect, -1, 90, 270, Ease:=New AniEaseInoutFluent(AniEasePower.Weak))
+                }, "Mod Sidebar")
+            Else
+                '不重复播放隐藏动画
+                If BottomBarShownCount = 0 Then Return
+                BottomBarShownCount = 0
+                '隐藏动画
+                AniStart({
+                    AaOpacity(CardSelect, -CardSelect.Opacity, 90),
+                    AaTranslateY(CardSelect, -10 - TransSelect.Y, 90, Ease:=New AniEaseInFluent(AniEasePower.Weak)),
+                    AaCode(Sub() CardSelect.Visibility = Visibility.Collapsed, After:=True)
+                }, "Mod Sidebar")
+            End If
         Else
-            PanListBack.Title = "无搜索结果 - "
+            AniStop("Mod Sidebar")
+            BottomBarShownCount = NewCount
+            If Selected Then
+                CardSelect.Visibility = Visibility.Visible
+                CardSelect.Opacity = 1
+                TransSelect.Y = -25
+            Else
+                CardSelect.Visibility = Visibility.Collapsed
+                CardSelect.Opacity = 0
+                TransSelect.Y = -10
+            End If
         End If
-        Select Case ViewModType
-            Case ViewType.All
-                PanListBack.Title += BtnTypeAll.Text
-            Case ViewType.Enabled
-                PanListBack.Title += BtnTypeEnabled.Text
-            Case ViewType.Disabled
-                PanListBack.Title += BtnTypeDisabled.Text
-            Case ViewType.CanUpdate
-                PanListBack.Title += BtnTypeCanUpdate.Text
-            Case ViewType.InError
-                PanListBack.Title += BtnTypeError.Text
-        End Select
-        PanList.Visibility = If(PanList.Children.Count > 0, Visibility.Visible, Visibility.Collapsed)
     End Sub
+    Private BottomBarShownCount As Integer = 0
 
 #End Region
 
@@ -247,15 +299,7 @@
     ''' 全选。
     ''' </summary>
     Private Sub BtnManageSelectAll_Click(sender As Object, e As MouseButtonEventArgs) Handles BtnManageSelectAll.Click
-        Dim CurrentSelected As Integer = 0
-        For Each Item In PanList.Children.OfType(Of MyLocalModItem).ToList
-            If Item.Checked Then CurrentSelected += 1
-        Next
-        If CurrentSelected < PanList.Children.Count Then
-            ChangeCurrentSelected(True)
-        Else
-            ChangeCurrentSelected(False)
-        End If
+        ChangeAllSelected(SelectedMods.Count < GetShowingMods(True).Count)
     End Sub
 
     ''' <summary>
@@ -282,110 +326,20 @@
         Else
             SelectedMods.Remove(SelectedKey)
         End If
-        '更新下边栏 UI
-        RefreshBottomBar()
-    End Sub
-
-    '改变下边栏状态
-    Private ShownCount As Integer = 0
-    Public Sub RefreshBottomBar()
-        '计数
-        Dim NewCount As Integer = SelectedMods.Count
-        Dim Selected = NewCount > 0
-        If Selected Then LabSelect.Text = $"已选择 {NewCount} 个文件" '取消所有选择时不更新数字
-        '按钮可用性
-        If Selected Then
-            Dim HasUpdate As Boolean = False
-            Dim HasEnabled As Boolean = False
-            Dim HasDisabled As Boolean = False
-            For Each ModEntity In McModLoader.Output
-                If SelectedMods.Contains(ModEntity.RawFileName) Then
-                    If ModEntity.CanUpdate Then HasUpdate = True
-                    If ModEntity.State = McMod.McModState.Fine Then
-                        HasEnabled = True
-                    ElseIf ModEntity.State = McMod.McModState.Disabled Then
-                        HasDisabled = True
-                    End If
-                End If
-            Next
-            BtnSelectDisable.IsEnabled = HasEnabled
-            BtnSelectEnable.IsEnabled = HasDisabled
-            BtnSelectUpdate.IsEnabled = HasUpdate
-        End If
-        '更新显示状态
-        If AniControlEnabled = 0 Then
-            If Selected Then
-                PanListBack.Margin = New Thickness(0, 0, 0, 95)
-                '仅在数量增加时播放出现/跳跃动画
-                If ShownCount >= NewCount Then
-                    ShownCount = NewCount
-                    Return
-                Else
-                    ShownCount = NewCount
-                End If
-                '出现/跳跃动画
-                CardSelect.Visibility = Visibility.Visible
-                AniStart({
-                    AaOpacity(CardSelect, 1 - CardSelect.Opacity, 60),
-                    AaTranslateY(CardSelect, -27 - TransSelect.Y, 120, Ease:=New AniEaseOutFluent(AniEasePower.Weak)),
-                    AaTranslateY(CardSelect, 3, 150, 120, Ease:=New AniEaseInoutFluent(AniEasePower.Weak)),
-                    AaTranslateY(CardSelect, -1, 90, 270, Ease:=New AniEaseInoutFluent(AniEasePower.Weak))
-                }, "Mod Sidebar")
-            Else
-                PanListBack.Margin = New Thickness(0, 0, 0, 15)
-                '不重复播放隐藏动画
-                If ShownCount = 0 Then Return
-                ShownCount = 0
-                '隐藏动画
-                AniStart({
-                    AaOpacity(CardSelect, -CardSelect.Opacity, 90),
-                    AaTranslateY(CardSelect, -10 - TransSelect.Y, 90, Ease:=New AniEaseInFluent(AniEasePower.Weak)),
-                    AaCode(Sub() CardSelect.Visibility = Visibility.Collapsed, After:=True)
-                }, "Mod Sidebar")
-            End If
-        Else
-            AniStop("Mod Sidebar")
-            ShownCount = NewCount
-            If Selected Then
-                CardSelect.Visibility = Visibility.Visible
-                CardSelect.Opacity = 1
-                TransSelect.Y = -25
-            Else
-                CardSelect.Visibility = Visibility.Collapsed
-                CardSelect.Opacity = 0
-                TransSelect.Y = -10
-            End If
-        End If
+        RefreshBars()
     End Sub
 
     '切换所有项的选择状态
     Private Sub ChangeAllSelected(Value As Boolean)
         AniControlEnabled += 1
         SelectedMods.Clear()
-        For Each Item As MyLocalModItem In ModItems.Values.ToList
+        For Each Item As MyLocalModItem In GetShowingMods(True).Select(Function(m) ModItems(m.RawFileName))
             Item.Checked = Value
             If Value Then SelectedMods.Add(Item.Entry.RawFileName)
         Next
         AniControlEnabled -= 1
-        '更新下边栏 UI
-        RefreshBottomBar()
+        RefreshBars()
     End Sub
-
-    Private Sub ChangeCurrentSelected(Value As Boolean)
-        AniControlEnabled += 1
-        For Each Item As MyLocalModItem In PanList.Children
-            Item.Checked = Value
-            If Value Then
-                If Not SelectedMods.Contains(Item.Entry.RawFileName) Then SelectedMods.Add(Item.Entry.RawFileName)
-            Else
-                If SelectedMods.Contains(Item.Entry.RawFileName) Then SelectedMods.Remove(Item.Entry.RawFileName)
-            End If
-        Next
-        AniControlEnabled -= 1
-        '更新下边栏 UI
-        RefreshBottomBar()
-    End Sub
-
     Private Sub UnselectedAllWithAnimation() Handles Load.StateChanged, Me.PageExit
         Dim CacheAniControlEnabled = AniControlEnabled
         AniControlEnabled = 0
@@ -393,26 +347,76 @@
         AniControlEnabled += CacheAniControlEnabled
     End Sub
     Private Sub PageVersionMod_KeyDown(sender As Object, e As KeyEventArgs) Handles Me.KeyDown
-        If My.Computer.Keyboard.CtrlKeyDown AndAlso e.Key = Key.A Then ChangeCurrentSelected(True)
+        If My.Computer.Keyboard.CtrlKeyDown AndAlso e.Key = Key.A Then ChangeAllSelected(True)
     End Sub
 
-    Private ViewModType As ViewType = ViewType.All
+#End Region
+
+#Region "筛选"
 
-    Private Enum ViewType As Integer
-        All
-        Enabled
-        Disabled
-        CanUpdate
-        InError
+    Private _Filter As FilterType = FilterType.All
+    Private Property Filter As FilterType
+        Get
+            Return _Filter
+        End Get
+        Set(value As FilterType)
+            If _Filter = value Then Return
+            _Filter = value
+            Select Case value
+                Case FilterType.All
+                    BtnFilterAll.Checked = True
+                Case FilterType.Enabled
+                    BtnFilterEnabled.Checked = True
+                Case FilterType.Disabled
+                    BtnFilterDisabled.Checked = True
+                Case FilterType.CanUpdate
+                    BtnFilterCanUpdate.Checked = True
+                Case Else
+                    BtnFilterError.Checked = True
+            End Select
+            RefreshUI()
+        End Set
+    End Property
+    Private Enum FilterType As Integer
+        All = 0
+        Enabled = 1
+        Disabled = 2
+        CanUpdate = 3
+        Unavaliable = 4
     End Enum
 
-    Private Sub ChangeViewType(sender As MyRadioButton, raiseByMouse As Boolean) Handles BtnTypeAll.Check, BtnTypeCanUpdate.Check, BtnTypeDisabled.Check, BtnTypeEnabled.Check, BtnTypeError.Check
-        ViewModType = sender.Tag
-        If IsSearching Then
-            SearchRun()
-        Else
-            RefreshResult(McModLoader.Output)
-        End If
+    ''' <summary>
+    ''' 获取所有应该显示在 UI 中的 Mod。
+    ''' </summary>
+    Private Function GetShowingMods(ApplyFilter As Boolean) As IEnumerable(Of McMod)
+        If McModLoader.Output Is Nothing Then Return New List(Of McMod)
+        Return If(IsSearching, SearchResult, McModLoader.Output).Where(Function(m) Not ApplyFilter OrElse CanPassFilter(m))
+    End Function
+
+    ''' <summary>
+    ''' 检查该 Mod 项是否符合当前筛选的类别。
+    ''' </summary>
+    Private Function CanPassFilter(CheckingMod As McMod) As Boolean
+        Select Case Filter
+            Case FilterType.All
+                Return True
+            Case FilterType.Enabled
+                Return CheckingMod.State = McMod.McModState.Fine
+            Case FilterType.Disabled
+                Return CheckingMod.State = McMod.McModState.Disabled
+            Case FilterType.CanUpdate
+                Return CheckingMod.CanUpdate
+            Case FilterType.Unavaliable
+                Return CheckingMod.State = McMod.McModState.Unavaliable
+            Case Else
+                Return False
+        End Select
+    End Function
+
+    '点击筛选项触发的改变
+    Private Sub ChangeFilter(sender As MyRadioButton, raiseByMouse As Boolean) Handles BtnFilterAll.Check, BtnFilterCanUpdate.Check, BtnFilterDisabled.Check, BtnFilterEnabled.Check, BtnFilterError.Check
+        Filter = sender.Tag
+        RefreshUI()
     End Sub
 
 #End Region
@@ -424,7 +428,6 @@
         EDMods(McModLoader.Output.Where(Function(m) SelectedMods.Contains(m.RawFileName)),
                Not sender.Equals(BtnSelectDisable))
         ChangeAllSelected(False)
-        RefreshResult(McModLoader.Output)
     End Sub
     Private Sub EDMods(ModList As IEnumerable(Of McMod), IsEnable As Boolean)
         Dim IsSuccessful As Boolean = True
@@ -459,7 +462,7 @@
                 FileSystem.Rename(ModEntity.Path, NewPath)
             Catch ex As FileNotFoundException
                 Log(ex, $"未找到需要重命名的 Mod({If(ModEntity.Path, "null")})", LogLevel.Feedback)
-                RefreshList(True)
+                ReloadModList(True)
                 Return
             Catch ex As Exception
                 Log(ex, $"重命名 Mod 失败({If(ModEntity.Path, "null")})")
@@ -479,12 +482,11 @@
             PanList.Children.RemoveAt(IndexOfUi)
             PanList.Children.Insert(IndexOfUi, NewItem)
         Next
-        RefreshTitle() '改变数量显示
-        If Not IsSuccessful Then
-            Hint("由于文件被占用,Mod 的状态切换失败,请尝试关闭正在运行的游戏后再试!", HintType.Critical)
-            RefreshList(True)
+        If IsSuccessful Then
+            RefreshBars()
         Else
-            RefreshBottomBar()
+            Hint("由于文件被占用,Mod 的状态切换失败,请尝试关闭正在运行的游戏后再试!", HintType.Critical)
+            ReloadModList(True)
         End If
     End Sub
 
@@ -601,7 +603,7 @@
             LoaderTaskbarAdd(Loader)
             FrmMain.BtnExtraDownload.ShowRefresh()
             FrmMain.BtnExtraDownload.Ribble()
-            RefreshList(True)
+            ReloadModList(True)
         Catch ex As Exception
             Log(ex, "初始化 Mod 更新失败")
         End Try
@@ -635,7 +637,7 @@
                     End If
                 Catch ex As OperationCanceledException
                     Log(ex, "删除 Mod 被主动取消")
-                    RefreshList(True)
+                    ReloadModList(True)
                     Return
                 Catch ex As Exception
                     Log(ex, $"删除 Mod 失败({ModEntity.Path})", LogLevel.Msgbox)
@@ -649,14 +651,14 @@
                 Dim IndexOfUi As Integer = PanList.Children.IndexOf(PanList.Children.OfType(Of MyLocalModItem).FirstOrDefault(Function(i) i.Entry.Equals(ModEntity)))
                 If IndexOfUi >= 0 Then PanList.Children.RemoveAt(IndexOfUi)
             Next
-            RefreshTitle()
+            RefreshBars()
             If Not IsSuccessful Then
                 Hint("由于文件被占用,Mod 删除失败,请尝试关闭正在运行的游戏后再试!", HintType.Critical)
-                RefreshList(True)
+                ReloadModList(True)
             ElseIf PanList.Children.Count = 0 Then
-                RefreshList(True) '删除了全部文件
+                ReloadModList(True) '删除了全部文件
             Else
-                RefreshBottomBar()
+                RefreshBars()
             End If
             '显示结果提示
             If Not IsSuccessful Then Exit Sub
@@ -675,10 +677,10 @@
             End If
         Catch ex As OperationCanceledException
             Log(ex, "删除 Mod 被主动取消")
-            RefreshList(True)
+            ReloadModList(True)
         Catch ex As Exception
             Log(ex, "删除 Mod 出现未知错误", LogLevel.Feedback)
-            RefreshList(True)
+            ReloadModList(True)
         End Try
     End Sub
 
@@ -791,8 +793,8 @@
             Return Not String.IsNullOrWhiteSpace(SearchBox.Text)
         End Get
     End Property
+    Private SearchResult As List(Of McMod)
     Public Sub SearchRun() Handles SearchBox.TextChanged
-        ChangeAllSelected(False)
         If IsSearching Then
             '构造请求
             Dim QueryList As New List(Of SearchEntry(Of McMod))
@@ -815,12 +817,9 @@
                 QueryList.Add(New SearchEntry(Of McMod) With {.Item = Entry, .SearchSource = SearchSource})
             Next
             '进行搜索
-            Dim SearchResult = Search(QueryList, SearchBox.Text, MaxBlurCount:=6, MinBlurSimilarity:=0.35)
-            RefreshResult(SearchResult.Select(Function(r) r.Item).ToList)
-        Else
-            '退出搜索状态
-            RefreshResult(McModLoader.Output)
+            SearchResult = Search(QueryList, SearchBox.Text, MaxBlurCount:=6, MinBlurSimilarity:=0.35).Select(Function(r) r.Item).ToList
         End If
+        RefreshUI()
     End Sub
 
 #End Region

+ 2 - 2
Plain Craft Launcher 2/Resources/ModData.txt

@@ -3234,7 +3234,7 @@ memory-cleaner-mod|内存自动清理*
 ore-flowers|矿物花卉*
 aqua-creepers|水下苦力怕 (Aqua Creepers!)
 extrastorage|更多存储 (ExtraStorage)
-thermal-locomotion@thermal-integration|热力运输*
+thermal-locomotion|热力运输*
 external-tweaker
 morebiomesxl
 notes@|笔记*
@@ -3821,7 +3821,7 @@ cat-jammies
 rhino@|犀牛*
 fastsuite|配方性能优化 (FastSuite)
 twerkitmeal|跳舞生长一切 (Twerk To Grow All The Things)
-thermal-integration@thermal-locomotion|热力集成*
+thermal-integration|热力集成*
 piggybanks|存钱罐*
 in-control@
 mystical-agriculture-refabricated|神秘农业:Fabric版 (Mystical Agriculture: Refabricated)