Merge 64d49d261d660a19d3c67a88a0c8f6e25d6abb44 into 9969dc60c5278340b6b9a4d7facdde620e99d1f5

This commit is contained in:
竹若泠 2025-08-02 23:00:10 +08:00 committed by GitHub
commit b75c064e53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 132 additions and 15 deletions

View File

@ -23,6 +23,8 @@ import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.event.EventBus;
@ -37,7 +39,9 @@ import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import java.io.File;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
@ -117,6 +121,12 @@ public final class Profile implements Observable {
this.useRelativePath.set(useRelativePath);
}
private final ObservableList<String> pinnedVersions = FXCollections.observableArrayList();
public ObservableList<String> getPinnedVersions() {
return pinnedVersions;
}
public Profile(String name) {
this(name, new File(".minecraft"));
}
@ -126,13 +136,16 @@ public final class Profile implements Observable {
}
public Profile(String name, File initialGameDir, VersionSetting global) {
this(name, initialGameDir, global, null, false);
this(name, initialGameDir, global, null, false, null);
}
public Profile(String name, File initialGameDir, VersionSetting global, String selectedVersion, boolean useRelativePath) {
public Profile(String name, File initialGameDir, VersionSetting global, String selectedVersion, boolean useRelativePath, List<String> pinnedVersions) {
this.name = new SimpleStringProperty(this, "name", name);
gameDir = new SimpleObjectProperty<>(this, "gameDir", initialGameDir);
repository = new HMCLGameRepository(this, initialGameDir);
if (pinnedVersions != null) {
this.pinnedVersions.addAll(pinnedVersions);
}
this.global.set(global == null ? new VersionSetting() : global);
this.selectedVersion.set(selectedVersion);
this.useRelativePath.set(useRelativePath);
@ -140,10 +153,18 @@ public final class Profile implements Observable {
gameDir.addListener((a, b, newValue) -> repository.changeDirectory(newValue));
this.selectedVersion.addListener(o -> checkSelectedVersion());
listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkSelectedVersion(), EventPriority.HIGHEST));
this.pinnedVersions.addListener((InvalidationListener) o -> checkPinnedVersion());
listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkPinnedVersion(), EventPriority.HIGHEST));
addPropertyChangedListener(onInvalidating(this::invalidate));
}
private void checkPinnedVersion() {
runInFX(() -> {
if (!repository.isLoaded()) return;
pinnedVersions.removeIf(pinnedVersion -> !repository.hasVersion(pinnedVersion));
});
}
private void checkSelectedVersion() {
runInFX(() -> {
if (!repository.isLoaded()) return;
@ -190,6 +211,7 @@ public final class Profile implements Observable {
useRelativePath.addListener(listener);
global.get().addListener(listener);
selectedVersion.addListener(listener);
pinnedVersions.addListener(listener);
}
private ObservableHelper observableHelper = new ObservableHelper(this);
@ -237,7 +259,11 @@ public final class Profile implements Observable {
jsonObject.addProperty("gameDir", src.getGameDir().getPath());
jsonObject.addProperty("useRelativePath", src.isUseRelativePath());
jsonObject.addProperty("selectedMinecraftVersion", src.getSelectedVersion());
JsonArray jsonArray = new JsonArray();
for (String pinnedVersion : src.pinnedVersions) {
jsonArray.add(pinnedVersion);
}
jsonObject.add("pinnedVersions", jsonArray);
return jsonObject;
}
@ -251,7 +277,14 @@ public final class Profile implements Observable {
new File(gameDir),
context.deserialize(obj.get("global"), VersionSetting.class),
Optional.ofNullable(obj.get("selectedMinecraftVersion")).map(JsonElement::getAsString).orElse(""),
Optional.ofNullable(obj.get("useRelativePath")).map(JsonElement::getAsBoolean).orElse(false));
Optional.ofNullable(obj.get("useRelativePath")).map(JsonElement::getAsBoolean).orElse(false),
Optional.ofNullable(obj.get("pinnedVersions")).map(it -> {
if (it.isJsonArray()) {
return it.getAsJsonArray().asList().stream().map(JsonElement::getAsString).collect(Collectors.toList());
}
return null;
}).orElse(null)
);
}
}

View File

@ -104,7 +104,7 @@ public final class Profiles {
private static void checkProfiles() {
if (profiles.isEmpty()) {
Profile current = new Profile(Profiles.DEFAULT_PROFILE, new File(".minecraft"), new VersionSetting(), null, true);
Profile current = new Profile(Profiles.DEFAULT_PROFILE, new File(".minecraft"), new VersionSetting(), null, true, null);
Profile home = new Profile(Profiles.HOME_PROFILE, Metadata.MINECRAFT_DIRECTORY.toFile());
Platform.runLater(() -> profiles.addAll(current, home));
}

View File

@ -112,6 +112,7 @@ public enum SVG {
VISIBILITY_OFF("M16.1 13.3l-1.45-1.45q.225-1.175-.675-2.2t-2.325-.8L10.2 7.4q.425-.2.8625-.3T12 7q1.875 0 3.1875 1.3125T16.5 11.5q0 .5-.1.9375t-.3.8625Zm3.2 3.15-1.45-1.4q.95-.725 1.6875-1.5875T20.8 11.5q-1.25-2.525-3.5875-4.0125T12 6q-.725 0-1.425.1T9.2 6.4L7.65 4.85q1.025-.425 2.1-.6375T12 4q3.775 0 6.725 2.0875T23 11.5q-.575 1.475-1.5125 2.7375T19.3 16.45Zm.5 6.15-4.2-4.15q-.875.275-1.7625.4125T12 19q-3.775 0-6.725-2.0875T1 11.5q.525-1.325 1.325-2.4625T4.15 7L1.4 4.2 2.8 2.8 21.2 21.2l-1.4 1.4ZM5.55 8.4q-.725.65-1.325 1.425T3.2 11.5q1.25 2.525 3.5875 4.0125T12 17q.5 0 .975-.0625T13.95 16.8l-.9-.95q-.275.075-.525.1125T12 16q-1.875 0-3.1875-1.3125T7.5 11.5q0-.275.0375-.525T7.65 10.45L5.55 8.4Zm7.975 2.325ZM9.75 12.6Z"),
WARNING("M1 21 12 2 23 21H1ZM4.45 19H19.55L12 6 4.45 19ZM12 18Q12.425 18 12.7125 17.7125T13 17Q13 16.575 12.7125 16.2875T12 16Q11.575 16 11.2875 16.2875T11 17Q11 17.425 11.2875 17.7125T12 18ZM11 15H13V10H11V15ZM12 12.5Z"),
WB_SUNNY("M11 4V1H13V4H11ZM11 23V20H13V23H11ZM20 13V11H23V13H20ZM1 13V11H4V13H1ZM18.7 6.7 17.3 5.3 19.05 3.5 20.5 4.95 18.7 6.7ZM4.95 20.5 3.5 19.05 5.3 17.3 6.7 18.7 4.95 20.5ZM19.05 20.5 17.3 18.7 18.7 17.3 20.5 19.05 19.05 20.5ZM5.3 6.7 3.5 4.95 4.95 3.5 6.7 5.3 5.3 6.7ZM12 18Q9.5 18 7.75 16.25T6 12Q6 9.5 7.75 7.75T12 6Q14.5 6 16.25 7.75T18 12Q18 14.5 16.25 16.25T12 18ZM12 16Q13.675 16 14.8375 14.8375T16 12Q16 10.325 14.8375 9.1625T12 8Q10.325 8 9.1625 9.1625T8 12Q8 13.675 9.1625 14.8375T12 16ZM12 12Z"),
THUMBTACK("M 16.000004,10.999995 18,13.000006 v 1.999992 h -4.999999 v 6.000014 l -1,0.99999 L 11,21.000012 V 14.999998 H 6 V 13.000006 L 8.0000002,10.999995 V 3.9999876 H 6.9999991 V 2 H 16.999999 V 3.9999876 H 16.000004 Z M 8.8500016,13.000006 H 15.150002 L 14.000003,11.850004 V 3.9999876 h -4.000002 v 7.8500164 z m 3.1499994,0 z")
;
public static final double DEFAULT_SIZE = 24;

View File

@ -31,13 +31,11 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.layout.*;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.TextFlow;
import javafx.util.Duration;
@ -86,10 +84,14 @@ public final class MainPage extends StackPane implements DecoratorPage {
private final BooleanProperty showUpdate = new SimpleBooleanProperty(this, "showUpdate");
private final ObjectProperty<RemoteVersion> latestVersion = new SimpleObjectProperty<>(this, "latestVersion");
private final ObservableList<Version> versions = FXCollections.observableArrayList();
private final ObservableList<String> pinnedVersions = FXCollections.observableArrayList();
private final ObservableList<Node> versionNodes;
private final ObservableList<Node> pinnedVersionNodes;
private Profile profile;
private TransitionPane announcementPane;
private final VBox pinnedVersionsBox = new VBox(16);
private final ScrollPane pinnedVersionsScroll = new ScrollPane(pinnedVersionsBox);
private final StackPane updatePane;
private final JFXButton menuButton;
@ -105,7 +107,9 @@ public final class MainPage extends StackPane implements DecoratorPage {
state.setValue(new State(null, titleNode, false, false, true));
setPadding(new Insets(20));
pinnedVersionsScroll.setFitToWidth(true);
FXUtils.smoothScrolling(pinnedVersionsScroll);
pinnedVersionsBox.setPadding(new Insets(20, 20, 80, 20));
if (Metadata.isNightly() || (Metadata.isDev() && !Objects.equals(Metadata.VERSION, config().getShownTips().get(ANNOUNCEMENT)))) {
String title;
@ -148,9 +152,19 @@ public final class MainPage extends StackPane implements DecoratorPage {
announcementPane = new TransitionPane();
announcementPane.setContent(announcementBox, ContainerAnimations.NONE);
getChildren().add(announcementPane);
pinnedVersionsBox.getChildren().add(announcementPane);
}
this.pinnedVersionNodes = MappedObservableList.create(pinnedVersions, this::createQuickLaunch);
FlowPane pane = new FlowPane();
pane.setHgap(8);
pane.setVgap(8);
Bindings.bindContent(pane.getChildren(), pinnedVersionNodes);
Label text = new Label(i18n("version.pinned"));
text.setStyle("-fx-font-size: 15px; -fx-font-weight: bold");
text.visibleProperty().bind(Bindings.createBooleanBinding(() -> !pinnedVersions.isEmpty(), pinnedVersions));
pinnedVersionsBox.getChildren().addAll(text, pane);
updatePane = new StackPane();
updatePane.setVisible(false);
updatePane.getStyleClass().add("bubble");
@ -159,7 +173,6 @@ public final class MainPage extends StackPane implements DecoratorPage {
StackPane.setAlignment(updatePane, Pos.TOP_RIGHT);
FXUtils.onClicked(updatePane, this::onUpgrade);
FXUtils.onChange(showUpdateProperty(), this::showUpdate);
{
HBox hBox = new HBox();
hBox.setSpacing(12);
@ -266,9 +279,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
menuButton.addEventHandler(MouseEvent.MOUSE_CLICKED, secondaryClickHandle);
launchPane.getChildren().setAll(launchButton, separator, menuButton);
StackPane.setMargin(launchPane, new Insets(20));
}
getChildren().addAll(updatePane, launchPane);
getChildren().addAll(pinnedVersionsScroll, updatePane, launchPane);
menu.setMaxHeight(365);
menu.setMaxWidth(545);
@ -285,6 +299,38 @@ public final class MainPage extends StackPane implements DecoratorPage {
Bindings.bindContent(menu.getContent(), versionNodes);
}
private Node createQuickLaunch(String versionName) {
Version version = profile.getRepository().getVersion(versionName);
VBox card = new VBox();
card.getStyleClass().add("card");
card.setSpacing(16);
card.setPrefHeight(100);
card.setPrefWidth(100);
card.alignmentProperty().set(Pos.CENTER);
card.setCursor(Cursor.HAND);
ImageView image = new ImageView(profile.getRepository().getVersionIconImage(versionName));
image.setScaleX(1.5);
image.setScaleY(1.5);
image.setScaleZ(1.5);
Label name = new Label(version.getId());
VBox.setMargin(image, new Insets(4, 0, 0, 0));
VBox.setMargin(name, new Insets(4, 0, 0, 0));
name.setStyle("-fx-font-size: 13px; -fx-font-weight: bold");
card.getChildren().addAll(image, name);
card.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY && e.getClickCount() == 2) {
if (e.isShiftDown()) {
Versions.testGame(Profiles.getSelectedProfile(), versionName);
return;
}
Versions.launch(Profiles.getSelectedProfile(), versionName);
}
});
FXUtils.installFastTooltip(card, i18n("version.doubleClickToLaunch"));
return card;
}
private void showUpdate(boolean show) {
doAnimation(show);
@ -404,5 +450,9 @@ public final class MainPage extends StackPane implements DecoratorPage {
FXUtils.checkFxUserThread();
this.profile = profile;
this.versions.setAll(versions);
Bindings.bindContent(
this.pinnedVersions,
profile.getPinnedVersions()
);
}
}

View File

@ -62,6 +62,7 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
private final TabHeader.Tab<SchematicsPage> schematicsTab = new TabHeader.Tab<>("schematicsTab");
private final TransitionPane transitionPane = new TransitionPane();
private final BooleanProperty currentVersionUpgradable = new SimpleBooleanProperty();
private final BooleanProperty currentVersionPinned = new SimpleBooleanProperty();
private final ObjectProperty<Profile.ProfileVersion> version = new SimpleObjectProperty<>();
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
@ -140,6 +141,7 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
if (schematicsTab.isInitialized())
schematicsTab.getNode().loadVersion(profile, version);
currentVersionUpgradable.set(profile.getRepository().isModpack(version));
currentVersionPinned.set(profile.getPinnedVersions().contains(version));
}
private void onNavigated(Navigator.NavigationEvent event) {
@ -185,6 +187,16 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
Versions.testGame(getProfile(), getVersion());
}
private void pinVersion() {
if (currentVersionPinned.get()) {
currentVersionPinned.set(false);
getProfile().getPinnedVersions().remove(getVersion());
return;
}
currentVersionPinned.set(true);
getProfile().getPinnedVersions().add(getVersion());
}
private void updateGame() {
Versions.updateVersion(getProfile(), getVersion());
}
@ -326,6 +338,15 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
.addNavigationDrawerItem(i18n("version.update"), SVG.UPDATE, control::updateGame, upgradeItem -> {
upgradeItem.visibleProperty().bind(control.currentVersionUpgradable);
})
.addNavigationDrawerItem("", SVG.THUMBTACK, control::pinVersion, pinVersionItem -> {
pinVersionItem.titleProperty().bind(
Bindings.createStringBinding(
() -> control.currentVersionPinned.get() ? i18n("version.unpin") : i18n("version.pin"),
control.currentVersionPinned
)
);
pinVersionItem.activeProperty().bindBidirectional(control.currentVersionPinned);
})
.addNavigationDrawerItem(i18n("version.launch.test"), SVG.ROCKET_LAUNCH, control::testGame)
.addNavigationDrawerItem(i18n("settings.game.exploration"), SVG.FOLDER_OPEN, null, browseMenuItem -> {
browseMenuItem.setOnAction(e -> browsePopup.show(browseMenuItem, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, browseMenuItem.getWidth(), 0));
@ -334,7 +355,7 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
managementItem.setOnAction(e -> managementPopup.show(managementItem, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, managementItem.getWidth(), 0));
});
toolbar.getStyleClass().add("advanced-list-box-clear-padding");
FXUtils.setLimitHeight(toolbar, 40 * 4 + 12 * 2);
FXUtils.setLimitHeight(toolbar, 40 * 5 + 12 * 2);
setLeft(sideBar, toolbar);
}

View File

@ -1452,6 +1452,10 @@ version.manage.rename.message=Enter New Instance Name
version.manage.rename.fail=Failed to rename the instance. Some files might be in use, or the name contains an invalid character.
version.settings=Settings
version.update=Update Modpack
version.pin=Pin Instance
version.unpin=Unpin Instance
version.doubleClickToLaunch=Double Click to Launch Instance
version.pinned=Pinned Instances
wiki.tooltip=Minecraft Wiki Page
wiki.version.game.release=https://minecraft.wiki/w/Java_Edition_%s

View File

@ -1245,6 +1245,10 @@ version.manage.rename.message=請輸入新名稱
version.manage.rename.fail=重新命名實例失敗,可能檔案被佔用或者名稱有特殊字元。
version.settings=遊戲設定
version.update=更新模組包
version.pin=固定實例
version.unpin=取消固定實例
version.doubleClickToLaunch=雙擊啟動實例
version.pinned=已固定的實例
wiki.tooltip=Minecraft Wiki 頁面
wiki.version.game.release=https://zh.minecraft.wiki/w/Java%%E7%%89%%88%s?variant=zh-tw

View File

@ -1255,6 +1255,10 @@ version.manage.rename.message=请输入要修改的名称
version.manage.rename.fail=重命名版本失败,可能文件被占用或者名字有特殊字符。
version.settings=游戏设置
version.update=更新整合包
version.pin=固定版本
version.unpin=取消固定版本
version.doubleClickToLaunch=双击启动该版本
version.pinned=已固定的版本
wiki.tooltip=Minecraft Wiki 页面
wiki.version.game.release=https://zh.minecraft.wiki/w/Java%%E7%%89%%88%s?variant=zh-cn