diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index 45be78ddd..899fc1262 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -20,6 +20,7 @@ package org.jackhuang.hmcl.game; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import javafx.scene.image.Image; +import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.setting.EnumGameDirectory; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.VersionSetting; @@ -36,7 +37,9 @@ import static org.jackhuang.hmcl.ui.FXUtils.newImage; public class HMCLGameRepository extends DefaultGameRepository { private final Profile profile; - private final Map versionSettings = new HashMap<>(); + + // local version settings + private final Map localVersionSettings = new HashMap<>(); private final Set beingModpackVersions = new HashSet<>(); public boolean checkedModpack = false, checkingModpack = false; @@ -55,7 +58,7 @@ public class HMCLGameRepository extends DefaultGameRepository { if (beingModpackVersions.contains(id) || isModpack(id)) return getVersionRoot(id); else { - VersionSetting vs = profile.getVersionSetting(id); + VersionSetting vs = getVersionSetting(id); switch (vs.getGameDirType()) { case VERSION_FOLDER: return getVersionRoot(id); case ROOT_FOLDER: return super.getRunDirectory(id); @@ -67,9 +70,9 @@ public class HMCLGameRepository extends DefaultGameRepository { @Override protected void refreshVersionsImpl() { - versionSettings.clear(); + localVersionSettings.clear(); super.refreshVersionsImpl(); - versions.keySet().forEach(this::loadVersionSetting); + versions.keySet().forEach(this::loadLocalVersionSetting); try { File file = new File(getBaseDirectory(), "launcher_profiles.json"); @@ -95,19 +98,55 @@ public class HMCLGameRepository extends DefaultGameRepository { clean(getRunDirectory(id)); } - private File getVersionSettingFile(String id) { + public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException { + File srcDir = getVersionRoot(srcId); + File dstDir = getVersionRoot(dstId); + + if (dstDir.exists()) throw new IOException("Version exists"); + FileUtils.copyDirectory(srcDir.toPath(), dstDir.toPath()); + VersionSetting oldVersionSetting = getVersionSetting(srcId).clone(); + EnumGameDirectory originalGameDirType = oldVersionSetting.getGameDirType(); + oldVersionSetting.setUsesGlobal(false); + oldVersionSetting.setGameDirType(EnumGameDirectory.VERSION_FOLDER); + VersionSetting newVersionSetting = initLocalVersionSetting(dstId, oldVersionSetting); + saveVersionSetting(dstId); + + File srcGameDir = getRunDirectory(srcId); + File dstGameDir = getRunDirectory(dstId); + + List blackList = new ArrayList<>(Arrays.asList( + "regex:(.*?)\\.log", + "usernamecache.json", "usercache.json", // Minecraft + "launcher_profiles.json", "launcher.pack.lzma", // Minecraft Launcher + "backup", "pack.json", "launcher.jar", "cache", // HMCL + ".curseclient", // Curse + ".fabric", ".mixin.out", // Fabric + "jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft + "downloads", // Curse + "asm", "backups", "TCNodeTracker", "CustomDISkins", "data", "CustomSkinLoader/caches" // Mods + )); + blackList.add(srcId + ".jar"); + blackList.add(srcId + ".json"); + if (!copySaves) + blackList.add("saves"); + + if (originalGameDirType != EnumGameDirectory.VERSION_FOLDER) + FileUtils.copyDirectory(srcGameDir.toPath(), dstGameDir.toPath(), path -> Modpack.acceptFile(path, blackList, null)); + } + + private File getLocalVersionSettingFile(String id) { return new File(getVersionRoot(id), "hmclversion.cfg"); } - private void loadVersionSetting(String id) { - File file = getVersionSettingFile(id); + private void loadLocalVersionSetting(String id) { + File file = getLocalVersionSettingFile(id); if (file.exists()) try { VersionSetting versionSetting = GSON.fromJson(FileUtils.readText(file), VersionSetting.class); - initVersionSetting(id, versionSetting); + initLocalVersionSetting(id, versionSetting); } catch (Exception ex) { // If [JsonParseException], [IOException] or [NullPointerException] happens, the json file is malformed and needed to be recreated. - initVersionSetting(id, new VersionSetting()); + initLocalVersionSetting(id, new VersionSetting()); } } @@ -116,18 +155,18 @@ public class HMCLGameRepository extends DefaultGameRepository { * @param id the version id. * @return new version setting, null if given version does not exist. */ - public VersionSetting createVersionSetting(String id) { + public VersionSetting createLocalVersionSetting(String id) { if (!hasVersion(id)) return null; - if (versionSettings.containsKey(id)) - return getVersionSetting(id); + if (localVersionSettings.containsKey(id)) + return getLocalVersionSetting(id); else - return initVersionSetting(id, new VersionSetting()); + return initLocalVersionSetting(id, new VersionSetting()); } - private VersionSetting initVersionSetting(String id, VersionSetting vs) { + private VersionSetting initLocalVersionSetting(String id, VersionSetting vs) { + localVersionSettings.put(id, vs); vs.addPropertyChangedListener(a -> saveVersionSetting(id)); - versionSettings.put(id, vs); return vs; } @@ -136,17 +175,27 @@ public class HMCLGameRepository extends DefaultGameRepository { * * @param id version id * - * @return may return null if the id not exists + * @return corresponding version setting, null if the version has no its own version setting. */ - public VersionSetting getVersionSetting(String id) { - if (!versionSettings.containsKey(id)) - loadVersionSetting(id); - VersionSetting setting = versionSettings.get(id); + public VersionSetting getLocalVersionSetting(String id) { + if (!localVersionSettings.containsKey(id)) + loadLocalVersionSetting(id); + VersionSetting setting = localVersionSettings.get(id); if (setting != null && isModpack(id)) setting.setGameDirType(EnumGameDirectory.VERSION_FOLDER); return setting; } + public VersionSetting getVersionSetting(String id) { + VersionSetting vs = getLocalVersionSetting(id); + if (vs == null || vs.isUsesGlobal()) { + profile.getGlobal().setGlobal(true); // always keep global.isGlobal = true + profile.getGlobal().setUsesGlobal(true); + return profile.getGlobal(); + } else + return vs; + } + public File getVersionIconFile(String id) { return new File(getVersionRoot(id), "icon.png"); } @@ -169,14 +218,14 @@ public class HMCLGameRepository extends DefaultGameRepository { } public boolean saveVersionSetting(String id) { - if (!versionSettings.containsKey(id)) + if (!localVersionSettings.containsKey(id)) return false; - File file = getVersionSettingFile(id); + File file = getLocalVersionSettingFile(id); if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile())) return false; try { - FileUtils.writeText(file, GSON.toJson(versionSettings.get(id))); + FileUtils.writeText(file, GSON.toJson(localVersionSettings.get(id))); return true; } catch (IOException e) { Logging.LOG.log(Level.SEVERE, "Unable to save version setting of " + id, e); @@ -190,9 +239,9 @@ public class HMCLGameRepository extends DefaultGameRepository { * @return specialized version setting, null if given version does not exist. */ public VersionSetting specializeVersionSetting(String id) { - VersionSetting vs = getVersionSetting(id); + VersionSetting vs = getLocalVersionSetting(id); if (vs == null) - vs = createVersionSetting(id); + vs = createLocalVersionSetting(id); if (vs == null) return null; vs.setUsesGlobal(false); @@ -200,7 +249,7 @@ public class HMCLGameRepository extends DefaultGameRepository { } public void globalizeVersionSetting(String id) { - VersionSetting vs = getVersionSetting(id); + VersionSetting vs = getLocalVersionSetting(id); if (vs != null) vs.setUsesGlobal(true); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java index 00a3aa575..0dc9afc05 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java @@ -166,13 +166,7 @@ public final class Profile implements Observable { } public VersionSetting getVersionSetting(String id) { - VersionSetting vs = repository.getVersionSetting(id); - if (vs == null || vs.isUsesGlobal()) { - getGlobal().setGlobal(true); // always keep global.isGlobal = true - getGlobal().setUsesGlobal(true); - return getGlobal(); - } else - return vs; + return repository.getVersionSetting(id); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java index cb206b6ce..a15a41e9a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java @@ -45,7 +45,7 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config; * @author huangyuhui */ @JsonAdapter(VersionSetting.Serializer.class) -public final class VersionSetting { +public final class VersionSetting implements Cloneable { public transient String id; @@ -563,6 +563,34 @@ public final class VersionSetting { return builder.create(); } + @Override + public VersionSetting clone() { + VersionSetting versionSetting = new VersionSetting(); + versionSetting.setUsesGlobal(isUsesGlobal()); + versionSetting.setJava(getJava()); + versionSetting.setDefaultJavaPath(getDefaultJavaPath()); + versionSetting.setJavaDir(getJavaDir()); + versionSetting.setWrapper(getWrapper()); + versionSetting.setPermSize(getPermSize()); + versionSetting.setMaxMemory(getMaxMemory()); + versionSetting.setMinMemory(getMinMemory()); + versionSetting.setPreLaunchCommand(getPreLaunchCommand()); + versionSetting.setJavaArgs(getJavaArgs()); + versionSetting.setMinecraftArgs(getMinecraftArgs()); + versionSetting.setNoJVMArgs(isNoJVMArgs()); + versionSetting.setNotCheckGame(isNotCheckGame()); + versionSetting.setNotCheckJVM(isNotCheckJVM()); + versionSetting.setShowLogs(isShowLogs()); + versionSetting.setServerIp(getServerIp()); + versionSetting.setFullscreen(isFullscreen()); + versionSetting.setWidth(getWidth()); + versionSetting.setHeight(getHeight()); + versionSetting.setGameDirType(getGameDirType()); + versionSetting.setGameDir(getGameDir()); + versionSetting.setLauncherVisibility(getLauncherVisibility()); + return versionSetting; + } + public static class Serializer implements JsonSerializer, JsonDeserializer { @Override public JsonElement serialize(VersionSetting src, Type typeOfSrc, JsonSerializationContext context) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index 88068bc4a..89da51ab4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -31,6 +31,7 @@ import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.construct.InputDialogPane; import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType; +import org.jackhuang.hmcl.ui.construct.PromptDialogPane; import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane; import org.jackhuang.hmcl.ui.decorator.DecoratorController; import org.jackhuang.hmcl.ui.main.RootPage; @@ -40,6 +41,7 @@ import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.platform.JavaVersion; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; @@ -143,14 +145,19 @@ public final class Controllers { dialog(new MessageDialogPane(text, title, onAccept, onCancel)); } - public static CompletableFuture prompt(String text, FutureCallback onResult) { - return prompt(text, onResult, ""); + public static CompletableFuture prompt(String title, FutureCallback onResult) { + return prompt(title, onResult, ""); } - public static CompletableFuture prompt(String text, FutureCallback onResult, String initialValue) { - InputDialogPane pane = new InputDialogPane(text, onResult); + public static CompletableFuture prompt(String title, FutureCallback onResult, String initialValue) { + InputDialogPane pane = new InputDialogPane(title, initialValue, onResult); + dialog(pane); + return pane.getCompletableFuture(); + } + + public static CompletableFuture>> prompt(PromptDialogPane.Builder builder) { + PromptDialogPane pane = new PromptDialogPane(builder); dialog(pane); - pane.setInitialValue(initialValue); return pane.getCompletableFuture(); } 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 790f4dcbc..534022c20 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -67,6 +67,10 @@ public final class SVG { return createSVGPath("M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z", fill, width, height); } + public static Node copy(ObjectBinding fill, double width, double height) { + return createSVGPath("M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z", fill, width, height); + } + public static Node dotsVertical(ObjectBinding fill, double width, double height) { return createSVGPath("M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z", fill, width, height); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/InputDialogPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/InputDialogPane.java index 9e7c9de3e..791c3739c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/InputDialogPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/InputDialogPane.java @@ -19,10 +19,10 @@ package org.jackhuang.hmcl.ui.construct; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXTextField; -import javafx.beans.binding.Bindings; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.util.FutureCallback; @@ -36,17 +36,20 @@ public class InputDialogPane extends StackPane { @FXML private JFXButton cancelButton; @FXML - private JFXTextField textField; + private Label title; @FXML - private Label content; + private VBox vbox; @FXML private Label lblCreationWarning; @FXML private SpinnerPane acceptPane; - public InputDialogPane(String text, FutureCallback onResult) { + public InputDialogPane(String text, String initialValue, FutureCallback onResult) { FXUtils.loadFXML(this, "/assets/fxml/input-dialog.fxml"); - content.setText(text); + title.setText(text); + JFXTextField textField = new JFXTextField(); + textField.setText(initialValue); + vbox.getChildren().setAll(textField); cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent())); acceptButton.setOnMouseClicked(e -> { acceptPane.showSpinner(); @@ -60,15 +63,6 @@ public class InputDialogPane extends StackPane { lblCreationWarning.setText(msg); }); }); - - acceptButton.disableProperty().bind(Bindings.createBooleanBinding( - () -> !textField.validate(), - textField.textProperty() - )); - } - - public void setInitialValue(String value) { - textField.setText(value); } public CompletableFuture getCompletableFuture() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/PromptDialogPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/PromptDialogPane.java new file mode 100644 index 000000000..7d1512d58 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/PromptDialogPane.java @@ -0,0 +1,157 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2020 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.ui.construct; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXCheckBox; +import com.jfoenix.controls.JFXTextField; +import com.jfoenix.validation.base.ValidatorBase; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.fxml.FXML; +import javafx.geometry.Insets; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.util.FutureCallback; +import org.jackhuang.hmcl.util.StringUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class PromptDialogPane extends StackPane { + private final CompletableFuture>> future = new CompletableFuture<>(); + + @FXML + private JFXButton acceptButton; + @FXML + private JFXButton cancelButton; + @FXML + private VBox vbox; + @FXML + private Label title; + @FXML + private Label lblCreationWarning; + @FXML + private SpinnerPane acceptPane; + + public PromptDialogPane(Builder builder) { + FXUtils.loadFXML(this, "/assets/fxml/input-dialog.fxml"); + this.title.setText(builder.title); + + List bindings = new ArrayList<>(); + for (Builder.Question question : builder.questions) { + if (question instanceof Builder.StringQuestion) { + JFXTextField textField = new JFXTextField(); + textField.textProperty().addListener((a, b, newValue) -> ((Builder.StringQuestion) question).value = textField.getText()); + textField.setText(((Builder.StringQuestion) question).value); + textField.setValidators(((Builder.StringQuestion) question).validators.toArray(new ValidatorBase[0])); + bindings.add(Bindings.createBooleanBinding(textField::validate, textField.textProperty())); + + if (StringUtils.isNotBlank(question.question)) { + vbox.getChildren().add(new Label(question.question)); + } + VBox.setMargin(textField, new Insets(0, 0, 20, 0)); + vbox.getChildren().add(textField); + } else if (question instanceof Builder.BooleanQuestion) { + HBox hBox = new HBox(); + JFXCheckBox checkBox = new JFXCheckBox(); + hBox.getChildren().setAll(checkBox); + HBox.setMargin(checkBox, new Insets(0, 0, 0, -10)); + checkBox.setSelected(((Builder.BooleanQuestion) question).value); + checkBox.selectedProperty().addListener((a, b, newValue) -> ((Builder.BooleanQuestion) question).value = newValue); + checkBox.setText(question.question); + vbox.getChildren().add(hBox); + } + } + + cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent())); + acceptButton.disableProperty().bind(Bindings.createBooleanBinding( + () -> bindings.stream().map(BooleanBinding::get).anyMatch(x -> !x), + bindings.toArray(new BooleanBinding[0]) + )); + + acceptButton.setOnMouseClicked(e -> { + acceptPane.showSpinner(); + + builder.callback.call(builder.questions, () -> { + acceptPane.hideSpinner(); + future.complete(builder.questions); + fireEvent(new DialogCloseEvent()); + }, msg -> { + acceptPane.hideSpinner(); + lblCreationWarning.setText(msg); + }); + }); + } + + public CompletableFuture>> getCompletableFuture() { + return future; + } + + public static class Builder { + private final List> questions = new ArrayList<>(); + private final String title; + private final FutureCallback>> callback; + + public Builder(String title, FutureCallback>> callback) { + this.title = title; + this.callback = callback; + } + + public Builder addQuestion(Question question) { + questions.add(question); + return this; + } + + public static class Question { + public final String question; + protected T value; + + public Question(String question) { + this.question = question; + } + + public T getValue() { + return value; + } + } + + public static class StringQuestion extends Question { + protected final List validators; + + public StringQuestion(String question, String defaultValue, ValidatorBase... validators) { + super(question); + this.value = defaultValue; + this.validators = Arrays.asList(validators); + } + } + + public static class BooleanQuestion extends Question { + + public BooleanQuestion(String question, boolean defaultValue) { + super(question); + this.value = defaultValue; + } + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java index 89e0f2ee6..79820e5c9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java @@ -69,6 +69,10 @@ public class GameListItem extends Control { Versions.renameVersion(profile, version); } + public void duplicate() { + Versions.duplicateVersion(profile, version); + } + public void remove() { Versions.deleteVersion(profile, version); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java index 93b95971a..a23336dc7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java @@ -69,6 +69,7 @@ public class GameListItemSkin extends SkinBase { new IconedMenuItem(FXUtils.limitingSize(SVG.gear(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.manage"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.modifyGameSettings(), popup)), new MenuSeparator(), new IconedMenuItem(FXUtils.limitingSize(SVG.pencil(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.rename"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.rename(), popup)), + new IconedMenuItem(FXUtils.limitingSize(SVG.copy(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.duplicate"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.duplicate(), popup)), new IconedMenuItem(FXUtils.limitingSize(SVG.delete(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.remove(), popup)), new IconedMenuItem(FXUtils.limitingSize(SVG.export(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("modpack.export"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.export(), popup)), new MenuSeparator(), 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 0a1ebc1c9..b77852622 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 @@ -192,6 +192,10 @@ public class VersionPage extends Control implements DecoratorPage { Versions.deleteVersion(profile, version); } + private void duplicate() { + Versions.duplicateVersion(profile, version); + } + @Override protected Skin createDefaultSkin() { return new Skin(this); @@ -228,6 +232,9 @@ public class VersionPage extends Control implements DecoratorPage { new IconedMenuItem(FXUtils.limitingSize(SVG.pencil(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.rename"), FXUtils.withJFXPopupClosing(() -> { Versions.renameVersion(getSkinnable().profile, currentVersion).thenApply(name -> getSkinnable().preferredVersionName = name); }, listViewItemPopup)), + new IconedMenuItem(FXUtils.limitingSize(SVG.copy(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.duplicate"), FXUtils.withJFXPopupClosing(() -> { + Versions.duplicateVersion(getSkinnable().profile, currentVersion); + }, listViewItemPopup)), new IconedMenuItem(FXUtils.limitingSize(SVG.delete(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(() -> { Versions.deleteVersion(getSkinnable().profile, currentVersion); }, listViewItemPopup)), @@ -313,6 +320,7 @@ public class VersionPage extends Control implements DecoratorPage { new IconedMenuItem(FXUtils.limitingSize(SVG.script(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch_script"), FXUtils.withJFXPopupClosing(control::generateLaunchScript, managementPopup)), new MenuSeparator(), new IconedMenuItem(FXUtils.limitingSize(SVG.pencil(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.rename"), FXUtils.withJFXPopupClosing(control::rename, managementPopup)), + new IconedMenuItem(FXUtils.limitingSize(SVG.copy(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.duplicate"), FXUtils.withJFXPopupClosing(control::duplicate, managementPopup)), new IconedMenuItem(FXUtils.limitingSize(SVG.delete(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(control::remove, managementPopup)), new IconedMenuItem(FXUtils.limitingSize(SVG.export(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("modpack.export"), FXUtils.withJFXPopupClosing(control::export, managementPopup)), new MenuSeparator(), diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java index fb267e306..2aa7f5591 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java @@ -24,12 +24,17 @@ import org.jackhuang.hmcl.game.LauncherHelper; import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.setting.EnumGameDirectory; import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.TaskExecutor; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.construct.PromptDialogPane; +import org.jackhuang.hmcl.ui.construct.Validator; import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; import org.jackhuang.hmcl.ui.export.ExportWizardProvider; import org.jackhuang.hmcl.util.Logging; +import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.platform.OperatingSystem; @@ -72,6 +77,27 @@ public class Versions { FXUtils.openFolder(profile.getRepository().getRunDirectory(version)); } + public static void duplicateVersion(Profile profile, String version) { + Controllers.prompt( + new PromptDialogPane.Builder(i18n("version.manage.duplicate.prompt"), (res, resolve, reject) -> { + String newVersionName = ((PromptDialogPane.Builder.StringQuestion) res.get(0)).getValue(); + boolean copySaves = ((PromptDialogPane.Builder.BooleanQuestion) res.get(1)).getValue(); + Task.runAsync(() -> profile.getRepository().duplicateVersion(version, newVersionName, copySaves)) + .thenComposeAsync(profile.getRepository().refreshVersionsAsync()) + .whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception == null) { + resolve.run(); + } else { + reject.accept(StringUtils.getStackTrace(exception)); + profile.getRepository().removeVersionFromDisk(newVersionName); + } + }).start(); + }) + .addQuestion(new PromptDialogPane.Builder.StringQuestion(i18n("version.manage.duplicate.confirm"), version, + new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().hasVersion(newVersionName)))) + .addQuestion(new PromptDialogPane.Builder.BooleanQuestion(i18n("version.manage.duplicate.duplicate_save"), false))); + } + public static void updateVersion(Profile profile, String version) { Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(profile, version)); } diff --git a/HMCL/src/main/resources/assets/fxml/input-dialog.fxml b/HMCL/src/main/resources/assets/fxml/input-dialog.fxml index 3cd8215bc..f8ad63db1 100644 --- a/HMCL/src/main/resources/assets/fxml/input-dialog.fxml +++ b/HMCL/src/main/resources/assets/fxml/input-dialog.fxml @@ -6,15 +6,20 @@ + + - - + +