From 1b835a416096217894a5a2e3d953157dd9c8d380 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 15 Mar 2026 22:37:24 +0800 Subject: [PATCH 01/16] ScreenshotsPage --- .../main/java/org/jackhuang/hmcl/ui/SVG.java | 1 + .../hmcl/ui/versions/ScreenshotsPage.java | 236 ++++++++++++++++++ .../hmcl/ui/versions/VersionPage.java | 11 +- .../resources/assets/lang/I18N.properties | 2 + .../resources/assets/lang/I18N_zh.properties | 2 + .../assets/lang/I18N_zh_CN.properties | 2 + 6 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ScreenshotsPage.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java index c7a99881db..12d0f936f8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -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"), diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ScreenshotsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ScreenshotsPage.java new file mode 100644 index 0000000000..def261e518 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ScreenshotsPage.java @@ -0,0 +1,236 @@ +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 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.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 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 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.path()); + refresh(); + } catch (IOException e) { + LOG.warning("Failed to delete screenshot: " + screenshot.path(), e); + } + } + + public record Screenshot(Path path, String fileName, Instant creationTime, Image thumbnail) implements Comparable { + + public static Screenshot fromFile(Path path) { + if (!Files.isRegularFile(path) || !path.toString().endsWith(".png")) return null; + Image thumbnail = null; + Instant creationTime = null; + try { + thumbnail = FXUtils.loadImage(path, 72, 72, true, false); + } catch (Exception e) { + LOG.warning("Failed to load screenshot thumbnail at: " + path, e); + } + 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, thumbnail); + } + + public Image loadFullImage() { + Image image = null; + try { + image = FXUtils.loadImage(path); + } catch (Exception e) { + LOG.warning("Failed to load screenshot content at: " + path, e); + } + return image; + } + + @Override + public int compareTo(@NotNull ScreenshotsPage.Screenshot o) { + return this.fileName().compareTo(o.fileName()); + } + } + + public static final class ScreenshotCell extends ListCell { + + 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; + } + + var image = item.thumbnail(); + if (image != null) { + double width = 36; + double height = width / image.getWidth() * image.getHeight(); + if (height > 36) { + height = 36; + width = height / image.getHeight() * image.getWidth(); + } + ImageContainer imageContainer = new ImageContainer(width, height); + imageContainer.setImage(image); + imagePane.getChildren().setAll(imageContainer); + } else { + imagePane.getChildren().setAll(SVG.SCREENSHOT_MONITOR.createIcon(36)); + } + + content.setTitle(item.fileName()); + content.setSubtitle(I18n.formatDateTime(item.creationTime())); + + setGraphic(graphics); + } + } + + public static final class ScreenshotDialog extends JFXDialogLayout { + + public ScreenshotDialog(Screenshot screenshot) { + TwoLineListItem head = new TwoLineListItem(); + head.setTitle(screenshot.fileName()); + head.setSubtitle(I18n.formatDateTime(screenshot.creationTime())); + setHeading(head); + + var image = screenshot.loadFullImage(); + if (image != null) { + double maxHeight = Controllers.getScene().getHeight() * 0.5; + double width = Math.min(Controllers.getScene().getWidth() * 0.8, image.getWidth()); + double height = width / image.getWidth() * image.getHeight(); + if (height > maxHeight) { + height = maxHeight; + width = height / image.getHeight() * image.getWidth(); + } + ImageContainer imageContainer = new ImageContainer(width, height); + imageContainer.setImage(image); + setBody(imageContainer); + } else { + setBody(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 { + + private final ScreenshotsPage skinnable; + + public ScreenshotsPageSkin(ScreenshotsPage skinnable) { + super(skinnable); + this.skinnable = skinnable; + } + + @Override + protected List initializeToolbar(ScreenshotsPage skinnable) { + return List.of( + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh) + ); + } + + @Override + protected ListCell createListCell(JFXListView listView) { + return new ScreenshotCell(skinnable); + } + } + +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java index 0184026260..5d922df857 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java @@ -56,6 +56,7 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage private final TabHeader.Tab worldListTab = new TabHeader.Tab<>("worldList"); private final TabHeader.Tab schematicsTab = new TabHeader.Tab<>("schematicsTab"); private final TabHeader.Tab resourcePackTab = new TabHeader.Tab<>("resourcePackTab"); + private final TabHeader.Tab screenshotsTab = new TabHeader.Tab<>("screenshotsTab"); private final TransitionPane transitionPane = new TransitionPane(); private final BooleanProperty currentVersionUpgradable = new SimpleBooleanProperty(); private final ObjectProperty version = new SimpleObjectProperty<>(); @@ -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); @@ -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()); } }); @@ -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)); } @@ -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(); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 2aeadb39df..7d10336142 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -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 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 663980d42b..7c10b8b7b0 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1072,6 +1072,8 @@ schematics.info.version=原理圖版本 schematics.manage=原理圖管理 schematics.sub_items=%d 個子項 +screenshots.manage=截圖管理 + search=搜尋 search.hint.chinese=支援中英文搜尋 search.hint.english=僅支援英文搜尋 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index a3c977c0f8..5f75fb2cd0 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1077,6 +1077,8 @@ schematics.info.version=原理图版本 schematics.manage=原理图管理 schematics.sub_items=%d 个子项 +screenshots.manage=截图管理 + search=搜索 search.hint.chinese=支持中英文搜索 search.hint.english=仅支持英文搜索 From 9ad2041a23269fbe410797027e6cfdd5c9c592ec Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 16 Mar 2026 11:11:13 +0800 Subject: [PATCH 02/16] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/construct/ImageContainer.java | 27 ++++ .../hmcl/ui/versions/ScreenshotsPage.java | 150 ++++++++++++------ 2 files changed, 128 insertions(+), 49 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/ImageContainer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/ImageContainer.java index c9aa50c8c0..c2a96531e8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/ImageContainer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/ImageContainer.java @@ -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); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ScreenshotsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ScreenshotsPage.java index def261e518..15e6e19eb3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ScreenshotsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ScreenshotsPage.java @@ -19,6 +19,7 @@ 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; @@ -28,6 +29,7 @@ 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; @@ -68,46 +70,110 @@ public void refresh() { private void delete(Screenshot screenshot) { try { - Files.deleteIfExists(screenshot.path()); + Files.deleteIfExists(screenshot.getPath()); refresh(); } catch (IOException e) { - LOG.warning("Failed to delete screenshot: " + screenshot.path(), e); + LOG.warning("Failed to delete screenshot: " + screenshot.getPath(), e); } } - public record Screenshot(Path path, String fileName, Instant creationTime, Image thumbnail) implements Comparable { + public static final class Screenshot implements Comparable { + 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; - Image thumbnail = null; Instant creationTime = null; - try { - thumbnail = FXUtils.loadImage(path, 72, 72, true, false); - } catch (Exception e) { - LOG.warning("Failed to load screenshot thumbnail at: " + path, e); - } 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, thumbnail); + return new Screenshot(path, FileUtils.getName(path), creationTime); } - public Image loadFullImage() { - Image image = null; - try { - image = FXUtils.loadImage(path); - } catch (Exception e) { - LOG.warning("Failed to load screenshot content at: " + path, e); - } - return image; + 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.fileName().compareTo(o.fileName()); + 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 { @@ -156,23 +222,19 @@ protected void updateItem(Screenshot item, boolean empty) { return; } - var image = item.thumbnail(); - if (image != null) { - double width = 36; - double height = width / image.getWidth() * image.getHeight(); - if (height > 36) { - height = 36; - width = height / image.getHeight() * image.getWidth(); - } - ImageContainer imageContainer = new ImageContainer(width, height); - imageContainer.setImage(image); - imagePane.getChildren().setAll(imageContainer); + 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.fileName()); - content.setSubtitle(I18n.formatDateTime(item.creationTime())); + content.setTitle(item.getFileName()); + content.setSubtitle(I18n.formatDateTime(item.getCreationTime())); setGraphic(graphics); } @@ -182,25 +244,15 @@ public static final class ScreenshotDialog extends JFXDialogLayout { public ScreenshotDialog(Screenshot screenshot) { TwoLineListItem head = new TwoLineListItem(); - head.setTitle(screenshot.fileName()); - head.setSubtitle(I18n.formatDateTime(screenshot.creationTime())); + head.setTitle(screenshot.getFileName()); + head.setSubtitle(I18n.formatDateTime(screenshot.getCreationTime())); setHeading(head); - var image = screenshot.loadFullImage(); - if (image != null) { - double maxHeight = Controllers.getScene().getHeight() * 0.5; - double width = Math.min(Controllers.getScene().getWidth() * 0.8, image.getWidth()); - double height = width / image.getWidth() * image.getHeight(); - if (height > maxHeight) { - height = maxHeight; - width = height / image.getHeight() * image.getWidth(); - } - ImageContainer imageContainer = new ImageContainer(width, height); - imageContainer.setImage(image); - setBody(imageContainer); - } else { - setBody(SVG.SCREENSHOT_MONITOR.createIcon(360)); - } + 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"); From 2e8ba2b59bb2094f0072c10bd4e13d1c1a0310cd Mon Sep 17 00:00:00 2001 From: Calboot Date: Tue, 17 Mar 2026 17:31:18 +0800 Subject: [PATCH 03/16] =?UTF-8?q?=E6=B8=85=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/ScreenshotsPage.java | 204 +++++++++++++----- 1 file changed, 153 insertions(+), 51 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ScreenshotsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ScreenshotsPage.java index 15e6e19eb3..30af25b695 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ScreenshotsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ScreenshotsPage.java @@ -3,18 +3,22 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXDialogLayout; import com.jfoenix.controls.JFXListView; +import javafx.beans.binding.Bindings; 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.control.*; import javafx.scene.image.Image; -import javafx.scene.layout.BorderPane; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; 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.animation.ContainerAnimations; +import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.FileUtils; @@ -26,12 +30,14 @@ import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.time.Instant; +import java.util.Collection; 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.ui.FXUtils.ignoreEvent; +import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -53,7 +59,6 @@ public void loadVersion(Profile profile, String version) { public void refresh() { setLoading(true); Task.supplyAsync(Schedulers.io(), () -> { - getItems().clear(); try (Stream stream = Files.list(screenshotsDir)) { return stream.map(Screenshot::fromFile).filter(Objects::nonNull).sorted(Comparator.reverseOrder()).toList(); } @@ -68,12 +73,29 @@ public void refresh() { }).start(); } - private void delete(Screenshot screenshot) { + private void deleteAt(Path path) { try { - Files.deleteIfExists(screenshot.getPath()); - refresh(); + Files.deleteIfExists(path); } catch (IOException e) { - LOG.warning("Failed to delete screenshot: " + screenshot.getPath(), e); + LOG.warning("Failed to delete screenshot: " + path, e); + } + } + + private void delete(Screenshot screenshot) { + deleteAt(screenshot.getPath()); + refresh(); + } + + private void delete(Collection screenshots) { + screenshots.stream().map(Screenshot::getPath).forEach(this::deleteAt); + refresh(); + } + + private void clear() { + try (var stream = Files.list(screenshotsDir)) { + stream.filter(Screenshot::isFileScreenshot).forEach(this::deleteAt); + } catch (IOException e) { + LOG.warning("Failed to clear screenshots at: " + screenshotsDir, e); } } @@ -83,8 +105,12 @@ public static final class Screenshot implements Comparable { private final Instant creationTime; private Image thumbnail, fullImage; + public static boolean isFileScreenshot(Path path) { + return Files.isRegularFile(path) && "png".equalsIgnoreCase(FileUtils.getExtension(path)); + } + public static Screenshot fromFile(Path path) { - if (!Files.isRegularFile(path) || !path.toString().endsWith(".png")) return null; + if (!isFileScreenshot(path)) return null; Instant creationTime = null; try { creationTime = Files.readAttributes(path, BasicFileAttributes.class).creationTime().toInstant(); @@ -176,27 +202,32 @@ public String toString() { } - public static final class ScreenshotCell extends ListCell { + public static final class ScreenshotCell extends MDListCell { - 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(); + public ScreenshotCell(JFXListView listView, ScreenshotsPage page) { + super(listView); + + setSelectable(); - container.getStyleClass().add("md-list-cell"); - container.setPadding(new Insets(8)); + HBox container = new HBox(8); + container.setPickOnBounds(false); + container.setAlignment(Pos.CENTER_LEFT); - imagePane.setPadding(new Insets(0, 8, 0, 0)); - BorderPane.setAlignment(imagePane, Pos.CENTER); - container.setLeft(imagePane); + content.setMouseTransparent(true); + HBox.setHgrow(content, Priority.ALWAYS); - container.setCenter(content); + JFXButton infoButton = FXUtils.newToggleButton4(SVG.INFO); + infoButton.setOnAction(e -> { + Screenshot screenshot = getItem(); + if (screenshot != null) { + Controllers.dialog(new ScreenshotDialog(screenshot)); + } + }); - BorderPane.setAlignment(deleteButton, Pos.CENTER_RIGHT); + JFXButton deleteButton = FXUtils.newToggleButton4(SVG.DELETE_FOREVER); deleteButton.setOnAction(e -> { Screenshot screenshot = getItem(); if (screenshot != null) { @@ -204,23 +235,16 @@ public ScreenshotCell(ScreenshotsPage page) { () -> 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)); - }); + container.getChildren().setAll(imagePane, content, infoButton, deleteButton); + + StackPane.setMargin(container, new Insets(8)); + getContainer().getChildren().setAll(container); } @Override - protected void updateItem(Screenshot item, boolean empty) { - super.updateItem(item, empty); - - if (item == null || empty) { - setGraphic(null); - return; - } + protected void updateControl(Screenshot item, boolean empty) { + if (item == null || empty) return; if (item.isThumbnailLoaded()) { imagePane.getChildren().setAll(new ImageContainer(item.getThumbnail(), 36, 36)); @@ -235,8 +259,6 @@ protected void updateItem(Screenshot item, boolean empty) { content.setTitle(item.getFileName()); content.setSubtitle(I18n.formatDateTime(item.getCreationTime())); - - setGraphic(graphics); } } @@ -263,25 +285,105 @@ public ScreenshotDialog(Screenshot screenshot) { } } - public static final class ScreenshotsPageSkin extends ToolbarListPageSkin { + public static final class ScreenshotsPageSkin extends SkinBase { + + private final TransitionPane toolbarPane; + private final HBox toolbarNormal; + private final HBox toolbarSelecting; - private final ScreenshotsPage skinnable; + private final JFXListView listView; public ScreenshotsPageSkin(ScreenshotsPage skinnable) { super(skinnable); - this.skinnable = skinnable; - } - @Override - protected List initializeToolbar(ScreenshotsPage skinnable) { - return List.of( - createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh) - ); + StackPane pane = new StackPane(); + pane.setPadding(new Insets(10)); + pane.getStyleClass().addAll("notice-pane"); + + ComponentList root = new ComponentList(); + root.getStyleClass().add("no-padding"); + listView = new JFXListView<>(); + listView.getStyleClass().add("no-horizontal-scrollbar"); + + { + toolbarPane = new TransitionPane(); + + toolbarNormal = new HBox(); + toolbarSelecting = new HBox(); + + // Toolbar Normal + toolbarNormal.getChildren().setAll( + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), + createToolbarButton2(i18n("button.clear"), SVG.DELETE_FOREVER, () -> { + if (!listView.getItems().isEmpty()) { + Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), skinnable::clear, null); + } + }) + ); + + // Toolbar Selecting + toolbarSelecting.getChildren().setAll( + createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> { + Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> + skinnable.delete(listView.getSelectionModel().getSelectedItems()), null); + }), + createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> + listView.getSelectionModel().selectAll()), + createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> + listView.getSelectionModel().clearSelection()) + ); + + FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(), + selectedItem -> { + if (selectedItem == null) + changeToolbar(toolbarNormal); + else + changeToolbar(toolbarSelecting); + }); + root.getContent().add(toolbarPane); + + // Clear selection when pressing ESC + root.addEventHandler(KeyEvent.KEY_PRESSED, e -> { + if (e.getCode() == KeyCode.ESCAPE) { + if (listView.getSelectionModel().getSelectedItem() != null) { + listView.getSelectionModel().clearSelection(); + e.consume(); + } + } + }); + } + + { + SpinnerPane center = new SpinnerPane(); + ComponentList.setVgrow(center, Priority.ALWAYS); + center.loadingProperty().bind(skinnable.loadingProperty()); + + listView.setCellFactory(x -> new ScreenshotCell(listView, skinnable)); + listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + Bindings.bindContent(listView.getItems(), skinnable.getItems()); + + listView.setOnContextMenuRequested(event -> { + Screenshot selectedItem = listView.getSelectionModel().getSelectedItem(); + if (selectedItem != null && listView.getSelectionModel().getSelectedItems().size() == 1) { + listView.getSelectionModel().clearSelection(); + Controllers.dialog(new ScreenshotDialog(selectedItem)); + } + }); + + // ListViewBehavior would consume ESC pressed event, preventing us from handling it + // So we ignore it here + ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); + + center.setContent(listView); + root.getContent().add(center); + } + + pane.getChildren().setAll(root); + getChildren().setAll(pane); } - @Override - protected ListCell createListCell(JFXListView listView) { - return new ScreenshotCell(skinnable); + private void changeToolbar(HBox newToolbar) { + if (newToolbar != toolbarPane.getCurrentNode()) toolbarPane.setContent(newToolbar, ContainerAnimations.FADE); } } From 40540afb59ac68c4eb619e78edfdabd80121b349 Mon Sep 17 00:00:00 2001 From: Calboot Date: Tue, 17 Mar 2026 18:34:34 +0800 Subject: [PATCH 04/16] reveal --- .../java/org/jackhuang/hmcl/ui/FXUtils.java | 14 +++++++++ .../hmcl/ui/versions/ScreenshotsPage.java | 31 ++++++++++++++++--- .../resources/assets/lang/I18N.properties | 1 + .../resources/assets/lang/I18N_zh.properties | 1 + .../assets/lang/I18N_zh_CN.properties | 1 + 5 files changed, 44 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index 41c6db9e5c..5ccee2c815 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -1514,6 +1514,20 @@ public static void copyText(String text, @Nullable String toastMessage) { } } + public static void copyFiles(List paths) { + copyFiles(paths.stream().map(Path::toFile).toList(), i18n("message.copied")); + } + + public static void copyFiles(List files, @Nullable String toastMessage) { + ClipboardContent content = new ClipboardContent(); + content.putFiles(files); + Clipboard.getSystemClipboard().setContent(content); + + if (toastMessage != null && !Controllers.isStopped()) { + Controllers.showToast(toastMessage); + } + } + public static List parseSegment(String segment, Consumer hyperlinkAction) { if (segment.indexOf('<') < 0) return Collections.singletonList(new Text(segment)); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ScreenshotsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ScreenshotsPage.java index 30af25b695..1c3841a428 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ScreenshotsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ScreenshotsPage.java @@ -32,6 +32,7 @@ import java.time.Instant; import java.util.Collection; import java.util.Comparator; +import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.stream.Stream; @@ -73,6 +74,10 @@ public void refresh() { }).start(); } + private void revealFolder() { + FXUtils.openFolder(screenshotsDir); + } + private void deleteAt(Path path) { try { Files.deleteIfExists(path); @@ -219,6 +224,22 @@ public ScreenshotCell(JFXListView listView, ScreenshotsPage page) { content.setMouseTransparent(true); HBox.setHgrow(content, Priority.ALWAYS); + JFXButton copyButton = FXUtils.newToggleButton4(SVG.CONTENT_COPY); + copyButton.setOnAction(e -> { + Screenshot screenshot = getItem(); + if (screenshot != null) { + FXUtils.copyFiles(List.of(screenshot.getPath())); + } + }); + + JFXButton revealButton = FXUtils.newToggleButton4(SVG.FOLDER); + revealButton.setOnAction(e -> { + Screenshot screenshot = getItem(); + if (screenshot != null) { + FXUtils.showFileInExplorer(screenshot.getPath()); + } + }); + JFXButton infoButton = FXUtils.newToggleButton4(SVG.INFO); infoButton.setOnAction(e -> { Screenshot screenshot = getItem(); @@ -236,7 +257,7 @@ public ScreenshotCell(JFXListView listView, ScreenshotsPage page) { } }); - container.getChildren().setAll(imagePane, content, infoButton, deleteButton); + container.getChildren().setAll(imagePane, content, copyButton, revealButton, infoButton, deleteButton); StackPane.setMargin(container, new Insets(8)); getContainer().getChildren().setAll(container); @@ -314,15 +335,17 @@ public ScreenshotsPageSkin(ScreenshotsPage skinnable) { // Toolbar Normal toolbarNormal.getChildren().setAll( createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), + createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::revealFolder), createToolbarButton2(i18n("button.clear"), SVG.DELETE_FOREVER, () -> { - if (!listView.getItems().isEmpty()) { - Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), skinnable::clear, null); - } + Controllers.confirm(i18n("screenshots.clear.confirm"), i18n("button.clear"), skinnable::clear, null); }) ); // Toolbar Selecting toolbarSelecting.getChildren().setAll( + createToolbarButton2(i18n("menu.copy"), SVG.FOLDER_COPY, () -> { + FXUtils.copyFiles(listView.getSelectionModel().getSelectedItems().stream().map(Screenshot::getPath).toList()); + }), createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> { Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> skinnable.delete(listView.getSelectionModel().getSelectedItems()), null); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 7d10336142..b5f43c98b5 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1282,6 +1282,7 @@ schematics.info.version=Schematic Version schematics.manage=Schematics schematics.sub_items=%d sub-item(s) +screenshots.clear.confirm=Are you sure you want to permanently remove the screenshots? This action cannot be undone! screenshots.manage=Screenshots search=Search diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 7c10b8b7b0..0180af57e2 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1072,6 +1072,7 @@ schematics.info.version=原理圖版本 schematics.manage=原理圖管理 schematics.sub_items=%d 個子項 +screenshots.clear.confirm=你確認要清除所有截圖嗎?該操作無法復原! screenshots.manage=截圖管理 search=搜尋 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 5f75fb2cd0..364c351003 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1077,6 +1077,7 @@ schematics.info.version=原理图版本 schematics.manage=原理图管理 schematics.sub_items=%d 个子项 +screenshots.clear.confirm=你确定要清除所有截图吗?此操作无法撤销! screenshots.manage=截图管理 search=搜索 From bb526d674e8b236aa21e829c64d66872786d55c6 Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 18 Mar 2026 18:28:25 +0800 Subject: [PATCH 05/16] =?UTF-8?q?=E6=8C=89=E6=97=A5=E6=9C=9F=E9=80=89?= =?UTF-8?q?=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCL/build.gradle.kts | 1 + .../java/org/jackhuang/hmcl/ui/FXUtils.java | 61 ++++++++++++++++++- .../main/java/org/jackhuang/hmcl/ui/SVG.java | 1 + .../hmcl/ui/versions/ScreenshotsPage.java | 23 ++++++- .../resources/assets/lang/I18N.properties | 3 + .../resources/assets/lang/I18N_zh.properties | 3 + .../assets/lang/I18N_zh_CN.properties | 3 + 7 files changed, 91 insertions(+), 4 deletions(-) diff --git a/HMCL/build.gradle.kts b/HMCL/build.gradle.kts index b9fd7c0d56..b677336fa7 100644 --- a/HMCL/build.gradle.kts +++ b/HMCL/build.gradle.kts @@ -137,6 +137,7 @@ val addOpens = listOf( "javafx.controls/com.sun.javafx.scene.control", "javafx.controls/com.sun.javafx.scene.control.behavior", "javafx.graphics/com.sun.javafx.tk.quantum", + "javafx.controls/javafx.scene.control", "javafx.controls/javafx.scene.control.skin", "jdk.attach/sun.tools.attach", ) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index 5ccee2c815..4a1cae7acc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -38,6 +38,7 @@ import javafx.event.EventHandler; import javafx.event.EventType; import javafx.geometry.Bounds; +import javafx.geometry.Pos; import javafx.geometry.Rectangle2D; import javafx.scene.Cursor; import javafx.scene.Node; @@ -48,9 +49,7 @@ import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.*; -import javafx.scene.layout.ColumnConstraints; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; +import javafx.scene.layout.*; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; import javafx.scene.shape.Rectangle; @@ -68,12 +67,14 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.animation.AnimationUtils; import org.jackhuang.hmcl.ui.animation.Motion; +import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; import org.jackhuang.hmcl.ui.construct.IconedMenuItem; import org.jackhuang.hmcl.ui.construct.MenuSeparator; import org.jackhuang.hmcl.ui.construct.PopupMenu; import org.jackhuang.hmcl.ui.image.ImageLoader; import org.jackhuang.hmcl.ui.image.ImageUtils; import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.ResourceNotFoundError; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.NetworkUtils; @@ -107,6 +108,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.PathMatcher; +import java.time.Clock; +import java.time.LocalDate; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.*; @@ -1693,4 +1696,56 @@ public static void useJFXContextMenu(TextInputControl control) { e.consume(); }); } + + public static void chooseDateRange(Consumer> onConfirm) { + Controllers.dialog(new DateRangeDialog(onConfirm)); + } + + private static class DateRangeDialog extends JFXDialogLayout { + + private final JFXDatePicker fromPicker, toPicker; + + public DateRangeDialog(Consumer> onConfirm) { + super(); + + setHeading(new Label(i18n("button.select_date"))); + + VBox body = new VBox(8); + { + HBox fromBox = new HBox(4); + fromBox.setAlignment(Pos.CENTER_LEFT); + Label fromLabel = new Label(i18n("button.select_date.from")); + this.fromPicker = new JFXDatePicker(LocalDate.now(Clock.systemDefaultZone())); + fromPicker.setOverLay(true); + fromPicker.setDialogParent(Controllers.getDecorator().getDecorator().getDrawerWrapper()); + fromBox.getChildren().setAll(fromLabel, fromPicker); + body.getChildren().add(fromBox); + } + { + HBox toBox = new HBox(4); + toBox.setAlignment(Pos.CENTER_LEFT); + Label toLabel = new Label(i18n("button.select_date.to")); + this.toPicker = new JFXDatePicker(LocalDate.now(Clock.systemDefaultZone())); + toPicker.setOverLay(true); + toPicker.setDialogParent(Controllers.getDecorator().getDecorator().getDrawerWrapper()); + toBox.getChildren().setAll(toLabel, toPicker); + body.getChildren().add(toBox); + } + setBody(body); + + JFXButton cancelButton = new JFXButton(i18n("button.cancel")); + cancelButton.getStyleClass().add("dialog-cancel"); + cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); + + JFXButton okButton = new JFXButton(i18n("button.ok")); + okButton.getStyleClass().add("dialog-accept"); + okButton.setOnAction(e -> { + if (onConfirm != null) onConfirm.accept(Pair.pair(fromPicker.getValue(), toPicker.getValue())); + fireEvent(new DialogCloseEvent()); + }); + + setActions(cancelButton, okButton); + } + + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java index 12d0f936f8..8fb8265d50 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -49,6 +49,7 @@ public enum SVG { CONTENT_COPY("M9 18Q8.175 18 7.5875 17.4125T7 16V4Q7 3.175 7.5875 2.5875T9 2H18Q18.825 2 19.4125 2.5875T20 4V16Q20 16.825 19.4125 17.4125T18 18H9ZM9 16H18V4H9V16ZM5 22Q4.175 22 3.5875 21.4125T3 20V6H5V20H16V22H5ZM9 16V4 16Z"), CONTENT_PASTE("M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h4.175q.275-.875 1.075-1.437T12 1q1 0 1.788.563T14.85 3H19q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm0-2h14V5h-2v3H7V5H5zm7-14q.425 0 .713-.288T13 4t-.288-.712T12 3t-.712.288T11 4t.288.713T12 5"), CREATE_NEW_FOLDER("M14 16h2V14h2V12H16V10H14v2H12v2h2v2ZM4 20q-.825 0-1.4125-.5875T2 18V6q0-.825.5875-1.4125T4 4h6l2 2h8q.825 0 1.4125.5875T22 8V18q0 .825-.5875 1.4125T20 20H4Zm0-2H20V8H11.175l-2-2H4V18ZV6 18Z"), + DATE_RANGE("M7.2875 13.7125Q7 13.425 7 13t0.2875 -0.7125Q7.575 12 8 12t0.7125 0.2875Q9 12.575 9 13t-0.2875 0.7125Q8.425 14 8 14t-0.7125 -0.2875zm4 0Q11 13.425 11 13t0.2875 -0.7125Q11.575 12 12 12t0.7125 0.2875Q13 12.575 13 13t-0.2875 0.7125Q12.425 14 12 14t-0.7125 -0.2875zm4 0Q15 13.425 15 13t0.2875 -0.7125Q15.575 12 16 12t0.7125 0.2875Q17 12.575 17 13t-0.2875 0.7125Q16.425 14 16 14t-0.7125 -0.2875zM5 22q-0.825 0 -1.4125 -0.5875T3 20l0 -14q0 -0.825 0.5875 -1.4125T5 4l1 0l0 -2l2 0l0 2l8 0l0 -2l2 0l0 2l1 0q0.825 0 1.4125 0.5875T21 6l0 14q0 0.825 -0.5875 1.4125T19 22L5 22zm0 -2l14 0l0 -10L5 10l0 10zm0 -12l14 0l0 -2L5 6l0 2zm0 0l0 -2 0 2z"), DELETE("M7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM9 17H11V8H9V17ZM13 17H15V8H13V17ZM7 6V19 6Z"), DELETE_FOREVER("M9.4 16.5 12 13.9 14.6 16.5 16 15.1 13.4 12.5 16 9.9 14.6 8.5 12 11.1 9.4 8.5 8 9.9 10.6 12.5 8 15.1 9.4 16.5ZM7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM7 6V19 6Z"), DEPLOYED_CODE("M11 19.425V12.575L5 9.1V15.95L11 19.425ZM13 19.425 19 15.95V9.1L13 12.575V19.425ZM12 10.85 17.925 7.425 12 4 6.075 7.425 12 10.85ZM4 17.7Q3.525 17.425 3.2625 16.975T3 15.975V8.025Q3 7.475 3.2625 7.025T4 6.3L11 2.275Q11.475 2 12 2T13 2.275L20 6.3Q20.475 6.575 20.7375 7.025T21 8.025V15.975Q21 16.525 20.7375 16.975T20 17.7L13 21.725Q12.525 22 12 22T11 21.725L4 17.7ZM12 12Z"), diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ScreenshotsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ScreenshotsPage.java index 1c3841a428..71d3951f6d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ScreenshotsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ScreenshotsPage.java @@ -30,6 +30,7 @@ import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.time.Instant; +import java.time.ZoneId; import java.util.Collection; import java.util.Comparator; import java.util.List; @@ -104,6 +105,25 @@ private void clear() { } } + private void selectDate(JFXListView listView) { + FXUtils.chooseDateRange(pair -> { + Instant from = pair.key().atStartOfDay().atZone(ZoneId.systemDefault()).toInstant(); + Instant to = pair.value().plusDays(1).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant(); + if (from.compareTo(to) >= 0) return; + List items = listView.getItems(); + int start = -1, end = -1; + for (int i = 0; i < items.size(); i++) { + // Reversed order + if (items.get(i).getCreationTime().compareTo(from) < 0 && end < 0) end = i; + if (items.get(i).getCreationTime().compareTo(to) < 0 && start < 0) start = i; + } + if (start < 0) return; + if (end < 0) end = items.size(); + listView.getSelectionModel().selectRange(start, end); + listView.scrollTo(start); + }); + } + public static final class Screenshot implements Comparable { private final Path path; private final String fileName; @@ -338,7 +358,8 @@ public ScreenshotsPageSkin(ScreenshotsPage skinnable) { createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::revealFolder), createToolbarButton2(i18n("button.clear"), SVG.DELETE_FOREVER, () -> { Controllers.confirm(i18n("screenshots.clear.confirm"), i18n("button.clear"), skinnable::clear, null); - }) + }), + createToolbarButton2(i18n("button.select_date"), SVG.DATE_RANGE, () -> skinnable.selectDate(listView)) ); // Toolbar Selecting diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index b5f43c98b5..b2a354ef0d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -197,6 +197,9 @@ button.retry=Retry button.save=Save button.save_as=Save As button.select_all=Select All +button.select_date=Select Date... +button.select_date.from=From +button.select_date.to=To button.view=View button.yes=Yes diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 0180af57e2..926d5eb51c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -194,6 +194,9 @@ button.retry=重試 button.save=儲存 button.save_as=另存新檔 button.select_all=全選 +button.select_date=選擇日期 +button.select_date.from=開始 +button.select_date.to=截止 button.view=查看 button.yes=是 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 364c351003..d728645dd9 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -196,6 +196,9 @@ button.retry=重试 button.save=保存 button.save_as=另存为 button.select_all=全选 +button.select_date=选择日期 +button.select_date.from=开始 +button.select_date.to=截止 button.view=查看 button.yes=是 From 8655534aa44edee7ee2a49b28313d185b7dcf4cb Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 20 Mar 2026 19:57:47 +0800 Subject: [PATCH 06/16] =?UTF-8?q?=E6=9B=B4=E6=96=B0UI=20(=E6=9C=AA?= =?UTF-8?q?=E5=AE=8C=E6=88=90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jfoenix/skins/JFXDatePickerContent.java | 708 ++++++++++++++++++ .../com/jfoenix/skins/JFXDatePickerSkin.java | 162 ++++ .../main/java/org/jackhuang/hmcl/ui/SVG.java | 1 + HMCL/src/main/resources/assets/css/root.css | 22 + 4 files changed, 893 insertions(+) create mode 100644 HMCL/src/main/java/com/jfoenix/skins/JFXDatePickerContent.java create mode 100644 HMCL/src/main/java/com/jfoenix/skins/JFXDatePickerSkin.java diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXDatePickerContent.java b/HMCL/src/main/java/com/jfoenix/skins/JFXDatePickerContent.java new file mode 100644 index 0000000000..5d6fe40fa5 --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/skins/JFXDatePickerContent.java @@ -0,0 +1,708 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by Fernflower decompiler) +// + +package com.jfoenix.skins; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXDatePicker; +import com.jfoenix.controls.JFXListCell; +import com.jfoenix.controls.JFXListView; +import com.jfoenix.svg.SVGGlyph; +import com.jfoenix.transitions.CachedTransition; +import javafx.animation.*; +import javafx.animation.Animation.Status; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.SnapshotParameters; +import javafx.scene.control.*; +import javafx.scene.image.ImageView; +import javafx.scene.image.WritableImage; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.scene.text.TextAlignment; +import javafx.util.Duration; + +import java.time.DateTimeException; +import java.time.LocalDate; +import java.time.YearMonth; +import java.time.chrono.ChronoLocalDate; +import java.time.chrono.Chronology; +import java.time.format.DateTimeFormatter; +import java.time.format.DecimalStyle; +import java.time.temporal.ChronoUnit; +import java.time.temporal.WeekFields; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class JFXDatePickerContent extends VBox { + private static final String SPINNER_LABEL = "spinner-label"; + private static final String ROBOTO = "Roboto"; + private static final Color DEFAULT_CELL_COLOR = Color.valueOf("#9C9C9C"); + private static final Color DEFAULT_COLOR = Color.valueOf("#313131"); + protected JFXDatePicker datePicker; + private JFXButton backMonthButton; + private JFXButton forwardMonthButton; + private final ObjectProperty