CI / Go Backend (push) Canceled after 0s
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
1184 lines
53 KiB
C#
1184 lines
53 KiB
C#
using System.Collections;
|
||
using System.Collections.ObjectModel;
|
||
using System.Diagnostics.Eventing.Reader;
|
||
using System.IO;
|
||
using System.Text;
|
||
using System.Windows;
|
||
using System.Windows.Controls;
|
||
using FluentValidation;
|
||
using PCL.Core.App;
|
||
using PCL.Core.App.Localization;
|
||
using PCL.Core.Logging;
|
||
using PCL.Core.Minecraft.ResourceProject;
|
||
using PCL.Core.UI;
|
||
using PCL.Core.Utils;
|
||
using PCL.Core.Utils.Validate;
|
||
using PCL.Network;
|
||
using PCL.Network.Loaders;
|
||
using Control = System.Windows.Forms.Control;
|
||
|
||
namespace PCL;
|
||
|
||
public partial class PageDownloadCompDetail
|
||
{
|
||
// 资源下载;整合包另存为
|
||
public static Dictionary<ModComp.CompType, string> cachedFolder = new(); // 仅在本次缓存的下载文件夹
|
||
private MyCompItem _compItem;
|
||
private bool _isFirstInit = true;
|
||
|
||
private void Init()
|
||
{
|
||
ModAnimation.AniControlEnabled += 1;
|
||
_project = ModMain.frmMain.pageCurrent.additional.Value.CompProject;
|
||
PanBack.ScrollToHome();
|
||
// 重启加载器
|
||
if (_isFirstInit)
|
||
// 在 Me.Initialized 已经初始化了加载器,不再重复初始化
|
||
_isFirstInit = false;
|
||
else
|
||
PageLoaderRestart(isForceRestart: true);
|
||
// 放置当前工程
|
||
if (_compItem is not null)
|
||
PanIntro.Children.Remove(_compItem);
|
||
_compItem = _project.ToCompItem(true, true);
|
||
_compItem.CanInteraction = false;
|
||
_compItem.ShowFavoriteBtn = false;
|
||
_compItem.Margin = new Thickness(-7, -7, 0d, 8d);
|
||
PanIntro.Children.Insert(0, _compItem);
|
||
|
||
// 决定按钮显示
|
||
BtnIntroWeb.Text = _project.FromCurseForge ? "CurseForge" : "Modrinth";
|
||
BtnIntroWiki.Visibility = Lang.IsChineseMainland && _project.WikiId != 0
|
||
? Visibility.Visible
|
||
: Visibility.Collapsed;
|
||
BtnTranslate.Visibility = Lang.IsChineseMainland
|
||
? Visibility.Visible
|
||
: Visibility.Collapsed;
|
||
RefreshFavoriteButton();
|
||
|
||
ModAnimation.AniControlEnabled -= 1;
|
||
}
|
||
|
||
// 整合包安装
|
||
public void Install_Click(MyListItem sender, EventArgs e)
|
||
{
|
||
try
|
||
{
|
||
// 获取基本信息
|
||
var file = (ModComp.CompFile)sender.Tag;
|
||
var loaderName =
|
||
$"{(_project.FromCurseForge ? "CurseForge" : "Modrinth")} {Lang.Text("Download.Comp.Detail.ModpackDownload")}:{_project.TranslatedName} ";
|
||
|
||
// 获取实例名
|
||
var packName = _project.TranslatedName.Replace(".zip", "").Replace(".rar", "").Replace(".mrpack", "")
|
||
.Replace(@"\", "\").Replace("/", "/").Replace("|", "|").Replace(":", ":").Replace("<", "<")
|
||
.Replace(">", ">").Replace("*", "*").Replace("?", "?").Replace("\"", "").Replace(": ", ":");
|
||
var validate = new FolderNameValidator(ModFolder.mcFolderSelected + "versions");
|
||
if (!validate.Validate(packName).IsValid)
|
||
packName = "";
|
||
var instanceName = ModMain.MyMsgBoxInput(Lang.Text("Download.Comp.Detail.InputInstanceName"), "", packName, [validate]);
|
||
if (string.IsNullOrEmpty(instanceName))
|
||
return;
|
||
|
||
// 构造步骤加载器
|
||
var loaders = new List<ModLoader.LoaderBase>();
|
||
var target =
|
||
$@"{ModFolder.mcFolderSelected}versions\{instanceName}\原始整合包.{(_project.FromCurseForge ? "zip" : "mrpack")}";
|
||
var logoFileAddress = MyImage.GetTempPath(_compItem.Logo);
|
||
loaders.Add(new LoaderDownload(Lang.Text("Download.Comp.Detail.DownloadModpackFile"),
|
||
new List<DownloadFile> { file.ToNetFile(target, ModComp.DownloadReason.ModPack) })
|
||
{ ProgressWeight = 10d, block = true });
|
||
loaders.Add(new ModLoader.LoaderTask<int, int>(Lang.Text("Download.Comp.Detail.PrepareModpackInstall"),
|
||
_ => ModModpack.ModpackInstall(target, instanceName,
|
||
System.IO.File.Exists(logoFileAddress) ? logoFileAddress : null, file.ProjectId,
|
||
true)) { ProgressWeight = 0.1d });
|
||
|
||
// 启动
|
||
var loader = new ModLoader.LoaderCombo<string>(loaderName, loaders)
|
||
{
|
||
OnStateChanged = myLoader =>
|
||
{
|
||
switch (myLoader.State)
|
||
{
|
||
case ModBase.LoadState.Failed:
|
||
{
|
||
HintService.Hint(
|
||
Lang.Text("Download.Comp.Detail.Task.Failed", myLoader.name,
|
||
myLoader.Error.ToString()), HintType.Error);
|
||
break;
|
||
}
|
||
case ModBase.LoadState.Aborted:
|
||
{
|
||
HintService.Hint(Lang.Text("Download.Comp.Detail.Task.Cancelled", myLoader.name));
|
||
break;
|
||
}
|
||
case ModBase.LoadState.Loading:
|
||
{
|
||
return; // 不重新加载版本列表
|
||
}
|
||
}
|
||
|
||
ModDownloadLib.McInstallFailedClearFolder(myLoader);
|
||
}
|
||
};
|
||
loader.Start(Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName));
|
||
ModLoader.LoaderTaskbarAdd(loader);
|
||
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
|
||
ModMain.frmMain.BtnExtraDownload.Ribble();
|
||
}
|
||
|
||
catch (Exception ex)
|
||
{
|
||
ModBase.Log(
|
||
ex,
|
||
"下载资源整合包失败",
|
||
ModBase.LogLevel.Feedback,
|
||
userSummary: Lang.Text("Download.Comp.Error.OperationFailed"));
|
||
}
|
||
}
|
||
|
||
// 世界下载
|
||
public void InstallWorld_Click(MyListItem sender, EventArgs e)
|
||
{
|
||
try
|
||
{
|
||
// 获取基本信息
|
||
var file = (ModComp.CompFile)sender.Tag;
|
||
var loaderName = $"{(_project.FromCurseForge ? "CurseForge" : "Modrinth")} {Lang.Text("Download.Comp.Detail.WorldDownload")}:{_project.TranslatedName} ";
|
||
|
||
// 确认默认保存位置
|
||
string defaultFolder = null;
|
||
var subFolder = @"saves\";
|
||
Func<McInstance, bool> isVersionSuitable = null;
|
||
// 获取资源所需的加载器
|
||
var allowedLoaders = new List<ModComp.CompLoaderType>();
|
||
if (file.ModLoaders.Any())
|
||
allowedLoaders = file.ModLoaders;
|
||
else if (_project.ModLoaders.Any()) allowedLoaders = _project.ModLoaders;
|
||
ModBase.Log("[Comp] 世界要求的加载器种类:" + (allowedLoaders.Any() ? allowedLoaders.Join(" / ") : "无要求"));
|
||
// 判断某个版本是否符合资源要求
|
||
isVersionSuitable = version =>
|
||
{
|
||
if (version is null)
|
||
return false;
|
||
if (!version.IsLoaded)
|
||
version.Load();
|
||
if (file.GameVersions.Any(v => v.Contains(".")) && !file.GameVersions.Any(v =>
|
||
v.Contains(".") && (v ?? "") == (version.Info.VanillaName ?? "")))
|
||
return false;
|
||
// 加载器
|
||
if (!allowedLoaders.Any())
|
||
return true; // 无要求
|
||
return false;
|
||
};
|
||
// 获取常规资源默认下载位置
|
||
if (cachedFolder.ContainsKey(file.Type) && !string.IsNullOrEmpty(cachedFolder[file.Type]))
|
||
{
|
||
defaultFolder = cachedFolder.GetOrDefault(file.Type,
|
||
ModInstanceList.McMcInstanceSelected?.PathIndie ?? ModBase.exePath);
|
||
ModBase.Log($"[Comp] 使用上次下载时的文件夹作为默认下载位置:{defaultFolder}");
|
||
}
|
||
else if (ModInstanceList.McMcInstanceSelected is not null && isVersionSuitable(ModInstanceList.McMcInstanceSelected))
|
||
{
|
||
defaultFolder = $"{ModInstanceList.McMcInstanceSelected.PathIndie}{subFolder}";
|
||
Directory.CreateDirectory(defaultFolder);
|
||
ModBase.Log($"[Comp] 使用当前实例作为默认下载位置:{defaultFolder}");
|
||
}
|
||
else
|
||
{
|
||
// 查找所有可能的实例
|
||
var needLoad = ModInstanceList.mcInstanceListLoader.State != ModBase.LoadState.Finished;
|
||
if (needLoad)
|
||
{
|
||
HintService.Hint(Lang.Text("Download.Comp.Detail.FindingApplicableInstance"));
|
||
ModLoader.LoaderFolderRun(ModInstanceList.mcInstanceListLoader, ModFolder.mcFolderSelected,
|
||
ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\", true);
|
||
}
|
||
|
||
var suitableVersions = ModInstanceList.mcInstanceList.Values.SelectMany(l => l)
|
||
.Where(v => isVersionSuitable(v)).Select(v => new DirectoryInfo($"{v.PathIndie}{subFolder}"));
|
||
if (suitableVersions.Any())
|
||
{
|
||
var selectedVersion = suitableVersions
|
||
.OrderByDescending(dir => dir.Exists ? dir.LastWriteTimeUtc : DateTime.MinValue)
|
||
.ThenByDescending(dir => dir.Exists ? dir.GetFiles().Length : -1).First(); // 先按文件夹更改时间降序
|
||
// 再按文件夹中的文件数量降序
|
||
defaultFolder = selectedVersion.FullName;
|
||
Directory.CreateDirectory(defaultFolder);
|
||
ModBase.Log($"[Comp] 使用适合的游戏实例作为默认下载位置:{defaultFolder}");
|
||
}
|
||
else
|
||
{
|
||
defaultFolder = ModFolder.mcFolderSelected;
|
||
if (needLoad)
|
||
HintService.Hint(Lang.Text("Download.Comp.Detail.NoApplicableInstance"));
|
||
else
|
||
ModBase.Log("[Comp] 由于当前实例不兼容,使用当前的 MC 文件夹作为默认下载位置");
|
||
}
|
||
}
|
||
|
||
var target = SystemDialogs.SelectSaveFile(Lang.Text("Download.Comp.Detail.SelectWorldInstallLocation"), file.FileName, Lang.Text("Download.Comp.Detail.WorldFile.Filter"),
|
||
defaultFolder);
|
||
if (string.IsNullOrEmpty(target))
|
||
return;
|
||
|
||
// 构造步骤加载器
|
||
var loaders = new List<ModLoader.LoaderBase>();
|
||
var targetPath = target.BeforeLast(@"\");
|
||
var logoFileAddress = MyImage.GetTempPath(_compItem.Logo);
|
||
loaders.Add(new LoaderDownload(Lang.Text("Download.Comp.Detail.DownloadWorldFile"),
|
||
new List<DownloadFile>
|
||
{
|
||
file.ToNetFile(target, ModComp.DownloadReason.Standalone, file.RawGameVersions.FirstOrDefault())
|
||
})
|
||
{ ProgressWeight = 10d, block = true });
|
||
loaders.Add(new ModLoader.LoaderTask<int, int>(Lang.Text("Download.Comp.Detail.InstallWorld"),
|
||
_ => ModBase.ExtractFile(target, targetPath, Encoding.UTF8)) { ProgressWeight = 0.1d, block = true });
|
||
loaders.Add(new ModLoader.LoaderTask<int, int>(Lang.Text("Download.Comp.Detail.CleanCache"),
|
||
_ => System.IO.File.Delete(target)));
|
||
|
||
// 启动
|
||
var loader = new ModLoader.LoaderCombo<int>(loaderName, loaders)
|
||
{ OnStateChanged = ModDownloadLib.LoaderStateChangedHintOnly };
|
||
loader.Start();
|
||
ModLoader.LoaderTaskbarAdd(loader);
|
||
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
|
||
ModMain.frmMain.BtnExtraDownload.Ribble();
|
||
}
|
||
|
||
catch (Exception ex)
|
||
{
|
||
ModBase.Log(
|
||
ex,
|
||
"下载世界资源失败",
|
||
ModBase.LogLevel.Feedback,
|
||
userSummary: Lang.Text("Download.Comp.Error.OperationFailed"));
|
||
}
|
||
}
|
||
|
||
public void Save_Click(object sender, EventArgs e)
|
||
{
|
||
// 获取点击项关联的文件对象
|
||
var file = sender switch
|
||
{
|
||
FrameworkElement Element when Element.Tag is ModComp.CompFile CompFile => CompFile,
|
||
FrameworkElement Element when Element.Parent is FrameworkElement Parent && Parent.Tag is ModComp.CompFile CompFile => CompFile,
|
||
FrameworkElement Element when Element.Parent is FrameworkElement Parent && Parent.Parent is FrameworkElement GrandParent && GrandParent.Tag is ModComp.CompFile CompFile => CompFile,
|
||
_ => null
|
||
};
|
||
|
||
ModBase.RunInNewThread(() =>
|
||
{
|
||
try
|
||
{
|
||
var desc = file.Type switch
|
||
{
|
||
ModComp.CompType.ModPack => Lang.Text("Download.Comp.Type.Modpack"),
|
||
ModComp.CompType.Mod => Lang.Text("Download.Comp.Type.Mod"),
|
||
ModComp.CompType.ResourcePack => Lang.Text("Download.Comp.Type.ResourcePack"),
|
||
ModComp.CompType.Shader => Lang.Text("Download.Comp.Type.Shader"),
|
||
ModComp.CompType.DataPack => Lang.Text("Download.Comp.Type.DataPack"),
|
||
ModComp.CompType.World => Lang.Text("Download.Comp.Type.World"),
|
||
_ => ""
|
||
};
|
||
|
||
// 确认默认保存位置
|
||
string defaultFolder = null;
|
||
var allowedLoaders = new List<ModComp.CompLoaderType>();
|
||
if (file.Type != ModComp.CompType.ModPack)
|
||
{
|
||
var subFolder = "";
|
||
switch (file.Type)
|
||
{
|
||
case ModComp.CompType.Mod: subFolder = "mods\\"; break;
|
||
case ModComp.CompType.ResourcePack: subFolder = "resourcepacks\\"; break;
|
||
case ModComp.CompType.Shader: subFolder = "shaderpacks\\"; break;
|
||
case ModComp.CompType.World: subFolder = "saves\\"; break;
|
||
case ModComp.CompType.DataPack: subFolder = ""; break; // 导航到版本根目录
|
||
}
|
||
|
||
// 获取资源所需的加载器
|
||
if (file.ModLoaders.Any())
|
||
allowedLoaders = file.ModLoaders;
|
||
else if (_project.ModLoaders.Any()) allowedLoaders = _project.ModLoaders;
|
||
ModBase.Log(
|
||
$"[Comp] {desc}要求的加载器种类:{(allowedLoaders.Any() ? string.Join(" / ", allowedLoaders) : "无要求")}");
|
||
|
||
// 判断某个版本是否符合资源要求 (局部函数)
|
||
Func<McInstance, bool> isVersionSuitable = version =>
|
||
{
|
||
if (version is null) return false;
|
||
if (!version.IsLoaded) version.Load();
|
||
|
||
// 只对 Mod 和数据包进行版本检测
|
||
if (file.Type == ModComp.CompType.Mod || file.Type == ModComp.CompType.DataPack)
|
||
if (file.GameVersions.Any(v => v.Contains(".")) &&
|
||
!file.GameVersions.Any(v => v.Contains(".") && v == version.Info.VanillaName))
|
||
return false;
|
||
|
||
// 加载器判定
|
||
if (!allowedLoaders.Any()) return true; // 无要求
|
||
if (allowedLoaders.Contains(ModComp.CompLoaderType.Forge) && version.Info.HasForge) return true;
|
||
if (allowedLoaders.Contains(ModComp.CompLoaderType.Forge) && version.Info.HasCleanroom)
|
||
return true;
|
||
if (allowedLoaders.Contains(ModComp.CompLoaderType.Fabric) &&
|
||
(version.Info.HasFabric || version.Info.HasLegacyFabric)) return true;
|
||
if (allowedLoaders.Contains(ModComp.CompLoaderType.NeoForge) && version.Info.HasNeoForge)
|
||
return true;
|
||
if (allowedLoaders.Contains(ModComp.CompLoaderType.LiteLoader) && version.Info.HasLiteLoader)
|
||
return true;
|
||
return false;
|
||
};
|
||
|
||
// 获取常规资源默认下载位置逻辑
|
||
if (cachedFolder.ContainsKey(file.Type) && !string.IsNullOrEmpty(cachedFolder[file.Type]))
|
||
{
|
||
defaultFolder = cachedFolder.GetOrDefault(file.Type,
|
||
ModInstanceList.McMcInstanceSelected?.PathIndie ?? ModBase.exePath);
|
||
ModBase.Log($"[Comp] 使用上次下载时的文件夹作为默认下载位置:{defaultFolder}");
|
||
}
|
||
else if (ModInstanceList.McMcInstanceSelected is not null &&
|
||
isVersionSuitable(ModInstanceList.McMcInstanceSelected))
|
||
{
|
||
defaultFolder = $"{ModInstanceList.McMcInstanceSelected.PathIndie}{subFolder}";
|
||
Directory.CreateDirectory(defaultFolder);
|
||
ModBase.Log($"[Comp] 使用当前实例作为默认下载位置:{defaultFolder}");
|
||
}
|
||
else
|
||
{
|
||
// 查找所有可能的实例
|
||
var needLoad = ModInstanceList.mcInstanceListLoader.State != ModBase.LoadState.Finished;
|
||
if (needLoad)
|
||
{
|
||
HintService.Hint(Lang.Text("Download.Comp.Detail.FindingApplicableInstance"));
|
||
ModLoader.LoaderFolderRun(ModInstanceList.mcInstanceListLoader, ModFolder.mcFolderSelected,
|
||
ModLoader.LoaderFolderRunType.ForceRun, 1, "versions\\", true);
|
||
}
|
||
|
||
var suitableVersions = ModInstanceList.mcInstanceList.Values.SelectMany(l => l)
|
||
.Where(v => isVersionSuitable(v))
|
||
.Select(v => new DirectoryInfo($"{v.PathIndie}{subFolder}"));
|
||
|
||
if (suitableVersions.Any())
|
||
{
|
||
var selectedVersion = suitableVersions
|
||
.OrderByDescending(dir => dir.Exists ? dir.LastWriteTimeUtc : DateTime.MinValue)
|
||
.ThenByDescending(dir => dir.Exists ? dir.GetFiles().Length : -1)
|
||
.First();
|
||
defaultFolder = selectedVersion.FullName;
|
||
Directory.CreateDirectory(defaultFolder);
|
||
ModBase.Log($"[Comp] 使用适合的游戏实例作为默认下载位置:{defaultFolder}");
|
||
}
|
||
else
|
||
{
|
||
defaultFolder = ModFolder.mcFolderSelected;
|
||
if (needLoad)
|
||
HintService.Hint(Lang.Text("Download.Comp.Detail.NoApplicableInstance"));
|
||
else
|
||
ModBase.Log("[Comp] 由于当前实例不兼容,使用当前的 MC 文件夹作为默认下载位置");
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取文件名并弹窗
|
||
var fileName = ModComp.CompFileNameGet(_project, file);
|
||
ModBase.RunInUi(() =>
|
||
{
|
||
var target = SystemDialogs.SelectSaveFile(Lang.Text("Download.Comp.Detail.SelectSaveLocation"),
|
||
fileName, Lang.Text("Download.Comp.Detail.ResourceFile.Filter", desc) + "|" +
|
||
(file.Type == ModComp.CompType.Mod
|
||
? file.FileName.EndsWith(".litemod") ? "*.litemod" : "*.jar"
|
||
: file.FileName.EndsWith(".mrpack")
|
||
? "*.mrpack"
|
||
: "*.zip"),
|
||
defaultFolder);
|
||
|
||
if (!target.Contains("\\")) return;
|
||
|
||
// 记录缓存路径
|
||
var targetDir = ModBase.GetPathFromFullPath(target);
|
||
if (target != defaultFolder)
|
||
{
|
||
if (cachedFolder.ContainsKey(file.Type))
|
||
cachedFolder[file.Type] = targetDir;
|
||
else
|
||
cachedFolder.Add(file.Type, targetDir);
|
||
}
|
||
|
||
if (file.Type == ModComp.CompType.Mod && Config.Download.Comp.AutoInstallDependencies &&
|
||
file.Dependencies.Any())
|
||
{
|
||
try
|
||
{
|
||
McInstance? targetInstance = null;
|
||
var knownInstances = new List<McInstance>();
|
||
if (ModInstanceList.McMcInstanceSelected is not null)
|
||
{
|
||
knownInstances.Add(ModInstanceList.McMcInstanceSelected);
|
||
}
|
||
|
||
knownInstances.AddRange(ModInstanceList.mcInstanceList.Values.SelectMany(list => list)
|
||
.Where(instance => instance is not null));
|
||
targetInstance = knownInstances
|
||
.Distinct()
|
||
.FirstOrDefault(instance =>
|
||
targetDir.StartsWith(instance.PathIndie, StringComparison.OrdinalIgnoreCase));
|
||
if (targetInstance is not null && !targetInstance.IsLoaded)
|
||
{
|
||
targetInstance.Load();
|
||
}
|
||
|
||
var mcVersion = targetInstance?.Info?.VanillaName
|
||
?? file.GameVersions.FirstOrDefault(version => version.Contains("."))
|
||
?? string.Empty;
|
||
var targetLoaders = new List<ModComp.CompLoaderType>();
|
||
if (targetInstance is not null)
|
||
{
|
||
if (targetInstance.Info.HasForge || targetInstance.Info.HasCleanroom)
|
||
targetLoaders.Add(ModComp.CompLoaderType.Forge);
|
||
if (targetInstance.Info.HasFabric || targetInstance.Info.HasLegacyFabric)
|
||
targetLoaders.Add(ModComp.CompLoaderType.Fabric);
|
||
if (targetInstance.Info.HasQuilt)
|
||
targetLoaders.Add(ModComp.CompLoaderType.Quilt);
|
||
if (targetInstance.Info.HasNeoForge)
|
||
targetLoaders.Add(ModComp.CompLoaderType.NeoForge);
|
||
if (targetInstance.Info.HasLiteLoader)
|
||
targetLoaders.Add(ModComp.CompLoaderType.LiteLoader);
|
||
}
|
||
|
||
if (!targetLoaders.Any())
|
||
{
|
||
targetLoaders = allowedLoaders.ToList();
|
||
}
|
||
|
||
ModBase.Log($"[CompDeps] 开始解析必需前置: {file.Dependencies.Count} 个依赖");
|
||
var request = ModCompDependency.BuildRequest(file, _project, mcVersion, targetLoaders,
|
||
targetDir);
|
||
var resolver = new ModDependencyResolver();
|
||
var result = resolver.Resolve(request);
|
||
|
||
void DownloadDependencies()
|
||
{
|
||
if (!result.ToInstall.Any())
|
||
{
|
||
ModBase.Log("[CompDeps] 所有前置均无法解析,仅下载 Mod 本体");
|
||
return;
|
||
}
|
||
|
||
ModBase.Log($"[CompDeps] 准备下载: {result.ToInstall.Count} 个前置");
|
||
var depDownloads = ModCompDependency.BuildDependencyDownloads(result, targetDir);
|
||
foreach (var (depFilename, downloadFile) in depDownloads)
|
||
{
|
||
var depLoaderName = Lang.Text("Download.Comp.Detail.DownloadResource", desc,
|
||
ModBase.GetFileNameWithoutExtentionFromPath(depFilename));
|
||
var depLoaders = new List<ModLoader.LoaderBase>
|
||
{
|
||
new LoaderDownload(Lang.Text("Download.Comp.Detail.DownloadFile"),
|
||
new List<DownloadFile> { downloadFile })
|
||
{
|
||
ProgressWeight = 6,
|
||
block = true
|
||
}
|
||
};
|
||
|
||
// 启动加载器
|
||
var depLoader = new ModLoader.LoaderCombo<int>(depLoaderName, depLoaders);
|
||
depLoader.OnStateChanged = ModDownloadLib.LoaderStateChangedHintOnly;
|
||
depLoader.Start(1);
|
||
ModLoader.LoaderTaskbarAdd(depLoader);
|
||
}
|
||
}
|
||
|
||
if (result.Unresolved.Any() || result.ToInstall.Any())
|
||
{
|
||
var installChoice = ModCompDependency.ConfirmDependencyInstall(result);
|
||
|
||
switch (installChoice)
|
||
{
|
||
case ModComp.CompDepsInstallTypes.Unresolved:
|
||
ModBase.Log("[CompDeps] 发现无法解析的前置");
|
||
DownloadDependencies();
|
||
break;
|
||
|
||
case ModComp.CompDepsInstallTypes.WithDeps:
|
||
DownloadDependencies();
|
||
break;
|
||
|
||
case ModComp.CompDepsInstallTypes.WithoutDeps:
|
||
ModBase.Log("[CompDeps] 用户选择仅下载 Mod 本体,跳过前置下载");
|
||
break;
|
||
|
||
case ModComp.CompDepsInstallTypes.Cancel:
|
||
ModBase.Log("[CompDeps] 用户取消安装");
|
||
return;
|
||
|
||
default:
|
||
ModBase.Log($"[CompDeps] 未知返回值: {installChoice} ,终止下载");
|
||
return;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
ModBase.Log("[CompDeps] 已满足: 所有必需前置已安装");
|
||
}
|
||
}
|
||
catch (Exception depEx)
|
||
{
|
||
ModBase.Log(depEx, "[CompDeps] 依赖解析失败,跳过前置安装");
|
||
var message = ExceptionDetails.Compose(
|
||
Lang.Text("Download.Comp.Dependency.ResolveFailed.Message"),
|
||
depEx);
|
||
ModMain.MyMsgBox(
|
||
message,
|
||
Lang.Text("Download.Comp.Dependency.ResolveFailed.Title"),
|
||
Lang.Text("Download.Comp.Dependency.ResolveFailed.Continue"),
|
||
isWarn: true,
|
||
forceWait: true);
|
||
}
|
||
}
|
||
|
||
// 构造下载任务
|
||
var loaderName = Lang.Text("Download.Comp.Detail.DownloadResource", desc,
|
||
ModBase.GetFileNameWithoutExtentionFromPath(target));
|
||
var loaders = new List<ModLoader.LoaderBase>
|
||
{
|
||
new LoaderDownload(Lang.Text("Download.Comp.Detail.DownloadFile"),
|
||
new List<DownloadFile>
|
||
{
|
||
file.Type == ModComp.CompType.Mod
|
||
? file.ToNetFile(target)
|
||
: file.ToNetFile(target, ModComp.DownloadReason.Standalone, null)
|
||
})
|
||
{
|
||
ProgressWeight = 6,
|
||
block = true
|
||
}
|
||
};
|
||
|
||
// 启动加载器
|
||
var loader = new ModLoader.LoaderCombo<int>(loaderName, loaders);
|
||
loader.OnStateChanged = ModDownloadLib.LoaderStateChangedHintOnly;
|
||
loader.Start(1);
|
||
ModLoader.LoaderTaskbarAdd(loader);
|
||
|
||
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
|
||
ModMain.frmMain.BtnExtraDownload.Ribble();
|
||
});
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
ModBase.Log(
|
||
ex,
|
||
"保存资源文件失败",
|
||
ModBase.LogLevel.Feedback,
|
||
userSummary: Lang.Text("Download.Comp.Error.OperationFailed"));
|
||
}
|
||
}, "Download CompDetail Save");
|
||
}
|
||
|
||
private void BtnIntroWeb_Click(object sender, EventArgs e)
|
||
{
|
||
ModBase.OpenWebsite(_project.Website);
|
||
}
|
||
|
||
private void BtnIntroWiki_Click(object sender, EventArgs e)
|
||
{
|
||
ModBase.OpenWebsite("https://www.mcmod.cn/class/" + _project.WikiId + ".html");
|
||
}
|
||
|
||
private void BtnIntroCopy_Click(object sender, EventArgs e)
|
||
{
|
||
ModBase.ClipboardSet(_compItem.LabTitle.Text + _compItem.LabTitleRaw.Text);
|
||
}
|
||
|
||
private void BtnFavorites_Click(object sender, EventArgs e)
|
||
{
|
||
ModComp.CompFavorites.ShowMenu(_project, (UIElement)sender, RefreshFavoriteButton);
|
||
}
|
||
|
||
private void BtnIntroLinkCopy_Click(object sender, EventArgs e)
|
||
{
|
||
ModComp.CompClipboard.currentText = _project.Website;
|
||
ModBase.ClipboardSet(_project.Website);
|
||
}
|
||
|
||
// 翻译简介
|
||
private async void BtnTranslate_Click(object sender, EventArgs e)
|
||
{
|
||
HintService.Hint(Lang.Text("Download.Comp.Detail.DescriptionTranslating", _project.TranslatedName));
|
||
var chineseDescription = await _project.ChineseDescription;
|
||
if (chineseDescription is null)
|
||
return;
|
||
ModMain.MyMsgBox(Lang.Text("Download.Comp.Detail.DescriptionTranslationResult", _project.Description,
|
||
chineseDescription));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 刷新收藏按钮的显示状态
|
||
/// </summary>
|
||
public void RefreshFavoriteButton()
|
||
{
|
||
try
|
||
{
|
||
if (_project is null) return;
|
||
|
||
var isFavourite = ModComp.CompFavorites.IsFavourite(_project.Id);
|
||
BtnFavorites.SvgIcon = isFavourite ? "lucide/heart-filled" : "lucide/heart";
|
||
|
||
// 刷新顶部的项目卡片收藏状态
|
||
if (_compItem is not null)
|
||
_compItem.RefreshFavoriteStatus();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
ModBase.Log(ex, "刷新收藏按钮状态时出错");
|
||
}
|
||
}
|
||
|
||
#region 加载器
|
||
|
||
private readonly ModLoader.LoaderTask<int, List<ModComp.CompFile>> _compFileLoader;
|
||
|
||
public PageDownloadCompDetail()
|
||
{
|
||
_compFileLoader = new ModLoader.LoaderTask<int, List<ModComp.CompFile>>("Comp File", task =>
|
||
{
|
||
LoadTargetFromAdditional();
|
||
var result = ModComp.CompFilesGet(_project.Id, _project.FromCurseForge);
|
||
if (task.IsAborted)
|
||
return;
|
||
task.output = result;
|
||
});
|
||
Initialized += PageDownloadCompDetail_Inited;
|
||
Loaded += (_, _) => LoadTargetFromAdditional();
|
||
PageEnter += Init;
|
||
InitializeComponent();
|
||
Load.StateChanged += Load_State;
|
||
BtnIntroWeb.Click += BtnIntroWeb_Click;
|
||
BtnIntroWiki.Click += BtnIntroWiki_Click;
|
||
BtnIntroCopy.Click += BtnIntroCopy_Click;
|
||
BtnFavorites.Click += BtnFavorites_Click;
|
||
BtnIntroLinkCopy.Click += BtnIntroLinkCopy_Click;
|
||
BtnTranslate.Click += BtnTranslate_Click;
|
||
}
|
||
|
||
// 初始化加载器信息
|
||
private void PageDownloadCompDetail_Inited(object sender, EventArgs e)
|
||
{
|
||
LoadTargetFromAdditional();
|
||
PageLoaderInit(Load, PanLoad, PanMain, CardIntro, _compFileLoader, _ => Load_OnFinish());
|
||
}
|
||
|
||
public void LoadTargetFromAdditional()
|
||
{
|
||
var additional = ModMain.frmMain.pageCurrent.additional.Value;
|
||
_project = additional.CompProject;
|
||
_targetInstance = additional.TargetVersion;
|
||
_targetLoader = additional.TargetLoader;
|
||
_pageType = additional.ResourceType;
|
||
}
|
||
|
||
private ModComp.CompProject _project;
|
||
private string _targetInstance;
|
||
private ModComp.CompLoaderType _targetLoader;
|
||
|
||
/// <summary>
|
||
/// 当前页面应展示的内容类别。可能为 Any。
|
||
/// </summary>
|
||
private ModComp.CompType _pageType;
|
||
|
||
// 自动重试
|
||
private void Load_State(object sender, MyLoading.MyLoadingState state, MyLoading.MyLoadingState oldState)
|
||
{
|
||
switch (_compFileLoader.State)
|
||
{
|
||
case ModBase.LoadState.Failed:
|
||
{
|
||
var errorMessage = "";
|
||
if (_compFileLoader.Error is not null)
|
||
errorMessage = _compFileLoader.Error.Message;
|
||
if (errorMessage.Contains(Lang.Text("Common.Error.InvalidJson")))
|
||
{
|
||
ModBase.Log("[Comp] 下载的文件 Json 列表损坏,已自动重试", ModBase.LogLevel.Debug);
|
||
PageLoaderRestart();
|
||
}
|
||
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 结果 UI 化
|
||
private class CardSorter : IComparer<string>
|
||
{
|
||
public readonly string topmost = "";
|
||
|
||
public CardSorter(string topmost = "")
|
||
{
|
||
this.topmost = topmost ?? "";
|
||
}
|
||
|
||
public int Compare(string x, string y)
|
||
{
|
||
// 相同
|
||
if ((x ?? "") == (y ?? ""))
|
||
return 0;
|
||
// 置顶
|
||
if ((x ?? "") == (topmost ?? ""))
|
||
return -1;
|
||
if ((y ?? "") == (topmost ?? ""))
|
||
return 1;
|
||
// 特殊版本
|
||
var isXSpecial = !x.Contains(".");
|
||
var isYSpecial = !y.Contains(".");
|
||
if (isXSpecial && isYSpecial)
|
||
return string.Compare(x, y, StringComparison.Ordinal);
|
||
if (isXSpecial)
|
||
return 1;
|
||
if (isYSpecial)
|
||
return -1;
|
||
// 比较版本号
|
||
var versionCodeSort = -McVersionComparer.CompareVersion(x.Replace(x.BeforeFirst(" ") + " ", ""),
|
||
y.Replace(y.BeforeFirst(" ") + " ", ""));
|
||
if (versionCodeSort != 0)
|
||
return versionCodeSort;
|
||
// 比较全部
|
||
return -McVersionComparer.CompareVersion(x, y);
|
||
}
|
||
}
|
||
|
||
private string? _instanceFilter;
|
||
private string? _modLoaderFilter;
|
||
private bool groupedDrop; // 是否按 Drop 筛选(1.21 / 1.20 / 1.19 / ...)而非小版本号(1.21.1 / 1.21 / 1.20.4 / ...)
|
||
|
||
private bool groupedOld; // 是否折叠远古版本为一个选项
|
||
|
||
// 筛选类型相同的结果(Modrinth 会返回 Mod、服务端插件、数据包混合的列表)
|
||
private List<ModComp.CompFile> GetResults()
|
||
{
|
||
var results = _compFileLoader.output;
|
||
if (_pageType == ModComp.CompType.Any)
|
||
{
|
||
results = results.Where(r => r.Type != ModComp.CompType.Plugin).ToList();
|
||
}
|
||
else if (_pageType == ModComp.CompType.Shader || _pageType == ModComp.CompType.ResourcePack)
|
||
{
|
||
}
|
||
// 不筛选光影和资源包,否则原版光影会因为是资源包格式而被过滤(Meloong-Git/#6473)
|
||
else
|
||
{
|
||
results = results.Where(r => r.Type == _pageType).ToList();
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
private void Load_OnFinish()
|
||
{
|
||
var results = GetResults();
|
||
|
||
// 初始化筛选器
|
||
List<string> instanceFilters = null;
|
||
List<string> modLoaderFilters = null;
|
||
|
||
void updateFilters()
|
||
{
|
||
instanceFilters = results.SelectMany(v => v.GameVersions)
|
||
.Select(v => GetGroupedVersionName(v, groupedDrop, groupedOld)).Distinct()
|
||
.OrderByDescending(s => s, new McVersionComparer.VersionComparer()).ToList();
|
||
modLoaderFilters = results.SelectMany(v => v.ModLoaders).Select(l => l.ToString()).Distinct()
|
||
.OrderByDescending(s => s).ToList();
|
||
}
|
||
|
||
;
|
||
|
||
// 确定分组方式
|
||
groupedDrop = false;
|
||
groupedOld = false;
|
||
updateFilters();
|
||
if (instanceFilters.Count >= 9)
|
||
{
|
||
groupedDrop = true;
|
||
groupedOld = false;
|
||
updateFilters();
|
||
if (instanceFilters.Count >= 9)
|
||
{
|
||
groupedDrop = false;
|
||
groupedOld = true;
|
||
updateFilters();
|
||
if (instanceFilters.Count >= 9)
|
||
{
|
||
groupedDrop = true;
|
||
groupedOld = true;
|
||
updateFilters();
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
// UI 化筛选器
|
||
PanInstanceFilter.Children.Clear();
|
||
PanModLoaderFilter.Children.Clear();
|
||
if (_pageType == ModComp.CompType.Mod)
|
||
{
|
||
PanInstanceFilter.Margin = new Thickness(10d, 10d, 0d, 5d);
|
||
PanModLoaderFilter.Margin = new Thickness(10d, 5d, 0d, 10d);
|
||
}
|
||
else
|
||
{
|
||
PanInstanceFilter.Margin = new Thickness(10d, 10d, 0d, 10d);
|
||
PanModLoaderFilter.Margin = new Thickness(0d);
|
||
}
|
||
|
||
if (instanceFilters.Count < 2)
|
||
{
|
||
CardFilter.Visibility = Visibility.Collapsed;
|
||
_instanceFilter = null;
|
||
}
|
||
else
|
||
{
|
||
CardFilter.Visibility = Visibility.Visible;
|
||
// 插入标签
|
||
if (_pageType == ModComp.CompType.Mod)
|
||
{
|
||
var instanceTextBlock = new TextBlock
|
||
{
|
||
Text = Lang.Text("Download.Comp.Detail.InstanceFilter"),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Margin = new Thickness(2d, 0d, 0d, 0d)
|
||
};
|
||
PanInstanceFilter.Children.Add(instanceTextBlock);
|
||
var modLoaderTextBlock = new TextBlock
|
||
{
|
||
Text = Lang.Text("Download.Comp.Detail.LoaderFilter"),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Margin = new Thickness(2d, 0d, 0d, 0d)
|
||
};
|
||
PanModLoaderFilter.Children.Add(modLoaderTextBlock);
|
||
}
|
||
|
||
instanceFilters.Insert(0, Lang.Text("Common.Option.All"));
|
||
modLoaderFilters.Insert(0, Lang.Text("Common.Option.All"));
|
||
// 转化为按钮
|
||
foreach (var version in instanceFilters)
|
||
{
|
||
var newButton = new MyRadioButton
|
||
{
|
||
Text = version, Margin = new Thickness(2d, 0d, 2d, 0d),
|
||
ColorType = MyRadioButton.ColorState.Highlight
|
||
};
|
||
newButton.LabText.Margin = new Thickness(-2, 0d, 10d, 0d);
|
||
newButton.Check += (sender, raiseByMouse) =>
|
||
{
|
||
_instanceFilter = sender.Text == Lang.Text("Common.Option.All") ? null : sender.Text;
|
||
UpdateFilterResult();
|
||
};
|
||
PanInstanceFilter.Children.Add(newButton);
|
||
}
|
||
|
||
if (_pageType == ModComp.CompType.Mod)
|
||
foreach (var loader in modLoaderFilters)
|
||
{
|
||
var newButton = new MyRadioButton
|
||
{
|
||
Text = loader,
|
||
Margin = new Thickness(2d, 0d, 2d, 0d),
|
||
ColorType = MyRadioButton.ColorState.Highlight
|
||
};
|
||
newButton.LabText.Margin = new Thickness(-2, 0d, 10d, 0d);
|
||
newButton.Check += (sender, raiseByMouse) =>
|
||
{
|
||
_modLoaderFilter = sender.Text == Lang.Text("Common.Option.All") ? null : sender.Text;
|
||
UpdateFilterResult();
|
||
};
|
||
PanModLoaderFilter.Children.Add(newButton);
|
||
}
|
||
|
||
// 自动选择
|
||
MyRadioButton instanceToCheck = null;
|
||
MyRadioButton modLoaderToCheck = null;
|
||
if (!string.IsNullOrEmpty(_targetInstance))
|
||
{
|
||
var targetFile = results.FirstOrDefault(v => v.GameVersions.Contains(_targetInstance));
|
||
if (targetFile is not null)
|
||
{
|
||
var targetGroup = GetGroupedVersionName(_targetInstance, groupedDrop, groupedOld);
|
||
var children = _pageType == ModComp.CompType.Mod
|
||
? PanInstanceFilter.Children.Cast<UIElement>().Skip(1)
|
||
: PanInstanceFilter.Children.Cast<UIElement>();
|
||
foreach (MyRadioButton button in (IEnumerable)children)
|
||
{
|
||
if ((button.Text ?? "") != (targetGroup ?? ""))
|
||
continue;
|
||
instanceToCheck = button;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (_pageType == ModComp.CompType.Mod)
|
||
if (_targetLoader != ModComp.CompLoaderType.Any)
|
||
{
|
||
var targetFile = results.FirstOrDefault(v => v.ModLoaders.Contains(_targetLoader));
|
||
if (targetFile is not null)
|
||
{
|
||
var children = _pageType == ModComp.CompType.Mod
|
||
? PanInstanceFilter.Children.Cast<UIElement>().Skip(1)
|
||
: PanInstanceFilter.Children.Cast<UIElement>();
|
||
foreach (MyRadioButton button in (IEnumerable)children)
|
||
{
|
||
if ((button.Text ?? "") != (_targetLoader.ToString() ?? ""))
|
||
continue;
|
||
modLoaderToCheck = button;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 注意:在 Mod 下 index 0 是 TextBlock
|
||
var index = _pageType == ModComp.CompType.Mod ? 1 : 0;
|
||
if (instanceToCheck is null)
|
||
instanceToCheck = (MyRadioButton)PanInstanceFilter.Children[index];
|
||
if (modLoaderToCheck is null && (_pageType == ModComp.CompType.Mod))
|
||
modLoaderToCheck = (MyRadioButton)PanModLoaderFilter.Children[index];
|
||
instanceToCheck.Checked = true;
|
||
if (_pageType == ModComp.CompType.Mod)
|
||
modLoaderToCheck.Checked = true;
|
||
}
|
||
|
||
// 更新筛选结果(文件列表 UI 化)
|
||
UpdateFilterResult();
|
||
}
|
||
|
||
private void UpdateFilterResult()
|
||
{
|
||
var results = GetResults();
|
||
if (results is null)
|
||
return;
|
||
|
||
// 1. 预处理基础变量
|
||
var targetVersionText = _targetLoader != ModComp.CompLoaderType.Any ? _targetLoader + " " : "";
|
||
var targetCardName = !string.IsNullOrEmpty(_targetInstance) || _targetLoader != ModComp.CompLoaderType.Any
|
||
? Lang.Text("Download.Comp.Detail.SelectedVersion", targetVersionText, _targetInstance)
|
||
: "";
|
||
|
||
// 使用 HashSet 提高查询性能 O(1)
|
||
var supportedLoaders =
|
||
new HashSet<ModComp.CompLoaderType>(Enum.GetValues(typeof(ModComp.CompLoaderType))
|
||
.Cast<ModComp.CompLoaderType>());
|
||
var ignoreQuilt = Config.Download.Comp.IgnoreQuilt;
|
||
var hasMultipleLoaders = _project.ModLoaders.Count > 1;
|
||
|
||
// 2. 核心数据归类 (使用 Dictionary 配合 HashSet 去重)
|
||
var dict = new SortedDictionary<string, List<ModComp.CompFile>>(new CardSorter(targetCardName));
|
||
dict.Add(Lang.Text("Download.Comp.Detail.VersionGroup.Other"), new List<ModComp.CompFile>());
|
||
|
||
// 用于记录每个卡片内已存在的 version,防止 Contains(version) 的 O(n) 消耗
|
||
var versionDuplicateChecker = new Dictionary<string, HashSet<ModComp.CompFile>>();
|
||
|
||
foreach (var version in results)
|
||
{
|
||
// 处理普通卡片归类
|
||
foreach (var gameVersion in version.GameVersions)
|
||
{
|
||
// 筛选器预检查
|
||
var currentGroupedName = GetGroupedVersionName(gameVersion, groupedDrop, groupedOld);
|
||
if (_instanceFilter is not null && (currentGroupedName ?? "") != (_instanceFilter ?? ""))
|
||
continue;
|
||
var verName = GetGroupedVersionName(gameVersion, false, false);
|
||
var loaders = new List<string>();
|
||
|
||
// 判定 Loader 逻辑
|
||
if (hasMultipleLoaders && version.Type == ModComp.CompType.Mod &&
|
||
McInstanceInfo.IsFormatFit(verName))
|
||
{
|
||
foreach (var loader in version.ModLoaders)
|
||
{
|
||
if (loader == ModComp.CompLoaderType.Quilt && ignoreQuilt)
|
||
continue;
|
||
if (!supportedLoaders.Contains(loader))
|
||
continue;
|
||
|
||
// 模组加载器筛选器
|
||
if (_modLoaderFilter is not null && (loader.ToString() ?? "") != (_modLoaderFilter ?? ""))
|
||
continue;
|
||
|
||
loaders.Add(loader + " ");
|
||
}
|
||
|
||
if (loaders.Count == 0 && _modLoaderFilter is not null) continue;
|
||
}
|
||
|
||
if (loaders.Count == 0)
|
||
loaders.Add("");
|
||
|
||
// 填充数据
|
||
foreach (var loaderPrefix in loaders)
|
||
{
|
||
var targetKey = loaderPrefix + verName;
|
||
AddVersionToDict(dict, versionDuplicateChecker, targetKey, version);
|
||
}
|
||
}
|
||
|
||
// 处理“所选版本”卡片 (逻辑合并,减少二次循环)
|
||
if (!string.IsNullOrEmpty(targetCardName))
|
||
{
|
||
var isMatchFilter = _instanceFilter is null ||
|
||
GetGroupedVersionName(_targetInstance, groupedDrop, groupedOld)
|
||
.StartsWithF(_instanceFilter);
|
||
|
||
if (isMatchFilter && version.GameVersions.Contains(_targetInstance))
|
||
if (_targetLoader == ModComp.CompLoaderType.Any || version.ModLoaders.Contains(_targetLoader))
|
||
// 再次检查 version 是否符合筛选器(针对该文件的所有游戏版本)
|
||
if (_instanceFilter is null || version.GameVersions.Any(v =>
|
||
(GetGroupedVersionName(v, groupedDrop, groupedOld) ?? "") == (_instanceFilter ?? "")))
|
||
AddVersionToDict(dict, versionDuplicateChecker, targetCardName, version);
|
||
}
|
||
}
|
||
|
||
// 3. 渲染 UI
|
||
try
|
||
{
|
||
PanResults.Children.Clear();
|
||
var additional = ModMain.frmMain.pageCurrent.additional;
|
||
var additionalTitles = additional is not null
|
||
? additional.Value.ExpandedTitles
|
||
: new List<string>();
|
||
|
||
foreach (var pair in dict)
|
||
{
|
||
if (pair.Value.Count == 0)
|
||
continue;
|
||
|
||
// 创建卡片组件
|
||
var newCard = new MyCard
|
||
{
|
||
Title = pair.Key,
|
||
Margin = new Thickness(0d, 0d, 0d, 15d)
|
||
};
|
||
|
||
// 闭包引用:避免在 Sub 内做高耗时操作
|
||
var files = pair.Value;
|
||
var currentKey = pair.Key;
|
||
|
||
var newStack = new StackPanel
|
||
{
|
||
Margin = new Thickness(20d, MyCard.SwapedHeight, 18d, 0d),
|
||
VerticalAlignment = VerticalAlignment.Top,
|
||
Tag = files
|
||
};
|
||
|
||
newCard.Children.Add(newStack);
|
||
newCard.SwapControl = newStack;
|
||
|
||
// 延迟加载安装项的逻辑
|
||
newCard.InstallMethod = stack =>
|
||
{
|
||
var list = (List<ModComp.CompFile>)stack.Tag;
|
||
// 排序和去重检查
|
||
list.Sort((a, b) => b.ReleaseDate.CompareTo(a.ReleaseDate));
|
||
var distinctCount = list.Select(f => f.DisplayName).Distinct().Count();
|
||
var badDisplayName = distinctCount != list.Count;
|
||
|
||
// 批量添加子项
|
||
switch (_project.Type)
|
||
{
|
||
case ModComp.CompType.ModPack:
|
||
{
|
||
foreach (var item in list)
|
||
stack.Children.Add(item.ToListItem(
|
||
(sender, e) => ModMain.frmDownloadCompDetail.Install_Click((MyListItem)sender, e),
|
||
ModMain.frmDownloadCompDetail.Save_Click, badDisplayName));
|
||
break;
|
||
}
|
||
case ModComp.CompType.World:
|
||
{
|
||
foreach (var item in list)
|
||
stack.Children.Add(item.ToListItem(
|
||
(sender, e) =>
|
||
ModMain.frmDownloadCompDetail.InstallWorld_Click((MyListItem)sender, e),
|
||
ModMain.frmDownloadCompDetail.Save_Click, badDisplayName));
|
||
break;
|
||
}
|
||
|
||
default:
|
||
{
|
||
ModComp.CompFilesCardPreload(stack, list);
|
||
foreach (var item in list)
|
||
stack.Children.Add(item.ToListItem(ModMain.frmDownloadCompDetail.Save_Click,
|
||
badDisplayName: badDisplayName));
|
||
break;
|
||
}
|
||
}
|
||
};
|
||
|
||
PanResults.Children.Add(newCard);
|
||
|
||
// 展开逻辑
|
||
if ((currentKey ?? "") == (targetCardName ?? "") || additionalTitles.Contains(newCard.Title))
|
||
newCard.StackInstall();
|
||
else
|
||
newCard.IsSwapped = true;
|
||
|
||
// 特殊提示
|
||
if (currentKey == Lang.Text("Download.Comp.Detail.VersionGroup.Other"))
|
||
newStack.Children.Add(new MyHint
|
||
{
|
||
Text = Lang.Text("Download.Comp.Detail.VersionRecognitionDelayHint"),
|
||
Theme = MyHint.Themes.Yellow,
|
||
Margin = new Thickness(5d, 0d, 0d, 8d)
|
||
});
|
||
}
|
||
|
||
// 单卡片自动展开
|
||
if (PanResults.Children.Count == 1) ((MyCard)PanResults.Children[0]).IsSwapped = false;
|
||
}
|
||
|
||
catch (Exception ex)
|
||
{
|
||
ModBase.Log(
|
||
ex,
|
||
"可视化工程下载列表出错",
|
||
ModBase.LogLevel.Feedback,
|
||
userSummary: Lang.Text("Download.Comp.Error.OperationFailed"));
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 辅助方法:向字典添加数据并处理去重
|
||
/// </summary>
|
||
private void AddVersionToDict(SortedDictionary<string, List<ModComp.CompFile>> dict,
|
||
Dictionary<string, HashSet<ModComp.CompFile>> checker, string key, ModComp.CompFile version)
|
||
{
|
||
if (!dict.ContainsKey(key))
|
||
{
|
||
dict.Add(key, new List<ModComp.CompFile>());
|
||
checker.Add(key, new HashSet<ModComp.CompFile>());
|
||
}
|
||
|
||
// 使用 HashSet.Add 判断是否重复,比 List.Contains 快得多
|
||
if (checker[key].Add(version)) dict[key].Add(version);
|
||
}
|
||
|
||
private string GetGroupedVersionName(string name, bool groupedByDrop, bool foldOld)
|
||
{
|
||
if (name is null)
|
||
return Lang.Text("Download.Comp.Detail.VersionGroup.Other");
|
||
if (name.Contains('w'))
|
||
return Lang.Text("Download.Comp.Detail.VersionGroup.Snapshot");
|
||
if (foldOld && McInstanceInfo.VersionToDrop(name, true) < 120)
|
||
return Lang.Text("Download.Comp.Detail.VersionGroup.Old");
|
||
if (groupedByDrop)
|
||
{
|
||
var drop = McInstanceInfo.VersionToDrop(name, true);
|
||
if (drop >= 0)
|
||
return McInstanceInfo.DropToVersion(drop);
|
||
return name;
|
||
}
|
||
|
||
return name;
|
||
}
|
||
|
||
#endregion
|
||
}
|