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 index 4a45486ce..b48de3bb2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java @@ -48,8 +48,8 @@ public class WorldListItem extends Control { this.world = world; title.set(parseColorEscapes(world.getWorldName())); - subtitle.set(i18n("world.description", world.getFileName(), formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())), world.getGameVersion() == null ? i18n("message.unknown") : world.getGameVersion())); + image.set(world.getIcon()); FXUtils.onClicked(this, this::showInfo); } 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 index 27a8f3dbd..d7b7c51bf 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java @@ -19,9 +19,11 @@ package org.jackhuang.hmcl.ui.versions; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXPopup; +import javafx.beans.value.ChangeListener; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.SkinBase; +import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; @@ -38,6 +40,9 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class WorldListItemSkin extends SkinBase { + @SuppressWarnings({"FieldCanBeLocal", "unused"}) + private final ChangeListener iconListener; + public WorldListItemSkin(WorldListItem skinnable) { super(skinnable); @@ -54,14 +59,15 @@ public class WorldListItemSkin extends SkinBase { ImageView imageView = new ImageView(); FXUtils.limitSize(imageView, 32, 32); - imageView.imageProperty().bind(skinnable.imageProperty()); + iconListener = FXUtils.onWeakChangeAndOperate(skinnable.imageProperty(), image -> + imageView.setImage(image == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : image)); imageViewContainer.getChildren().setAll(imageView); TwoLineListItem item = new TwoLineListItem(); item.titleProperty().bind(skinnable.titleProperty()); item.subtitleProperty().bind(skinnable.subtitleProperty()); BorderPane.setAlignment(item, Pos.CENTER); - center.getChildren().setAll(imageView, item); + center.getChildren().setAll(imageViewContainer, item); root.setCenter(center); PopupMenu menu = new PopupMenu(); diff --git a/HMCL/src/main/resources/assets/img/unknown_server.png b/HMCL/src/main/resources/assets/img/unknown_server.png new file mode 100644 index 000000000..0de327412 Binary files /dev/null and b/HMCL/src/main/resources/assets/img/unknown_server.png differ diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java index 2c6ed74c8..881f00fde 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -22,6 +22,7 @@ import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.LongTag; 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.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.Unzipper; @@ -46,6 +47,7 @@ public class World { private String worldName; private String gameVersion; private long lastPlayed; + private Image icon; public World(Path file) throws IOException { this.file = file; @@ -62,6 +64,17 @@ public class World { fileName = FileUtils.getName(file); Path levelDat = file.resolve("level.dat"); getWorldName(levelDat); + + Path iconFile = file.resolve("icon.png"); + if (Files.isRegularFile(iconFile)) { + try (InputStream inputStream = Files.newInputStream(iconFile)) { + icon = new Image(inputStream, 64, 64, true, false); + if (icon.isError()) + throw icon.getException(); + } catch (Exception e) { + LOG.warning("Failed to load world icon", e); + } + } } public Path getFile() { @@ -88,12 +101,27 @@ public class World { return gameVersion; } + public Image getIcon() { + return icon; + } + private void loadFromZipImpl(Path root) throws IOException { Path levelDat = root.resolve("level.dat"); if (!Files.exists(levelDat)) throw new IOException("Not a valid world zip file since level.dat cannot be found."); getWorldName(levelDat); + + Path iconFile = root.resolve("icon.png"); + if (Files.isRegularFile(iconFile)) { + try (InputStream inputStream = Files.newInputStream(iconFile)) { + icon = new Image(inputStream, 64, 64, true, false); + if (icon.isError()) + throw icon.getException(); + } catch (Exception e) { + LOG.warning("Failed to load world icon", e); + } + } } private void loadFromZip() throws IOException {