diff --git a/PCL.Core/App/Config.cs b/PCL.Core/App/Config.cs index c6433f36e..e6ad442a8 100644 --- a/PCL.Core/App/Config.cs +++ b/PCL.Core/App/Config.cs @@ -91,6 +91,7 @@ public static partial class Config [ConfigItem("ToolDownloadClipboard", false)] public partial bool ReadClipboard { get; set; } [ConfigItem("ToolDownloadMod", 1)] public partial int CompSourceSolution { get; set; } [ConfigItem("ToolModLocalNameStyle", 0)] public partial int UiCompNameSolution { get; set; } + [ConfigItem("ToolDownloadQuickBehavior", 0)] public partial int QuickDownloadBehavior { get; set; } } } diff --git a/PCL.Core/App/Localization/Languages/en-US.xaml b/PCL.Core/App/Localization/Languages/en-US.xaml index c0d15111c..096e92cce 100644 --- a/PCL.Core/App/Localization/Languages/en-US.xaml +++ b/PCL.Core/App/Localization/Languages/en-US.xaml @@ -1131,6 +1131,22 @@ Batch download applicable resources Batch download resources ({0}) + + + Download the latest version + Getting resource version info, please wait… + No downloadable files found + No Minecraft instance selected! + No compatible Minecraft instances found + No compatible version found for this instance + Select a folder to save the file + Started downloading: {0} + Select a download destination + Download to the current instance + Select an instance… + Select a folder… + Select a target Minecraft instance + Cleanroom official source @@ -1713,6 +1729,12 @@ Display style of mod items on the Mod management page Title shows translated name, details show file name Title shows file name, details show translated name + Quick download behavior + Choose what happens when you click the quick download button on a resource card + Always ask + Download to the current instance + Ask which instance to use + Ask where to save Hide Quilt loader Hide Quilt from the community resource loader display. Auto-check and install required dependencies when downloading mods diff --git a/PCL.Core/App/Localization/Languages/zh-CN.xaml b/PCL.Core/App/Localization/Languages/zh-CN.xaml index f603ed26d..678b14a74 100644 --- a/PCL.Core/App/Localization/Languages/zh-CN.xaml +++ b/PCL.Core/App/Localization/Languages/zh-CN.xaml @@ -1131,6 +1131,22 @@ 批量下载合适资源 批量下载资源({0}) + + + 快速下载最新版本 + 正在获取资源版本信息,请稍候…… + 未找到可下载的文件 + 当前未选中任何 Minecraft 实例 + 没有与该资源兼容的 Minecraft 实例 + 未找到适用于该实例的版本 + 请选择保存位置 + 已开始下载:{0} + 选择下载方式 + 下载到当前选中实例 + 选择一个实例… + 选择一个文件夹… + 选择要下载到的 Minecraft 实例 + Cleanroom 官方源 @@ -1713,6 +1729,12 @@ 在模组管理页面中,模组项的显示方式 标题显示译名,详情显示文件名 标题显示文件名,详情显示译名 + 快速下载行为 + 点击资源搜索结果卡片上的快速下载按钮时的行为。 + 总是询问 + 下载到当前选中实例 + 询问并下载到选择的实例 + 询问并下载到一个路径 不显示 Quilt 加载器 不在社区资源的加载器展示中显示 Quilt 加载器 下载模组时自动检查并安装必需前置 diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModComp.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModComp.cs index 9a08bc780..e3f8872a1 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModComp.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModComp.cs @@ -19,6 +19,7 @@ using PCL.Core.Utils; using PCL.Core.Utils.Hash; using PCL.Network; +using PCL.Network.Loaders; using ProtoBuf; using PCL.Core.App.Localization; using PCL.Core.UI; @@ -1509,7 +1510,9 @@ public JsonObject ToJson() /// /// 将当前工程信息实例化为控件。 /// - public MyVirtualizingElement ToCompItem(bool showMcVersionDesc, bool showLoaderDesc) + /// 是否在卡片右侧显示快速下载按钮(仅搜索结果页应传 true)。 + public MyVirtualizingElement ToCompItem(bool showMcVersionDesc, bool showLoaderDesc, + bool showQuickDownload = false) { // --- 1. 获取版本描述 (核心算法优化) --- string gameVersionDescription; @@ -1620,6 +1623,7 @@ public MyVirtualizingElement ToCompItem(bool showMcVersionDesc, bool newItem.Tags = Tags; newItem.Description = Description.Replace("\r", "").Replace("\n", ""); + newItem.ShowDownloadBtn = showQuickDownload; // 下边栏逻辑切换 newItem.LabVersion.Text = (showMcVersionDesc, showLoaderDesc) switch @@ -3360,6 +3364,265 @@ _ when char.IsControl(c) => '_', return result is "" or "." or ".." ? "download" : result; } + #region 快速下载(资源卡片下载按钮) + + /// + /// 资源卡片的快速下载入口:按 指定的行为, + /// 下载该资源最新(优先 Release、其次最新发布)的兼容版本到目标实例或文件夹。 + /// 由 上的快速下载按钮调用。快速下载不会自动安装前置。 + /// + public static void QuickDownload(CompProject project) + { + ModBase.RunInNewThread(() => + { + try + { + HintService.Hint(Lang.Text("Download.Comp.QuickDownload.Hint.Loading"), HintType.Info); + var files = FilterFilesByType( + CompFilesGet(project.Id, project.FromCurseForge).Where(f => f.Available).ToList(), + project.Type); + if (files.Count == 0) + { + HintService.Hint(Lang.Text("Download.Comp.QuickDownload.Hint.NoFile"), HintType.Info); + return; + } + + var behavior = Config.Download.Comp.QuickDownloadBehavior; + if (behavior == 0) + { + // 总是询问:弹「方式选择」 + int? choice = ModBase.RunInUiWait(() => + { + var options = new List + { + new MyRadioBox { Text = Lang.Text("Download.Comp.QuickDownload.ChooseMethod.CurrentInstance") }, + new MyRadioBox { Text = Lang.Text("Download.Comp.QuickDownload.ChooseMethod.AskInstance") }, + new MyRadioBox { Text = Lang.Text("Download.Comp.QuickDownload.ChooseMethod.AskPath") } + }; + return ModMain.MyMsgBoxSelect(options, + Lang.Text("Download.Comp.QuickDownload.ChooseMethod.Title"), + button1: Lang.Text("Common.Action.Continue"), + button2: Lang.Text("Common.Action.Cancel")); + }); + if (choice is null) return; // 用户取消 + behavior = choice.Value + 1; // 0→1 当前实例, 1→2 选实例, 2→3 选路径 + } + + switch (behavior) + { + case 1: // 下载到当前选中实例 + _QuickDownloadToInstance(project, files, ModInstanceList.McMcInstanceSelected); + break; + case 2: // 询问并下载到选择的实例 + { + var instance = _QuickDownloadPickInstance(project, files); + if (instance is null) return; + _QuickDownloadToInstance(project, files, instance); + break; + } + case 3: // 询问并下载到一个路径 + _QuickDownloadToFolder(project, files); + break; + } + } + catch (Exception ex) + { + ModBase.Log(ex, "[Comp] 快速下载失败", ModBase.LogLevel.Feedback); + } + }, "Comp QuickDownload"); + } + + /// 下载到指定实例的最新兼容版本。 + private static void _QuickDownloadToInstance(CompProject project, List files, McInstance? instance) + { + if (instance is null) + { + HintService.Hint(Lang.Text("Download.Comp.QuickDownload.Hint.NoInstance"), HintType.Info); + return; + } + if (!instance.IsLoaded) instance.Load(); + var compatible = files + .Where(f => IsInstanceSuitableForFile(instance, f, _ResolveLoaders(f, project))) + .ToList(); + var file = _PickLatestFile(compatible); + if (file is null) + { + HintService.Hint(Lang.Text("Download.Comp.QuickDownload.Hint.NoCompatibleFile"), HintType.Info); + return; + } + var folder = instance.PathIndie + _GetSubFolder(project.Type); + Directory.CreateDirectory(folder); + var target = Path.Combine(folder, CompFileNameGet(project, file)); + _StartQuickDownload(file, target); + HintService.Hint(Lang.Text("Download.Comp.QuickDownload.Hint.DownloadStarted", project.RawName), HintType.Success); + } + + /// 弹实例列表让用户选择,返回选中的实例(兼容者优先、当前选中实例居首);取消或无兼容实例返回 null。 + private static McInstance? _QuickDownloadPickInstance(CompProject project, List files) + { + var needLoad = ModInstanceList.mcInstanceListLoader.State != ModBase.LoadState.Finished; + if (needLoad) + { + HintService.Hint(Lang.Text("Download.Comp.QuickDownload.Hint.Loading"), HintType.Info); + ModLoader.LoaderFolderRun(ModInstanceList.mcInstanceListLoader, ModFolder.mcFolderSelected, + ModLoader.LoaderFolderRunType.ForceRun, 1, "versions\\", true); + } + var compatible = ModInstanceList.mcInstanceList.Values + .SelectMany(l => l) + .Where(v => v is not null && files.Any(f => IsInstanceSuitableForFile(v, f, _ResolveLoaders(f, project)))) + .ToList(); + if (compatible.Count == 0) + { + HintService.Hint(Lang.Text("Download.Comp.QuickDownload.Hint.NoCompatibleInstance"), HintType.Info); + return null; + } + // 当前选中实例排首位(呼应 issue「第一行应为当前所选实例」) + var current = ModInstanceList.McMcInstanceSelected; + if (current is not null) + compatible = compatible + .OrderBy(v => v == current ? 0 : 1) + .ThenBy(v => v.Name) + .ToList(); + int? idx = ModBase.RunInUiWait(() => + { + var options = compatible + .Select(v => (IMyRadio)new MyRadioBox { Text = v.Name }) + .ToList(); + return ModMain.MyMsgBoxSelect(options, + Lang.Text("Download.Comp.QuickDownload.ChooseInstance.Title"), + button1: Lang.Text("Common.Action.Continue"), + button2: Lang.Text("Common.Action.Cancel")); + }); + if (idx is null) return null; + return compatible[idx.Value]; + } + + /// 下载最新版本到用户选择的文件夹。 + private static void _QuickDownloadToFolder(CompProject project, List files) + { + var file = _PickLatestFile(files); + if (file is null) + { + HintService.Hint(Lang.Text("Download.Comp.QuickDownload.Hint.NoFile"), HintType.Info); + return; + } + var saveFolder = ModBase.RunInUiWait(() => + SystemDialogs.SelectFolder(Lang.Text("Download.Comp.QuickDownload.Hint.SelectFolder"))); + if (string.IsNullOrWhiteSpace(saveFolder)) return; // 取消 + var target = Path.Combine(saveFolder, CompFileNameGet(project, file)); + _StartQuickDownload(file, target); + HintService.Hint(Lang.Text("Download.Comp.QuickDownload.Hint.DownloadStarted", project.RawName), HintType.Success); + } + + /// 构造并启动单文件下载任务(与详情页 Save_Click 末段一致)。 + private static void _StartQuickDownload(CompFile file, string target) + { + var desc = file.Type switch + { + CompType.Mod => Lang.Text("Download.Comp.Type.Mod"), + CompType.ResourcePack => Lang.Text("Download.Comp.Type.ResourcePack"), + CompType.Shader => Lang.Text("Download.Comp.Type.Shader"), + CompType.DataPack => Lang.Text("Download.Comp.Type.DataPack"), + CompType.World => Lang.Text("Download.Comp.Type.World"), + _ => Lang.Text("Download.Comp.Type.Mod") + }; + var loaderName = Lang.Text("Download.Comp.Detail.DownloadResource", desc, + ModBase.GetFileNameWithoutExtentionFromPath(target)); + var loaders = new List + { + new LoaderDownload(Lang.Text("Download.Comp.Detail.DownloadFile"), + new List { file.ToNetFile(target) }) + { + ProgressWeight = 6, + block = true + } + }; + if (file.Type == CompType.World) + { + var extractDir = Path.GetDirectoryName(target); + loaders.Add(new ModLoader.LoaderTask( + Lang.Text("Download.Comp.Detail.InstallWorld"), + _ => ModBase.ExtractFile(target, extractDir, Encoding.UTF8)) + { + ProgressWeight = 0.1d, + block = true + }); + loaders.Add(new ModLoader.LoaderTask( + Lang.Text("Download.Comp.Detail.CleanCache"), + _ => System.IO.File.Delete(target))); + } + var loader = new ModLoader.LoaderCombo(loaderName, loaders) + { + OnStateChanged = ModDownloadLib.LoaderStateChangedHintOnly + }; + loader.Start(1); + ModLoader.LoaderTaskbarAdd(loader); + ModMain.frmMain.BtnExtraDownload.ShowRefresh(); + ModMain.frmMain.BtnExtraDownload.Ribble(); + } + + /// 根据资源类型返回实例内的目标子文件夹(与 Save_Click 一致)。 + private static string _GetSubFolder(CompType type) => type switch + { + CompType.Mod => "mods\\", + CompType.ResourcePack => "resourcepacks\\", + CompType.Shader => "shaderpacks\\", + CompType.World => "saves\\", + CompType.DataPack => "", // 导航到版本根目录 + _ => "" + }; + + /// 取文件自身声明的加载器,缺失时回退到工程的加载器。 + private static List _ResolveLoaders(CompFile file, CompProject project) + => file.ModLoaders.Count > 0 ? file.ModLoaders : project.ModLoaders; + + /// + /// 按资源类型筛选文件,与详情页 GetResults 一致:Modrinth 会返回 Mod / 服务端插件 / 数据包混合的列表, + /// 需过滤回当前类型,避免快速下载到另一种产物。光影与资源包不筛(原版光影以资源包格式发布)。 + /// + private static List FilterFilesByType(List files, CompType type) + { + if (type == CompType.Shader || type == CompType.ResourcePack) + return files; + return files.Where(f => f.Type == type).ToList(); + } + + /// 判断某实例是否兼容该文件(基于 Save_Click 的 isVersionSuitable,补全了 Quilt 判定)。 + public static bool IsInstanceSuitableForFile(McInstance? version, CompFile file, List allowedLoaders) + { + if (version is null) return false; + if (!version.IsLoaded) version.Load(); + + // 只对 Mod 和数据包进行版本检测 + if (file.Type == CompType.Mod || file.Type == CompType.DataPack) + if (file.GameVersions.Any(v => v.Contains(".")) && + !file.GameVersions.Any(v => v.Contains(".") && v == version.Info.VanillaName)) + return false; + + // 加载器判定 + if (allowedLoaders.Count == 0) return true; // 无要求 + if (allowedLoaders.Contains(CompLoaderType.Forge) && version.Info.HasForge) return true; + if (allowedLoaders.Contains(CompLoaderType.Fabric) && + (version.Info.HasFabric || version.Info.HasLegacyFabric)) return true; + if (allowedLoaders.Contains(CompLoaderType.NeoForge) && version.Info.HasNeoForge) return true; + if (allowedLoaders.Contains(CompLoaderType.Quilt) && version.Info.HasQuilt) return true; + if (allowedLoaders.Contains(CompLoaderType.LiteLoader) && version.Info.HasLiteLoader) return true; + return false; + } + + /// 挑选最新文件:优先 Release,其次按发布日期最新。compatFilter 为空时不做兼容过滤。 + private static CompFile? _PickLatestFile(List files, Func? compatFilter = null) + { + var candidates = (compatFilter is null ? files : files.Where(compatFilter)).ToList(); + if (candidates.Count == 0) return null; + return candidates + .OrderByDescending(f => f.Status == CompFileStatus.Release) + .ThenByDescending(f => f.ReleaseDate) + .First(); + } + + #endregion + /// /// 预载包含大量 CompFile 的卡片,添加必要的元素和前置列表。 /// 前置列表(必要 / 可选)会被放入可折叠栏:必要前置默认展开,可选前置默认收起。 diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/MyCompItem.xaml b/Plain Craft Launcher 2/Pages/PageDownload/Comp/MyCompItem.xaml index ae7456aa9..a6f88a603 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/Comp/MyCompItem.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/MyCompItem.xaml @@ -12,7 +12,7 @@ - + @@ -133,6 +133,11 @@ Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,5,0" SnapsToDevicePixels="False" UseLayoutRounding="False" Opacity="0"> + + + diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/MyCompItem.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/Comp/MyCompItem.xaml.cs index 0d59e6e5e..2cebfa044 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/Comp/MyCompItem.xaml.cs +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/MyCompItem.xaml.cs @@ -50,7 +50,7 @@ public void RefreshColor(object sender, EventArgs e) var ani = new List(); if (IsMouseOver) { - if (PanButtons is not null && ShowFavoriteBtn) + if (PanButtons is not null && _HasActionButtons) ani.Add(ModAnimation.AaOpacity(PanButtons, 1d - PanButtons.Opacity, (int)Math.Round(time * 0.35d), (int)Math.Round(time * 0.15d))); ani.AddRange(new[] @@ -71,7 +71,7 @@ public void RefreshColor(object sender, EventArgs e) } else { - if (PanButtons is not null && ShowFavoriteBtn) + if (PanButtons is not null && _HasActionButtons) ani.Add(ModAnimation.AaOpacity(PanButtons, -PanButtons.Opacity, (int)Math.Round(time * 0.4d))); ani.AddRange(new[] { @@ -160,6 +160,7 @@ public MyCompItem() // Handles LabInfo.MouseEnter += LabInfo_MouseEnter; BtnDelete.Click += BtnDelete_Click; + BtnDownload.Click += _BtnDownload_Click; } // 指向时扩展描述 @@ -219,8 +220,32 @@ public List Tags // ‘收藏按钮 public bool ShowFavoriteBtn { - set => PanButtons.Visibility = value ? Visibility.Visible : Visibility.Collapsed; - get => PanButtons.Visibility == Visibility.Visible; + get => BtnDelete.Visibility == Visibility.Visible; + set + { + BtnDelete.Visibility = value ? Visibility.Visible : Visibility.Collapsed; + _UpdatePanButtons(); + } + } + + // 快速下载按钮 + public bool ShowDownloadBtn + { + get => BtnDownload.Visibility == Visibility.Visible; + set + { + BtnDownload.Visibility = value ? Visibility.Visible : Visibility.Collapsed; + _UpdatePanButtons(); + } + } + + /// 右侧是否存在任意可见的操作按钮(收藏 / 下载),用于决定悬停时是否淡入按钮区。 + private bool _HasActionButtons => ShowFavoriteBtn || ShowDownloadBtn; + + private void _UpdatePanButtons() + { + if (PanButtons is null) return; + PanButtons.Visibility = _HasActionButtons ? Visibility.Visible : Visibility.Collapsed; } /// @@ -252,6 +277,12 @@ private void BtnDelete_Click(object sender, EventArgs e) } } + private void _BtnDownload_Click(object sender, EventArgs e) + { + if (PanButtons.Opacity > 0d && Tag is ModComp.CompProject project) + ModComp.QuickDownload(project); + } + private void MyCompItem_Click(MyCompItem sender, EventArgs e) { // 记录当前展开的卡片标题(#2712) @@ -363,10 +394,8 @@ private void Button_MouseDown(object sender, MouseButtonEventArgs e) var isClickOnButton = false; if (PanButtons.Visibility == Visibility.Visible) - { - var buttonBounds = new Rect(BtnDelete.TranslatePoint(new Point(0d, 0d), this), BtnDelete.RenderSize); - isClickOnButton = buttonBounds.Contains(clickPosition); - } + isClickOnButton = _IsClickOnActionButton(BtnDelete, clickPosition) || + _IsClickOnActionButton(BtnDownload, clickPosition); // 如果点击在按钮上,不处理主项目点击事件 if (isClickOnButton) return; @@ -388,6 +417,14 @@ private void Button_MouseLeave(object sender, object e) isMouseDown = false; } + // 判断点击是否落在某个操作按钮(收藏 / 下载)上 + private bool _IsClickOnActionButton(FrameworkElement button, Point clickPosition) + { + if (button is null || button.Visibility != Visibility.Visible) return false; + var bounds = new Rect(button.TranslatePoint(new Point(0d, 0d), this), button.RenderSize); + return bounds.Contains(clickPosition); + } + #endregion #region 后加载指向背景 diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml.cs index fe0b6c188..00bd2528c 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml.cs +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml.cs @@ -27,10 +27,13 @@ private void Load_OnFinish() // 列表项 PanProjects.Children.Clear(); var index = Math.Min(page * pageSize, storage.results.Count - 1); + // 整合包需要安装而非直接下载,不显示快速下载按钮 + var showQuickDownload = PageType != ModComp.CompType.ModPack; foreach (var result in storage.results.GetRange(index, Math.Min(storage.results.Count - index, pageSize))) PanProjects.Children.Add(result.ToCompItem(loader.input.gameVersion is null, loader.input.modLoader == ModComp.CompLoaderType.Any && - (PageType == ModComp.CompType.Mod || PageType == ModComp.CompType.ModPack))); + (PageType == ModComp.CompType.Mod || PageType == ModComp.CompType.ModPack), + showQuickDownload)); // 页码 CardPages.Visibility = storage.results.Count > 40 || storage.curseForgeOffset < storage.curseForgeTotal || diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameManage.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameManage.xaml index 8c2f0b829..4ca2f95b1 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameManage.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameManage.xaml @@ -85,6 +85,8 @@ + + + + + + + + + diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameManage.xaml.cs b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameManage.xaml.cs index 163dc50b4..fbbbb9b50 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameManage.xaml.cs +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameManage.xaml.cs @@ -61,6 +61,7 @@ public void Reload() ComboDownloadTranslateV2.SelectedIndex = Config.Download.Comp.NameFormatV2; ComboDownloadMod.SelectedIndex = Config.Download.Comp.CompSourceSolution; ComboModLocalNameStyle.SelectedIndex = Config.Download.Comp.UiCompNameSolution; + ComboDownloadQuickBehavior.SelectedIndex = Config.Download.Comp.QuickDownloadBehavior; CheckDownloadIgnoreQuilt.Checked = Config.Download.Comp.IgnoreQuilt; CheckDownloadAutoInstallDependencies.Checked = Config.Download.Comp.AutoInstallDependencies; CheckDownloadClipboard.Checked = Config.Download.Comp.ReadClipboard;