Просмотр исходного кода

Merge pull request #4758 from youzi-3222/feat-98

导出整合包 复活
龙腾猫跃 1 год назад
Родитель
Сommit
1a0c7b0520

+ 23 - 7
Plain Craft Launcher 2/Controls/MyListItem.xaml.vb

@@ -337,7 +337,16 @@
             Return _Checked
         End Get
         Set(value As Boolean)
-            SetChecked(value, False, True)
+            SetChecked(value, False, True, _Half)
+        End Set
+    End Property
+    Private _Half As Boolean = False
+    Public Property Half As Boolean
+        Get
+            Return _Half
+        End Get
+        Set(value As Boolean)
+            SetChecked(_Checked, False, True, value)
         End Set
     End Property
     ''' <summary>
@@ -346,7 +355,8 @@
     ''' <param name="value">新的 Checked 属性。</param>
     ''' <param name="user">是否由用户引发。</param>
     ''' <param name="anime">是否执行动画。</param>
-    Public Sub SetChecked(value As Boolean, user As Boolean, anime As Boolean)
+    ''' <param name="half">是否为部分选中。</param>
+    Public Sub SetChecked(value As Boolean, user As Boolean, anime As Boolean, Optional half As Boolean = False)
         Try
 
             '自定义属性基础
@@ -364,8 +374,9 @@
                 End If
                 _Checked = value
             Else
-                If value = _Checked Then Exit Sub
+                If value = _Checked AndAlso half = _Half Then Exit Sub
                 _Checked = value
+                _Half = half
                 If IsInitialized Then
                     RaiseEvent Changed(Me, ChangedEventArgs)
                     If ChangedEventArgs.Handled Then
@@ -429,9 +440,9 @@
                     '由无变有
                     If Not IsNothing(RectCheck) Then
                         Dim Delta = ActualHeight - RectCheck.ActualHeight - 12
-                        Anim.Add(AaHeight(RectCheck, Delta * 0.4, 200,, New AniEaseOutFluent(AniEasePower.Weak)))
-                        Anim.Add(AaHeight(RectCheck, Delta * 0.6, 300,, New AniEaseOutBack(AniEasePower.Weak)))
-                        Anim.Add(AaOpacity(RectCheck, 1 - RectCheck.Opacity, 30))
+                        Anim.Add(AaHeight(RectCheck, Delta * If(half, 0.2, 0.4), 200,, New AniEaseOutFluent(AniEasePower.Weak)))
+                        Anim.Add(AaHeight(RectCheck, Delta * If(half, 0.3, 0.6), 300,, New AniEaseOutBack(AniEasePower.Weak)))
+                        Anim.Add(AaOpacity(RectCheck, If(half, 0.5, 1) - RectCheck.Opacity, 30))
                         RectCheck.VerticalAlignment = VerticalAlignment.Center
                         RectCheck.Margin = New Thickness(-1, 0, 0, 0)
                     End If
@@ -450,12 +461,17 @@
             Else
                 '不使用动画
                 AniStop("MyListItem Checked " & Uuid)
-                If Checked Then
+
+                If Checked OrElse half Then
                     If Not IsNothing(RectCheck) Then
                         RectCheck.Height = Double.NaN
                         RectCheck.Margin = New Thickness(-1, 6, 0, 6)
                         RectCheck.Opacity = 1
                         RectCheck.VerticalAlignment = VerticalAlignment.Stretch
+                        If half Then
+                            RectCheck.Height = Height * 0.4
+                            RectCheck.Opacity = 0.5
+                        End If
                     End If
                     SetResourceReference(ForegroundProperty, If(Height < 40, "ColorBrush3", "ColorBrush2"))
                 Else

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

@@ -987,6 +987,7 @@ Public Class FormMain
         VersionSetup = 1
         VersionMod = 2
         VersionModDisabled = 3
+        VersionExport = 4
     End Enum
     ''' <summary>
     ''' 获取次级页面的名称。若并非次级页面则返回空字符串,故可以以此判断是否为次级页面。

+ 4 - 0
Plain Craft Launcher 2/Modules/Base/ModBase.vb

@@ -180,6 +180,10 @@ Public Module ModBase
         ''' </summary>
         Public Const IconButtonServer As String = "M224 160a64 64 0 0 0-64 64v576a64 64 0 0 0 64 64h576a64 64 0 0 0 64-64V224a64 64 0 0 0-64-64H224z m0 384h576v256H224v-256z m192 96v64h320v-64H416z m-128 0v64h64v-64H288zM224 224h576v256H224V224z m192 96v64h320v-64H416z m-128 0v64h64v-64H288z"
         ''' <summary>
+        ''' 图标,向右箭头,1x
+        ''' </summary>
+        Public Const IconButtonRight As String = "M757.792745 435.407215L419.597482 96.904967c-40.010393-40.010393-104.886579-40.010393-144.896972 0-40.010393 40.010393-40.010393 104.988908 0 144.9993L540.344959 507.855701 274.70051 773.807135c-40.010393 40.112721-40.010393 104.988908 0 144.9993 40.010393 40.010393 104.886579 40.010393 144.896972 0l338.092935-338.39992c40.112721-40.010393 40.112721-104.988908 0.102328-144.9993z"
+        ''' <summary>
         ''' 图标,音符,1x
         ''' </summary>
         Public Const IconMusic As String = "M348.293565 716.53287V254.797913c0-41.672348 28.004174-78.358261 68.919652-90.37913L815.994435 40.826435c62.775652-18.610087 125.907478 26.579478 125.907478 89.933913v539.158261c8.013913 42.25113-8.94887 89.177043-47.014956 127.109565a232.848696 232.848696 0 0 1-170.785392 65.758609c-61.885217-2.938435-111.081739-33.435826-129.113043-80.050087-18.031304-46.614261-2.137043-102.177391 41.672348-145.853218a232.848696 232.848696 0 0 1 170.785391-65.80313c21.014261 1.024 40.514783 5.164522 57.878261 12.065391V233.338435c0-12.109913-10.551652-20.034783-20.569044-20.034783a24.620522 24.620522 0 0 0-5.787826 0.934957L439.785739 338.18713a19.545043 19.545043 0 0 0-14.825739 19.144348v438.984348H423.846957c11.53113 43.987478-5.164522 94.208-45.412174 134.322087a232.848696 232.848696 0 0 1-170.785392 65.758609c-61.885217-2.938435-111.081739-33.435826-129.113043-80.050087-18.031304-46.614261-2.137043-102.177391 41.672348-145.853218a232.848696 232.848696 0 0 1 170.785391-65.80313c20.791652 1.024 40.069565 5.075478 57.299478 11.842783z"

+ 10 - 6
Plain Craft Launcher 2/Modules/Base/ModValidate.vb

@@ -262,6 +262,7 @@ Public Class ValidateFileName
     Public Property IgnoreCase As Boolean = True
     Public Property ParentFolder As String = Nothing
     Public Property RequireParentFolderExists = True
+    Public Property AllowNull As Boolean = False
     Public Sub New()
     End Sub
     Public Sub New(Name As String, Optional UseMinecraftCharCheck As Boolean = True, Optional IgnoreCase As Boolean = True)
@@ -271,14 +272,16 @@ Public Class ValidateFileName
     End Sub
     Public Overrides Function Validate(Str As String) As String
         Try
-            '检查是否为空
-            Dim LengthCheck As String = New ValidateNullOrWhiteSpace().Validate(Str)
-            If Not LengthCheck = "" Then Return LengthCheck
+            If Not AllowNull Then
+                '检查是否为空
+                Dim NullCheck As String = New ValidateNullOrWhiteSpace().Validate(Str)
+                If Not NullCheck = "" Then Return NullCheck
+            End If
             '检查空格
             If Str.StartsWithF(" ") Then Return "文件名不能以空格开头!"
             If Str.EndsWithF(" ") Then Return "文件名不能以空格结尾!"
             '检查长度
-            LengthCheck = New ValidateLength(1, 253).Validate(Str & If(ParentFolder, ""))
+            Dim LengthCheck = New ValidateLength(If(AllowNull, 0, 1), 253).Validate(Str & If(ParentFolder, ""))
             If Not LengthCheck = "" Then Return LengthCheck
             '检查尾部小数点
             If Str.EndsWithF(".") Then Return "文件名不能以小数点结尾!"
@@ -315,6 +318,7 @@ End Class
 Public Class ValidateFolderPath
     Inherits Validate
     Public Property UseMinecraftCharCheck As Boolean = True
+    Public Property AllowNull As Boolean = False
     Public Sub New()
     End Sub
     Public Sub New(UseMinecraftCharCheck As Boolean)
@@ -326,9 +330,9 @@ Public Class ValidateFolderPath
         If Not Str.TrimEnd("\").EndsWithF(":") Then Str = Str.TrimEnd("\")
         '检查是否为空
         Dim LengthCheck As String = New ValidateNullOrWhiteSpace().Validate(Str)
-        If Not LengthCheck = "" Then Return LengthCheck
+        If Not AllowNull AndAlso Not LengthCheck = "" Then Return LengthCheck
         '检查长度
-        LengthCheck = New ValidateLength(1, 254).Validate(Str)
+        LengthCheck = New ValidateLength(If(AllowNull, 0, 1), 254).Validate(Str)
         If Not LengthCheck = "" Then Return LengthCheck
         '检查开头
         If Str.StartsWithF("\\Mac\") Then GoTo Fin

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

@@ -1137,7 +1137,7 @@ Finished:
         '刷新边栏
         RunInUi(Sub() FrmVersionMod?.RefreshBars())
     End Sub
-    Private Function GetTargetModLoaders() As List(Of CompModLoaderType)
+    Public Function GetTargetModLoaders() As List(Of CompModLoaderType)
         Dim ModLoaders As New List(Of CompModLoaderType)
         If PageVersionLeft.Version.Version.HasForge Then ModLoaders.Add(CompModLoaderType.Forge)
         If PageVersionLeft.Version.Version.HasNeoForge Then ModLoaders.Add(CompModLoaderType.NeoForge)

+ 335 - 0
Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb

@@ -3,6 +3,7 @@ Imports System.Linq.Expressions
 
 Public Module ModModpack
 
+#Region "安装"
     '触发整合包安装的外部接口
     ''' <summary>
     ''' 弹窗要求选择一个整合包文件并进行安装。
@@ -905,6 +906,340 @@ Retry:
         Return Loader
     End Function
 
+#End Region
+#End Region
+
+#Region "导出"
+    Private ExpTempDir As String = PathTemp & "PackExport\"
+    ''' <summary>
+    ''' 整合包类型。
+    ''' </summary>
+    Public Enum ModpackType
+        Modrinth
+    End Enum
+    ''' <summary>
+    ''' 导出整合包的选项。
+    ''' </summary>
+    Public Class ExportOptions
+        ''' <summary>
+        ''' 要导出的版本。
+        ''' </summary>
+        Public Version As McVersion
+        ''' <summary>
+        ''' 要保存整合包的位置。
+        ''' </summary>
+        Public Dest As String
+        ''' <summary>
+        ''' 是否包括 PCL 文件。
+        ''' </summary>
+        Public IncludePCL As Boolean
+        ''' <summary>
+        ''' 整合包类型。
+        ''' </summary>
+        Public Type As ModpackType
+        ''' <summary>
+        ''' 要保留到整合包的文件。
+        ''' </summary>
+        Public Additional As String()
+        ''' <summary>
+        ''' 是否需要保留 PCL 全局设置。仅在包括 PCL 时有效。
+        ''' </summary>
+        Public PCLSetupGlobal As Boolean = True
+        ''' <summary>
+        ''' 整合包名称。
+        ''' </summary>
+        Public Name As String = ""
+        ''' <summary>
+        ''' 整合包描述。
+        ''' </summary>
+        Public Desc As String = ""
+        ''' <summary>
+        ''' 版本号。
+        ''' </summary>
+        Public VerID As String = ""
+        Public Sub New(Version As McVersion,
+                       Dest As String,
+                       IncludePCL As Boolean,
+                       Additional As String(),
+                       Optional PCLSetupGlobal As Boolean = True,
+                       Optional Name As String = "",
+                       Optional Desc As String = "",
+                       Optional VerID As String = "")
+            Me.Version = Version
+            Me.Dest = Dest
+            Me.IncludePCL = IncludePCL
+            Me.Additional = Additional
+            Me.PCLSetupGlobal = PCLSetupGlobal
+            Me.Name = Name
+            Me.Desc = Desc
+            Me.VerID = VerID
+        End Sub
+    End Class
+    ''' <summary>
+    ''' 导出整合包,返回是否成功。
+    ''' </summary>
+    Public Function ModpackExport(Task As LoaderTask(Of ExportOptions, Boolean)) As Boolean
+        If Task.Input.IncludePCL Then Return ExportCompressed(Task)
+        Return ExportModrinth(Task)
+    End Function
+
+#Region "冗余"
+    Private VersionRedundant As String() = {"^screenshots", "^backups", "^command_history\.txt$", '个人文件
+        "^.*-natives", "^server-resource-packs", "^user.*cache\.json$", "^\.optifine", "^\.fabric", "^\.mixin\.out", '缓存
+        "^.*\.jar$", "^downloads", "^realms_persistence.json$", "^\$\{natives_directory\}", "^essential", '可联网更新
+        "^logs", "^crash-reports", ".*\.log$", "^debug", '日志
+        ".*\.dat_old$", ".*\.old$", '备份
+        "^\$\{quickPlayPath\}", '服务器
+        "^\.replay_cache", "^replay_recordings", "^replay_videos", 'ReplayMod
+        "^irisUpdateInfo\.json$", 'Iris
+        "^modernfix", 'ModernFix 模组
+        "^modtranslations", 'Mod 翻译
+        "^schematics", 'schematics 模组
+        "^journeymap\\data", 'JourneyMap 模组
+        "^mods\\\.connector", '信雅互联
+        "^.*\.BakaCoreInfo$", 'BakaXL 配置
+        "^hmclversion\.cfg$", "^log4j2\.xml$", 'HMCL 配置
+        "^assets$", "^libraries$", "^\$natives$", "^launcher_profiles\.json$", "^versions$" '开启版本隔离时排除的文件
+    }
+    Public Function IsVerRedundant(FilePath As String, PathIndie As String) As Boolean
+        FilePath = FilePath.Replace("/", "\")
+        For Each regex In VersionRedundant
+            If RegexCheck(FilePath.Replace(PathIndie, ""), regex, RegularExpressions.RegexOptions.IgnoreCase) Then Return True
+        Next
+        Return False
+    End Function
+    Private MustExport As String() = {
+        "mods", "PCL", ".*\.json"
+    }
+    Public Function IsMustExport(FilePath As String, PathIndie As String) As Boolean
+        For Each regex In MustExport
+            If RegexCheck(FilePath.Replace(PathIndie, ""), regex, RegularExpressions.RegexOptions.IgnoreCase) AndAlso Not (FilePath.EndsWithF(".zip") OrElse FilePath.EndsWithF("\")) Then Return True
+        Next
+        Return False
+    End Function
+
+#End Region
+
+#Region "不同类型整合包的导出方法"
+    Private Function ExportModrinth(Task As LoaderTask(Of ExportOptions, Boolean)) As Boolean
+        Try
+            Dim Version = Task.Input.Version
+            Dim DestPath = Task.Input.Dest
+            Dim Additional = Task.Input.Additional
+            Dim Name = Task.Input.Name
+            Dim Description = Task.Input.Desc
+            Dim VerID = Task.Input.VerID
+            Log($"[Export] 导出整合包(Modrinth):{Version.Path} -> {DestPath},额外版本文件 {Additional.Count} 个")
+            Dim tempDir As String = $"{ExpTempDir}{GetUuid()}\"
+            Log($"[Export] 缓存文件夹:{tempDir}")
+            DeleteDirectory(tempDir)
+            Directory.CreateDirectory(tempDir)
+
+#Region "0.04-0.40:从 Modrinth 获取 Mod 工程信息,得到 URL"
+            Dim Mods As New Dictionary(Of String, McMod)
+            If Directory.Exists(Version.Path & "mods\") Then
+                For Each m In Directory.EnumerateFiles(Version.Path & "mods\")
+                    If m.EndsWithF(".jar") Then Mods.Add(GetFileNameFromPath(m), New McMod(m))
+                Next
+            End If
+
+            '从 Modrinth 获取信息
+            Dim ModrinthMapping As New Dictionary(Of String, JObject) 'Modrinth 获取到的 Mod
+            Dim CurseForgeMapping As New Dictionary(Of String, JObject) 'CurseForge 获取到的 Mod
+            Dim Failed = Mods.ToArray().ToDictionary(Function(a) a.Key, Function(a) a.Value) '获取失败的 Mod
+            Dim ModrinthHashes = Mods.Select(Function(m) m.Value.ModrinthHash).ToList()
+            If Mods.Count = 0 Then GoTo JumpMod
+            Dim ModrinthRaw = CType(GetJson(NetRequestRetry("https://api.modrinth.com/v2/version_files", "POST",
+    $"{{""hashes"": [""{ModrinthHashes.Join(""",""")}""], ""algorithm"": ""sha1""}}", "application/json")), JObject)
+            Log($"[Export] 从 Modrinth 获取到 {ModrinthRaw.Count} 个本地 Mod 的对应信息")
+            Task.Progress = 0.35
+
+            For Each Entry In Mods
+                If (Not ModrinthRaw.ContainsKey(Entry.Value.ModrinthHash)) OrElse
+                   (ModrinthRaw(Entry.Value.ModrinthHash)("files")(0)("hashes")("sha1") <> Entry.Value.ModrinthHash) Then
+                    Continue For
+                End If
+                Failed.Remove(Entry.Key)
+                ModrinthMapping.Add(Entry.Key, ModrinthRaw(Entry.Value.ModrinthHash)("files")(0))
+            Next
+            Task.Progress = 0.4
+#End Region
+
+#Region "0.40-0.80:从 CurseForge 继续获取"
+            Dim CurseForgeHashes = Mods.Select(Function(m) m.Value.CurseForgeHash).ToList()
+            Dim CurseForgeRaw = CType(CType(GetJson(NetRequestRetry("https://api.curseforge.com/v1/fingerprints/432/", "POST",
+                                    $"{{""fingerprints"": [{CurseForgeHashes.Join(",")}]}}", "application/json")), JObject)("data")("exactMatches"), JContainer)
+            Log($"[Export] 从 CurseForge 获取到 {CurseForgeRaw.Count} 个本地 Mod 的对应信息")
+            Task.Progress = 0.7
+
+            For Each m In CurseForgeRaw
+                Dim hash As String = ""
+                For Each h In m("file")("hashes") '获取 Modrinth Hash
+                    If h("algo").ToString = 1 Then
+                        hash = h("value")
+                        Exit For
+                    End If
+                Next
+                If String.IsNullOrEmpty(hash) OrElse String.IsNullOrEmpty(m("file")("downloadUrl")) Then Continue For
+
+                m("file")("ModrinthHash") = hash
+                Dim f = Mods.ToArray()
+                For Each file In f
+                    If file.Value.ModrinthHash = hash Then
+                        CurseForgeMapping.Add(file.Key, m("file"))
+                        Failed.Remove(file.Key)
+                        Exit For
+                    End If
+                Next
+            Next
+            Task.Progress = 0.8
+#End Region
+
+JumpMod:
+#Region "0.80-0.90:合并下载链接,复制 Logo,写入 Json 文件"
+            '获取作为检查目标的加载器和版本
+            Dim ModLoaders = GetTargetModLoaders()
+            Dim McVersion = Version.Version.McName
+            Log($"[Export] 目标加载器:{ModLoaders.Join("/")},版本:{McVersion}")
+
+            '合并下载链接
+            Dim BothUrls As New Dictionary(Of String, String()) '两个下载源都有 URL 的 Mod
+            For Each modrinth In ModrinthMapping
+                Dim curseforge As New JObject
+                If CurseForgeMapping.TryGetValue(modrinth.Key, curseforge) Then
+                    BothUrls.Add(modrinth.Key, {modrinth.Value("url"), curseforge("downloadUrl")})
+                End If
+            Next
+            Task.Progress = 0.83
+
+            Dim files As New JArray
+            For Each m In ModrinthMapping
+                files.Add(New JObject From {
+                    {"path", $"mods/{m.Key}"},
+                    {"hashes", m.Value("hashes")},
+                    {"env", New JObject From {{"client", "required"}, {"server", "required"}}},
+                    {"downloads", If(BothUrls.ContainsKey(m.Key), JArray.FromObject(BothUrls(m.Key)), New JArray From {m.Value("url")})},
+                    {"fileSize", m.Value("size")}
+                })
+            Next
+            For Each m In CurseForgeMapping
+                If Not BothUrls.ContainsKey(m.Key) Then
+                    files.Add(New JObject From {
+                        {"path", $"mods/{m.Key}"},
+                        {"hashes", New JObject From {{"sha1", m.Value("ModrinthHash").ToString}}},
+                        {"env", New JObject From {{"client", "required"}, {"server", "required"}}},
+                        {"downloads", New JArray From {m.Value("downloadUrl")}},
+                        {"fileSize", m.Value("fileLength")}
+                    })
+                End If
+            Next
+            Task.Progress = 0.86
+            'If Not String.IsNullOrEmpty(Logo) Then
+            '    Dim LogoObj As New MyBitmap(Logo)
+            '    LogoObj.Save(tempDir & "Logo.png")
+            'End If
+            'Task.Progress = 0.88
+
+            Dim depend As New JObject From {{"minecraft", McVersion}}
+            If Version.Version.HasForge Then depend.Add("forge", Version.Version.ForgeVersion)
+            If Version.Version.HasFabric Then depend.Add("fabric-loader", Version.Version.FabricVersion)
+            If Version.Version.HasNeoForge Then depend.Add("neoforge", Version.Version.NeoForgeVersion)
+
+            Dim json As New JObject From {
+                {"game", "minecraft"},
+                {"formatVersion", 1},
+                {"versionId", VerID},
+                {"name", If(String.IsNullOrEmpty(Name), Version.Name, Name)},
+                {"summary", Description},
+                {"files", files},
+                {"dependencies", depend}
+            }
+            'If Not String.IsNullOrEmpty(Logo) Then json.Add("logo", tempDir & "Logo.png")
+
+            File.WriteAllText(tempDir & "modrinth.index.json", json.ToString)
+            Task.Progress = 0.9
+#End Region
+
+#Region "0.90-0.99:将其余文件保存到 overrides 目录"
+            For Each m In Failed
+                CopyFile(m.Value.Path, tempDir & "overrides\mods\" & m.Value.FileName)
+            Next
+            Task.Progress = 0.92
+
+            '额外文件
+            For Each p In Additional
+                If String.IsNullOrWhiteSpace(p) Then Continue For '传空字串进去会直接把整个版本文件夹拷过去
+                If Not p.StartsWithF(GetPathFromFullPath(GetPathFromFullPath(Version.Path)), IgnoreCase:=True) Then Continue For
+                Dim relative As String = p.Replace(Version.Path, "").Replace(GetPathFromFullPath(GetPathFromFullPath(Version.Path)), "")
+                If File.Exists(p) AndAlso Not IsVerRedundant(p, Version.PathIndie) Then
+                    CopyFile(p, $"{tempDir}overrides\{relative}")
+                End If
+            Next
+            Task.Progress = 0.96
+
+            '如果 resourcepacks shaderpacks 文件夹中有文件夹格式的包,则先将文件夹压缩成 Zip 文件后再打包入对应位置
+            '这可以避免安装时出现路径长度限制而出错
+            If Directory.Exists($"{tempDir}overrides\resourcepacks\") Then
+                For Each d In Directory.EnumerateDirectories($"{tempDir}overrides\resourcepacks\")
+                    If Not File.Exists(d + ".zip") Then ZipFile.CreateFromDirectory(d, d + ".zip")
+                    DeleteDirectory(d)
+                Next
+            End If
+            If Directory.Exists($"{tempDir}overrides\shaderpacks\") Then
+                For Each d In Directory.EnumerateDirectories($"{tempDir}overrides\shaderpacks\")
+                    If Not File.Exists(d + ".zip") Then ZipFile.CreateFromDirectory(d, d + ".zip")
+                    DeleteDirectory(d)
+                Next
+            End If
+#End Region
+
+            If File.Exists(DestPath) Then File.Delete(DestPath) '选择文件的时候已经确认了要替换
+            ZipFile.CreateFromDirectory(tempDir, DestPath)
+            DeleteDirectory(tempDir)
+            Return True
+        Catch ex As Exception
+            Log(ex, "导出整合包失败", LogLevel.Msgbox)
+            Return False
+        End Try
+    End Function
+    Private Function ExportCompressed(Task As LoaderTask(Of ExportOptions, Boolean)) As Boolean
+        Try
+            Dim Version = Task.Input.Version
+            Dim DestPath = Task.Input.Dest
+            Dim Additional = Task.Input.Additional
+            Dim Name = Task.Input.Name
+            Dim VerID = Task.Input.VerID
+            Dim PCLSetupGlobal = Task.Input.PCLSetupGlobal
+            Log($"[Export] 导出整合包(含启动器):{Version.Path} -> {DestPath},额外版本文件 {Additional.Count} 个,全局设置 {If(PCLSetupGlobal, "导出", "不导出")}")
+            Dim tempDir As String = $"{ExpTempDir}{GetUuid()}\"
+            Log($"[Export] 最终压缩包的缓存文件夹:{tempDir}")
+            Directory.CreateDirectory(tempDir)
+
+            Task.Progress = 0.04
+
+            Log("[Export] 开始导出 Modrinth 整合包")
+            Task.Input.Dest = $"{tempDir}modpack.mrpack"
+            ExportModrinth(Task)
+
+            Log($"[Export] 正在复制 PCL 本体")
+            CopyFile(PathWithName, tempDir & GetFileNameFromPath(PathWithName))
+
+            If PCLSetupGlobal Then
+                Log($"[Export] 正在复制 PCL 全局配置")
+                CopyFile(Path & "PCL\Setup.ini", tempDir & "PCL\Setup.ini")
+            End If
+
+            If File.Exists(DestPath) Then File.Delete(DestPath) '选择文件的时候已经确认了要替换
+            ZipFile.CreateFromDirectory(tempDir, DestPath)
+            DeleteDirectory(tempDir)
+            Return True
+        Catch ex As Exception
+            Log(ex, "导出整合包失败", LogLevel.Msgbox)
+            Return False
+        End Try
+    End Function
+#End Region
+
 #End Region
 
 End Module

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

@@ -434,6 +434,7 @@ EndHint:
     Public FrmVersionMod As PageVersionMod
     Public FrmVersionModDisabled As PageVersionModDisabled
     Public FrmVersionSetup As PageVersionSetup
+    Public FrmVersionExport As PageVersionExport
 
     '资源信息分页声明
     Public FrmDownloadCompDetail As PageDownloadCompDetail

+ 121 - 0
Plain Craft Launcher 2/Pages/PageVersion/PageVersionExport.xaml

@@ -0,0 +1,121 @@
+<local:MyPageRight
+    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
+    xmlns:System="clr-namespace:System;assembly=mscorlib" 
+    mc:Ignorable="d" x:Class="PageVersionExport"
+    PanScroll="{Binding ElementName=PanBack}">
+    <local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
+        <StackPanel Margin="25,10">
+            <local:MyHint Margin="0,10,0,0" IsWarn="False" Text="当前页面导出的整合包是 Modrinth 格式(.mrpack)。" />
+
+            <local:MyCard Margin="0,15" Title="导出预览">
+                <StackPanel Margin="25,38,15,15">
+                    <Grid Margin="-10,-5,10,0">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="1*" />
+                            <ColumnDefinition Width="150" />
+                        </Grid.ColumnDefinitions>
+                        <Grid Margin="10,7,10,7" Name="PanDisplayItem" Grid.Column="0" />
+                        <local:MyButton x:Name="BtnExportExport" Grid.Column="1" HorizontalAlignment="Right" 
+                                    Text="导出整合包" Width="150" Height="35" ColorType="Highlight" />
+                    </Grid>
+                    <Grid Margin="0,5,10,5">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="Auto" SharedSizeGroup="Name" />
+                            <ColumnDefinition Width="1*" />
+                            <ColumnDefinition Width="60" />
+                        </Grid.ColumnDefinitions>
+                        <Grid.RowDefinitions>
+                            <RowDefinition Height="28" />
+                            <RowDefinition Height="9" />
+                            <RowDefinition Height="28" />
+                            <!--<RowDefinition Height="9" />
+                            <RowDefinition Height="28" />-->
+                            <RowDefinition Height="9" />
+                            <RowDefinition Height="28" />
+                        </Grid.RowDefinitions>
+                        <TextBlock Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left" Text="整合包名称" Margin="0,0,25,0" />
+                        <local:MyTextBox Grid.Row="0" x:Name="TbExportName" Grid.ColumnSpan="2" Grid.Column="1" MaxLength="100" ShowValidateResult="True" />
+                        <TextBlock Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left" Text="整合包版本" Margin="0,0,25,0" />
+                        <local:MyTextBox Grid.Row="2" x:Name="TbExportVersion" HintText="1.0.0" Grid.ColumnSpan="2" Tag="VersionArgumentTitle" Grid.Column="1" MaxLength="100" />
+                        <!--<TextBlock Grid.Row="4" VerticalAlignment="Center" HorizontalAlignment="Left" Text="图标" Margin="0,0,25,0"  />
+                        <local:MyComboBox x:Name="ComboDisplayLogo" Grid.Row="4" Grid.ColumnSpan="2" Grid.Column="1">
+                            <local:MyComboBoxItem Content="自动" IsSelected="True" Tag="" />
+                            <local:MyComboBoxItem x:Name="ItemDisplayLogoCustom" Content="自定义..." />
+                            <local:MyComboBoxItem Content="圆石" Tag="pack://application:,,,/images/Blocks/CobbleStone.png" />
+                            <local:MyComboBoxItem Content="命令方块" Tag="pack://application:,,,/images/Blocks/CommandBlock.png" />
+                            <local:MyComboBoxItem Content="金块" Tag="pack://application:,,,/images/Blocks/GoldBlock.png" />
+                            <local:MyComboBoxItem Content="草方块" Tag="pack://application:,,,/images/Blocks/Grass.png" />
+                            <local:MyComboBoxItem Content="草径" Tag="pack://application:,,,/images/Blocks/GrassPath.png" />
+                            <local:MyComboBoxItem Content="铁砧" Tag="pack://application:,,,/images/Blocks/Anvil.png" />
+                            <local:MyComboBoxItem Content="红石块" Tag="pack://application:,,,/images/Blocks/RedstoneBlock.png" />
+                            <local:MyComboBoxItem Content="红石灯(开)" Tag="pack://application:,,,/images/Blocks/RedstoneLampOn.png" />
+                            <local:MyComboBoxItem Content="红石灯(关)" Tag="pack://application:,,,/images/Blocks/RedstoneLampOff.png" />
+                            <local:MyComboBoxItem Content="鸡蛋" Tag="pack://application:,,,/images/Blocks/Egg.png" />
+                            <local:MyComboBoxItem Content="布料(Fabric)" Tag="pack://application:,,,/images/Blocks/Fabric.png" />
+                            <local:MyComboBoxItem Content="狐狸(NeoForge)" Tag="pack://application:,,,/images/Blocks/NeoForge.png" />
+                        </local:MyComboBox>-->
+                        <TextBlock Grid.Row="4" VerticalAlignment="Center" HorizontalAlignment="Left" Text="描述" Margin="0,0,25,0" />
+                        <local:MyTextBox Grid.Row="4" x:Name="TbExportDesc" Grid.ColumnSpan="2" Grid.Column="1" TextWrapping="Wrap" VerticalContentAlignment="Top" Padding="0,5" />
+                    </Grid>
+                </StackPanel>
+            </local:MyCard>
+
+            <local:MyCard x:Name="CardSwitch" Margin="0,0,0,15" Title="基础选项">
+                <StackPanel Margin="25,38,15,15">
+                    <TextBlock x:Name="TbHint" Margin="0,1,0,1" Text="你可以在此勾选需要包含进整合包的文件或文件夹,其他文件或文件夹在高级选项添加。" TextWrapping="Wrap" />
+                    <!-- 注意:以上 TextBlock 的 Text 属性可能会在代码中被改动。 -->
+                    <Grid x:Name="PanCommonFiles">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="Auto" />
+                            <ColumnDefinition Width="18" />
+                            <ColumnDefinition Width="1.7*" />
+                            <ColumnDefinition Width="1.9*" />
+                            <ColumnDefinition Width="1.4*" />
+                            <ColumnDefinition Width="1.0*" />
+                            <ColumnDefinition Width="1.0*" />
+                            <ColumnDefinition Width="1.0*" />
+                        </Grid.ColumnDefinitions>
+                        <Grid.RowDefinitions>
+                            <RowDefinition Height="4" />
+                            <RowDefinition Height="30" />
+                            <RowDefinition Height="30" />
+                            <RowDefinition Height="30" />
+                            <RowDefinition Height="Auto" />
+                        </Grid.RowDefinitions>
+                        <TextBlock Text="游戏配置" VerticalAlignment="Center" Grid.Column="0" Grid.Row="1" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="1" Grid.Column="2" Text="服务器列表"
+                                          Tag="servers.dat" ToolTip="servers.dat 文件" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="1" Grid.Column="3" Text="Mod 配置"
+                                          Tag="config" ToolTip="config 文件夹" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="1" Grid.Column="4" Text="游戏配置"
+                                          Tag="options.txt" ToolTip="options.txt 文件" />
+                        <TextBlock Text="游戏文件" VerticalAlignment="Center" Grid.Column="0" Grid.Row="2" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="2" Grid.Column="2" Text="存档"
+                                          Tag="saves" ToolTip="saves 文件夹" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="2" Grid.Column="3" Text="资源包"
+                                          Tag="resourcepacks" ToolTip="resourcepacks 文件夹" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="2" Grid.Column="4" Text="光影包"
+                                          Tag="shaderpacks" ToolTip="shaderpacks 文件夹" />
+                        <TextBlock Text="PCL 配置" VerticalAlignment="Center" Grid.Column="0" Grid.Row="3" />
+                        <local:MyCheckBox x:Name="CheckIncludePCL" Height="22" VerticalAlignment="Center" Grid.Row="3" Grid.Column="2" Text="包含启动器"
+                                          Tag="" ToolTip="将 PCL 程序包含在整合包内" />
+                        <local:MyCheckBox x:Name="CheckIncludeSetup" Height="22" VerticalAlignment="Center" Grid.Row="3" Grid.Column="3" Text="PCL 全局配置"
+                                          Tag="" ToolTip="将 PCL 全局配置包含在整合包内" />
+                    </Grid>
+                </StackPanel>
+
+            </local:MyCard>
+            <local:MyCard x:Name="CardMore" Title="高级选项" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
+                <StackPanel x:Name="PanFileList" Margin="25,40,23,15">
+                    <TextBlock Margin="0,1,0,10" Text="你可以在此勾选其他需要包含进整合包的内容。&#xa;请注意:部分必须包含在整合包中的内容(如 Mods 等),以及被排除的冗余文件(如游戏日志等),将不会在列表中展示。" TextWrapping="Wrap" />
+                    <local:MyListItem Type="CheckBox" Title="游戏存档" Height="35" Logo="M889.018182 418.909091H884.363636V316.509091a93.090909 93.090909 0 0 0-99.607272-89.832727h-302.545455l-93.090909-76.334546A46.545455 46.545455 0 0 0 358.865455 139.636364H146.152727A93.090909 93.090909 0 0 0 46.545455 229.469091V837.818182a46.545455 46.545455 0 0 0 46.545454 46.545454 46.545455 46.545455 0 0 0 16.756364-3.258181 109.381818 109.381818 0 0 0 25.134545 3.258181h586.472727a85.178182 85.178182 0 0 0 87.04-63.301818l163.374546-302.545454a46.545455 46.545455 0 0 0 5.585454-21.876364A82.385455 82.385455 0 0 0 889.018182 418.909091z m-744.727273-186.181818h198.283636l93.09091 76.334545a46.545455 46.545455 0 0 0 29.323636 10.705455h319.301818a12.101818 12.101818 0 0 1 6.516364 0V418.909091H302.545455a85.178182 85.178182 0 0 0-87.04 63.301818L139.636364 622.778182V232.727273a19.549091 19.549091 0 0 1 6.516363 0z m578.094546 552.029091a27.461818 27.461818 0 0 0-2.792728 6.516363H154.530909l147.083636-272.290909a27.461818 27.461818 0 0 0 2.792728-6.981818h565.061818z" />
+                    <local:MyListItem Type="CheckBox" Title="游戏存档" Height="35" Logo="M586.9 436h-74.7V62.4H177.1c-20.6 0-37.4 16.7-37.4 37.4v821.9c0 20.6 16.7 37.4 37.4 37.4h672.5c20.6 0 37.4-16.7 37.4-37.4V436H586.9z m0-74.7h297.8c-7.3-57.4-31.2-70.9-58.4-98.1l-66.5-66.5-0.1-0.1 0.1 0.1-0.9-0.9-22.8-22.8-0.9-0.9 0.1 0.1-0.1-0.1-66.5-66.5c-21.8-21.8-38.8-36.5-81.7-41.4l-0.1 297.1z" />
+                    <local:MyListItem Type="CheckBox" Title="游戏存档" Height="35" Logo="M757.792745 435.407215L419.597482 96.904967c-40.010393-40.010393-104.886579-40.010393-144.896972 0-40.010393 40.010393-40.010393 104.988908 0 144.9993L540.344959 507.855701 274.70051 773.807135c-40.010393 40.112721-40.010393 104.988908 0 144.9993 40.010393 40.010393 104.886579 40.010393 144.896972 0l338.092935-338.39992c40.112721-40.010393 40.112721-104.988908 0.102328-144.9993z" />
+                </StackPanel>
+            </local:MyCard>
+
+        </StackPanel>
+    </local:MyScrollViewer>
+</local:MyPageRight>

+ 346 - 0
Plain Craft Launcher 2/Pages/PageVersion/PageVersionExport.xaml.vb

@@ -0,0 +1,346 @@
+Public Class PageVersionExport
+    Private CurrentVer As String = ""
+    Private Sub PageVersionExport_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
+        Reload()
+    End Sub
+
+    Public ItemVersion As MyListItem
+    Public Sub Reload()
+        AniControlEnabled += 1
+
+        If CurrentVer <> PageVersionLeft.Version.Path Then
+            '说明切换到另一个版本了,所有的设置都要重置
+            CurrentPath = ""
+            Selected = New List(Of String)
+            CurrentVer = PageVersionLeft.Version.Path
+
+            TbExportName.Text = ""
+            TbExportVersion.Text = ""
+            'ComboDisplayLogo.SelectedIndex = 0
+            CustomLogoVal = Nothing
+            TbExportDesc.Text = ""
+        End If
+
+        TbExportName.ShowValidateResult = True
+        TbExportName.ValidateRules = New ObjectModel.Collection(Of Validate) From {New ValidateFileName() With {.AllowNull = True}}
+        TbExportName.Validate()
+
+        TbExportName.HintText = PageVersionLeft.Version.Name
+        TbExportDesc.HintText = PageVersionLeft.Version.Info
+        PanDisplayItem.Children.Clear()
+        ItemVersion = PageSelectRight.McVersionListItem(PageVersionLeft.Version.Load())
+        ItemVersion.IsHitTestVisible = False
+        If Not String.IsNullOrEmpty(CustomLogo) Then ItemVersion.Logo = CustomLogo
+        PanDisplayItem.Children.Add(ItemVersion)
+        ItemVersion.Title = If(String.IsNullOrWhiteSpace(TbExportName.Text), PageVersionLeft.Version.Name, TbExportName.Text)
+        ItemVersion.Info = If(String.IsNullOrWhiteSpace(TbExportDesc.Text), PageVersionLeft.Version.Info, TbExportDesc.Text)
+#If BETA Then
+        CheckIncludePCL.IsEnabled = True
+#Else
+        CheckIncludePCL.IsEnabled = False
+        TbHint.Text = "你可以在此勾选需要包含进整合包的文件或文件夹,其他文件或文件夹在高级选项添加。" & vbCrLf & "你当前的 PCL 不是正式版,有二次分发的限制,因此整合包不能包含 PCL 程序。"
+#End If
+        CheckIncludeSetup.IsEnabled = CheckIncludePCL.IsEnabled
+        ReloadFileList(CurrentPath)
+
+        AniControlEnabled -= 1
+    End Sub
+
+#Region "文件列表"
+    ''' <summary>
+    ''' 当前所在的文件夹。注意是绝对路径!以 \ 结尾。
+    ''' </summary>
+    Private CurrentPath As String = ""
+    Private Sub ReloadFileList(Optional NewPath As String = "")
+        If String.IsNullOrWhiteSpace(NewPath) OrElse NewPath = PathMcFolder Then NewPath = PageVersionLeft.Version.Path
+        If Not NewPath.EndsWithF("\") Then NewPath &= "\"
+        Dim tb As TextBlock = PanFileList.Children(0)
+        PanFileList.Children.Clear()
+        PanFileList.Children.Add(tb)
+
+        If NewPath <> PageVersionLeft.Version.Path Then
+            Dim listItem As New MyListItem With {.Title = "返回上一级目录", .Tag = "..", .Height = 35, .Type = MyListItem.CheckType.Clickable}
+            AddHandler listItem.Click, AddressOf ListItem_Click
+            PanFileList.Children.Add(listItem)
+        End If
+
+        Dim fileCount As Integer = 0
+        Dim directories = Directory.EnumerateDirectories(NewPath)
+        Dim files = Directory.EnumerateFiles(NewPath)
+        If (NewPath = PageVersionLeft.Version.Path) AndAlso PageVersionLeft.Version.PathIndie = PathMcFolder Then
+            '未开启版本隔离时 .minecraft 下的文件
+            directories = directories.Concat(Directory.EnumerateDirectories(PathMcFolder))
+            files = files.Concat(Directory.EnumerateFiles(PathMcFolder))
+        End If
+
+        For Each d In directories
+            If Directory.EnumerateFileSystemEntries(d).Count = 0 Then Continue For
+            If IsVerRedundant(d, PageVersionLeft.Version.PathIndie) OrElse IsMustExport(d, PageVersionLeft.Version.PathIndie) Then Continue For
+            Dim title As String = ""
+            Dim listItem As New MyListItem
+            If Titles.TryGetValue(GetFolderNameFromPath(d), title) Then 'title 作为引用类型传入
+                listItem.Title = title
+            Else
+                listItem.Title = GetFolderNameFromPath(d)
+            End If
+            listItem.Info = d
+            '图标按钮
+            Dim btnSwap As New MyIconButton
+            btnSwap.Logo = Logo.IconButtonRight
+            btnSwap.LogoScale = 1.0
+            btnSwap.Tag = d & "\"
+            AddHandler btnSwap.Click, AddressOf BtnSwap_Click
+            listItem.Buttons = {btnSwap}
+            listItem.Type = MyListItem.CheckType.CheckBox
+            listItem.Height = 35
+            listItem.Logo = Logo.IconButtonOpen
+            listItem.Tag = d & "\"
+            Select Case IsSelected(d & "\")
+                Case 1
+                    listItem.Half = True
+                Case 2
+                    listItem.Checked = True
+            End Select
+            AddHandler listItem.Click, AddressOf ListItem_Click
+            PanFileList.Children.Add(listItem)
+            fileCount += 1
+        Next
+
+        For Each f In files
+            If IsVerRedundant(f, PageVersionLeft.Version.PathIndie) OrElse IsMustExport(f, PageVersionLeft.Version.PathIndie) Then Continue For
+            Dim title As String = ""
+            Dim listItem As New MyListItem
+            If Titles.TryGetValue(f, title) Then 'title 作为引用类型传入
+                listItem.Title = title
+            Else
+                listItem.Title = GetFileNameFromPath(f)
+            End If
+            listItem.Info = f
+            listItem.Type = MyListItem.CheckType.CheckBox
+            listItem.Height = 35
+            listItem.Tag = f
+            listItem.Checked = (IsSelected(f) = 2)
+            AddHandler listItem.Click, AddressOf ListItem_Click
+            PanFileList.Children.Add(listItem)
+            fileCount += 1
+        Next
+
+        CurrentPath = NewPath
+        CardMore.Title = If(NewPath = PageVersionLeft.Version.Path, "高级选项", $"高级选项 ({NewPath.Replace(PageVersionLeft.Version.Path, "").TrimEnd("\")})")
+    End Sub
+    Private Titles As New Dictionary(Of String, String) From {
+        {"saves", "存档"},
+        {"resourcepacks", "资源包"},
+        {"config", "Mod 配置"},
+        {"shaderpacks", "光影包"},
+        {"options.txt", "游戏配置"},
+        {"servers.dat", "服务器列表"}
+    }
+    Private Sub BtnSwap_Click(sender As MyIconButton, e As Object)
+        Dim actualPath As String = sender.Tag
+        'Dim relative As String = actualPath.Replace(PageVersionLeft.Version.Path, "")
+
+        If Not Directory.Exists(actualPath) Then
+            Hint($"找不到目录:{actualPath}", HintType.Critical)
+            Exit Sub
+        End If
+        ReloadFileList(actualPath)
+    End Sub
+    ''' <summary>
+    ''' 选中的文件列表,绝对路径。
+    ''' </summary>
+    Private Selected As New List(Of String)
+    Private Sub SelectedChange(IsAdd As Boolean, TargetPath As String)
+        If File.Exists(TargetPath) Then
+            If IsAdd Then Selected.Add(TargetPath) Else Selected.Remove(TargetPath)
+            Exit Sub
+        End If
+        If Directory.Exists(TargetPath) Then
+            For Each d In Directory.EnumerateDirectories(TargetPath)
+                SelectedChange(IsAdd, d)
+            Next
+            For Each f In Directory.EnumerateFiles(TargetPath)
+                If IsAdd Then Selected.Add(f) Else Selected.Remove(f)
+            Next
+        End If
+        Selected = Selected.Distinct().ToList()
+    End Sub
+    ''' <summary>
+    ''' 获取 TargetPath 是否选中。
+    ''' </summary>
+    ''' <returns>全部未选中为 0,部分选中为 1,全部选中为 2。</returns>
+    Private Function IsSelected(TargetPath As String, Optional AllowEmpty As Boolean = False) As Integer
+        If File.Exists(TargetPath) Then
+            Return If(Selected.Contains(TargetPath), 2, 0)
+        End If
+        If Directory.Exists(TargetPath) Then
+            Dim di As New DirectoryInfo(TargetPath)
+            If di.GetDirectories.Length + di.GetFiles.Length = 0 Then Return If(AllowEmpty, 2, 0)
+            '如果有一个文件不在列表,则 hasExclude = True
+            Dim hasExclude As Boolean = False
+            '如果有一个文件在列表,则 hasInclude = True
+            Dim hasInclude As Boolean = False
+            For Each d In Directory.EnumerateDirectories(TargetPath)
+                If Directory.EnumerateFileSystemEntries(d).Count = 0 Then Continue For
+                Select Case IsSelected(d, True)
+                    Case 0
+                        hasExclude = True
+                    Case 1
+                        Return 1
+                    Case 2
+                        hasInclude = True
+                End Select
+                If hasInclude AndAlso hasExclude Then Return 1 '如果有文件在、有文件不在列表中,可以断定部分选中
+            Next
+            For Each f In Directory.EnumerateFiles(TargetPath)
+                If Selected.Contains(f) Then hasInclude = True Else hasExclude = True
+                If hasInclude AndAlso hasExclude Then Return 1 '如果有文件在、有文件不在列表中,可以断定部分选中
+            Next
+            If hasExclude AndAlso Not hasInclude Then Return 0
+            If hasInclude AndAlso Not hasExclude Then Return 2
+        End If
+        Return 0
+    End Function
+    Private Sub ListItem_Click(sender As MyListItem, e As Object)
+        If sender.Tag = ".." Then '回到上一级目录
+            ReloadFileList(GetPathFromFullPath(CurrentPath))
+            Exit Sub
+        End If
+        SelectedChange(Not sender.Checked, '非常难绷的逻辑,很难描述清楚,自己打断点看 Checked 属性值吧……
+                       sender.Tag)
+    End Sub
+
+    'Private ReadOnly Property Saves As List(Of String)
+    '    Get
+    '        If Directory.Exists(PageVersionLeft.Version.Path & "saves\") Then
+    '            Return Directory.EnumerateDirectories(PageVersionLeft.Version.Path & "saves\").ToList
+    '        End If
+    '        Return New List(Of String)
+    '    End Get
+    'End Property
+
+    '复选框与高级选项同步
+    Private Sub PanCommonFiles_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles PanCommonFiles.MouseLeftButtonUp
+        For Each c In PanCommonFiles.Children
+            If String.IsNullOrEmpty(c.Tag) Then Continue For
+            For Each l In PanFileList.Children
+                If TypeOf l Is MyListItem Then
+                    If If(l.Tag.EndsWith("\"), GetFolderNameFromPath(l.Tag), GetFileNameFromPath(l.Tag)) = c.Tag Then
+                        l.Checked = c.Checked
+                        SelectedChange(c.Checked, PageVersionLeft.Version.Path & c.Tag)
+                    End If
+                End If
+            Next
+        Next
+    End Sub
+    '高级选项与复选框同步
+    Private Sub PanFileList_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles PanFileList.MouseLeftButtonUp
+        For Each c In PanCommonFiles.Children
+            If TypeOf c Is MyCheckBox Then
+                c.Checked = IsSelected(PageVersionLeft.Version.Path & c.Tag) = 2 OrElse IsSelected(PathMcFolder & c.Tag) = 2
+            End If
+        Next
+    End Sub
+#End Region
+
+#Region "整合包信息 & 预览栏"
+    Private Sub TbExportName_TextChanged(sender As MyTextBox, e As TextChangedEventArgs) Handles TbExportName.TextChanged
+        If String.IsNullOrEmpty(TbExportName.ValidateResult) Then
+            ItemVersion.Title = If(String.IsNullOrWhiteSpace(sender.Text), PageVersionLeft.Version.Name, sender.Text)
+        End If
+    End Sub
+
+    Private Sub TbExportDesc_TextChanged(sender As MyTextBox, e As TextChangedEventArgs) Handles TbExportDesc.TextChanged
+        ItemVersion.Info = If(String.IsNullOrWhiteSpace(sender.Text), PageVersionLeft.Version.Info, sender.Text)
+    End Sub
+
+    Private CustomLogoVal As String
+    ''' <summary>
+    ''' 自定义 Logo 的路径。
+    ''' </summary>
+    Public Property CustomLogo() As String
+        Get
+            Return CustomLogoVal
+        End Get
+        Set(value As String)
+            If AniControlEnabled <> 0 Then Exit Property
+            CustomLogoVal = value
+            ItemVersion.Logo = value
+            Reload()
+        End Set
+    End Property
+
+    'Private Sub ComboDisplayLogo_SelectionChanged(sender As MyComboBox, e As SelectionChangedEventArgs) Handles ComboDisplayLogo.SelectionChanged
+    '    If Not AniControlEnabled = 0 Then Exit Sub
+    '    '选择 自定义 时修改图片
+    '    If ComboDisplayLogo.SelectedItem Is ItemDisplayLogoCustom Then
+    '        Dim FileName As String = SelectFile("常用图片文件(*.png;*.jpg;*.gif)|*.png;*.jpg;*.gif", "选择图片")
+    '        If String.IsNullOrWhiteSpace(FileName) Then Exit Sub
+    '        CustomLogo = FileName
+    '    Else
+    '        CustomLogo = sender.SelectedItem.Tag
+    '    End If
+    'End Sub
+#End Region
+
+#Region "基本信息"
+    Private Sub CheckIncludePCL_Change(sender As Object, user As Boolean) Handles CheckIncludePCL.Change
+        CheckIncludeSetup.IsEnabled = CheckIncludePCL.IsEnabled
+    End Sub
+#End Region
+
+#Region "导出"
+    'Private Sub ComboExportType_Change(sender As ComboBox, e As SelectionChangedEventArgs) Handles ComboExportType.SelectionChanged
+    '    If AniControlEnabled <> 0 Then Exit Sub
+    '    Type = sender.SelectedIndex
+    '    Select Case Type
+    '        Case ModpackType.Modrinth
+    '            If IncludePCL() Then 'TODO:修改这里的条件
+    '                If GridCompressed IsNot Nothing Then GridCompressed.Visibility = Visibility.Visible
+    '                If GridModrinth IsNot Nothing Then GridModrinth.Visibility = Visibility.Collapsed
+    '            Else
+    '                If GridCompressed IsNot Nothing Then GridCompressed.Visibility = Visibility.Collapsed
+    '                If GridModrinth IsNot Nothing Then GridModrinth.Visibility = Visibility.Visible
+    '            End If
+    '    End Select
+    '    CardExport.TriggerForceResize()
+    'End Sub
+    Private Sub BtnExportExport_Click() Handles BtnExportExport.Click
+        Dim savePath As String = SelectAs(
+            "选择导出位置", $"整合包 - {If(String.IsNullOrWhiteSpace(TbExportName.Text), PageVersionLeft.Version.Name, TbExportName.Text)}" & If(CheckIncludePCL.Checked, ".zip", ".mrpack"),
+            If(CheckIncludePCL.Checked, "整合包文件(*.zip)|*.zip", "Modrinth 整合包(*.mrpack)|*.mrpack"))
+        If String.IsNullOrEmpty(savePath) Then Exit Sub
+
+        '获取需要包含的文件
+        Dim contains As List(Of String) = Selected
+        For Each c In PanCommonFiles.Children
+            If TypeOf c IsNot MyCheckBox Then Continue For
+            If String.IsNullOrEmpty(c.Tag) Then Continue For
+            If c.Checked Then contains.Add(PageVersionLeft.Version.Path & c.Tag)
+        Next
+        '重复任务检查
+        For Each OngoingLoader In LoaderTaskbar
+            If OngoingLoader.Name <> $"导出整合包 {PageVersionLeft.Version.Name}" Then Continue For
+            Hint("正在处理中,请稍候!", HintType.Critical)
+            Exit Sub
+        Next
+        '启动!
+        Dim Loader As New LoaderTask(Of ExportOptions, Boolean)($"导出整合包 {PageVersionLeft.Version.Name}",
+        Sub(Task As LoaderTask(Of ExportOptions, Boolean))
+            Task.Output = ModpackExport(Task)
+            Task.Progress = 1.0
+            If Task.Output Then
+                Hint($"{Task.Name} 成功!", HintType.Finish)
+                OpenExplorer($"/select,""{Task.Input.Dest}""")
+            End If
+        End Sub)
+        Dim Combo = New LoaderCombo(Of ExportOptions)($"导出整合包 {PageVersionLeft.Version.Name}", {Loader})
+        Combo.Start(New ExportOptions(PageVersionLeft.Version, savePath, CheckIncludePCL.Checked, contains.Distinct.ToArray,
+                                       PCLSetupGlobal:=CheckIncludeSetup.Checked, Name:=TbExportName.Text, Desc:=TbExportDesc.Text,
+                                       VerID:=TbExportVersion.Text))
+        LoaderTaskbarAdd(Combo)
+    End Sub
+
+#End Region
+
+End Class

+ 3 - 0
Plain Craft Launcher 2/Pages/PageVersion/PageVersionLeft.xaml

@@ -23,5 +23,8 @@
         </local:MyListItem>
         <local:MyListItem x:Name="ItemModDisabled" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="3" MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="Mod 管理"
                           Logo="M789.504 912.896h-195.072a35.328 35.328 0 0 1-34.816-35.328v-77.312a39.936 39.936 0 0 0-40.448-39.936H481.28a40.448 40.448 0 0 0-40.448 39.936v77.312a34.816 34.816 0 0 1-34.816 35.328H204.8a41.984 41.984 0 0 1-40.448-42.496v-200.192a35.328 35.328 0 0 1 35.328-35.328h72.704a39.936 39.936 0 0 0 39.936-39.936v-37.888a39.936 39.936 0 0 0-39.936-40.448H199.68a35.328 35.328 0 0 1-35.328-35.328V287.744A41.984 41.984 0 0 1 204.8 245.76h176.64v-32.768a102.4 102.4 0 0 1 102.4-102.4h33.792a102.4 102.4 0 0 1 102.4 102.4v32.768h170.496a41.984 41.984 0 0 1 41.984 41.984V460.8h28.672a102.4 102.4 0 0 1 102.4 102.4v33.792a102.4 102.4 0 0 1-102.4 102.4h-28.672V870.4a41.984 41.984 0 0 1-43.008 42.496z m-159.744-70.144h131.584V665.6a34.304 34.304 0 0 1 34.816-34.816h64a31.744 31.744 0 0 0 31.744-31.744V563.2a31.744 31.744 0 0 0-31.744-31.744h-64a34.816 34.816 0 0 1-34.816-35.328V316.416h-177.152a35.328 35.328 0 0 1-35.328-35.328V212.992a31.744 31.744 0 0 0-31.744-31.744h-33.792a31.744 31.744 0 0 0-31.744 31.744v68.096a35.328 35.328 0 0 1-34.816 35.328H234.496v130.048h37.888a110.592 110.592 0 0 1 110.08 110.592v37.888a110.592 110.592 0 0 1-110.08 110.592h-37.888v137.216h136.192v-42.496a110.592 110.592 0 0 1 110.592-110.08h37.888a110.592 110.592 0 0 1 110.592 110.08z" />
+        <local:MyListItem x:Name="ItemExport" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="4" MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="导出整合包"
+                          Logo="M511.488 995.328a128.654222 128.654222 0 0 1-57.116444-13.112889L70.769778 791.808a126.833778 126.833778 0 0 1-70.769778-113.777778V311.608889a126.179556 126.179556 0 0 1 15.36-60.103111V248.604444c1.479111-2.901333 3.356444-5.603556 5.518222-8.021333a127.630222 127.630222 0 0 1 49.891556-42.325333L454.371556 13.368889a128.739556 128.739556 0 0 1 112.981333 0l383.601778 190.407111a126.862222 126.862222 0 0 1 72.049777 113.379556v360.874666a126.805333 126.805333 0 0 1-70.769777 115.939556L568.604444 984.32c-17.92 7.964444-37.461333 11.747556-57.116444 11.008z m42.638222-470.897778v370.204445l360.192-178.545778c14.449778-7.253333 23.552-21.987556 23.438222-38.087111v-335.928889L554.097778 524.430222zM85.248 330.666667v347.335111a42.268444 42.268444 0 0 0 23.438222 38.087111l360.192 178.545778V523.576889L85.248 330.666667zM135.537778 260.835556l375.950222 189.952 137.671111-65.564445L286.435556 188.074667 135.537778 260.864z m245.105778-118.471112l363.576888 197.973334 150.897778-71.480889-365.283555-180.224a42.922667 42.922667 0 0 0-37.518223 0l-111.672888 53.731555z" >
+        </local:MyListItem>
     </StackPanel>
 </local:MyPageLeft>

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

@@ -25,7 +25,7 @@
     ''' <summary>
     ''' 勾选事件改变页面。
     ''' </summary>
-    Private Sub PageCheck(sender As MyListItem, e As RouteEventArgs) Handles ItemOverall.Check, ItemMod.Check, ItemModDisabled.Check, ItemSetup.Check
+    Private Sub PageCheck(sender As MyListItem, e As RouteEventArgs) Handles ItemOverall.Check, ItemMod.Check, ItemModDisabled.Check, ItemSetup.Check, ItemExport.Check
         '尚未初始化控件属性时,sender.Tag 为 Nothing,会导致切换到页面 0
         '若使用 IsLoaded,则会导致模拟点击不被执行(模拟点击切换页面时,控件的 IsLoaded 为 False)
         If sender.Tag IsNot Nothing Then PageChange(Val(sender.Tag))
@@ -46,6 +46,9 @@
             Case FormMain.PageSubType.VersionSetup
                 If IsNothing(FrmVersionSetup) Then FrmVersionSetup = New PageVersionSetup
                 Return FrmVersionSetup
+            Case FormMain.PageSubType.VersionExport
+                If FrmVersionExport Is Nothing Then FrmVersionExport = New PageVersionExport
+                Return FrmVersionExport
             Case Else
                 Throw New Exception("未知的版本设置子页面种类:" & ID)
         End Select

+ 9 - 2
Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj

@@ -290,6 +290,9 @@
     <Compile Include="Pages\PageLink\PageLinkLeft.xaml.vb">
       <DependentUpon>PageLinkLeft.xaml</DependentUpon>
     </Compile>
+    <Compile Include="Pages\PageVersion\PageVersionExport.xaml.vb">
+      <DependentUpon>PageVersionExport.xaml</DependentUpon>
+    </Compile>
     <Compile Include="Pages\PageVersion\PageVersionLeft.xaml.vb">
       <DependentUpon>PageVersionLeft.xaml</DependentUpon>
     </Compile>
@@ -476,10 +479,10 @@
       <DependentUpon>MyRadioButton.xaml</DependentUpon>
     </Compile>
     <Compile Include="FormMain.xaml.vb">
+
       <DependentUpon>FormMain.xaml</DependentUpon>
       <SubType>Code</SubType>
     </Compile>
-
     <Page Include="Modules\Minecraft\MyLocalModItem.xaml">
       <Generator>MSBuild:Compile</Generator>
       <SubType>Designer</SubType>
@@ -561,6 +564,10 @@
       <Generator>MSBuild:Compile</Generator>
       <SubType>Designer</SubType>
     </Page>
+    <Page Include="Pages\PageVersion\PageVersionExport.xaml">
+      <Generator>MSBuild:Compile</Generator>
+      <SubType>Designer</SubType>
+    </Page>
     <Page Include="Resources\Language\en_US.xaml">
       <SubType>Designer</SubType>
       <Generator>MSBuild:Compile</Generator>
@@ -910,11 +917,11 @@
     <Resource Include="Images\Icons\Disabled.png" />
     <Resource Include="Images\Icons\Unavailable.png" />
   </ItemGroup>
+
   <ItemGroup>
     <Resource Include="Images\Heads\MCBBS.png" />
   </ItemGroup>
   <ItemGroup>
-
   </ItemGroup>
   <ItemGroup>
     <Resource Include="Images\Blocks\NeoForge.png" />