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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,21 @@ public MapProperty<String, Profile> getConfigurations() {
return configurations;
}

@SerializedName("saveCustomGameIcons")
private final ObjectProperty<EnumAskable> saveCustomGameIcons = new SimpleObjectProperty<>(EnumAskable.ASK);

public ObjectProperty<EnumAskable> saveCustomGameIconsProperty() {
return saveCustomGameIcons;
}

public EnumAskable getSaveCustomGameIcons() {
return saveCustomGameIcons.get();
}

public void setSaveCustomGameIcons(EnumAskable saveCustomGameIcons) {
this.saveCustomGameIcons.set(saveCustomGameIcons);
}

public static final class Adapter extends ObservableSetting.Adapter<Config> {
@Override
protected Config createInstance() {
Expand Down
22 changes: 22 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/setting/EnumAskable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;

public enum EnumAskable {
TRUE, FALSE, ASK
}
17 changes: 17 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui;

import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.animation.KeyFrame;
Expand Down Expand Up @@ -77,6 +78,7 @@
import java.time.LocalDate;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;

import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
Expand Down Expand Up @@ -558,6 +560,21 @@ public static void confirmWithCountdown(String text, String title, int seconds,
timeline.play();
}

/// @param consumer Consumer for the result, with the first boolean for yes or no and the second for whether no more asking is needed
/// @see EnumAskable
public static void ask(String text, String title, BiConsumer<Boolean, Boolean> consumer) {
var check = new JFXCheckBox(i18n("button.do_not_show_again"));
var dialog = new MessageDialogPane.Builder(
text,
title,
MessageDialogPane.MessageType.QUESTION
)
.addActionNoClosing(check)
.yesOrNo(() -> consumer.accept(true, check.isSelected()), () -> consumer.accept(false, check.isSelected()))
.build();
dialog(dialog);
}

public static CompletableFuture<String> prompt(String title, FutureCallback<String> onResult) {
return prompt(title, onResult, "");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@ public Builder addAction(String text, @Nullable Runnable action) {
return this;
}

public Builder addActionNoClosing(Node actionNode) {
dialog.actions.getChildren().add(actionNode);
return this;
}

public Builder ok(@Nullable Runnable ok) {
JFXButton btnOk = new JFXButton(i18n("button.ok"));
btnOk.getStyleClass().add("dialog-accept");
Expand Down
15 changes: 15 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import javafx.scene.layout.*;
import javafx.scene.text.TextAlignment;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.EnumAskable;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
Expand Down Expand Up @@ -269,6 +270,20 @@ else if (locale.isSameLanguage(currentLocale))
settingsPane.getContent().add(allowAutoAgentPane);
}

{
LineSelectButton<EnumAskable> saveCustomGameIconsPane = new LineSelectButton<>();
saveCustomGameIconsPane.setTitle(i18n("settings.launcher.save_custom_game_icons"));
saveCustomGameIconsPane.setConverter(a -> switch (a) {
case ASK -> i18n("message.ask");
case TRUE -> i18n("button.yes");
case FALSE -> i18n("button.no");
});
saveCustomGameIconsPane.setItems(EnumAskable.values());
saveCustomGameIconsPane.valueProperty().bindBidirectional(config().saveCustomGameIconsProperty());

settingsPane.getContent().add(saveCustomGameIconsPane);
}

{
BorderPane debugPane = new BorderPane();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@
import javafx.scene.image.ImageView;
import javafx.scene.layout.FlowPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
Expand All @@ -33,12 +32,17 @@
import org.jackhuang.hmcl.util.io.FileUtils;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Objects;

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

public class VersionIconDialog extends DialogPane {
public static final Path GAME_ICONS_DIR = Metadata.HMCL_CURRENT_DIRECTORY.resolve("game_icons");

private final Profile profile;
private final String versionId;
private final Runnable onFinish;
Expand Down Expand Up @@ -71,24 +75,67 @@ public VersionIconDialog(Profile profile, String versionId, Runnable onFinish) {
createIcon(VersionIconType.FURNACE),
createIcon(VersionIconType.QUILT)
);

if (Files.isDirectory(GAME_ICONS_DIR)) {
try (var stream = Files.list(GAME_ICONS_DIR)) {
pane.getChildren().addAll(
stream.filter(p -> Files.isRegularFile(p) && FXUtils.IMAGE_EXTENSIONS.contains(FileUtils.getExtension(p).toLowerCase(Locale.ROOT)))
.map(this::createIcon)
.filter(Objects::nonNull)
.toList()
);
} catch (Exception e) {
LOG.warning("Failed to load custom game icons", e);
}
}
}

private void exploreIcon() {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().add(FXUtils.getImageExtensionFilter());
Path selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
if (selectedFile != null) {
try {
profile.getRepository().setVersionIconFile(versionId, selectedFile);
EnumAskable saveOption = ConfigHolder.config().getSaveCustomGameIcons();
if (saveOption == EnumAskable.ASK && !GAME_ICONS_DIR.equals(selectedFile.getParent())) {
Controllers.ask(
i18n("settings.icon.save_custom"),
i18n("message.question"),
(res, doNotAsk) -> {
if (doNotAsk) ConfigHolder.config().setSaveCustomGameIcons(res ? EnumAskable.TRUE : EnumAskable.FALSE);
setCustomIcon(selectedFile, res);
}
);
} else {
setCustomIcon(selectedFile, saveOption == EnumAskable.TRUE);
}
}
}

if (vs != null) {
vs.setVersionIcon(VersionIconType.DEFAULT);
private void setCustomIcon(Path selectedFile, boolean save) {
try {
Path dest;
if (GAME_ICONS_DIR.equals(selectedFile.getParent()) || !save) {
dest = selectedFile;
} else {
dest = GAME_ICONS_DIR.resolve(selectedFile.getFileName());
int i = 1;
String name = FileUtils.getNameWithoutExtension(selectedFile);
String ext = FileUtils.getExtension(selectedFile);
while (Files.exists(dest)) {
dest = GAME_ICONS_DIR.resolve(name + " " + i + "." + ext);
i++;
}
FileUtils.copyFile(selectedFile, dest);
}
profile.getRepository().setVersionIconFile(versionId, dest);

onAccept();
} catch (IOException | IllegalArgumentException e) {
LOG.error("Failed to set icon file: " + selectedFile, e);
if (vs != null) {
vs.setVersionIcon(VersionIconType.DEFAULT);
}

onAccept();
} catch (IOException | IllegalArgumentException e) {
LOG.error("Failed to set icon file: " + selectedFile, e);
}
}

Expand Down Expand Up @@ -117,6 +164,33 @@ private Node createIcon(VersionIconType type) {
return container;
}

private Node createIcon(Path path) {
ImageView imageView;
try {
imageView = new ImageView(FXUtils.loadImage(path, 72, 72, true, false));
} catch (Exception e) {
LOG.warning("Failed to load custom game icon: " + path, e);
return null;
}
imageView.setMouseTransparent(true);
FXUtils.limitSize(imageView, 36, 36);
RipplerContainer container = new RipplerContainer(imageView);
FXUtils.setLimitWidth(container, 36);
FXUtils.setLimitHeight(container, 36);
FXUtils.onClicked(container, () -> {
try {
profile.getRepository().setVersionIconFile(versionId, path);
} catch (IOException e) {
LOG.error("Failed to set icon file: " + path, e);
}
if (vs != null) {
vs.setVersionIcon(VersionIconType.DEFAULT);
onAccept();
}
});
return container;
}

@Override
protected void onAccept() {
profile.getRepository().onVersionIconChanged.fireEvent(new Event(this));
Expand Down
3 changes: 3 additions & 0 deletions HMCL/src/main/resources/assets/lang/I18N.properties
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,7 @@ logwindow.export_dump.no_dependency=Your Java does not contain the dependencies

main_page=Home

message.ask=Ask
message.cancelled=Operation Canceled
message.confirm=Confirm
message.copied=Copied to clipboard
Expand Down Expand Up @@ -1422,6 +1423,7 @@ settings.game.working_directory.hint=Enable the "Isolated" option in "Working Di
It is recommended to enable this option to avoid mod conflicts, but you will need to move your worlds manually.

settings.icon=Icon
settings.icon.save_custom=Save custom icon?

settings.launcher=Launcher Settings
settings.launcher.allow_auto_agent=Allow HMCL to modify the game
Expand Down Expand Up @@ -1465,6 +1467,7 @@ settings.launcher.proxy.password=Password
settings.launcher.proxy.port=Port
settings.launcher.proxy.socks=SOCKS
settings.launcher.proxy.username=Username
settings.launcher.save_custom_game_icons=Save Custom Game Icons
settings.launcher.theme=Theme Color
settings.launcher.title_transparent=Transparent Titlebar
settings.launcher.turn_off_animations=Disable Animation
Expand Down
3 changes: 3 additions & 0 deletions HMCL/src/main/resources/assets/lang/I18N_zh.properties
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,7 @@ logwindow.export_dump.no_dependency=你的 Java 不包含用於建立遊戲執

main_page=首頁

message.ask=詢問
message.cancelled=操作被取消
message.confirm=提示
message.copied=已複製到剪貼簿
Expand Down Expand Up @@ -1207,6 +1208,7 @@ settings.game.working_directory.choose=選取執行目錄
settings.game.working_directory.hint=在「執行路徑」選項中選取「各實例獨立」使目前實例獨立存放設定、世界、模組等資料。使用模組時建議開啟此選項以避免不同實例模組衝突。修改此選項後需自行移動世界等檔案。

settings.icon=遊戲圖示
settings.icon.save_custom=是否要保存自定義遊戲圖示?

settings.launcher=啟動器設定
settings.launcher.allow_auto_agent=允許 HMCL 修改遊戲
Expand Down Expand Up @@ -1250,6 +1252,7 @@ settings.launcher.proxy.password=密碼
settings.launcher.proxy.port=連線埠
settings.launcher.proxy.socks=SOCKS
settings.launcher.proxy.username=帳戶
settings.launcher.save_custom_game_icons=保存自定義遊戲圖標
settings.launcher.theme=主題色
settings.launcher.title_transparent=標題欄透明
settings.launcher.turn_off_animations=關閉動畫
Expand Down
3 changes: 3 additions & 0 deletions HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,7 @@ logwindow.export_dump.no_dependency=你的 Java 不包含用于创建游戏运

main_page=主页

message.ask=询问
message.cancelled=操作被取消
message.confirm=提示
message.copied=已复制到剪贴板
Expand Down Expand Up @@ -1212,6 +1213,7 @@ settings.game.working_directory.choose=选择运行文件夹
settings.game.working_directory.hint=在“版本隔离”中选择“各实例独立”使当前实例独立存放设置、世界、模组等数据。使用模组时建议启用此选项以避免不同实例模组冲突。修改此选项后需自行移动世界等文件。

settings.icon=游戏图标
settings.icon.save_custom=是否要保存自定义游戏图标?

settings.launcher=启动器设置
settings.launcher.allow_auto_agent=允许 HMCL 修改游戏
Expand Down Expand Up @@ -1255,6 +1257,7 @@ settings.launcher.proxy.password=密码
settings.launcher.proxy.port=端口
settings.launcher.proxy.socks=SOCKS
settings.launcher.proxy.username=账户
settings.launcher.save_custom_game_icons=保存自定义游戏图标
settings.launcher.theme=主题色
settings.launcher.title_transparent=标题栏透明
settings.launcher.turn_off_animations=关闭动画
Expand Down