diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackList.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackList.java new file mode 100644 index 000000000..e4e144868 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackList.java @@ -0,0 +1,10 @@ +package org.jackhuang.hmcl.ui.versions; + +import org.jackhuang.hmcl.ui.ListPage; + +public class DatapackList extends ListPage { + @Override + public void add() { + + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListItem.java new file mode 100644 index 000000000..d8415b3ef --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListItem.java @@ -0,0 +1,46 @@ +package org.jackhuang.hmcl.ui.versions; + +import com.jfoenix.concurrency.JFXUtilities; +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXCheckBox; +import com.jfoenix.effects.JFXDepthManager; +import javafx.geometry.Pos; +import javafx.scene.layout.BorderPane; +import org.jackhuang.hmcl.mod.Datapack; +import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.SVG; +import org.jackhuang.hmcl.ui.construct.TwoLineListItem; + +import java.util.function.Consumer; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class DatapackListItem extends BorderPane { + + public DatapackListItem(Datapack root, Datapack.Pack info, Consumer deleteCallback) { + JFXCheckBox chkEnabled = new JFXCheckBox(); + BorderPane.setAlignment(chkEnabled, Pos.CENTER); + setLeft(chkEnabled); + + TwoLineListItem modItem = new TwoLineListItem(); + BorderPane.setAlignment(modItem, Pos.CENTER); + setCenter(modItem); + + JFXButton btnRemove = new JFXButton(); + JFXUtilities.runInFX(() -> { + FXUtils.installTooltip(btnRemove, i18n("mods.remove")); + }); + btnRemove.setOnMouseClicked(e -> deleteCallback.accept(this)); + btnRemove.getStyleClass().add("toggle-icon4"); + BorderPane.setAlignment(btnRemove, Pos.CENTER); + btnRemove.setGraphic(SVG.close(Theme.blackFillBinding(), 15, 15)); + setRight(btnRemove); + + setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;"); + JFXDepthManager.setDepth(this, 1); + modItem.setTitle(info.getId()); + modItem.setSubtitle(info.getDescription()); + chkEnabled.selectedProperty().bindBidirectional(info.activeProperty()); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ModItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModItem.java similarity index 96% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/ModItem.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModItem.java index 7488b3784..3b3f5cc89 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ModItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModItem.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see {http://www.gnu.org/licenses/}. */ -package org.jackhuang.hmcl.ui; +package org.jackhuang.hmcl.ui.versions; import com.jfoenix.concurrency.JFXUtilities; import com.jfoenix.controls.JFXButton; @@ -25,6 +25,8 @@ import javafx.geometry.Pos; import javafx.scene.layout.BorderPane; import org.jackhuang.hmcl.mod.ModInfo; import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import org.jackhuang.hmcl.util.StringUtils; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java index 799573aba..0b7628f2a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java @@ -28,7 +28,6 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.ListPage; -import org.jackhuang.hmcl.ui.ModItem; import org.jackhuang.hmcl.util.FileUtils; import org.jackhuang.hmcl.util.Logging; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldList.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldList.java new file mode 100644 index 000000000..1ee9f1886 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldList.java @@ -0,0 +1,12 @@ +package org.jackhuang.hmcl.ui.versions; + +import org.jackhuang.hmcl.ui.ListPage; + +public class WorldList extends ListPage { + + + @Override + public void add() { + // Not adding world here. + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java new file mode 100644 index 000000000..7cd809b78 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java @@ -0,0 +1,56 @@ +package org.jackhuang.hmcl.ui.versions; + +import javafx.beans.property.*; +import javafx.scene.control.Control; +import javafx.scene.control.Skin; +import javafx.scene.image.Image; +import javafx.stage.FileChooser; +import org.jackhuang.hmcl.game.World; +import org.jackhuang.hmcl.ui.Controllers; + +import java.io.File; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class WorldListItem extends Control { + private final StringProperty title = new SimpleStringProperty(); + private final StringProperty subtitle = new SimpleStringProperty(); + private final ObjectProperty image = new SimpleObjectProperty<>(); + private final World world; + + public WorldListItem(World world) { + this.world = world; + } + + @Override + protected Skin createDefaultSkin() { + return new WorldListItemSkin(this); + } + + public StringProperty titleProperty() { + return title; + } + + public StringProperty subtitleProperty() { + return subtitle; + } + + public ObjectProperty imageProperty() { + return image; + } + + public void export() { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle(i18n("world.export.title")); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("world"), "*.zip")); + File file = fileChooser.showSaveDialog(Controllers.getStage()); + if (file == null) { + return; + } + + + } + + public void manageDatapacks() { + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java new file mode 100644 index 000000000..56032ebd4 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java @@ -0,0 +1,90 @@ +package org.jackhuang.hmcl.ui.versions; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXPopup; +import com.jfoenix.effects.JFXDepthManager; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.SkinBase; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.SVG; +import org.jackhuang.hmcl.ui.construct.IconedMenuItem; +import org.jackhuang.hmcl.ui.construct.TwoLineListItem; + +import java.util.function.Function; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class WorldListItemSkin extends SkinBase { + + public WorldListItemSkin(WorldListItem skinnable) { + super(skinnable); + + BorderPane root = new BorderPane(); + + + HBox center = new HBox(); + center.setSpacing(8); + center.setAlignment(Pos.CENTER_LEFT); + + StackPane imageViewContainer = new StackPane(); + FXUtils.setLimitWidth(imageViewContainer, 32); + FXUtils.setLimitHeight(imageViewContainer, 32); + + ImageView imageView = new ImageView(); + FXUtils.limitSize(imageView, 32, 32); + imageView.imageProperty().bind(skinnable.imageProperty()); + imageViewContainer.getChildren().setAll(imageView); + + TwoLineListItem item = new TwoLineListItem(); + BorderPane.setAlignment(item, Pos.CENTER); + center.getChildren().setAll(imageView, item); + root.setCenter(center); + + VBox menu = new VBox(); + JFXPopup popup = new JFXPopup(menu); + + Function wrap = r -> () -> { + r.run(); + popup.hide(); + }; + + Function limitWidth = node -> { + StackPane pane = new StackPane(node); + pane.setAlignment(Pos.CENTER); + FXUtils.setLimitWidth(pane, 14); + FXUtils.setLimitHeight(pane, 14); + return pane; + }; + + menu.getChildren().setAll( + new IconedMenuItem(limitWidth.apply(SVG.gear(Theme.blackFillBinding(), 14, 14)), i18n("world.datapack"), wrap.apply(skinnable::manageDatapacks)), + new IconedMenuItem(limitWidth.apply(SVG.export(Theme.blackFillBinding(), 14, 14)), i18n("world.export"), wrap.apply(skinnable::export))); + + HBox right = new HBox(); + right.setAlignment(Pos.CENTER_RIGHT); + + JFXButton btnManage = new JFXButton(); + btnManage.setOnMouseClicked(e -> { + popup.show(root, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight()); + }); + btnManage.getStyleClass().add("toggle-icon4"); + BorderPane.setAlignment(btnManage, Pos.CENTER); + btnManage.setGraphic(SVG.dotsVertical(Theme.blackFillBinding(), -1, -1)); + right.getChildren().add(btnManage); + root.setRight(right); + + root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;"); + JFXDepthManager.setDepth(root, 1); + item.titleProperty().bind(skinnable.titleProperty()); + item.subtitleProperty().bind(skinnable.subtitleProperty()); + + getChildren().setAll(root); + } +} diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 7ed664349..c977c639a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -240,6 +240,15 @@ mods.add.success=Successfully added mods %s. mods.choose_mod=Choose your mods mods.remove=Remove +datapack=Data packs +datapack.add=Add data pack +datapack.remove=Remove + +world=Worlds +world.datapack=Manage data packs +world.export=Export this world +world.export.title=Choose a file location to hold your world + profile=Game Directories profile.default=Current directory profile.home=User home diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibraryDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibraryDownloadTask.java index 4d2ec9b5e..aee197075 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibraryDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibraryDownloadTask.java @@ -15,6 +15,7 @@ import org.tukaani.xz.XZInputStream; import java.io.*; import java.net.URL; import java.nio.charset.Charset; +import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.jar.JarEntry; @@ -75,7 +76,7 @@ public class LibraryDownloadTask extends Task { else throw new LibraryDownloadException(library, t); } else { - if (xz) unpackLibrary(jar, FileUtils.readBytes(xzFile)); + if (xz) unpackLibrary(jar, Files.readAllBytes(xzFile.toPath())); if (!checksumValid(jar, library.getChecksums())) { jar.delete(); throw new IOException("Checksum failed for " + library); @@ -125,7 +126,7 @@ public class LibraryDownloadTask extends Task { if (checksums == null || checksums.isEmpty()) { return true; } - byte[] fileData = FileUtils.readBytes(libPath); + byte[] fileData = Files.readAllBytes(libPath.toPath()); boolean valid = checksums.contains(encodeHex(digest("SHA-1", fileData))); if (!valid && libPath.getName().endsWith(".jar")) { valid = validateJar(fileData, checksums); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java new file mode 100644 index 000000000..44c0c9ff2 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -0,0 +1,4 @@ +package org.jackhuang.hmcl.game; + +public class World { +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Datapack.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Datapack.java new file mode 100644 index 000000000..01fc19e94 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Datapack.java @@ -0,0 +1,185 @@ +package org.jackhuang.hmcl.mod; + +import com.google.gson.JsonParseException; +import com.google.gson.annotations.SerializedName; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import org.jackhuang.hmcl.util.*; + +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +public class Datapack { + private final Path path; + private final List info; + + private Datapack(Path path, List info) { + this.path = path; + this.info = Collections.unmodifiableList(info); + + for (Pack pack : info) { + pack.datapack = this; + } + } + + public Path getPath() { + return path; + } + + public List getInfo() { + return info; + } + + public void installTo(Path worldPath) throws IOException { + Path datapacks = worldPath.resolve("datapacks"); + + Set packs = new HashSet<>(); + for (Pack pack : info) packs.add(pack.getId()); + + for (Path datapack : Files.newDirectoryStream(datapacks)) { + if (packs.contains(FileUtils.getName(datapack))) + FileUtils.deleteDirectory(datapack.toFile()); + } + + new Unzipper(path, worldPath).setReplaceExistentFile(true).unzip(); + } + + public static Datapack fromZip(Path path) throws IOException { + try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(path)) { + Datapack datapack = fromDir(fs.getPath("/datapacks/")); + return new Datapack(path, datapack.info); + } + } + + /** + * + * @param dir + * @return + * @throws IOException + */ + public static Datapack fromDir(Path dir) throws IOException { + List info = new LinkedList<>(); + + for (Path subDir : Files.newDirectoryStream(dir)) { + Path mcmeta = subDir.resolve("pack.mcmeta"); + + if (!Files.exists(mcmeta)) + continue; + + PackMcMeta pack = JsonUtils.fromNonNullJson(FileUtils.readText(mcmeta), PackMcMeta.class); + info.add(new Pack(mcmeta, FileUtils.getName(subDir), pack.getPackInfo().getDescription())); + } + return new Datapack(dir, info); + } + + public static class Pack { + private Path packMcMeta; + private final BooleanProperty active; + private final String id; + private final String description; + private Datapack datapack; + + public Pack(Path packMcMeta, String id, String description) { + this.packMcMeta = packMcMeta; + this.id = id; + this.description = description; + + active = new SimpleBooleanProperty(this, "active", !DISABLED_EXT.equals(FileUtils.getExtension(packMcMeta))) { + @Override + protected void invalidated() { + Path f = Pack.this.packMcMeta.toAbsolutePath(), newF; + if (DISABLED_EXT.equals(FileUtils.getExtension(f))) + newF = f.getParent().resolve(FileUtils.getNameWithoutExtension(f)); + else + newF = f.getParent().resolve(FileUtils.getName(f) + DISABLED_EXT); + + try { + Files.move(f, newF); + Pack.this.packMcMeta = newF; + } catch (IOException e) { + // Mod file is occupied. + Logging.LOG.warning("Unable to rename file " + f + " to " + newF); + } + } + }; + } + + public String getId() { + return id; + } + + public String getDescription() { + return description; + } + + public Datapack getDatapack() { + return datapack; + } + + public BooleanProperty activeProperty() { + return active; + } + + public boolean isActive() { + return active.get(); + } + + public void setActive(boolean active) { + this.active.set(active); + } + } + + private static class PackMcMeta implements Validation { + + @SerializedName("pack") + private final PackInfo pack; + + public PackMcMeta() { + this(new PackInfo()); + } + + public PackMcMeta(PackInfo packInfo) { + this.pack = packInfo; + } + + public PackInfo getPackInfo() { + return pack; + } + + @Override + public void validate() throws JsonParseException { + if (pack == null) + throw new JsonParseException("pack cannot be null"); + } + + public static class PackInfo { + @SerializedName("pack_format") + private final int packFormat; + + @SerializedName("description") + private final String description; + + public PackInfo() { + this(0, ""); + } + + public PackInfo(int packFormat, String description) { + this.packFormat = packFormat; + this.description = description; + } + + public int getPackFormat() { + return packFormat; + } + + public String getDescription() { + return description; + } + } + } + + private static final String DISABLED_EXT = ".disabled"; +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/FileUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/FileUtils.java index 22c01febf..91c3e5d32 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/FileUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/FileUtils.java @@ -43,10 +43,18 @@ public final class FileUtils { return StringUtils.substringBeforeLast(file.getName(), '.'); } + public static String getNameWithoutExtension(Path file) { + return StringUtils.substringBeforeLast(getName(file), '.'); + } + public static String getExtension(File file) { return StringUtils.substringAfterLast(file.getName(), '.'); } + public static String getExtension(Path file) { + return StringUtils.substringAfterLast(getName(file), '.'); + } + /** * This method is for normalizing ZipPath since Path.normalize of ZipFileSystem does not work properly. */ @@ -63,11 +71,15 @@ public final class FileUtils { } public static String readText(File file, Charset charset) throws IOException { - return new String(readBytes(file), charset); + return new String(Files.readAllBytes(file.toPath()), charset); } - public static byte[] readBytes(File file) throws IOException { - return Files.readAllBytes(file.toPath()); + public static String readText(Path file) throws IOException { + return readText(file, UTF_8); + } + + public static String readText(Path file, Charset charset) throws IOException { + return new String(Files.readAllBytes(file), charset); } public static void writeText(File file, String text) throws IOException { diff --git a/build.gradle b/build.gradle index 3642ef741..a962e84bf 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,7 @@ subprojects { compile group: 'org.tukaani', name: 'xz', version: '1.8' compile group: 'org.hildan.fxgson', name: 'fx-gson', version: '3.1.0' compile group: 'org.jenkins-ci', name: 'constant-pool-scanner', version: '1.2' + compile group: 'org.spacehq', name: 'opennbt', version: '1.0' testCompile group: 'junit', name: 'junit', version: '4.12' }