创建世界管理页面 (#3991)

This commit is contained in:
Glavo 2025-06-13 14:28:51 +08:00 committed by GitHub
parent b72056f7fe
commit 0616c37de7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 562 additions and 242 deletions

View File

@ -58,12 +58,14 @@ public enum SVG {
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"),
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"),
FEEDBACK("M12 15Q12.425 15 12.7125 14.7125T13 14Q13 13.575 12.7125 13.2875T12 13Q11.575 13 11.2875 13.2875T11 14Q11 14.425 11.2875 14.7125T12 15ZM11 11H13V5H11V11ZM2 22V4Q2 3.175 2.5875 2.5875T4 2H20Q20.825 2 21.4125 2.5875T22 4V16Q22 16.825 21.4125 17.4125T20 18H6L2 22ZM5.15 16H20V4H4V17.125L5.15 16ZM4 16V4 16Z"),
FOLDER("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8V18Q22 18.825 21.4125 19.4125T20 20H4ZM4 18H20V8H11.175L9.175 6H4V18ZM4 18V6 18Z"),
FOLDER_COPY("M3 21Q2.175 21 1.5875 20.4125T1 19V6H3V19H20V21H3ZM7 17Q6.175 17 5.5875 16.4125T5 15V4Q5 3.175 5.5875 2.5875T7 2H12L14 4H21Q21.825 4 22.4125 4.5875T23 6V15Q23 15.825 22.4125 16.4125T21 17H7ZM7 15H21V6H13.175L11.175 4H7V15ZM7 15V4 15Z"),
FOLDER_OPEN("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8H11.175L9.175 6H4V18L6.4 10H23.5L20.925 18.575Q20.725 19.225 20.1875 19.6125T19 20H4ZM6.1 18H19L20.8 12H7.9L6.1 18ZM6.1 18 7.9 12 6.1 18ZM4 8V6 8Z"),
FORMAT_LIST_BULLETED("M9 19V17H21V19H9ZM9 13V11H21V13H9ZM9 7V5H21V7H9ZM5 20Q4.175 20 3.5875 19.4125T3 18Q3 17.175 3.5875 16.5875T5 16Q5.825 16 6.4125 16.5875T7 18Q7 18.825 6.4125 19.4125T5 20ZM5 14Q4.175 14 3.5875 13.4125T3 12Q3 11.175 3.5875 10.5875T5 10Q5.825 10 6.4125 10.5875T7 12Q7 12.825 6.4125 13.4125T5 14ZM5 8Q4.175 8 3.5875 7.4125T3 6Q3 5.175 3.5875 4.5875T5 4Q5.825 4 6.4125 4.5875T7 6Q7 6.825 6.4125 7.4125T5 8Z"),
FORT("M1 21V17l2-2V9L1 7V3H3V5H5V3H7V5H9V3h2V7L9 9v1h6V9L13 7V3h2V5h2V3h2V5h2V3h2V7L21 9v6l2 2v4H14V18q0-.825-.5875-1.4125T12 16q-.825 0-1.4125.5875T10 18v3H1Zm2-2H8V18q0-1.65 1.175-2.825T12 14q1.65 0 2.825 1.175T16 18v1h5V17.825l-2-2V8.175L20.175 7h-4.35L17 8.175V12H7V8.175L8.175 7H3.825L5 8.175v7.65l-2 2V19Zm9-6Z"),
FOR_YOU("M12 12Q14.025 12 16.225 11.5875T20 10.5V20.5Q18.5 21.175 16.35 21.5875T12 22Q9.8 22 7.65 21.5875T4 20.5V10.5Q5.575 11.175 7.775 11.5875T12 12ZM18 19V13.25Q16.75 13.6 15.1125 13.8T12 14Q10.525 14 8.8875 13.8T6 13.25V19Q7.25 19.45 8.875 19.725T12 20Q13.5 20 15.125 19.725T18 19ZM12 2Q13.65 2 14.825 3.175T16 6Q16 7.65 14.825 8.825T12 10Q10.35 10 9.175 8.825T8 6Q8 4.35 9.175 3.175T12 2ZM12 8Q12.825 8 13.4125 7.4125T14 6Q14 5.175 13.4125 4.5875T12 4Q11.175 4 10.5875 4.5875T10 6Q10 6.825 10.5875 7.4125T12 8ZM12 6ZM12 16.625Z"),
GAMEPAD("M12 7.65ZM16.35 12ZM7.65 12ZM12 16.35ZM12 10.5 9 7.5V2H15V7.5L12 10.5ZM16.5 15 13.5 12 16.5 9H22V15H16.5ZM2 15V9H7.5L10.5 12 7.5 15H2ZM9 22V16.5L12 13.5 15 16.5V22H9ZM12 7.65 13 6.65V4H11V6.65L12 7.65ZM4 13H6.65L7.65 12 6.65 11H4V13ZM11 20H13V17.35L12 16.35 11 17.35V20ZM17.35 13H20V11H17.35L16.35 12 17.35 13Z"),
GLOBE_BOOK("M3.075 13Q3.05 12.75 3.0375 12.5T3.025 12Q3.025 10.125 3.725 8.4875T5.65 5.6375Q6.875 4.425 8.5 3.7125T12 3Q13.875 3 15.5125 3.7125T18.3625 5.6375Q19.575 6.85 20.2875 8.4875T21 12Q21 12.25 20.9875 12.5T20.95 13H18.925Q18.975 12.75 18.9875 12.5T19 12Q19 11.75 18.9875 11.5T18.925 11H15.975Q16 11.25 16 11.5V12.5Q16 12.75 15.975 13H14V12.175Q14 11.875 13.9875 11.575T13.95 11H10.075Q10.05 11.275 10.0375 11.575T10.025 12.175V13H8.05Q8.025 12.75 8.025 12.5V11.5Q8.025 11.25 8.05 11H5.1Q5.05 11.25 5.0375 11.5T5.025 12Q5.025 12.25 5.0375 12.5T5.1 13H3.075ZM5.7 9H8.275Q8.475 7.925 8.775 7.0625T9.425 5.5Q8.225 5.95 7.25 6.8625T5.7 9ZM10.35 9H13.65Q13.4 7.925 13.025 6.9T12 5Q11.35 5.875 10.9625 6.9T10.35 9ZM15.75 9H18.325Q17.75 7.775 16.7625 6.8625T14.575 5.5Q14.925 6.25 15.2375 7.0875T15.75 9ZM11 21V20Q11 18.75 10.125 17.875T8 17H2V15H8Q9.2 15 10.2375 15.525T12 17Q12.725 16.05 13.7625 15.525T16 15H22V17H16Q14.75 17 13.875 17.875T13 20V21H11Z"),
@ -73,9 +75,11 @@ public enum SVG {
INFO("M11 17H13V11H11V17ZM12 9Q12.425 9 12.7125 8.7125T13 8Q13 7.575 12.7125 7.2875T12 7Q11.575 7 11.2875 7.2875T11 8Q11 8.425 11.2875 8.7125T12 9ZM12 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"),
KEYBOARD_ARROW_DOWN("M12 15.4 6 9.4 7.4 8 12 12.6 16.6 8 18 9.4 12 15.4Z"),
KEYBOARD_ARROW_UP("M12 10.8 7.4 15.4 6 14 12 8 18 14 16.6 15.4 12 10.8Z"),
LANDSCAPE("M1 18l6-8 4.5 6H19L14 9.35l-2.5 3.3L10.25 11 14 6l9 12H1Zm13.025-2ZM5 16H9L7 13.325 5 16ZH9 5Z"),
LIST("M7 9V7H21V9H7ZM7 13V11H21V13H7ZM7 17V15H21V17H7ZM4 9Q3.575 9 3.2875 8.7125T3 8Q3 7.575 3.2875 7.2875T4 7Q4.425 7 4.7125 7.2875T5 8Q5 8.425 4.7125 8.7125T4 9ZM4 13Q3.575 13 3.2875 12.7125T3 12Q3 11.575 3.2875 11.2875T4 11Q4.425 11 4.7125 11.2875T5 12Q5 12.425 4.7125 12.7125T4 13ZM4 17Q3.575 17 3.2875 16.7125T3 16Q3 15.575 3.2875 15.2875T4 15Q4.425 15 4.7125 15.2875T5 16Q5 16.425 4.7125 16.7125T4 17Z"),
LISTS("M2 20V16H6V20H2ZM8 20V16H22V20H8ZM2 14V10H6V14H2ZM8 14V10H22V14H8ZM2 8V4H6V8H2ZM8 8V4H22V8H8Z"),
LOCAL_CAFE("M4 21V19H20V21H4ZM8 17Q6.35 17 5.175 15.825T4 13V3H20Q20.825 3 21.4125 3.5875T22 5V8Q22 8.825 21.4125 9.4125T20 10H18V13Q18 14.65 16.825 15.825T14 17H8ZM8 15H14Q14.825 15 15.4125 14.4125T16 13V5H6V13Q6 13.825 6.5875 14.4125T8 15ZM18 8H20V5H18V8ZM8 15H6 16 8Z"),
LOCATION_CITY("M3 21V7H9V5l3-3 3 3v6h6V21H3Zm2-2H7V17H5v2Zm0-4H7V13H5v2Zm0-4H7V9H5v2Zm6 8h2V17H11v2Zm0-4h2V13H11v2Zm0-4h2V9H11v2Zm0-4h2V5H11V7Zm6 12h2V17H17v2Zm0-4h2V13H17v2Z"),
MENU("M3 18V16H21V18H3ZM3 13V11H21V13H3ZM3 8V6H21V8H3Z"),
MICROSOFT("M4 20H22v2H4V13H20v7h2V4H20v7H4V4h7V20h2V4h9V2H2V22H4"), // Not Material
MINIMIZE("M6 21V19H18V21H6Z"),
@ -104,6 +108,8 @@ public enum SVG {
TRIP("M4 21Q3.175 21 2.5875 20.4125T2 19V8Q2 7.175 2.5875 6.5875T4 6H8V4Q8 3.175 8.5875 2.5875T10 2H14Q14.825 2 15.4125 2.5875T16 4V6H20Q20.825 6 21.4125 6.5875T22 8V19Q22 19.825 21.4125 20.4125T20 21H4ZM10 6H14V4H10V6ZM6 8H4V19H6V8ZM16 19V8H8V19H16ZM18 8V19H20V8H18ZM12 13.5Z"),
TUNE("M11 21V15H13V17H21V19H13V21H11ZM3 19V17H9V19H3ZM7 15V13H3V11H7V9H9V15H7ZM11 13V11H21V13H11ZM15 9V3H17V5H21V7H17V9H15ZM3 7V5H13V7H3Z"),
UPDATE("M12 21Q10.125 21 8.4875 20.2875T5.6375 18.3625Q4.425 17.15 3.7125 15.5125T3 12Q3 10.125 3.7125 8.4875T5.6375 5.6375Q6.85 4.425 8.4875 3.7125T12 3Q14.05 3 15.8875 3.875T19 6.35V4H21V10H15V8H17.75Q16.725 6.6 15.225 5.8T12 5Q9.075 5 7.0375 7.0375T5 12Q5 14.925 7.0375 16.9625T12 19Q14.625 19 16.5875 17.3T18.9 13H20.95Q20.575 16.425 18.0125 18.7125T12 21ZM14.8 16.2 11 12.4V7H13V11.6L16.2 14.8 14.8 16.2Z"),
VISIBILITY("M12 16q1.875 0 3.1875-1.3125T16.5 11.5 15.1875 8.3125 12 7 8.8125 8.3125 7.5 11.5t1.3125 3.1875T12 16Zm0-1.8q-1.125 0-1.9125-.7875T9.3 11.5t.7875-1.9125T12 8.8q1.125 0 1.9125.7875T14.7 11.5q0 1.125-.7875 1.9125T12 14.2ZM12 19q-3.65 0-6.65-2.0375T1 11.5Q2.35 8.075 5.35 6.0375T12 4q3.65 0 6.65 2.0375T23 11.5q-1.35 3.425-4.35 5.4625T12 19Zm0-7.5ZM12 17q2.825 0 5.1875-1.4875T20.8 11.5q-1.25-2.525-3.6125-4.0125T12 6 6.8125 7.4875 3.2 11.5q1.25 2.525 3.6125 4.0125T12 17Z"),
VISIBILITY_OFF("M16.1 13.3l-1.45-1.45q.225-1.175-.675-2.2t-2.325-.8L10.2 7.4q.425-.2.8625-.3T12 7q1.875 0 3.1875 1.3125T16.5 11.5q0 .5-.1.9375t-.3.8625Zm3.2 3.15-1.45-1.4q.95-.725 1.6875-1.5875T20.8 11.5q-1.25-2.525-3.5875-4.0125T12 6q-.725 0-1.425.1T9.2 6.4L7.65 4.85q1.025-.425 2.1-.6375T12 4q3.775 0 6.725 2.0875T23 11.5q-.575 1.475-1.5125 2.7375T19.3 16.45Zm.5 6.15-4.2-4.15q-.875.275-1.7625.4125T12 19q-3.775 0-6.725-2.0875T1 11.5q.525-1.325 1.325-2.4625T4.15 7L1.4 4.2 2.8 2.8 21.2 21.2l-1.4 1.4ZM5.55 8.4q-.725.65-1.325 1.425T3.2 11.5q1.25 2.525 3.5875 4.0125T12 17q.5 0 .975-.0625T13.95 16.8l-.9-.95q-.275.075-.525.1125T12 16q-1.875 0-3.1875-1.3125T7.5 11.5q0-.275.0375-.525T7.65 10.45L5.55 8.4Zm7.975 2.325ZM9.75 12.6Z"),
WARNING("M1 21 12 2 23 21H1ZM4.45 19H19.55L12 6 4.45 19ZM12 18Q12.425 18 12.7125 17.7125T13 17Q13 16.575 12.7125 16.2875T12 16Q11.575 16 11.2875 16.2875T11 17Q11 17.425 11.2875 17.7125T12 18ZM11 15H13V10H11V15ZM12 12.5Z"),
WB_SUNNY("M11 4V1H13V4H11ZM11 23V20H13V23H11ZM20 13V11H23V13H20ZM1 13V11H4V13H1ZM18.7 6.7 17.3 5.3 19.05 3.5 20.5 4.95 18.7 6.7ZM4.95 20.5 3.5 19.05 5.3 17.3 6.7 18.7 4.95 20.5ZM19.05 20.5 17.3 18.7 18.7 17.3 20.5 19.05 19.05 20.5ZM5.3 6.7 3.5 4.95 4.95 3.5 6.7 5.3 5.3 6.7ZM12 18Q9.5 18 7.75 16.25T6 12Q6 9.5 7.75 7.75T12 6Q14.5 6 16.25 7.75T18 12Q18 14.5 16.25 16.25T12 18ZM12 16Q13.675 16 14.8375 14.8375T16 12Q16 10.325 14.8375 9.1625T12 8Q10.325 8 9.1625 9.1625T8 12Q8 13.675 9.1625 14.8375T12 16ZM12 12Z"),
;
@ -127,11 +133,10 @@ public enum SVG {
return pane;
}
Group svg = new Group(path);
double scale = size / 24;
svg.setScaleX(scale);
svg.setScaleY(scale);
return svg;
path.setScaleX(scale);
path.setScaleY(scale);
return new Group(path);
}
public Node createIcon(ObservableValue<? extends Paint> fill, double size) {

View File

@ -311,8 +311,9 @@ public class DecoratorController {
if (navigator.getCurrentPage() instanceof DecoratorPage) {
DecoratorPage page = (DecoratorPage) navigator.getCurrentPage();
// FIXME: Get WorldPage working first, and revisit this later
page.closePage();
if (page.isPageCloseable()) {
page.closePage();
return;
}
}

View File

@ -17,8 +17,6 @@
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.ObservableList;
import javafx.scene.control.Skin;
import javafx.stage.FileChooser;
@ -28,7 +26,6 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.ListPageBase;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
@ -41,24 +38,17 @@ import java.util.Objects;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class DatapackListPage extends ListPageBase<DatapackListPageSkin.DatapackInfoObject> implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.DatapackInfoObject> {
private final Path worldDir;
private final Datapack datapack;
// Strongly referencing items, preventing GC collection
@SuppressWarnings("FieldCanBeLocal")
private final ObservableList<DatapackListPageSkin.DatapackInfoObject> items;
public DatapackListPage(String worldName, Path worldDir) {
this.worldDir = worldDir;
state.set(State.fromTitle(i18n("datapack.title", worldName)));
public DatapackListPage(WorldManagePage worldManagePage) {
this.worldDir = worldManagePage.getWorld().getFile();
datapack = new Datapack(worldDir.resolve("datapacks"));
datapack.loadFromDir();
setItems(items = MappedObservableList.create(datapack.getInfo(), DatapackListPageSkin.DatapackInfoObject::new));
setItems(MappedObservableList.create(datapack.getInfo(), DatapackListPageSkin.DatapackInfoObject::new));
FXUtils.applyDragListener(this, it -> Objects.equals("zip", FileUtils.getExtension(it)),
mods -> mods.forEach(this::installSingleDatapack), this::refresh);
@ -91,11 +81,6 @@ public class DatapackListPage extends ListPageBase<DatapackListPageSkin.Datapack
.start();
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
public void add() {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("datapack.choose_datapack"));

View File

@ -20,7 +20,6 @@ package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.geometry.Insets;
@ -29,87 +28,57 @@ import javafx.scene.control.SelectionMode;
import javafx.scene.control.SkinBase;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.mod.Datapack;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.FloatListCell;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.StringUtils;
import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
class DatapackListPageSkin extends SkinBase<DatapackListPage> {
final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
DatapackListPageSkin(DatapackListPage skinnable) {
super(skinnable);
BorderPane root = new BorderPane();
root.getStyleClass().add("content-background");
StackPane pane = new StackPane();
pane.setPadding(new Insets(10));
pane.getStyleClass().addAll("notice-pane");
ComponentList root = new ComponentList();
root.getStyleClass().add("no-padding");
JFXListView<DatapackInfoObject> listView = new JFXListView<>();
{
HBox toolbar = new HBox();
toolbar.getStyleClass().add("jfx-tool-bar-second");
JFXDepthManager.setDepth(toolbar, 1);
toolbar.setPickOnBounds(false);
toolbar.getChildren().add(createToolbarButton(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh));
toolbar.getChildren().add(createToolbarButton(i18n("datapack.add"), SVG.ADD, skinnable::add));
toolbar.getChildren().add(createToolbarButton(i18n("button.remove"), SVG.DELETE, () -> {
toolbar.getChildren().add(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh));
toolbar.getChildren().add(createToolbarButton2(i18n("datapack.add"), SVG.ADD, skinnable::add));
toolbar.getChildren().add(createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> {
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
skinnable.removeSelected(listView.getSelectionModel().getSelectedItems());
}, null);
}));
toolbar.getChildren().add(createToolbarButton(i18n("mods.enable"), SVG.CHECK, () ->
toolbar.getChildren().add(createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () ->
skinnable.enableSelected(listView.getSelectionModel().getSelectedItems())));
toolbar.getChildren().add(createToolbarButton(i18n("mods.disable"), SVG.CLOSE, () ->
toolbar.getChildren().add(createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () ->
skinnable.disableSelected(listView.getSelectionModel().getSelectedItems())));
root.setTop(toolbar);
root.getContent().add(toolbar);
}
{
SpinnerPane center = new SpinnerPane();
ComponentList.setVgrow(center, Priority.ALWAYS);
center.getStyleClass().add("large-spinner-pane");
center.loadingProperty().bind(skinnable.loadingProperty());
listView.setCellFactory(x -> new FloatListCell<DatapackInfoObject>(listView) {
JFXCheckBox checkBox = new JFXCheckBox();
TwoLineListItem content = new TwoLineListItem();
BooleanProperty booleanProperty;
{
Region clippedContainer = (Region)listView.lookup(".clipped-container");
HBox container = new HBox(8);
container.setPadding(new Insets(0, 0, 0, 6));
container.setAlignment(Pos.CENTER_LEFT);
pane.getChildren().add(container);
pane.setPadding(new Insets(8, 8, 8, 0));
if (clippedContainer != null) {
maxWidthProperty().bind(clippedContainer.widthProperty());
prefWidthProperty().bind(clippedContainer.widthProperty());
minWidthProperty().bind(clippedContainer.widthProperty());
}
container.getChildren().setAll(checkBox, content);
}
@Override
protected void updateControl(DatapackInfoObject dataItem, boolean empty) {
if (empty) return;
content.setTitle(dataItem.getTitle());
content.setSubtitle(dataItem.getSubtitle());
if (booleanProperty != null) {
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
}
checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.active);
}
});
Holder<Object> lastCell = new Holder<>();
listView.setCellFactory(x -> new DatapackInfoListCell(listView, lastCell));
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
Bindings.bindContent(listView.getItems(), skinnable.getItems());
@ -117,10 +86,11 @@ class DatapackListPageSkin extends SkinBase<DatapackListPage> {
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
center.setContent(listView);
root.setCenter(center);
root.getContent().add(center);
}
getChildren().setAll(root);
pane.getChildren().setAll(root);
getChildren().setAll(pane);
}
static class DatapackInfoObject extends RecursiveTreeObject<DatapackInfoObject> {
@ -144,4 +114,36 @@ class DatapackListPageSkin extends SkinBase<DatapackListPage> {
return packInfo;
}
}
private static final class DatapackInfoListCell extends MDListCell<DatapackInfoObject> {
final JFXCheckBox checkBox = new JFXCheckBox();
final TwoLineListItem content = new TwoLineListItem();
BooleanProperty booleanProperty;
DatapackInfoListCell(JFXListView<DatapackInfoObject> listView, Holder<Object> lastCell) {
super(listView, lastCell);
HBox container = new HBox(8);
container.setPickOnBounds(false);
container.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(content, Priority.ALWAYS);
content.setMouseTransparent(true);
setSelectable();
StackPane.setMargin(container, new Insets(8));
container.getChildren().setAll(checkBox, content);
getContainer().getChildren().setAll(container);
}
@Override
protected void updateControl(DatapackInfoObject dataItem, boolean empty) {
if (empty) return;
content.setTitle(dataItem.getTitle());
content.setSubtitle(dataItem.getSubtitle());
if (booleanProperty != null) {
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
}
checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.active);
}
}
}

View File

@ -37,15 +37,17 @@ public final class WorldBackupTask extends Task<Path> {
private final World world;
private final Path backupsDir;
private final boolean needLock;
public WorldBackupTask(World world, Path backupsDir) {
public WorldBackupTask(World world, Path backupsDir, boolean needLock) {
this.world = world;
this.backupsDir = backupsDir;
this.needLock = needLock;
}
@Override
public void execute() throws Exception {
try (FileChannel lockChannel = world.lock()) {
try (FileChannel lockChannel = needLock ? world.lock() : null) {
Files.createDirectories(backupsDir);
String time = LocalDateTime.now().format(WorldBackupsPage.TIME_FORMATTER);
String baseName = time + "_" + world.getFileName();

View File

@ -18,9 +18,6 @@
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@ -41,7 +38,6 @@ import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jetbrains.annotations.NotNull;
@ -65,33 +61,24 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.BackupInfo> implements DecoratorPage {
public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.BackupInfo> {
static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
private final World world;
private final Path backupsDir;
private final boolean isReadOnly;
private final Pattern backupFileNamePattern;
private final ObjectProperty<State> state = new SimpleObjectProperty<>();
public WorldBackupsPage(World world, Path backupsDir) {
this.backupsDir = backupsDir;
this.world = world;
public WorldBackupsPage(WorldManagePage worldManagePage) {
this.world = worldManagePage.getWorld();
this.backupsDir = worldManagePage.getBackupsDir();
this.isReadOnly = worldManagePage.isReadOnly();
this.backupFileNamePattern = Pattern.compile("(?<datetime>[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2})_" + Pattern.quote(world.getFileName()) + "( (?<count>[0-9]+))?\\.zip");
this.state.set(State.fromTitle(i18n("world.backup.title", world.getWorldName())));
loadBackups();
refresh();
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state;
}
@Override
public void refresh() {
loadBackups();
}
private void loadBackups() {
setLoading(true);
Task.supplyAsync(() -> {
if (Files.isDirectory(backupsDir)) {
@ -140,7 +127,7 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
}
void createBackup() {
Controllers.taskDialog(new WorldBackupTask(world, backupsDir).setName(i18n("world.backup.processing")).thenApplyAsync(path -> {
Controllers.taskDialog(new WorldBackupTask(world, backupsDir, false).setName(i18n("world.backup.processing")).thenApplyAsync(path -> {
Matcher matcher = backupFileNamePattern.matcher(path.getFileName().toString());
if (!matcher.matches()) {
throw new AssertionError("Wrong backup file name" + path);
@ -176,7 +163,13 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
@Override
protected List<Node> initializeToolbar(WorldBackupsPage skinnable) {
return Arrays.asList(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), createToolbarButton2(i18n("world.backup.create.new_one"), SVG.ARCHIVE, skinnable::createBackup));
JFXButton createBackup = createToolbarButton2(i18n("world.backup.create.new_one"), SVG.ARCHIVE, skinnable::createBackup);
createBackup.setDisable(isReadOnly);
return Arrays.asList(
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
createBackup
);
}
}

View File

@ -20,25 +20,27 @@ package org.jackhuang.hmcl.ui.versions;
import com.github.steveice10.opennbt.tag.builtin.*;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.effect.BoxBlur;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.text.DecimalFormat;
import java.time.Instant;
@ -52,38 +54,14 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
* @author Glavo
*/
public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
public final class WorldInfoPage extends SpinnerPane {
private final WorldManagePage worldManagePage;
private final World world;
private CompoundTag levelDat;
private final ObjectProperty<State> stateProperty;
private FileChannel sessionLockChannel;
@Override
public boolean back() {
closePage();
return true;
}
@Override
public void closePage() {
if (sessionLockChannel != null) {
try {
sessionLockChannel.close();
} catch (IOException e) {
LOG.warning("Failed to close session lock channel", e);
}
sessionLockChannel = null;
}
}
public WorldInfoPage(World world) {
this.world = world;
this.stateProperty = new SimpleObjectProperty<>(State.fromTitle(i18n("world.info.title", world.getWorldName())));
this.getStyleClass().add("gray-background");
public WorldInfoPage(WorldManagePage worldManagePage) {
this.worldManagePage = worldManagePage;
this.world = worldManagePage.getWorld();
this.setLoading(true);
Task.supplyAsync(this::loadWorldInfo)
@ -103,18 +81,9 @@ public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
if (!Files.isDirectory(world.getFile()))
throw new IOException("Not a valid world directory");
try {
sessionLockChannel = world.lock();
} catch (IOException ignored) {
}
return world.readLevelDat();
}
private boolean isReadOnly() {
return sessionLockChannel == null;
}
private void updateControls() {
CompoundTag dataTag = levelDat.get("Data");
CompoundTag worldGenSettings = dataTag.get("WorldGenSettings");
@ -161,9 +130,22 @@ public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
BorderPane randomSeedPane = new BorderPane();
{
HBox left = new HBox(8);
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
left.setAlignment(Pos.CENTER_LEFT);
randomSeedPane.setLeft(left);
Label label = new Label(i18n("world.info.random_seed"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
randomSeedPane.setLeft(label);
SimpleBooleanProperty visibility = new SimpleBooleanProperty();
StackPane visibilityButton = new StackPane();
visibilityButton.setCursor(Cursor.HAND);
FXUtils.setLimitWidth(visibilityButton, 12);
FXUtils.setLimitHeight(visibilityButton, 12);
FXUtils.onClicked(visibilityButton, () -> visibility.set(!visibility.get()));
left.getChildren().setAll(label, visibilityButton);
Label randomSeedLabel = new Label();
FXUtils.copyOnDoubleClick(randomSeedLabel);
@ -174,6 +156,14 @@ public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
if (tag instanceof LongTag) {
randomSeedLabel.setText(tag.getValue().toString());
}
BoxBlur blur = new BoxBlur();
blur.setIterations(3);
FXUtils.onChangeAndOperate(visibility, isVisibility -> {
SVG icon = isVisibility ? SVG.VISIBILITY : SVG.VISIBILITY_OFF;
visibilityButton.getChildren().setAll(icon.createIcon(Theme.blackFill(), 12));
randomSeedLabel.setEffect(isVisibility ? null : blur);
});
}
BorderPane lastPlayedPane = new BorderPane();
@ -210,7 +200,7 @@ public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
OptionToggleButton allowCheatsButton = new OptionToggleButton();
{
allowCheatsButton.setTitle(i18n("world.info.allow_cheats"));
allowCheatsButton.setDisable(isReadOnly());
allowCheatsButton.setDisable(worldManagePage.isReadOnly());
Tag tag = dataTag.get("allowCommands");
if (tag instanceof ByteTag) {
@ -233,7 +223,7 @@ public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
OptionToggleButton generateFeaturesButton = new OptionToggleButton();
{
generateFeaturesButton.setTitle(i18n("world.info.generate_features"));
generateFeaturesButton.setDisable(isReadOnly());
generateFeaturesButton.setDisable(worldManagePage.isReadOnly());
Tag tag = worldGenSettings != null ? worldGenSettings.get("generate_features") : dataTag.get("MapFeatures");
if (tag instanceof ByteTag) {
@ -260,7 +250,7 @@ public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
difficultyPane.setLeft(label);
JFXComboBox<Difficulty> difficultyBox = new JFXComboBox<>(Difficulty.items);
difficultyBox.setDisable(isReadOnly());
difficultyBox.setDisable(worldManagePage.isReadOnly());
BorderPane.setAlignment(difficultyBox, Pos.CENTER_RIGHT);
difficultyPane.setRight(difficultyBox);
@ -366,7 +356,7 @@ public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
playerGameTypePane.setLeft(label);
JFXComboBox<GameType> gameTypeBox = new JFXComboBox<>(GameType.items);
gameTypeBox.setDisable(isReadOnly());
gameTypeBox.setDisable(worldManagePage.isReadOnly());
BorderPane.setAlignment(gameTypeBox, Pos.CENTER_RIGHT);
playerGameTypePane.setRight(gameTypeBox);
@ -397,7 +387,7 @@ public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
healthPane.setLeft(label);
JFXTextField healthField = new JFXTextField();
healthField.setDisable(isReadOnly());
healthField.setDisable(worldManagePage.isReadOnly());
healthField.setPrefWidth(50);
healthField.setAlignment(Pos.CENTER_RIGHT);
BorderPane.setAlignment(healthField, Pos.CENTER_RIGHT);
@ -431,7 +421,7 @@ public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
foodLevelPane.setLeft(label);
JFXTextField foodLevelField = new JFXTextField();
foodLevelField.setDisable(isReadOnly());
foodLevelField.setDisable(worldManagePage.isReadOnly());
foodLevelField.setPrefWidth(50);
foodLevelField.setAlignment(Pos.CENTER_RIGHT);
BorderPane.setAlignment(foodLevelField, Pos.CENTER_RIGHT);
@ -465,7 +455,7 @@ public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
xpLevelPane.setLeft(label);
JFXTextField xpLevelField = new JFXTextField();
xpLevelField.setDisable(isReadOnly());
xpLevelField.setDisable(worldManagePage.isReadOnly());
xpLevelField.setPrefWidth(50);
xpLevelField.setAlignment(Pos.CENTER_RIGHT);
BorderPane.setAlignment(xpLevelField, Pos.CENTER_RIGHT);
@ -510,11 +500,6 @@ public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
}
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return stateProperty;
}
private static final class Dimension {
static final Dimension OVERWORLD = new Dimension(null);
static final Dimension THE_NETHER = new Dimension(i18n("world.info.dimension.the_nether"));

View File

@ -24,7 +24,6 @@ import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.File;
import java.nio.file.Path;
@ -66,20 +65,7 @@ public final class WorldListItem extends Control {
FXUtils.openFolder(world.getFile().toFile());
}
public void manageDatapacks() {
if (world.getGameVersion() == null || // old game will not write game version to level.dat
GameVersionNumber.compare(world.getGameVersion(), "1.13") < 0) {
Controllers.dialog(i18n("world.datapack.1_13"));
return;
}
Controllers.navigate(new DatapackListPage(world.getWorldName(), world.getFile()));
}
public void showInfo() {
Controllers.navigate(new WorldInfoPage(world));
}
public void showBackupPage() {
Controllers.navigate(new WorldBackupsPage(world, backupsDir));
public void showManagePage() {
Controllers.navigate(new WorldManagePage(world, backupsDir));
}
}

View File

@ -18,10 +18,12 @@
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.SkinBase;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
@ -29,8 +31,9 @@ import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.ChunkBaseApp;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.time.Instant;
@ -40,12 +43,14 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class WorldListItemSkin extends SkinBase<WorldListItem> {
private final BorderPane root;
public WorldListItemSkin(WorldListItem skinnable) {
super(skinnable);
World world = skinnable.getWorld();
BorderPane root = new BorderPane();
root = new BorderPane();
root.getStyleClass().add("md-list-cell");
root.setPadding(new Insets(8));
@ -64,6 +69,7 @@ public final class WorldListItemSkin extends SkinBase<WorldListItem> {
{
TwoLineListItem item = new TwoLineListItem();
root.setCenter(item);
item.setMouseTransparent(true);
if (world.getWorldName() != null)
item.setTitle(parseColorEscapes(world.getWorldName()));
item.setSubtitle(i18n("world.datetime", formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())), world.getGameVersion() == null ? i18n("message.unknown") : world.getGameVersion()));
@ -79,42 +85,58 @@ public final class WorldListItemSkin extends SkinBase<WorldListItem> {
root.setRight(right);
right.setAlignment(Pos.CENTER_RIGHT);
JFXButton btnReveal = new JFXButton();
right.getChildren().add(btnReveal);
FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager"));
btnReveal.getStyleClass().add("toggle-icon4");
btnReveal.setGraphic(SVG.FOLDER_OPEN.createIcon(Theme.blackFill(), -1));
btnReveal.setOnAction(event -> skinnable.reveal());
JFXButton btnExport = new JFXButton();
right.getChildren().add(btnExport);
FXUtils.installFastTooltip(btnExport, i18n("world.export"));
btnExport.getStyleClass().add("toggle-icon4");
btnExport.setGraphic(SVG.OUTPUT.createIcon(Theme.blackFill(), -1));
btnExport.setOnAction(event -> skinnable.export());
JFXButton btnBackup = new JFXButton();
right.getChildren().add(btnBackup);
FXUtils.installFastTooltip(btnBackup, i18n("world.backup"));
btnBackup.getStyleClass().add("toggle-icon4");
btnBackup.setGraphic(SVG.ARCHIVE.createIcon(Theme.blackFill(), -1));
btnBackup.setOnAction(event -> skinnable.showBackupPage());
JFXButton btnDatapack = new JFXButton();
right.getChildren().add(btnDatapack);
FXUtils.installFastTooltip(btnDatapack, i18n("world.datapack"));
btnDatapack.getStyleClass().add("toggle-icon4");
btnDatapack.setGraphic(SVG.EXTENSION.createIcon(Theme.blackFill(), -1));
btnDatapack.setOnAction(event -> skinnable.manageDatapacks());
JFXButton btnInfo = new JFXButton();
right.getChildren().add(btnInfo);
FXUtils.installFastTooltip(btnInfo, i18n("world.info"));
btnInfo.getStyleClass().add("toggle-icon4");
btnInfo.setGraphic(SVG.INFO.createIcon(Theme.blackFill(), -1));
btnInfo.setOnAction(event -> skinnable.showInfo());
JFXButton btnMore = new JFXButton();
right.getChildren().add(btnMore);
btnMore.getStyleClass().add("toggle-icon4");
btnMore.setGraphic(SVG.MORE_VERT.createIcon(Theme.blackFill(), -1));
btnMore.setOnAction(event -> showPopupMenu(JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight()));
}
getChildren().setAll(new RipplerContainer(root));
RipplerContainer container = new RipplerContainer(root);
container.setOnMouseClicked(event -> {
if (event.getClickCount() != 1)
return;
if (event.getButton() == MouseButton.PRIMARY)
skinnable.showManagePage();
else if (event.getButton() == MouseButton.SECONDARY)
showPopupMenu(JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
});
getChildren().setAll(container);
}
// Popup Menu
public void showPopupMenu(JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) {
PopupMenu popupMenu = new PopupMenu();
JFXPopup popup = new JFXPopup(popupMenu);
WorldListItem item = getSkinnable();
World world = item.getWorld();
popupMenu.getContent().addAll(
new IconedMenuItem(SVG.SETTINGS, i18n("world.manage.button"), item::showManagePage, popup));
if (ChunkBaseApp.isSupported(world)) {
popupMenu.getContent().addAll(
new MenuSeparator(),
new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(world), popup),
new IconedMenuItem(SVG.VISIBILITY, i18n("world.chunkbase.stronghold"), () -> ChunkBaseApp.openStrongholdFinder(world), popup),
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), popup)
);
if (GameVersionNumber.compare(world.getGameVersion(), "1.13") >= 0) {
popupMenu.getContent().add(new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"),
() -> ChunkBaseApp.openEndCityFinder(world), popup));
}
}
popupMenu.getContent().addAll(
new MenuSeparator(),
new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), item::export, popup),
new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.world"), item::reveal, popup));
popup.show(root, JFXPopup.PopupVPosition.TOP, hPosition, initOffsetX, initOffsetY);
}
}

View File

@ -0,0 +1,168 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXPopup;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.game.World;
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.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.ChunkBaseApp;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class WorldManagePage extends DecoratorAnimatedPage implements DecoratorPage {
private final ObjectProperty<State> state;
private final World world;
private final Path backupsDir;
private final TabHeader header;
private final TabHeader.Tab<WorldInfoPage> worldInfoTab = new TabHeader.Tab<>("worldInfoPage");
private final TabHeader.Tab<WorldBackupsPage> worldBackupsTab = new TabHeader.Tab<>("worldBackupsPage");
private final TabHeader.Tab<DatapackListPage> datapackTab = new TabHeader.Tab<>("datapackListPage");
private final TransitionPane transitionPane = new TransitionPane();
private FileChannel sessionLockChannel;
public WorldManagePage(World world, Path backupsDir) {
this.world = world;
this.backupsDir = backupsDir;
this.state = new SimpleObjectProperty<>(State.fromTitle(i18n("world.manage.title", world.getWorldName())));
this.header = new TabHeader(worldInfoTab, worldBackupsTab);
if (world.getGameVersion() != null && // old game will not write game version to level.dat
GameVersionNumber.compare(world.getGameVersion(), "1.13") >= 0) {
header.getTabs().add(datapackTab);
}
worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this));
worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this));
datapackTab.setNodeSupplier(() -> new DatapackListPage(this));
header.select(worldInfoTab);
transitionPane.setContent(worldInfoTab.getNode(), ContainerAnimations.NONE);
FXUtils.onChange(header.getSelectionModel().selectedItemProperty(), newValue ->
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE));
setCenter(transitionPane);
BorderPane left = new BorderPane();
FXUtils.setLimitWidth(left, 200);
VBox.setVgrow(left, Priority.ALWAYS);
setLeft(left);
AdvancedListBox sideBar = new AdvancedListBox()
.addNavigationDrawerTab(header, worldInfoTab, i18n("world.info"), SVG.INFO)
.addNavigationDrawerTab(header, worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE)
.addNavigationDrawerTab(header, datapackTab, i18n("world.datapack"), SVG.EXTENSION);
left.setTop(sideBar);
AdvancedListBox toolbar = new AdvancedListBox();
if (ChunkBaseApp.isSupported(world)) {
PopupMenu popupMenu = new PopupMenu();
JFXPopup popup = new JFXPopup(popupMenu);
popupMenu.getContent().addAll(
new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(world), popup),
new IconedMenuItem(SVG.VISIBILITY, i18n("world.chunkbase.stronghold"), () -> ChunkBaseApp.openStrongholdFinder(world), popup),
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), popup)
);
if (GameVersionNumber.compare(world.getGameVersion(), "1.13") >= 0) {
popupMenu.getContent().add(
new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), popup));
}
toolbar.addNavigationDrawerItem(i18n("world.chunkbase"), SVG.EXPLORE, null, chunkBaseMenuItem ->
chunkBaseMenuItem.setOnAction(e ->
popup.show(chunkBaseMenuItem,
JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT,
chunkBaseMenuItem.getWidth(), 0)));
}
toolbar.addNavigationDrawerItem(i18n("settings.game.exploration"), SVG.FOLDER_OPEN, () -> FXUtils.openFolder(world.getFile().toFile()), null);
BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0));
left.setBottom(toolbar);
// Does it need to be done in the background?
try {
sessionLockChannel = world.lock();
LOG.info("Acquired lock on world " + world.getFileName());
} catch (IOException ignored) {
}
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state;
}
public World getWorld() {
return world;
}
public Path getBackupsDir() {
return backupsDir;
}
public boolean isReadOnly() {
return sessionLockChannel == null;
}
@Override
public boolean back() {
closePage();
return true;
}
@Override
public void closePage() {
if (sessionLockChannel != null) {
try {
sessionLockChannel.close();
LOG.info("Releases the lock on world " + world.getFileName());
} catch (IOException e) {
LOG.warning("Failed to close session lock channel", e);
}
sessionLockChannel = null;
}
}
}

View File

@ -0,0 +1,127 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
public final class ChunkBaseApp {
private static final String CHUNK_BASE_URL = "https://www.chunkbase.com";
private static final GameVersionNumber MIN_GAME_VERSION = GameVersionNumber.asGameVersion("1.7");
private static final String[] SEED_MAP_GAME_VERSIONS = {
"1.21.5", "1.21.4", "1.21.2", "1.21", "1.20", "1.19.3", "1.19",
"1.18", "1.17", "1.16", "1.15", "1.14", "1.13", "1.12", "1.11",
"1.10", "1.9", "1.8", "1.7"
};
public static final String[] STRONGHOLD_FINDER_GAME_VERSIONS = {
"1.20", "1.19.3", "1.19", "1.18", "1.16", "1.13", "1.9", "1.7"
};
public static final String[] NETHER_FORTRESS_GAME_VERSIONS = {
"1.18", "1.16", "1.7"
};
public static final String[] END_CITY_GAME_VERSIONS = {
"1.19", "1.13"
};
public static boolean isSupported(@NotNull World world) {
return world.getSeed() != null && world.getGameVersion() != null &&
GameVersionNumber.asGameVersion(world.getGameVersion()).compareTo(MIN_GAME_VERSION) >= 0;
}
public static ChunkBaseApp newBuilder(String app, long seed) {
return new ChunkBaseApp(new StringBuilder(CHUNK_BASE_URL).append("/apps/").append(app).append("#seed=").append(seed));
}
public static void openSeedMap(World world) {
assert isSupported(world);
newBuilder("seed-map", Objects.requireNonNull(world.getSeed()))
.addPlatform(GameVersionNumber.asGameVersion(world.getGameVersion()), world.isLargeBiomes(), SEED_MAP_GAME_VERSIONS)
.open();
}
public static void openStrongholdFinder(World world) {
assert isSupported(world);
newBuilder("stronghold-finder", Objects.requireNonNull(world.getSeed()))
.addPlatform(GameVersionNumber.asGameVersion(world.getGameVersion()), world.isLargeBiomes(), STRONGHOLD_FINDER_GAME_VERSIONS)
.open();
}
public static void openNetherFortressFinder(World world) {
assert isSupported(world);
newBuilder("nether-fortress-finder", Objects.requireNonNull(world.getSeed()))
.addPlatform(GameVersionNumber.asGameVersion(world.getGameVersion()), false, NETHER_FORTRESS_GAME_VERSIONS)
.open();
}
public static void openEndCityFinder(World world) {
assert isSupported(world);
newBuilder("endcity-finder", Objects.requireNonNull(world.getSeed()))
.addPlatform(GameVersionNumber.asGameVersion(world.getGameVersion()), false, END_CITY_GAME_VERSIONS)
.open();
}
private final StringBuilder builder;
private ChunkBaseApp(StringBuilder builder) {
this.builder = builder;
}
public ChunkBaseApp add(String key, String value) {
builder.append('&').append(key).append('=').append(value);
return this;
}
public ChunkBaseApp addPlatform(GameVersionNumber gameVersion, boolean largeBiomes, String[] versionList) {
String version = null;
for (String candidateVersion : versionList) {
if (gameVersion.compareTo(candidateVersion) >= 0) {
version = candidateVersion;
break;
}
}
if (version == null) {
version = versionList[versionList.length - 1]; // Use the last version if no suitable version found
}
add("platform", "java_" + version.replace('.', '_') + (largeBiomes ? "_lb" : ""));
return this;
}
public void open() {
FXUtils.openLink(builder.toString());
}
@Override
public String toString() {
return builder.toString();
}
}

View File

@ -438,6 +438,7 @@ folder.shaderpacks=Shader Packs
folder.saves=Saves
folder.schematics=Schematics
folder.screenshots=Screenshots
folder.world=World Directory
game=Games
game.crash.feedback=<b>Please do not share screenshots of this window with others!</b> If you ask for help from others, please click <b>"Export Crash Logs"</b> and send the exported file to others for analysis.
@ -1073,9 +1074,12 @@ world.backup.create.locked=The world is currently in use. Please close the game
world.backup.create.success=Successfully created a new backup: %s
world.backup.delete=Delete this backup
world.backup.processing=Backing up ...
world.backup.title=World [%s] - Backups
world.datapack=Manage Datapacks
world.datapack.1_13=Only Minecraft 1.13 or later supports datapacks.
world.chunkbase=Chunk Base
world.chunkbase.end_city=End City
world.chunkbase.seed_map=Seed Map
world.chunkbase.stronghold=Stronghold
world.chunkbase.nether_fortress=Nether Fortress
world.datapack=Datapacks
world.datetime=Last played on %s
world.download=Download World
world.download.title=Download World - %1s
@ -1090,7 +1094,6 @@ world.import.choose=Choose world archive you want to import
world.import.failed=Failed to import this world: %s
world.import.invalid=Failed to parse the world.
world.info=World Information
world.info.title=World [%s] - Information
world.info.basic=Basic Information
world.info.allow_cheats=Allow Commands/Cheats
world.info.dimension.the_nether=The Nether
@ -1121,6 +1124,8 @@ world.info.time=Game Time
world.info.time.format=%s days
world.locked=In use
world.manage=Worlds
world.manage.button=World Management
world.manage.title=World - %s
world.name=World Name
world.name.enter=Enter the world name
world.show_all=Show All

View File

@ -1078,9 +1078,7 @@ world.backup.create.locked=El mundo está actualmente en uso. Por favor, cierra
world.backup.create.success=Creada con éxito una nueva copia de seguridad: %s
world.backup.delete=Eliminar esta copia de seguridad
world.backup.processing=Creando nueva copia de seguridad ...
world.backup.title=Mundo [%s] - Copias de seguridad
world.datapack=Gestionar paquetes de datos
world.datapack.1_13=Sólo Minecraft 1.13 o posterior soporta paquetes de datos.
world.datetime=Jugado por última vez en %s
world.download=Descargar Mundo
world.download.title=Descargar mundo - %1s
@ -1095,7 +1093,6 @@ world.import.choose=Elija el archivo de mundo que desea importar
world.import.failed=No se ha podido importar este mundo: %s
world.import.invalid=No se ha podido analizar el mundo.
world.info=Información del mundo
world.info.title=Mundo [%s] - Información
world.info.basic=Información básica
world.info.allow_cheats=Permitir comandos/trucos
world.info.dimension.the_nether=El Nether
@ -1126,6 +1123,7 @@ world.info.time=Tiempo de juego
world.info.time.format=%s días
world.locked=En uso
world.manage=Mundos
world.manage.title=Mundos - %s
world.name=Nombre del mundo
world.name.enter=Introducir el nombre del mundo
world.show_all=Mostrar todo

View File

@ -701,7 +701,6 @@ datapack.title=World %s -データパック
world=マップ
world.add=マップを追加(.zip
world.datapack=データパックの管理
world.datapack.1_13=Minecraft1.13以降のバージョンのみがデータパックをサポートします。
world.datetime=最終ゲーム時刻:%s
world.download=ダウンロード
world.export=このマップをエクスポートする
@ -714,7 +713,8 @@ world.import.already_exists=このマップはすでに存在しています。
world.import.choose=インポートするzipファイルを選択してください
world.import.failed=このマップをインポートできません:%s
world.import.invalid=無効なワールドzipファイル
world.manage=マップ / データパック
world.manage=マップ
world.manage.title=マップ - %s
world.name=マップ名
world.name.enter=マップ名を入力してください
world.show_all=すべて表示

View File

@ -1077,9 +1077,7 @@ world.backup.create.locked=В настоящее время мир находи
world.backup.create.success=Успешно создано новое резервное копирование: %s
world.backup.delete=Удалить эту резервную копию
world.backup.processing=Создание новой резервной копии ...
world.backup.title=Мир [%s] - Резервные копии
world.datapack=Управлять наборами данных
world.datapack.1_13=Только Minecraft 1.13 или новее поддерживает наборы данных.
world.datetime=Последний запуск игры %s
world.download=Скачать мир
world.download.title=Скачать мир - %1s
@ -1094,7 +1092,6 @@ world.import.choose=Выберите архив мира, который хот
world.import.failed=Не удалось импортировать этот мир\: %s
world.import.invalid=Не удалось разобрать мир.
world.info=Сведения о мире
world.info.title=Мир [%s] - Сведения
world.info.basic=Основные сведения
world.info.allow_cheats=Разрешить команды/читы
world.info.dimension.the_nether=Нижний мир
@ -1125,6 +1122,7 @@ world.info.time=Время игры
world.info.time.format=%s дн.
world.locked=В эксплуатации
world.manage=Миры
world.manage.title=Миры - %s
world.name=Название мира
world.name.enter=Введите название мира
world.show_all=Показать все

View File

@ -409,6 +409,7 @@ folder.shaderpacks=著色器包目錄
folder.saves=遊戲存檔目錄
folder.schematics=原理圖目錄
folder.screenshots=截圖目錄
folder.world=世界目錄
game=遊戲
game.crash.feedback=<b>請不要將本介面截圖給他人!</b>如果你要求助他人,請你點擊左下角「匯出遊戲崩潰資訊」後將匯出的檔案發送給他人以供分析。\n你可以點擊下方的「幫助」前往社群尋求幫助。
@ -875,9 +876,12 @@ world.backup.create.locked=該世界正在使用中,請關閉遊戲後重試
world.backup.create.success=成功創建新備份:%s
world.backup.delete=删除此備份
world.backup.processing=正在備份中……
world.backup.title=世界 [%s] - 備份
world.datapack=管理資料包
world.datapack.1_13=僅 Minecraft 1.13 及之後的版本支援資料包
world.chunkbase=世界地圖
world.chunkbase.end_city=終界城地圖
world.chunkbase.seed_map=種子地圖
world.chunkbase.stronghold=要塞地圖
world.chunkbase.nether_fortress=地獄要塞地圖
world.datapack=資料包管理
world.datetime=上一次遊戲時間: %s
world.download=下載世界
world.download.title=世界下載 - %1s
@ -891,7 +895,6 @@ world.import.choose=選取要匯入的存檔壓縮檔
world.import.failed=無法匯入此世界: %s
world.import.invalid=無法識別的存檔壓縮檔
world.info=世界資訊
world.info.title=世界 [%s] - 世界資訊
world.info.basic=基本資訊
world.info.allow_cheats=允許指令(作弊)
world.info.dimension.the_nether=地獄
@ -922,7 +925,9 @@ world.info.time=遊戲內時間
world.info.time.format=%s 天
world.locked=使用中
world.game_version=遊戲版本
world.manage=世界/資料包
world.manage=世界管理
world.manage.button=世界管理
world.manage.title=世界管理 - %s
world.name=世界名稱
world.name.enter=輸入世界名稱
world.show_all=全部顯示

View File

@ -418,6 +418,7 @@ folder.shaderpacks=光影包文件夹
folder.saves=存档文件夹
folder.schematics=原理图文件夹
folder.screenshots=截图文件夹
folder.world=世界文件夹
game=游戏
game.crash.feedback=<b>请不要将本界面截图给他人!</b>如果你要向他人求助,请你点击左下角<b>“导出游戏崩溃信息”</b>后将导出的文件发送给他人以供分析。\n你可以点击下方的<b>“帮助”</b>前往交流群寻求帮助。
@ -878,16 +879,19 @@ web.view_in_browser=在浏览器中查看完整日志
world=世界
world.add=添加世界
world.backup=世界备份
world.backup=备份管理
world.backup.create.new_one=创建新备份
world.backup.create.failed=创建备份失败。\n%s
world.backup.create.locked=该世界正在使用中,请关闭游戏后重试。
world.backup.create.success=成功创建新备份:%s
world.backup.delete=删除此备份
world.backup.processing=正在备份中……
world.backup.title=世界 [%s] - 备份
world.datapack=管理数据包
world.datapack.1_13=仅 Minecraft 1.13 及之后的版本支持数据包
world.chunkbase=世界地图
world.chunkbase.end_city=末地城地图
world.chunkbase.seed_map=种子地图
world.chunkbase.stronghold=要塞地图
world.chunkbase.nether_fortress=下界要塞地图
world.datapack=数据包管理
world.datetime=上一次游戏时间: %s
world.download=下载世界
world.download.title=世界下载 - %1s
@ -902,7 +906,6 @@ world.import.choose=选择要导入的存档压缩包
world.import.failed=无法导入此世界:%s
world.import.invalid=无法识别该存档压缩包
world.info=世界信息
world.info.title=世界 [%s] - 世界信息
world.info.basic=基本信息
world.info.allow_cheats=允许命令(作弊)
world.info.dimension.the_nether=下界
@ -932,7 +935,9 @@ world.info.random_seed=种子
world.info.time=游戏内时间
world.info.time.format=%s 天
world.locked=使用中
world.manage=世界/数据包
world.manage=世界管理
world.manage.button=世界管理
world.manage.title=世界管理 - %s
world.name=世界名称
world.name.enter=输入世界名称
world.show_all=显示全部

View File

@ -24,6 +24,7 @@ import com.github.steveice10.opennbt.tag.builtin.StringTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.util.io.*;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
@ -31,6 +32,7 @@ import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.List;
@ -41,7 +43,7 @@ import java.util.zip.GZIPOutputStream;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class World {
public final class World {
private final Path file;
private String fileName;
@ -49,6 +51,8 @@ public class World {
private String gameVersion;
private long lastPlayed;
private Image icon;
private Long seed;
private boolean largeBiomes;
private boolean isLocked;
public World(Path file) throws IOException {
@ -65,7 +69,7 @@ public class World {
private void loadFromDirectory() throws IOException {
fileName = FileUtils.getName(file);
Path levelDat = file.resolve("level.dat");
getWorldName(levelDat);
loadWorldInfo(levelDat);
isLocked = isLocked(getSessionLockFile());
Path iconFile = file.resolve("icon.png");
@ -108,6 +112,14 @@ public class World {
return gameVersion;
}
public @Nullable Long getSeed() {
return seed;
}
public boolean isLargeBiomes() {
return largeBiomes;
}
public Image getIcon() {
return icon;
}
@ -121,7 +133,7 @@ public class World {
if (!Files.exists(levelDat))
throw new IOException("Not a valid world zip file since level.dat cannot be found.");
getWorldName(levelDat);
loadWorldInfo(levelDat);
Path iconFile = root.resolve("icon.png");
if (Files.isRegularFile(iconFile)) {
@ -154,7 +166,7 @@ public class World {
}
}
private void getWorldName(Path levelDat) throws IOException {
private void loadWorldInfo(Path levelDat) throws IOException {
CompoundTag nbt = parseLevelDat(levelDat);
CompoundTag data = nbt.get("Data");
@ -178,6 +190,27 @@ public class World {
if (version.get("Name") instanceof StringTag)
gameVersion = version.<StringTag>get("Name").getValue();
}
Tag worldGenSettings = data.get("WorldGenSettings");
if (worldGenSettings instanceof CompoundTag) {
Tag seedTag = ((CompoundTag) worldGenSettings).get("seed");
if (seedTag instanceof LongTag) {
seed = ((LongTag) seedTag).getValue();
}
}
if (seed == null) {
Tag seedTag = data.get("RandomSeed");
if (seedTag instanceof LongTag) {
seed = ((LongTag) seedTag).getValue();
}
}
// FIXME: Only work for 1.15 and below
if (data.get("generatorName") instanceof StringTag) {
largeBiomes = "largeBiomes".equals(data.<StringTag>get("generatorName").getValue());
} else {
largeBiomes = false;
}
}
public void rename(String newName) throws IOException {
@ -293,7 +326,7 @@ public class World {
private static boolean isLocked(Path sessionLockFile) {
try (FileChannel fileChannel = FileChannel.open(sessionLockFile, StandardOpenOption.WRITE)) {
return fileChannel.tryLock() == null;
} catch (AccessDeniedException accessDeniedException) {
} catch (AccessDeniedException | OverlappingFileLockException accessDeniedException) {
return true;
} catch (NoSuchFileException noSuchFileException) {
return false;