Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ public enum SVG {
SCHEMA("M4 23V17H6.5V15H4V9H6.5V7H4V1h7V7H8.5V9H11v2h3V9h7v6H14V13H11v2H8.5v2H11v6H4Zm2-2H9V19H6v2Zm0-8H9V11H6v2Zm10 0h3V11H16v2ZM6 5H9V3H6V5ZM7.5 4Zm0 8Zm10 0Zm-10 8Z"),
SCHEMA_FILL("M4 23V17H6.5V15H4V9H6.5V7H4V1h7V7H8.5V9H11v2h3V9h7v6H14V13H11v2H8.5v2H11v6H4Z"),
SCREENSHOT_MONITOR("M15 16H19V12H17.5V14.5H15V16ZM5 10H6.5V7.5H9V6H5V10ZM8 21V19H4Q3.175 19 2.5875 18.4125T2 17V5Q2 4.175 2.5875 3.5875T4 3H20Q20.825 3 21.4125 3.5875T22 5V17Q22 17.825 21.4125 18.4125T20 19H16V21H8ZM4 17H20V5H4V17ZM4 17V5 17Z"),
SCREENSHOT_MONITOR_FILL("M15 16H19V12H17.5V14.5H15V16ZM5 10H6.5V7.5H9V6H5V10ZM8 21V19H4Q3.175 19 2.5875 18.4125T2 17V5Q2 4.175 2.5875 3.5875T4 3H20Q20.825 3 21.4125 3.5875T22 5V17Q22 17.825 21.4125 18.4125T20 19H16V21H8Z"),
SCRIPT("M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z"), // Not Material
SEARCH("M19.6 21 13.3 14.7Q12.55 15.3 11.575 15.65T9.5 16Q6.775 16 4.8875 14.1125T3 9.5Q3 6.775 4.8875 4.8875T9.5 3Q12.225 3 14.1125 4.8875T16 9.5Q16 10.6 15.65 11.575T14.7 13.3L21 19.6 19.6 21ZM9.5 14Q11.375 14 12.6875 12.6875T14 9.5Q14 7.625 12.6875 6.3125T9.5 5Q7.625 5 6.3125 6.3125T5 9.5Q5 11.375 6.3125 12.6875T9.5 14Z"),
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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,33 @@ public ImageContainer(double width, double height) {
this.getChildren().setAll(imageView);
}

public ImageContainer(Image image, double maxWidth, double maxHeight) {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);

double width = maxWidth;
double height = width / image.getWidth() * image.getHeight();
if (height > maxHeight) {
height = maxHeight;
width = height / image.getHeight() * image.getWidth();
}

FXUtils.setLimitWidth(this, width);
FXUtils.setLimitHeight(this, height);

imageView.setPreserveRatio(true);
FXUtils.limitSize(imageView, width, height);
StackPane.setAlignment(imageView, Pos.CENTER);

clip.setWidth(width);
clip.setHeight(height);
updateCornerRadius(getCornerRadius());
this.setClip(clip);

setImage(image);

this.getChildren().setAll(imageView);
}

private void updateCornerRadius(double radius) {
clip.setArcWidth(radius);
clip.setArcHeight(radius);
Expand Down
288 changes: 288 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ScreenshotsPage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
package org.jackhuang.hmcl.ui.versions;

import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXListView;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ListCell;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Instant;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;

import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;

public class ScreenshotsPage extends ListPageBase<ScreenshotsPage.Screenshot> implements VersionPage.VersionLoadable, PageAware {

private Path screenshotsDir;

@Override
protected Skin<?> createDefaultSkin() {
return new ScreenshotsPageSkin(this);
}

@Override
public void loadVersion(Profile profile, String version) {
screenshotsDir = profile.getRepository().getRunDirectory(version).resolve("screenshots");
refresh();
}

public void refresh() {
setLoading(true);
Task.supplyAsync(Schedulers.io(), () -> {
getItems().clear();
try (Stream<Path> stream = Files.list(screenshotsDir)) {
return stream.map(Screenshot::fromFile).filter(Objects::nonNull).sorted(Comparator.reverseOrder()).toList();
}
}).whenComplete(Schedulers.javafx(), (list, exception) -> {
if (exception != null) {
LOG.warning("Failed to load screenshots in: " + screenshotsDir, exception);
getItems().clear();
} else {
getItems().setAll(list);
}
setLoading(false);
}).start();
}

private void delete(Screenshot screenshot) {
try {
Files.deleteIfExists(screenshot.getPath());
refresh();
} catch (IOException e) {
LOG.warning("Failed to delete screenshot: " + screenshot.getPath(), e);
}
}

public static final class Screenshot implements Comparable<Screenshot> {
private final Path path;
private final String fileName;
private final Instant creationTime;
private Image thumbnail, fullImage;

public static Screenshot fromFile(Path path) {
if (!Files.isRegularFile(path) || !path.toString().endsWith(".png")) return null;
Instant creationTime = null;
try {
creationTime = Files.readAttributes(path, BasicFileAttributes.class).creationTime().toInstant();
} catch (IOException e) {
LOG.warning("Failed to read screenshot creation time at: " + path, e);
}
return new Screenshot(path, FileUtils.getName(path), creationTime);
}

public Screenshot(Path path, String fileName, Instant creationTime) {
this.path = path;
this.fileName = fileName;
this.creationTime = creationTime;
}

@Override
public int compareTo(@NotNull ScreenshotsPage.Screenshot o) {
return this.getFileName().compareTo(o.getFileName());
}

public Path getPath() {
return path;
}

public String getFileName() {
return fileName;
}

public Instant getCreationTime() {
return creationTime;
}

public boolean isThumbnailLoaded() {
return thumbnail != null;
}

public boolean isFullImageLoaded() {
return fullImage != null;
}

@Nullable
public Image getThumbnail() {
if (thumbnail == null) {
try {
thumbnail = FXUtils.loadImage(path, 72, 72, true, false);
} catch (Exception e) {
LOG.warning("Failed to load screenshot thumbnail at: " + path, e);
}
}
return thumbnail;
}

@Nullable
public Image getFullImage() {
if (fullImage == null) {
try {
fullImage = FXUtils.loadImage(path);
} catch (Exception e) {
LOG.warning("Failed to load screenshot content at: " + path, e);
}
}
return fullImage;
}

@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (Screenshot) obj;
return Objects.equals(this.path, that.path) &&
Objects.equals(this.fileName, that.fileName) &&
Objects.equals(this.creationTime, that.creationTime) &&
Objects.equals(this.thumbnail, that.thumbnail);
}

@Override
public int hashCode() {
return Objects.hash(path, fileName, creationTime, thumbnail);
}

@Override
public String toString() {
return "Screenshot[" +
"path=" + path + ", " +
"fileName=" + fileName + ", " +
"creationTime=" + creationTime + ", " +
"thumbnail=" + thumbnail + ']';
}

}

public static final class ScreenshotCell extends ListCell<Screenshot> {

private final RipplerContainer graphics;
private final StackPane imagePane = new StackPane();
private final BorderPane container = new BorderPane();
private final TwoLineListItem content = new TwoLineListItem();
private final JFXButton deleteButton = FXUtils.newToggleButton4(SVG.DELETE_FOREVER);

public ScreenshotCell(ScreenshotsPage page) {
super();

container.getStyleClass().add("md-list-cell");
container.setPadding(new Insets(8));

imagePane.setPadding(new Insets(0, 8, 0, 0));
BorderPane.setAlignment(imagePane, Pos.CENTER);
container.setLeft(imagePane);

container.setCenter(content);

BorderPane.setAlignment(deleteButton, Pos.CENTER_RIGHT);
deleteButton.setOnAction(e -> {
Screenshot screenshot = getItem();
if (screenshot != null) {
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"),
() -> page.delete(screenshot), null);
}
});
container.setRight(deleteButton);

graphics = new RipplerContainer(container);
FXUtils.onClicked(graphics, () -> {
Screenshot screenshot = getItem();
if (screenshot != null) Controllers.dialog(new ScreenshotDialog(screenshot));
});
}

@Override
protected void updateItem(Screenshot item, boolean empty) {
super.updateItem(item, empty);

if (item == null || empty) {
setGraphic(null);
return;
}

if (item.isThumbnailLoaded()) {
imagePane.getChildren().setAll(new ImageContainer(item.getThumbnail(), 36, 36));
} else {
imagePane.getChildren().setAll(SVG.SCREENSHOT_MONITOR.createIcon(36));
CompletableFuture
.supplyAsync(item::getThumbnail, Schedulers.io())
.whenCompleteAsync((image, t) -> {
if (image != null) imagePane.getChildren().setAll(new ImageContainer(image, 36, 36));
}, Schedulers.javafx());
}

content.setTitle(item.getFileName());
content.setSubtitle(I18n.formatDateTime(item.getCreationTime()));

setGraphic(graphics);
}
}

public static final class ScreenshotDialog extends JFXDialogLayout {

public ScreenshotDialog(Screenshot screenshot) {
TwoLineListItem head = new TwoLineListItem();
head.setTitle(screenshot.getFileName());
head.setSubtitle(I18n.formatDateTime(screenshot.getCreationTime()));
setHeading(head);

var image = screenshot.getFullImage();
setBody(image != null
? new ImageContainer(image, Math.min(Controllers.getScene().getWidth() * 0.8, image.getWidth()), Controllers.getScene().getHeight() * 0.5)
: SVG.SCREENSHOT_MONITOR.createIcon(360)
);

JFXButton okButton = new JFXButton();
okButton.getStyleClass().add("dialog-accept");
okButton.setText(i18n("button.ok"));
okButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
FXUtils.onEscPressed(this, okButton::fire);
setActions(okButton);
}
}

public static final class ScreenshotsPageSkin extends ToolbarListPageSkin<Screenshot, ScreenshotsPage> {

private final ScreenshotsPage skinnable;

public ScreenshotsPageSkin(ScreenshotsPage skinnable) {
super(skinnable);
this.skinnable = skinnable;
}

@Override
protected List<Node> initializeToolbar(ScreenshotsPage skinnable) {
return List.of(
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh)
);
}

@Override
protected ListCell<Screenshot> createListCell(JFXListView<Screenshot> listView) {
return new ScreenshotCell(skinnable);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
private final TabHeader.Tab<WorldListPage> worldListTab = new TabHeader.Tab<>("worldList");
private final TabHeader.Tab<SchematicsPage> schematicsTab = new TabHeader.Tab<>("schematicsTab");
private final TabHeader.Tab<ResourcepackListPage> resourcePackTab = new TabHeader.Tab<>("resourcePackTab");
private final TabHeader.Tab<ScreenshotsPage> screenshotsTab = new TabHeader.Tab<>("screenshotsTab");
private final TransitionPane transitionPane = new TransitionPane();
private final BooleanProperty currentVersionUpgradable = new SimpleBooleanProperty();
private final ObjectProperty<Profile.ProfileVersion> version = new SimpleObjectProperty<>();
Expand All @@ -77,8 +78,9 @@ public VersionPage() {
resourcePackTab.setNodeSupplier(loadVersionFor(ResourcepackListPage::new));
worldListTab.setNodeSupplier(loadVersionFor(WorldListPage::new));
schematicsTab.setNodeSupplier(loadVersionFor(SchematicsPage::new));
screenshotsTab.setNodeSupplier(loadVersionFor(ScreenshotsPage::new));

tab = new TabHeader(transitionPane, versionSettingsTab, installerListTab, modListTab, resourcePackTab, worldListTab, schematicsTab);
tab = new TabHeader(transitionPane, versionSettingsTab, installerListTab, modListTab, resourcePackTab, worldListTab, schematicsTab, screenshotsTab);
tab.select(versionSettingsTab);

addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
Expand All @@ -95,6 +97,8 @@ public VersionPage() {
worldListTab.getNode().loadVersion(getProfile(), getVersion());
if (schematicsTab.isInitialized())
schematicsTab.getNode().loadVersion(getProfile(), getVersion());
if (screenshotsTab.isInitialized())
screenshotsTab.getNode().loadVersion(getProfile(), getVersion());
}
});

Expand Down Expand Up @@ -159,6 +163,8 @@ public void loadVersion(String version, Profile profile) {
worldListTab.getNode().loadVersion(profile, version);
if (schematicsTab.isInitialized())
schematicsTab.getNode().loadVersion(profile, version);
if (screenshotsTab.isInitialized())
screenshotsTab.getNode().loadVersion(profile, version);
currentVersionUpgradable.set(profile.getRepository().isModpack(version));
}

Expand Down Expand Up @@ -265,7 +271,8 @@ protected Skin(VersionPage control) {
.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.worldListTab, i18n("world.manage"), SVG.PUBLIC)
.addNavigationDrawerTab(control.tab, control.schematicsTab, i18n("schematics.manage"), SVG.SCHEMA, SVG.SCHEMA_FILL);
.addNavigationDrawerTab(control.tab, control.schematicsTab, i18n("schematics.manage"), SVG.SCHEMA, SVG.SCHEMA_FILL)
.addNavigationDrawerTab(control.tab, control.screenshotsTab, i18n("screenshots.manage"), SVG.SCREENSHOT_MONITOR, SVG.SCREENSHOT_MONITOR_FILL);
VBox.setVgrow(sideBar, Priority.ALWAYS);

PopupMenu browseList = new PopupMenu();
Expand Down
2 changes: 2 additions & 0 deletions HMCL/src/main/resources/assets/lang/I18N.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1282,6 +1282,8 @@ schematics.info.version=Schematic Version
schematics.manage=Schematics
schematics.sub_items=%d sub-item(s)

screenshots.manage=Screenshots

search=Search
search.hint.chinese=Search in English and Chinese
search.hint.english=Search in English only
Expand Down
2 changes: 2 additions & 0 deletions HMCL/src/main/resources/assets/lang/I18N_zh.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,8 @@ schematics.info.version=原理圖版本
schematics.manage=原理圖管理
schematics.sub_items=%d 個子項

screenshots.manage=截圖管理

search=搜尋
search.hint.chinese=支援中英文搜尋
search.hint.english=僅支援英文搜尋
Expand Down
Loading
Loading