diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java index 3ace4df057d..fc8505dcf60 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java @@ -17,6 +17,8 @@ */ package org.jackhuang.hmcl.game; +import org.jackhuang.hmcl.mod.LocalModFile; +import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.IOUtils; import org.jackhuang.hmcl.util.io.Zipper; @@ -74,6 +76,68 @@ public static CompletableFuture exportLogs( zipper.putTextFile(logs, "minecraft.log"); zipper.putTextFile(Logger.filterForbiddenToken(launchScript), OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "launch.bat" : "launch.sh"); + try { + ModManager modManager = gameRepository.getModManager(versionId); + modManager.refreshMods(); + + StringBuilder infoBuilder = new StringBuilder(); + + infoBuilder.append("=== Mod List ===").append(System.lineSeparator()); + + Path modsDir = runDirectory.resolve("mods"); + + infoBuilder.append("Filesystem structure of: ").append(modsDir).append(System.lineSeparator()); + infoBuilder.append("|-> mods").append(System.lineSeparator()); + + modManager.getMods().stream() + .filter(LocalModFile::isActive) + .sorted((m1, m2) -> String.CASE_INSENSITIVE_ORDER.compare(m1.getName(), m2.getName())) + .forEach(mod -> { + infoBuilder.append("| |-> "); + infoBuilder.append(mod.getName()); + if (StringUtils.isNotBlank(mod.getVersion()) && !"${version}".equals(mod.getVersion())) { + infoBuilder.append(" (").append(mod.getVersion()).append(")"); + } + if (!mod.getName().equals(mod.getFileName())) { + infoBuilder.append(" [").append(mod.getFileName()).append("]"); + } + infoBuilder.append(System.lineSeparator()); + }); + + infoBuilder.append(System.lineSeparator()); + infoBuilder.append("----------------------------").append(System.lineSeparator()); + infoBuilder.append(System.lineSeparator()); + + infoBuilder.append("=== JIJ Info List (active mods only) ===").append(System.lineSeparator()); + + boolean hasJij = false; + for (LocalModFile mod : modManager.getMods()) { + if (mod.isActive() && mod.hasBundledMods()) { + hasJij = true; + infoBuilder.append(mod.getName()); + if (!mod.getName().equals(mod.getFileName())) { + infoBuilder.append("[").append(mod.getFileName()).append("]"); + } + infoBuilder.append(System.lineSeparator()); + + for (String bundled : mod.getBundledMods()) { + String name = bundled.contains("/") ? bundled.substring(bundled.lastIndexOf('/') + 1) : bundled; + infoBuilder.append("\t|-> ").append(name).append(System.lineSeparator()); + } + infoBuilder.append(System.lineSeparator()); + } + } + + if (!hasJij) { + infoBuilder.append("No JIJ INFO found").append(System.lineSeparator()); + } + + zipper.putTextFile(infoBuilder.toString(), "ALL_MOD_JIJ_INFO.txt"); + + } catch (Exception e) { + LOG.warning("Failed to export mod info to crash report package", e); + } + for (String id : versions) { Path versionJson = baseDirectory.resolve("versions").resolve(id).resolve(id + ".json"); if (Files.exists(versionJson)) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java index c7a99881db2..bfbba6f136c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -56,6 +56,7 @@ public enum SVG { DOWNLOAD("M12 16 7 11 8.4 9.55 11 12.15V4H13V12.15L15.6 9.55 17 11 12 16ZM6 20Q5.175 20 4.5875 19.4125T4 18V15H6V18H18V15H20V18Q20 18.825 19.4125 19.4125T18 20H6Z"), DRESSER("M4 21V5Q4 4.175 4.5875 3.5875T6 3H18Q18.825 3 19.4125 3.5875T20 5V21H18V19H6V21H4ZM6 11H11V5H6V11ZM13 7H18V5H13V7ZM13 11H18V9H13V11ZM10 16H14V14H10V16ZM6 13V17H18V13H6ZM6 13V17 13Z"), EDIT("M5 19H6.425L16.2 9.225 14.775 7.8 5 17.575V19ZM3 21V16.75L16.2 3.575Q16.5 3.3 16.8625 3.15T17.625 3Q18.025 3 18.4 3.15T19.05 3.6L20.425 5Q20.725 5.275 20.8625 5.65T21 6.4Q21 6.8 20.8625 7.1625T20.425 7.825L7.25 21H3ZM19 6.4 17.6 5 19 6.4ZM15.475 8.525 14.775 7.8 16.2 9.225 15.475 8.525Z"), + FILE_EXPORT("M19 19H5V5h7V3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59L7.05 15.54 8.46 16.95 19 6.41V10h2V3z"), ERROR("M12 17Q12.425 17 12.7125 16.7125T13 16Q13 15.575 12.7125 15.2875T12 15Q11.575 15 11.2875 15.2875T11 16Q11 16.425 11.2875 16.7125T12 17ZM11 13H13V7H11V13ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"), EXPLORE("M12 12Zm0 8q-3.325 0-5.6625-2.3375T4 12Q4 8.675 6.3375 6.3375T12 4q3.325-0 5.6625 2.3375T20 12q0 3.325-2.3375 5.6625T12 20Zm0 2q2.075-0 3.9-.7875t3.175-2.1375q1.35-1.35 2.1375-3.175T22 12q-0-2.075-.7875-3.9T19.075 4.925q-1.35-1.35-3.175-2.1375T12 2q-2.075 0-3.9.7875T4.925 4.925Q3.575 6.275 2.7875 8.1T2 12q0 2.075.7875 3.9T4.925 19.075q1.35 1.35 3.175 2.1375T12 22Zm0-8.5q.625 0 1.0625-.4375T13.5 12t-.4375-1.0625T12 10.5t-1.0625.4375T10.5 12t.4375 1.0625T12 13.5Zm-4.5 3 2-7 7-2-2 7-7 2Z"), EXTENSION("M8.8 21H5Q4.175 21 3.5875 20.4125T3 19V15.2Q4.2 15.2 5.1 14.4375T6 12.5Q6 11.325 5.1 10.5625T3 9.8V6Q3 5.175 3.5875 4.5875T5 4H9Q9 2.95 9.725 2.225T11.5 1.5Q12.55 1.5 13.275 2.225T14 4H18Q18.825 4 19.4125 4.5875T20 6V10Q21.05 10 21.775 10.725T22.5 12.5Q22.5 13.55 21.775 14.275T20 15V19Q20 19.825 19.4125 20.4125T18 21H14.2Q14.2 19.75 13.4125 18.875T11.5 18Q10.375 18 9.5875 18.875T8.8 21ZM5 19H7.125Q7.725 17.35 9.05 16.675T11.5 16Q12.625 16 13.95 16.675T15.875 19H18V13H20Q20.2 13 20.35 12.85T20.5 12.5Q20.5 12.3 20.35 12.15T20 12H18V6H12V4Q12 3.8 11.85 3.65T11.5 3.5Q11.3 3.5 11.15 3.65T11 4V6H5V8.2Q6.35 8.7 7.175 9.875T8 12.5Q8 13.925 7.175 15.1T5 16.8V19ZM11.5 12.5Z"), @@ -110,6 +111,7 @@ public enum SVG { SELECT_ALL("M7 17V7H17V17H7ZM9 15H15V9H9V15ZM5 19V21Q4.175 21 3.5875 20.4125T3 19H5ZM3 17V15H5V17H3ZM3 13V11H5V13H3ZM3 9V7H5V9H3ZM5 5H3Q3 4.175 3.5875 3.5875T5 3V5ZM7 21V19H9V21H7ZM7 5V3H9V5H7ZM11 21V19H13V21H11ZM11 5V3H13V5H11ZM15 21V19H17V21H15ZM15 5V3H17V5H15ZM19 21V19H21Q21 19.825 20.4125 20.4125T19 21ZM19 17V15H21V17H19ZM19 13V11H21V13H19ZM19 9V7H21V9H19ZM19 5V3Q19.825 3 20.4125 3.5875T21 5H19Z"), SETTINGS("M19.43 12.98C19.47 12.66 19.5 12.34 19.5 12 19.5 11.66 19.47 11.34 19.43 11.02L21.54 9.37C21.73 9.22 21.78 8.95 21.66 8.73L19.66 5.27C19.57 5.11 19.4 5.02 19.22 5.02 19.16 5.02 19.1 5.03 19.05 5.05L16.56 6.05C16.04 5.65 15.48 5.32 14.87 5.07L14.49 2.42C14.46 2.18 14.25 2 14 2H10C9.75 2 9.54 2.18 9.51 2.42L9.13 5.07C8.52 5.32 7.96 5.66 7.44 6.05L4.95 5.05C4.89 5.03 4.83 5.02 4.77 5.02 4.6 5.02 4.43 5.11 4.34 5.27L2.34 8.73C2.21 8.95 2.27 9.22 2.46 9.37L4.57 11.02C4.53 11.34 4.5 11.67 4.5 12 4.5 12.33 4.53 12.66 4.57 12.98L2.46 14.63C2.27 14.78 2.22 15.05 2.34 15.27L4.34 18.73C4.43 18.89 4.6 18.98 4.78 18.98 4.84 18.98 4.9 18.97 4.95 18.95L7.44 17.95C7.96 18.35 8.52 18.68 9.13 18.93L9.51 21.58C9.54 21.82 9.75 22 10 22H14C14.25 22 14.46 21.82 14.49 21.58L14.87 18.93C15.48 18.68 16.04 18.34 16.56 17.95L19.05 18.95C19.11 18.97 19.17 18.98 19.23 18.98 19.4 18.98 19.57 18.89 19.66 18.73L21.66 15.27C21.78 15.05 21.73 14.78 21.54 14.63L19.43 12.98ZM17.45 11.27C17.49 11.58 17.5 11.79 17.5 12 17.5 12.21 17.48 12.43 17.45 12.73L17.31 13.86 18.2 14.56 19.28 15.4 18.58 16.61 17.31 16.1 16.27 15.68 15.37 16.36C14.94 16.68 14.53 16.92 14.12 17.09L13.06 17.52 12.9 18.65 12.7 20H11.3L11.11 18.65 10.95 17.52 9.89 17.09C9.46 16.91 9.06 16.68 8.66 16.38L7.75 15.68 6.69 16.11 5.42 16.62 4.72 15.41 5.8 14.57 6.69 13.87 6.55 12.74C6.52 12.43 6.5 12.2 6.5 12S6.52 11.57 6.55 11.27L6.69 10.14 5.8 9.44 4.72 8.6 5.42 7.39 6.69 7.9 7.73 8.32 8.63 7.64C9.06 7.32 9.47 7.08 9.88 6.91L10.94 6.48 11.1 5.35 11.3 4H12.69L12.88 5.35 13.04 6.48 14.1 6.91C14.53 7.09 14.93 7.32 15.33 7.62L16.24 8.32 17.3 7.89 18.57 7.38 19.27 8.59 18.2 9.44 17.31 10.14 17.45 11.27ZM12 8C9.79 8 8 9.79 8 12S9.79 16 12 16 16 14.21 16 12 14.21 8 12 8ZM12 14C10.9 14 10 13.1 10 12S10.9 10 12 10 14 10.9 14 12 13.1 14 12 14Z"), // Material Icons SETTINGS_FILL("M9.25 22l-.4-3.2q-.325-.125-.6125-.3t-.5625-.375L4.7 19.375l-2.75-4.75 2.575-1.95Q4.5 12.5 4.5 12.3375v-.675q0-.1625.025-.3375L1.95 9.375 4.7 4.625l2.975 1.25q.275-.2.575-.375t.6-.3L9.25 2h5.5l.4 3.2q.325.125.6125.3t.5625.375L19.3 4.625l2.75 4.75-2.575 1.95q.025.175.025.3375v.675q0 .1625-.05.3375l2.575 1.95-2.75 4.75-2.95-1.25q-.275.2-.575.375t-.6.3l-.4 3.2H9.25Zm2.8-6.5q1.45 0 2.475-1.025T15.55 12 14.525 9.525 12.05 8.5q-1.475 0-2.4875 1.025T8.55 12q0 1.45 1.0125 2.475T12.05 15.5Z"), // Material Icons + STACKS("M11.99 18.54l-7.37-5.73L3 14.07l9 7 9-7-1.63-1.27-7.38 5.74zM12 16l7.36-5.73L21 9l-9-7-9 7 1.63 1.27L12 16z"), STADIA_CONTROLLER("M4.725 20Q3.225 20 2.1625 18.925T1.05 16.325Q1.05 16.1 1.075 15.875T1.15 15.425L3.25 7.025Q3.6 5.675 4.675 4.8375T7.125 4H16.875Q18.25 4 19.325 4.8375T20.75 7.025L22.85 15.425Q22.9 15.65 22.9375 15.8875T22.975 16.35Q22.975 17.875 21.8875 18.9375T19.275 20Q18.225 20 17.325 19.45T15.975 17.95L15.275 16.5Q15.15 16.25 14.9 16.125T14.375 16H9.625Q9.35 16 9.1 16.125T8.725 16.5L8.025 17.95Q7.575 18.9 6.675 19.45T4.725 20ZM4.8 18Q5.275 18 5.6625 17.75T6.25 17.075L6.95 15.65Q7.325 14.875 8.05 14.4375T9.625 14H14.375Q15.225 14 15.95 14.45T17.075 15.65L17.775 17.075Q17.975 17.5 18.3625 17.75T19.225 18Q19.925 18 20.425 17.5375T20.95 16.375Q20.95 16.4 20.9 15.9L18.8 7.525Q18.625 6.85 18.1 6.425T16.875 6H7.125Q6.425 6 5.8875 6.425T5.2 7.525L3.1 15.9Q3.05 16.05 3.05 16.35 3.05 17.05 3.5625 17.525T4.8 18ZM13.5 11Q13.925 11 14.2125 10.7125T14.5 10Q14.5 9.575 14.2125 9.2875T13.5 9Q13.075 9 12.7875 9.2875T12.5 10Q12.5 10.425 12.7875 10.7125T13.5 11ZM15.5 9Q15.925 9 16.2125 8.7125T16.5 8Q16.5 7.575 16.2125 7.2875T15.5 7Q15.075 7 14.7875 7.2875T14.5 8Q14.5 8.425 14.7875 8.7125T15.5 9ZM15.5 13Q15.925 13 16.2125 12.7125T16.5 12Q16.5 11.575 16.2125 11.2875T15.5 11Q15.075 11 14.7875 11.2875T14.5 12Q14.5 12.425 14.7875 12.7125T15.5 13ZM17.5 11Q17.925 11 18.2125 10.7125T18.5 10Q18.5 9.575 18.2125 9.2875T17.5 9Q17.075 9 16.7875 9.2875T16.5 10Q16.5 10.425 16.7875 10.7125T17.5 11ZM8.5 12.5Q8.825 12.5 9.0375 12.2875T9.25 11.75V10.75H10.25Q10.575 10.75 10.7875 10.5375T11 10Q11 9.675 10.7875 9.4625T10.25 9.25H9.25V8.25Q9.25 7.925 9.0375 7.7125T8.5 7.5Q8.175 7.5 7.9625 7.7125T7.75 8.25V9.25H6.75Q6.425 9.25 6.2125 9.4625T6 10Q6 10.325 6.2125 10.5375T6.75 10.75H7.75V11.75Q7.75 12.075 7.9625 12.2875T8.5 12.5ZM12 12Z"), STADIA_CONTROLLER_FILL("M4.725 20q-1.5 0-2.5625-1.075T1.05 16.325q0-.225.025-.45t.075-.45l2.1-8.4q.35-1.35 1.425-2.1875T7.125 4h9.75q1.375 0 2.45.8375T20.75 7.025l2.1 8.4q.05.225.0875.4625t.0375.4625q0 1.525-1.0875 2.5875T19.275 20q-1.05 0-1.95-.55t-1.35-1.5l-.7-1.45q-.125-.25-.375-.375T14.375 16H9.625q-.275 0-.525.125t-.375.375l-.7 1.45q-.45.95-1.35 1.5T4.725 20ZM13.5 11q.425 0 .7125-.2875T14.5 10t-.2875-.7125T13.5 9t-.7125.2875T12.5 10t.2875.7125T13.5 11Zm2-2q.425 0 .7125-.2875T16.5 8q0-.425-.2875-.7125T15.5 7q-.425 0-.7125.2875T14.5 8t.2875.7125T15.5 9Zm0 4q.425 0 .7125-.2875T16.5 12q0-.425-.2875-.7125T15.5 11q-.425 0-.7125.2875T14.5 12t.2875.7125T15.5 13Zm2-2q.425 0 .7125-.2875T18.5 10q0-.425-.2875-.7125T17.5 9q-.425 0-.7125.2875T16.5 10q0 .425.2875.7125T17.5 11Zm-9 1.5q.325 0 .5375-.2125T9.25 11.75v-1h1q.325 0 .5375-.2125T11 10t-.2125-.5375T10.25 9.25h-1v-1q0-.325-.2125-.5375T8.5 7.5q-.325 0-.5375.2125T7.75 8.25v1h-1q-.325 0-.5375.2125T6 10q0 .325.2125.5375T6.75 10.75h1v1q0 .325.2125.5375T8.5 12.5Z"), STYLE("M3.975 19.8 3.125 19.45Q2.35 19.125 2.0875 18.325T2.175 16.75L3.975 12.85V19.8ZM7.975 22Q7.15 22 6.5625 21.4125T5.975 20V14L8.625 21.35Q8.7 21.525 8.775 21.6875T8.975 22H7.975ZM13.125 21.9Q12.325 22.2 11.575 21.825T10.525 20.65L6.075 8.45Q5.775 7.65 6.125 6.8875T7.275 5.85L14.825 3.1Q15.625 2.8 16.375 3.175T17.425 4.35L21.875 16.55Q22.175 17.35 21.825 18.1125T20.675 19.15L13.125 21.9ZM10.975 10Q11.4 10 11.6875 9.7125T11.975 9Q11.975 8.575 11.6875 8.2875T10.975 8Q10.55 8 10.2625 8.2875T9.975 9Q9.975 9.425 10.2625 9.7125T10.975 10ZM12.425 20 19.975 17.25 15.525 5 7.975 7.75 12.425 20ZM7.975 7.75 15.525 5 7.975 7.75Z"), diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/BuiltInModListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/BuiltInModListPage.java new file mode 100644 index 00000000000..380c5c34b4c --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/BuiltInModListPage.java @@ -0,0 +1,84 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2021 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.ui.versions; + +import javafx.scene.control.Skin; +import org.jackhuang.hmcl.mod.ModManager; +import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.ui.ListPageBase; +import org.jackhuang.hmcl.ui.construct.PageAware; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public class BuiltInModListPage extends ListPageBase implements VersionPage.VersionLoadable, PageAware { + + private ModManager modManager; + + @Override + protected Skin createDefaultSkin() { + return new BuiltInModListPageSkin(this); + } + + @Override + public void loadVersion(Profile profile, String id) { + getItems().clear(); + this.modManager = profile.getRepository().getModManager(id); + loadMods(false); + } + + /** + * Called by the refresh button in the skin — forces a full rescan of the mods directory. + */ + public void refresh() { + loadMods(true); + } + + private void loadMods(boolean forceRefresh) { + if (modManager == null) return; + + getItems().clear(); + setLoading(true); + + CompletableFuture.supplyAsync(() -> { + try { + if (forceRefresh) { + modManager.refreshMods(); + } + return modManager.getMods().stream() + .filter(mod -> mod != null && mod.hasBundledMods()) + .map(ModListPageSkin.ModInfoObject::new) + .collect(Collectors.toList()); + } catch (Exception e) { + LOG.warning("Failed to load built-in mods", e); + return List.of(); + } + }, Schedulers.io()).whenCompleteAsync((list, ex) -> { + if (ex != null) { + LOG.warning("Async task failed in BuiltInModListPage", ex); + } else { + getItems().setAll(list); + } + setLoading(false); + }, Schedulers.javafx()); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/BuiltInModListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/BuiltInModListPageSkin.java new file mode 100644 index 00000000000..afe652da6e8 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/BuiltInModListPageSkin.java @@ -0,0 +1,503 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2021 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.ui.versions; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXListView; +import com.jfoenix.controls.JFXTextField; +import javafx.animation.PauseTransition; +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.geometry.Bounds; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.geometry.Rectangle2D; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.SkinBase; +import javafx.scene.control.Tooltip; +import javafx.scene.effect.DropShadow; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; +import javafx.stage.FileChooser; +import javafx.stage.Popup; +import javafx.util.Duration; +import org.jackhuang.hmcl.mod.LocalModFile; +import org.jackhuang.hmcl.mod.ModLoaderType; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.SVG; +import org.jackhuang.hmcl.ui.animation.ContainerAnimations; +import org.jackhuang.hmcl.ui.animation.TransitionPane; +import org.jackhuang.hmcl.ui.construct.*; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.i18n.I18n; + +import java.io.File; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.nio.file.Files; +import java.util.List; +import java.util.Locale; +import java.util.StringJoiner; +import java.util.function.Predicate; + +import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; +import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public class BuiltInModListPageSkin extends SkinBase { + + private final StackPane pane; + private final TransitionPane toolbarPane; + private final HBox searchBar; + private final HBox toolbarNormal; + + private final JFXListView listView; + private final JFXTextField searchField; + + private boolean isSearching = false; + + protected BuiltInModListPageSkin(BuiltInModListPage skinnable) { + super(skinnable); + + pane = new StackPane(); + pane.setPadding(new Insets(10)); + pane.getStyleClass().addAll("notice-pane"); + + ComponentList root = new ComponentList(); + root.getStyleClass().add("no-padding"); + listView = new JFXListView<>(); + + { + toolbarPane = new TransitionPane(); + + searchBar = new HBox(); + toolbarNormal = new HBox(); + + searchBar.setAlignment(Pos.CENTER); + searchBar.setPadding(new Insets(0, 5, 0, 5)); + searchField = new JFXTextField(); + searchField.setPromptText(i18n("search")); + HBox.setHgrow(searchField, Priority.ALWAYS); + PauseTransition pause = new PauseTransition(Duration.millis(100)); + pause.setOnFinished(e -> search()); + searchField.textProperty().addListener((observable, oldValue, newValue) -> { + pause.setRate(1); + pause.playFromStart(); + }); + + JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, + () -> { + changeToolbar(toolbarNormal); + isSearching = false; + searchField.clear(); + Bindings.bindContent(listView.getItems(), getSkinnable().getItems()); + }); + + onEscPressed(searchField, closeSearchBar::fire); + + searchBar.getChildren().setAll(searchField, closeSearchBar); + + toolbarNormal.getChildren().addAll( + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), + createToolbarButton2(i18n("mods.built_in.export.jij_info.all"), SVG.FILE_EXPORT, () -> exportAllJijList(listView.getItems())), + createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)) + ); + + root.getContent().add(toolbarPane); + changeToolbar(toolbarNormal); + } + + { + SpinnerPane center = new SpinnerPane(); + ComponentList.setVgrow(center, Priority.ALWAYS); + center.getStyleClass().add("large-spinner-pane"); + center.loadingProperty().bind(skinnable.loadingProperty()); + + listView.setCellFactory(param -> new JijModListCell(listView)); + Bindings.bindContent(listView.getItems(), skinnable.getItems()); + + center.setContent(listView); + root.getContent().add(center); + } + + pane.getChildren().add(root); + getChildren().add(pane); + } + + private void changeToolbar(HBox newToolbar) { + Node oldToolbar = toolbarPane.getCurrentNode(); + if (newToolbar != oldToolbar) { + toolbarPane.setContent(newToolbar, ContainerAnimations.FADE); + if (newToolbar == searchBar) { + Platform.runLater(searchField::requestFocus); + } + } + } + + private void search() { + isSearching = true; + Bindings.unbindContent(listView.getItems(), getSkinnable().getItems()); + + String queryString = searchField.getText(); + if (StringUtils.isBlank(queryString)) { + listView.getItems().setAll(getSkinnable().getItems()); + } else { + listView.getItems().clear(); + String lowerQueryString = queryString.toLowerCase(Locale.ROOT); + Predicate predicate = s -> s != null && s.toLowerCase(Locale.ROOT).contains(lowerQueryString); + + for (ModListPageSkin.ModInfoObject item : getSkinnable().getItems()) { + LocalModFile modInfo = item.getModInfo(); + if (predicate.test(modInfo.getFileName()) + || predicate.test(modInfo.getName()) + || predicate.test(modInfo.getId()) + || (item.getModTranslations() != null && predicate.test(item.getModTranslations().getDisplayName()))) { + listView.getItems().add(item); + } + } + } + } + + private class JijModListCell extends MDListCell { + private final ImageContainer imageContainer = new ImageContainer(24); + private final TwoLineListItem content = new TwoLineListItem(); + private Popup activePopup; + private boolean ignoreNextClick = false; + + public JijModListCell(JFXListView listView) { + super(listView); + this.getStyleClass().add("mod-info-list-cell"); + + HBox container = new HBox(8); + container.setPickOnBounds(false); + container.setAlignment(Pos.CENTER_LEFT); + StackPane.setMargin(container, new Insets(8, 8, 8, 18)); + + HBox.setHgrow(content, Priority.ALWAYS); + content.setMouseTransparent(true); + + container.getChildren().addAll(imageContainer, content); + getContainer().getChildren().setAll(container); + + setSelectable(); + + this.setOnMousePressed(e -> ignoreNextClick = activePopup != null && activePopup.isShowing()); + + this.setOnMouseClicked(e -> { + if (getItem() != null && getItem().getModInfo() != null) { + LocalModFile modFile = getItem().getModInfo(); + if (modFile.hasBundledMods()) { + + if (activePopup != null && activePopup.isShowing()) { + activePopup.hide(); + activePopup = null; + return; + } + + activePopup = showBundledPopup(this, modFile.getName(), modFile.getBundledMods()); + + activePopup.setOnHidden(event -> { + if (activePopup == event.getSource()) { + activePopup = null; + listView.getSelectionModel().clearSelection(); + } + }); + } + } + }); + } + + @Override + protected void updateControl(ModListPageSkin.ModInfoObject dataItem, boolean empty) { + if (empty || dataItem == null) return; + + LocalModFile modInfo = dataItem.getModInfo(); + ModTranslations.Mod modTranslations = dataItem.getModTranslations(); + ModLoaderType modLoaderType = modInfo.getModLoaderType(); + + dataItem.loadIcon(imageContainer, new WeakReference<>(this.itemProperty())); + + String displayName = modInfo.getName(); + if (modTranslations != null && I18n.isUseChinese()) { + String chineseName = modTranslations.getName(); + if (StringUtils.containsChinese(chineseName)) { + if (StringUtils.containsEmoji(chineseName)) { + StringBuilder builder = new StringBuilder(); + chineseName.codePoints().forEach(ch -> { + if (ch < 0x1F300 || ch > 0x1FAFF) builder.appendCodePoint(ch); + }); + chineseName = builder.toString().trim(); + } + if (StringUtils.isNotBlank(chineseName) && !displayName.equalsIgnoreCase(chineseName)) { + displayName = displayName + " (" + chineseName + ")"; + } + } + } + content.setTitle(displayName); + + StringJoiner joiner = new StringJoiner(" | "); + if (modLoaderType != ModLoaderType.UNKNOWN && StringUtils.isNotBlank(modInfo.getId())) + joiner.add(modInfo.getId()); + joiner.add(org.jackhuang.hmcl.util.io.FileUtils.getName(modInfo.getFile())); + content.setSubtitle(joiner.toString()); + + content.getTags().clear(); + if (modInfo.hasBundledMods()) { + content.addTag(i18n("mods.built_in") + ": " + modInfo.getBundledMods().size()); + } + + String modVersion = modInfo.getVersion(); + if (StringUtils.isNotBlank(modVersion) && !"${version}".equals(modVersion)) { + content.addTag(modVersion); + } + } + } + + private Popup showBundledPopup(Node anchor, String modName, List bundledMods) { + VBox root = new VBox(10); + root.setPadding(new Insets(15)); + root.setMaxHeight(400); + root.getStyleClass().add("card-pane"); + root.setStyle(root.getStyle() + """ + -fx-background-color: -fx-background; + -fx-background-radius: 8; + -fx-border-color: -fx-box-border; + -fx-border-radius: 8; + """); + + root.setEffect(new DropShadow(18, Color.rgb(0, 0, 0, 0.30))); + + HBox header = new HBox(10); + header.setAlignment(Pos.CENTER_LEFT); + + Label titleLabel = new Label(i18n("mods.built_in") + " (" + bundledMods.size() + ")"); + titleLabel.setStyle("-fx-font-size: 14px; -fx-font-weight: bold;"); + + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + JFXButton exportButton = new JFXButton(); + exportButton.setGraphic(SVG.FILE_EXPORT.createIcon(18)); + exportButton.getStyleClass().add("toggle-icon4"); + FXUtils.installFastTooltip(exportButton, i18n("mods.built_in.export.jij_info")); + exportButton.setOnAction(e -> exportJijList(modName, bundledMods)); + + header.getChildren().addAll(titleLabel, spacer, exportButton); + + JFXTextField searchField = new JFXTextField(); + searchField.setPromptText(i18n("mods.built_in.search")); + searchField.setFocusTraversable(false); + + FlowPane flowPane = new FlowPane(); + flowPane.setHgap(8); + flowPane.setVgap(8); + flowPane.setPrefWrapLength(450); + + Runnable refreshList = () -> { + flowPane.getChildren().clear(); + String query = searchField.getText().toLowerCase(Locale.ROOT); + + for (String path : bundledMods) { + if (path.toLowerCase(Locale.ROOT).contains(query)) { + String name = path.contains("/") ? path.substring(path.lastIndexOf('/') + 1) : path; + + Label tag = new Label(name); + tag.setStyle("-fx-background-color: -fx-background; " + + "-fx-padding: 4 8; " + + "-fx-background-radius: 4; " + + "-fx-border-color: -fx-box-border; " + + "-fx-border-radius: 4;"); + tag.setMaxWidth(430); + tag.setTooltip(new Tooltip(path)); + + flowPane.getChildren().add(tag); + } + } + + if (flowPane.getChildren().isEmpty()) { + Label emptyLabel = new Label(i18n("mods.built_in.noresult")); + emptyLabel.setStyle("-fx-text-fill: -fx-text-base-color-disabled;"); + flowPane.getChildren().add(emptyLabel); + } + }; + + refreshList.run(); + searchField.textProperty().addListener((obs, oldVal, newVal) -> refreshList.run()); + + ScrollPane scrollPane = new ScrollPane(flowPane); + scrollPane.setFitToWidth(true); + scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + scrollPane.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); + + root.getChildren().addAll(header, searchField, scrollPane); + + StackPane wrapper = new StackPane(root); + wrapper.setPadding(new Insets(12)); + wrapper.setStyle("-fx-background-color: transparent;"); + + Popup popup = new Popup(); + popup.getContent().add(wrapper); + popup.setAutoHide(true); + popup.setHideOnEscape(true); + + Platform.runLater(() -> { + Bounds aScreen = anchor.localToScreen(anchor.getBoundsInLocal()); + Bounds pScreen = pane.localToScreen(pane.getBoundsInLocal()); + if (aScreen == null || pScreen == null) return; + + Rectangle2D content = new Rectangle2D( + pScreen.getMinX(), + pScreen.getMinY(), + pScreen.getWidth(), + pScreen.getHeight() + ); + + final double padding = 8; + final double gap = 5; + + wrapper.applyCss(); + wrapper.layout(); + double popupW = wrapper.prefWidth(-1); + double popupH = wrapper.prefHeight(-1); + + double maxAllowedH = Math.max(120, content.getHeight() - padding * 2); + double wrapperExtraH = wrapper.getPadding().getTop() + wrapper.getPadding().getBottom(); + double maxRootH = Math.max(120, maxAllowedH - wrapperExtraH); + + if (root.prefHeight(-1) > maxRootH) { + root.setMaxHeight(maxRootH); + wrapper.applyCss(); + wrapper.layout(); + popupW = wrapper.prefWidth(-1); + popupH = wrapper.prefHeight(-1); + } + + double targetX = aScreen.getMaxX() - popupW; + + double downY = aScreen.getMaxY() + gap; + double upY = aScreen.getMinY() - gap - popupH; + + double targetY = downY; + if (targetY + popupH > content.getMaxY() - padding) targetY = upY; + + double minX = content.getMinX() + padding; + double maxX = content.getMaxX() - padding - popupW; + targetX = Math.max(minX, Math.min(targetX, maxX)); + + double minY = content.getMinY() + padding; + double maxY = content.getMaxY() - padding - popupH; + targetY = Math.max(minY, Math.min(targetY, maxY)); + + popup.show(Controllers.getStage(), targetX, targetY); + }); + + return popup; + } + + private static void exportJijList(String modName, List bundledMods) { + if (bundledMods == null || bundledMods.isEmpty()) return; + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle(i18n("mods.built_in.export.jij_info")); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.file"), "*.txt")); + fileChooser.setInitialFileName(modName + "_JIJ_INFO.txt"); + + File file = fileChooser.showSaveDialog(Controllers.getStage()); + + if (file != null) { + StringBuilder sb = new StringBuilder(); + sb.append(modName).append(System.lineSeparator()); + + for (String modPath : bundledMods) { + String fileName = modPath.contains("/") ? modPath.substring(modPath.lastIndexOf('/') + 1) : modPath; + sb.append("\t|-> ").append(fileName).append(System.lineSeparator()); + } + + Task.runAsync(() -> { + try { + Files.writeString(file.toPath(), sb.toString()); + LOG.info("Save to: " + file.getAbsolutePath()); + } catch (IOException ex) { + LOG.warning("Failed to export bundled mods list", ex); + } + }).start(); + } + } + + private static void exportAllJijList(List allMods) { + if (allMods == null || allMods.isEmpty()) return; + + StringBuilder sb = new StringBuilder(); + boolean hasData = false; + + for (ModListPageSkin.ModInfoObject item : allMods) { + LocalModFile modInfo = item.getModInfo(); + if (modInfo == null) continue; + List bundledMods = modInfo.getBundledMods(); + + if (bundledMods != null && !bundledMods.isEmpty()) { + hasData = true; + + String displayName = modInfo.getName(); + if (item.getModTranslations() != null && I18n.isUseChinese()) { + String chineseName = item.getModTranslations().getName(); + if (StringUtils.isNotBlank(chineseName)) { + displayName = displayName + " (" + chineseName + ")"; + } + } + + sb.append(displayName).append(System.lineSeparator()); + + for (String modPath : bundledMods) { + String fileName = modPath.contains("/") ? modPath.substring(modPath.lastIndexOf('/') + 1) : modPath; + sb.append("\t|-> ").append(fileName).append(System.lineSeparator()); + } + + sb.append(System.lineSeparator()); + } + } + + if (!hasData) { + FXUtils.runInFX(() -> Controllers.dialog(i18n("mods.built_in.cancelexport"))); + return; + } + + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle(i18n("mods.built_in.export.jij_info.all")); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.file"), "*.txt")); + fileChooser.setInitialFileName("ALL_JIJ_INFO.txt"); + + File file = fileChooser.showSaveDialog(Controllers.getStage()); + + if (file != null) { + Task.runAsync(() -> { + try { + Files.writeString(file.toPath(), sb.toString()); + LOG.info("Save to: " + file.getAbsolutePath()); + } catch (IOException ex) { + LOG.warning("Failed to export all bundled mods list", ex); + } + }).start(); + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java index 01840262602..91fba7238a2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java @@ -18,14 +18,20 @@ package org.jackhuang.hmcl.ui.versions; import com.jfoenix.controls.JFXPopup; +import javafx.animation.*; import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.*; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Cursor; import javafx.event.Event; import javafx.event.EventType; import javafx.scene.Node; import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; +import javafx.util.Duration; import org.jackhuang.hmcl.event.EventBus; import org.jackhuang.hmcl.event.EventPriority; import org.jackhuang.hmcl.event.RefreshedVersionsEvent; @@ -35,6 +41,9 @@ import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.WeakListenerHolder; +import org.jackhuang.hmcl.ui.animation.AnimationUtils; +import org.jackhuang.hmcl.ui.animation.ContainerAnimations; +import org.jackhuang.hmcl.ui.animation.Motion; import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; @@ -53,6 +62,7 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage private final TabHeader.Tab versionSettingsTab = new TabHeader.Tab<>("versionSettingsTab"); private final TabHeader.Tab installerListTab = new TabHeader.Tab<>("installerListTab"); private final TabHeader.Tab modListTab = new TabHeader.Tab<>("modListTab"); + private final TabHeader.Tab builtInModListTab = new TabHeader.Tab<>("builtInModListTab"); private final TabHeader.Tab worldListTab = new TabHeader.Tab<>("worldList"); private final TabHeader.Tab schematicsTab = new TabHeader.Tab<>("schematicsTab"); private final TabHeader.Tab resourcePackTab = new TabHeader.Tab<>("resourcePackTab"); @@ -74,11 +84,12 @@ public VersionPage() { versionSettingsTab.setNodeSupplier(loadVersionFor(() -> new VersionSettingsPage(false))); installerListTab.setNodeSupplier(loadVersionFor(InstallerListPage::new)); modListTab.setNodeSupplier(loadVersionFor(ModListPage::new)); + builtInModListTab.setNodeSupplier(loadVersionFor(BuiltInModListPage::new)); resourcePackTab.setNodeSupplier(loadVersionFor(ResourcepackListPage::new)); worldListTab.setNodeSupplier(loadVersionFor(WorldListPage::new)); schematicsTab.setNodeSupplier(loadVersionFor(SchematicsPage::new)); - tab = new TabHeader(transitionPane, versionSettingsTab, installerListTab, modListTab, resourcePackTab, worldListTab, schematicsTab); + tab = new TabHeader(transitionPane, versionSettingsTab, installerListTab, modListTab, builtInModListTab, resourcePackTab, worldListTab, schematicsTab); tab.select(versionSettingsTab); addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated); @@ -153,6 +164,8 @@ public void loadVersion(String version, Profile profile) { installerListTab.getNode().loadVersion(profile, version); if (modListTab.isInitialized()) modListTab.getNode().loadVersion(profile, version); + if (builtInModListTab.isInitialized()) + builtInModListTab.getNode().loadVersion(profile, version); if (resourcePackTab.isInitialized()) resourcePackTab.getNode().loadVersion(profile, version); if (worldListTab.isInitialized()) @@ -261,9 +274,123 @@ protected Skin(VersionPage control) { { AdvancedListBox sideBar = new AdvancedListBox() .addNavigationDrawerTab(control.tab, control.versionSettingsTab, i18n("settings.game"), SVG.SETTINGS, SVG.SETTINGS_FILL) - .addNavigationDrawerTab(control.tab, control.installerListTab, i18n("settings.tabs.installers"), SVG.DEPLOYED_CODE, SVG.DEPLOYED_CODE_FILL) - .addNavigationDrawerTab(control.tab, control.modListTab, i18n("mods.manage"), SVG.EXTENSION, SVG.EXTENSION_FILL) - .addNavigationDrawerTab(control.tab, control.resourcePackTab, i18n("resourcepack.manage"), SVG.TEXTURE) + .addNavigationDrawerTab(control.tab, control.installerListTab, i18n("settings.tabs.installers"), SVG.DEPLOYED_CODE, SVG.DEPLOYED_CODE_FILL); + + BooleanProperty isExpanded = new SimpleBooleanProperty(false); + + AdvancedListItem modListItem = new AdvancedListItem(); + modListItem.getStyleClass().add("navigation-drawer-item"); + modListItem.setTitle(i18n("mods.manage")); + + { + Node unselectedIcon = SVG.EXTENSION.createIcon(20); + Node selectedIcon = SVG.EXTENSION_FILL.createIcon(20); + TransitionPane leftGraphic = new TransitionPane(); + leftGraphic.setAlignment(Pos.CENTER); + FXUtils.setLimitWidth(leftGraphic, 30); + FXUtils.setLimitHeight(leftGraphic, 20); + leftGraphic.setPadding(Insets.EMPTY); + + modListItem.activeProperty().bind(control.tab.getSelectionModel().selectedItemProperty().isEqualTo(control.modListTab)); + + leftGraphic.setContent(modListItem.isActive() ? selectedIcon : unselectedIcon, ContainerAnimations.NONE); + FXUtils.onChange(modListItem.activeProperty(), active -> + leftGraphic.setContent(active ? selectedIcon : unselectedIcon, ContainerAnimations.FADE)); + modListItem.setLeftGraphic(leftGraphic); + } + + StackPane arrowContainer = new StackPane(); + FXUtils.setLimitWidth(arrowContainer, 40); + FXUtils.setLimitHeight(arrowContainer, 20); + arrowContainer.setCursor(Cursor.HAND); + + Node arrowIcon = SVG.KEYBOARD_ARROW_DOWN.createIcon(20); + arrowIcon.setRotate(isExpanded.get() ? 180 : 0); + + FXUtils.onChange(isExpanded, expanded -> { + RotateTransition rt = new RotateTransition(Duration.millis(200), arrowIcon); + rt.setToAngle(expanded ? 180 : 0); + rt.play(); + }); + + arrowContainer.getChildren().add(arrowIcon); + + arrowContainer.setOnMouseClicked(e -> { + isExpanded.set(!isExpanded.get()); + e.consume(); + }); + modListItem.setRightGraphic(arrowContainer); + + modListItem.setOnAction(e -> control.tab.select(control.modListTab)); + sideBar.add(modListItem); + + AdvancedListItem jijListItem = new AdvancedListItem(); + jijListItem.getStyleClass().add("navigation-drawer-item"); + jijListItem.setTitle(i18n("mods.built_in.mods")); + + jijListItem.setPadding(new Insets(0, 0, 0, 15)); + + jijListItem.setLeftGraphic(SVG.STACKS.createIcon(20)); + + jijListItem.activeProperty().bind(control.tab.getSelectionModel().selectedItemProperty().isEqualTo(control.builtInModListTab)); + jijListItem.setOnAction(e -> control.tab.select(control.builtInModListTab)); + + javafx.scene.shape.Rectangle jijClip = new javafx.scene.shape.Rectangle(); + jijClip.widthProperty().bind(jijListItem.widthProperty()); + jijClip.setHeight(0); + jijListItem.setClip(jijClip); + jijListItem.setMinHeight(0); + jijListItem.setMaxHeight(0); + jijListItem.setManaged(false); + sideBar.add(jijListItem); + + final Timeline[] currentAnimation = {null}; + FXUtils.onChange(isExpanded, expanded -> { + if (AnimationUtils.isAnimationEnabled()) { + // Stop any in-progress animation to prevent race conditions + if (currentAnimation[0] != null) { + currentAnimation[0].stop(); + currentAnimation[0] = null; + } + if (expanded) { + jijListItem.setManaged(true); + } + Platform.runLater(() -> { + double h = expanded ? jijListItem.prefHeight(-1) : 0; + Interpolator interpolator = Motion.EASE_IN_OUT_CUBIC_EMPHASIZED; + Timeline timeline = new Timeline( + new KeyFrame(Motion.LONG2, + new KeyValue(jijListItem.minHeightProperty(), h, interpolator), + new KeyValue(jijListItem.maxHeightProperty(), h, interpolator), + new KeyValue(jijClip.heightProperty(), h, interpolator)) + ); + if (!expanded) { + timeline.setOnFinished(e -> { + jijListItem.setManaged(false); + currentAnimation[0] = null; + }); + } else { + timeline.setOnFinished(e -> currentAnimation[0] = null); + } + currentAnimation[0] = timeline; + timeline.play(); + }); + } else { + double h = expanded ? jijListItem.prefHeight(-1) : 0; + jijListItem.setMinHeight(h); + jijListItem.setMaxHeight(h); + jijClip.setHeight(h); + jijListItem.setManaged(expanded); + } + }); + + FXUtils.onChange(control.tab.getSelectionModel().selectedItemProperty(), tab -> { + if (tab == control.builtInModListTab) { + isExpanded.set(true); + } + }); + + sideBar.addNavigationDrawerTab(control.tab, control.resourcePackTab, i18n("resourcepack.manage"), SVG.TEXTURE) .addNavigationDrawerTab(control.tab, control.worldListTab, i18n("world.manage"), SVG.PUBLIC) .addNavigationDrawerTab(control.tab, control.schematicsTab, i18n("schematics.manage"), SVG.SCHEMA, SVG.SCHEMA_FILL); VBox.setVgrow(sideBar, Priority.ALWAYS); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index ea351302312..f32d2e9c6c6 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -392,6 +392,7 @@ exception.ssl_handshake=Failed to establish SSL connection because the SSL certi exception.dns.pollution=Failed to establish an SSL connection. DNS resolution may be incorrect. Please try changing your DNS server or using a proxy service. extension.bat=Windows Batch File +extension.file=Text documents extension.png=Image File extension.ps1=Windows PowerShell Script extension.sh=Shell Script @@ -1073,6 +1074,13 @@ mods=Mods mods.add=Add mods.add.failed=Failed to add mod %s. mods.add.success=%s was successfully added. +mods.built_in=Nested +mods.built_in.cancelexport=No mod contains JiJ information. The operation will be canceled. +mods.built_in.export.jij_info=Export JiJ info +mods.built_in.export.jij_info.all=Export all JiJ info +mods.built_in.mods=Nested mods +mods.built_in.noresult=No matching results +mods.built_in.search=Search nested mods mods.add.title=Choose mod file you want to add mods.broken_dependency.title=Broken dependency mods.broken_dependency.desc=This dependency existed before, but it does not exist anymore. Try using another download source. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index f0a791bf75d..2618bbfb7c3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -384,6 +384,7 @@ exception.ssl_handshake=無法建立 SSL 連線。目前 Java 缺少相關的 SS exception.dns.pollution=無法建立 SSL 連線。可能是 DNS 解析有誤。請嘗試更換 DNS 伺服器或使用代理服務。 extension.bat=Windows 批次檔 +extension.file=文字文件 extension.png=圖片檔案 extension.ps1=PowerShell 指令碼 extension.sh=Bash 指令碼 @@ -867,6 +868,13 @@ mods=模組 mods.add=新增模組 mods.add.failed=新增模組「%s」失敗。 mods.add.success=成功新增模組「%s」。 +mods.built_in=嵌套模組數 +mods.built_in.cancelexport=無 JiJ 資訊包含。操作將取消。 +mods.built_in.export.jij_info=匯出 JiJ 資訊 +mods.built_in.export.jij_info.all=匯出所有 JiJ 資訊 +mods.built_in.mods=嵌套模組管理 +mods.built_in.noresult=沒有匹配結果 +mods.built_in.search=搜尋嵌套模組 mods.add.title=選取要新增的模組檔案 mods.broken_dependency.title=損壞的相依模組 mods.broken_dependency.desc=該相依模組曾經存在於模組儲存庫中,但現在已被刪除,請嘗試其他下載源。 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 378253ed63e..8d2da5e0264 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -387,6 +387,7 @@ exception.ssl_handshake=无法建立 SSL 连接。当前 Java 缺少相关的 SS exception.dns.pollution=无法建立 SSL 连接。可能是 DNS 解析有误。请尝试更换 DNS 服务器或使用代理服务。\n你可以点击右上角帮助按钮进行求助。 extension.bat=Windows 脚本 +extension.file=文本文档 extension.png=图片文件 extension.ps1=PowerShell 脚本 extension.sh=Bash 脚本 @@ -872,6 +873,13 @@ mods=模组 mods.add=添加模组 mods.add.failed=添加模组“%s”失败。\n如遇到问题,你可以点击右上角帮助按钮进行求助。 mods.add.success=成功添加模组 %s。 +mods.built_in=嵌套模组数 +mods.built_in.cancelexport=无 JiJ 信息包含。操作将取消。 +mods.built_in.export.jij_info=导出 JiJ 信息 +mods.built_in.export.jij_info.all=导出所有 JiJ 信息 +mods.built_in.mods=嵌套模组管理 +mods.built_in.noresult=没有匹配结果 +mods.built_in.search=搜索嵌套模组 mods.add.title=选择要添加的模组文件 mods.broken_dependency.title=损坏的前置模组 mods.broken_dependency.desc=该前置模组曾经在该模组仓库上存在过,但现在被删除了。换个下载源试试吧。 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java index cb8b341ea1e..09bb19bbb2d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java @@ -24,11 +24,7 @@ import java.io.IOException; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; -import java.util.Optional; +import java.util.*; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -50,12 +46,13 @@ public final class LocalModFile implements Comparable { private final String fileName; private final String logoPath; private final BooleanProperty activeProperty; + private final List bundledMods; public LocalModFile(ModManager modManager, LocalMod mod, Path file, String name, Description description) { - this(modManager, mod, file, name, description, "", "", "", "", ""); + this(modManager, mod, file, name, description, "", "", "", "", "", Collections.emptyList()); } - public LocalModFile(ModManager modManager, LocalMod mod, Path file, String name, Description description, String authors, String version, String gameVersion, String url, String logoPath) { + public LocalModFile(ModManager modManager, LocalMod mod, Path file, String name, Description description, String authors, String version, String gameVersion, String url, String logoPath, List bundledMods) { this.modManager = modManager; this.mod = mod; this.file = file; @@ -66,6 +63,9 @@ public LocalModFile(ModManager modManager, LocalMod mod, Path file, String name, this.gameVersion = gameVersion; this.url = url; this.logoPath = logoPath; + this.bundledMods = bundledMods == null + ? List.of() + : List.copyOf(bundledMods); activeProperty = new SimpleBooleanProperty(this, "active", !modManager.isDisabled(file)) { @Override @@ -142,6 +142,14 @@ public String getLogoPath() { return logoPath; } + public List getBundledMods() { + return bundledMods; + } + + public boolean hasBundledMods() { + return bundledMods != null && !bundledMods.isEmpty(); + } + public BooleanProperty activeProperty() { return activeProperty; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/FabricModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/FabricModMetadata.java index dd9b6c4d8aa..c604b9c5026 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/FabricModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/FabricModMetadata.java @@ -19,6 +19,7 @@ import com.google.gson.*; import com.google.gson.annotations.JsonAdapter; +import com.google.gson.annotations.SerializedName; import kala.compress.archivers.zip.ZipArchiveEntry; import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.mod.ModLoaderType; @@ -44,12 +45,13 @@ public final class FabricModMetadata { private final String icon; private final List authors; private final Map contact; + private final List jars; public FabricModMetadata() { - this("", "", "", "", "", Collections.emptyList(), Collections.emptyMap()); + this("", "", "", "", "", Collections.emptyList(), Collections.emptyMap(), Collections.emptyList()); } - public FabricModMetadata(String id, String name, String version, String icon, String description, List authors, Map contact) { + public FabricModMetadata(String id, String name, String version, String icon, String description, List authors, Map contact, List jars) { this.id = id; this.name = name; this.version = version; @@ -57,6 +59,7 @@ public FabricModMetadata(String id, String name, String version, String icon, St this.description = description; this.authors = authors; this.contact = contact; + this.jars = jars; } public static LocalModFile fromFile(ModManager modManager, Path modFile, ZipFileTree tree) throws IOException, JsonParseException { @@ -65,8 +68,12 @@ public static LocalModFile fromFile(ModManager modManager, Path modFile, ZipFile throw new IOException("File " + modFile + " is not a Fabric mod."); FabricModMetadata metadata = JsonUtils.fromNonNullJsonFully(tree.getInputStream(mcmod), FabricModMetadata.class); String authors = metadata.authors == null ? "" : metadata.authors.stream().map(author -> author.name).collect(Collectors.joining(", ")); + + List bundledMods = metadata.jars != null ? + metadata.jars.stream().map(jar -> jar.file).toList() : + Collections.emptyList(); return new LocalModFile(modManager, modManager.getLocalMod(metadata.id, ModLoaderType.FABRIC), modFile, metadata.name, new LocalModFile.Description(metadata.description), - authors, metadata.version, "", metadata.contact != null ? metadata.contact.getOrDefault("homepage", "") : "", metadata.icon); + authors, metadata.version, "", metadata.contact != null ? metadata.contact.getOrDefault("homepage", "") : "", metadata.icon, bundledMods); } @JsonAdapter(FabricModAuthorSerializer.class) @@ -93,4 +100,17 @@ public JsonElement serialize(FabricModAuthor src, Type typeOfSrc, JsonSerializat return src == null ? JsonNull.INSTANCE : new JsonPrimitive(src.name); } } + + public static final class FabricNestedJar { + @SerializedName("file") + private final String file; + + public FabricNestedJar() { + this(""); + } + + public FabricNestedJar(String file) { + this.file = file; + } + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/ForgeNewModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/ForgeNewModMetadata.java index 5e14f90262a..2ea36e46423 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/ForgeNewModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/ForgeNewModMetadata.java @@ -224,12 +224,28 @@ private static LocalModFile fromFile0( } } + List bundledMods = new ArrayList<>(); + ZipArchiveEntry jijEntry = tree.getEntry("META-INF/jarjar/metadata.json"); + if (jijEntry != null) { + try { + JarInJarMetadata jijMetadata = JsonUtils.fromJsonFully(tree.getInputStream(jijEntry), JarInJarMetadata.class); + if (jijMetadata != null && jijMetadata.jars != null) { + jijMetadata.jars.stream() + .map(jar -> jar.path) + .forEach(bundledMods::add); + } + } catch (Exception e) { + LOG.warning("Failed to parse JarInJar metadata for " + modFile, e); + } + } + ModLoaderType type = analyzeLoader(toml, mod.getModId(), modLoaderType); return new LocalModFile(modManager, modManager.getLocalMod(mod.getModId(), type), modFile, mod.getDisplayName(), new LocalModFile.Description(mod.getDescription()), mod.getAuthors(), jarVersion == null ? mod.getVersion() : mod.getVersion().replace("${file.jarVersion}", jarVersion), "", mod.getDisplayURL(), - metadata.getLogoFile()); + metadata.getLogoFile(), + bundledMods); } private static LocalModFile fromEmbeddedMod(ModManager modManager, Path modFile, ZipFileTree tree, ModLoaderType modLoaderType) throws IOException { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/ForgeOldModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/ForgeOldModMetadata.java index 53c6b09252d..6830df1aa8a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/ForgeOldModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/ForgeOldModMetadata.java @@ -33,6 +33,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.List; +import java.util.Collections; import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf; @@ -159,6 +160,6 @@ else if (firstToken == JsonToken.BEGIN_OBJECT) { return new LocalModFile(modManager, modManager.getLocalMod(metadata.getModId(), ModLoaderType.FORGE), modFile, metadata.getName(), new LocalModFile.Description(metadata.getDescription()), authors, metadata.getVersion(), metadata.getGameVersion(), StringUtils.isBlank(metadata.getUrl()) ? metadata.getUpdateUrl() : metadata.url, - metadata.getLogoFile()); + metadata.getLogoFile(), Collections.emptyList()); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/LiteModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/LiteModMetadata.java index 8a59a711be4..5238959afc3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/LiteModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/LiteModMetadata.java @@ -28,6 +28,7 @@ import java.io.IOException; import java.nio.file.Path; +import java.util.Collections; /** * @@ -117,7 +118,7 @@ public static LocalModFile fromFile(ModManager modManager, Path modFile, ZipFile if (metadata == null) throw new IOException("Mod " + modFile + " `litemod.json` is malformed."); return new LocalModFile(modManager, modManager.getLocalMod(metadata.getName(), ModLoaderType.LITE_LOADER), modFile, metadata.getName(), new LocalModFile.Description(metadata.getDescription()), metadata.getAuthor(), - metadata.getVersion(), metadata.getGameVersion(), metadata.getUpdateURI(), ""); + metadata.getVersion(), metadata.getGameVersion(), metadata.getUpdateURI(), "", Collections.emptyList()); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java index 024a0102ee2..2bf99cef722 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java @@ -36,6 +36,7 @@ import java.lang.reflect.Type; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -194,6 +195,6 @@ public static LocalModFile fromFile(ModManager modManager, Path modFile, ZipFile modFile, FileUtils.getNameWithoutExtension(modFile), metadata.pack.description, - "", "", "", "", ""); + "", "", "", "", "", Collections.emptyList()); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/QuiltModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/QuiltModMetadata.java index b0e9c5a4254..d5acec0f980 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/QuiltModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/QuiltModMetadata.java @@ -29,6 +29,8 @@ import java.io.IOException; import java.nio.file.Path; +import java.util.Collections; +import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -51,14 +53,21 @@ public Metadata(String name, String description, JsonObject contributors, String } } + private static final class NestedJar { + private final String file; + public NestedJar(String file) { this.file = file; } + } + private final String id; private final String version; private final Metadata metadata; + private List jars = List.of(); - public QuiltLoader(String id, String version, Metadata metadata) { + public QuiltLoader(String id, String version, Metadata metadata, List jars) { this.id = id; this.version = version; this.metadata = metadata; + this.jars = jars; } } @@ -81,17 +90,31 @@ public static LocalModFile fromFile(ModManager modManager, Path modFile, ZipFile throw new IOException("File " + modFile + " is not a supported Quilt mod."); } + List bundledMods = root.quilt_loader.jars != null ? + root.quilt_loader.jars.stream().map(jar -> jar.file).toList() : + Collections.emptyList(); + + String authors = root.quilt_loader.metadata.contributors != null + ? root.quilt_loader.metadata.contributors.entrySet().stream() + .map(entry -> String.format("%s (%s)", entry.getKey(), entry.getValue().getAsJsonPrimitive().getAsString())) + .collect(Collectors.joining(", ")) + : ""; + String homepage = root.quilt_loader.metadata.contact != null + ? Optional.ofNullable(root.quilt_loader.metadata.contact.get("homepage")) + .map(jsonElement -> jsonElement.getAsJsonPrimitive().getAsString()).orElse("") + : ""; + return new LocalModFile( modManager, modManager.getLocalMod(root.quilt_loader.id, ModLoaderType.QUILT), modFile, root.quilt_loader.metadata.name, new LocalModFile.Description(root.quilt_loader.metadata.description), - root.quilt_loader.metadata.contributors.entrySet().stream().map(entry -> String.format("%s (%s)", entry.getKey(), entry.getValue().getAsJsonPrimitive().getAsString())).collect(Collectors.joining(", ")), + authors, root.quilt_loader.version, "", - Optional.ofNullable(root.quilt_loader.metadata.contact.get("homepage")).map(jsonElement -> jsonElement.getAsJsonPrimitive().getAsString()).orElse(""), - root.quilt_loader.metadata.icon - ); + homepage, + root.quilt_loader.metadata.icon, + bundledMods); } }