Compare commits

..
Author SHA1 Message Date
Glavoand辞庐 e57a41c2f3 [release/3.11] 迁移微软授权代码流 client_secret 到 PKCE (#5649)
https://github.com/HMCL-dev/HMCL/pull/5575

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2026-02-25 21:14:39 +08:00
Glavo d63b0bb088 [release/3.11] 优化设备代码流登录流程 (#5650)
https://github.com/HMCL-dev/HMCL/pull/5610
2026-02-25 21:11:29 +08:00
Glavo 8e9ddfb248 [release/3.11] 修复 MultipleSourceVersionList 不会使用备用下载源的问题 (#5648)
https://github.com/HMCL-dev/HMCL/pull/5585
https://github.com/HMCL-dev/HMCL/pull/5602
2026-02-25 21:10:29 +08:00
Glavoand辞庐 1edd36819c [release/3.11] fix: 打开微软对话框有一个加载圈一闪而过 (#5647)
https://github.com/HMCL-dev/HMCL/pull/5584

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2026-02-25 21:08:47 +08:00
a5f6028b3a [release/3.11] 修复微软登录报错的超链接显示 (#5646)
https://github.com/HMCL-dev/HMCL/pull/5549

Co-authored-by: Xirren <michaelqiancn@gmail.com>
Co-authored-by: xirren <xirren@outlook.com>
2026-02-25 21:02:56 +08:00
GlavoandCiiLu 52f57c783e [release/3.11] fix: 安装整合包时任务弹窗可能空白 (#5645)
https://github.com/HMCL-dev/HMCL/pull/5550

Co-authored-by: CiiLu <109708109+CiiLu@users.noreply.github.com>
2026-02-25 20:59:30 +08:00
3gf8jv4dv 77a86ef963 docs: fix typos [skip ci] (#5542) 2026-02-15 20:28:09 +08:00
Glavo 32a741e2de Fix #5525: 修复 Task 进度异常的问题 (#5543) 2026-02-15 12:30:23 +08:00
CiiLu d1ead74177 fix: 微软登录完成页 svg 图标不会跟随深色模式 (#5541) 2026-02-15 02:55:57 +08:00
NoClassDefFoundErrorandGlavo 80ba1ec1a7 [Feature] 让Java下载页面可以同时下载多个Java (#5394)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-02-14 22:10:04 +08:00
Glavo 25fef6b5c2 删除 Lang.timer (#5537) 2026-02-14 22:04:04 +08:00
CiiLu 64ce1854f5 feat: 微软登录二维码生成添加 otc 参数 (#5538) 2026-02-14 22:03:23 +08:00
Glavo fb4f0be899 优化 SpinnerPane (#5533) 2026-02-14 20:45:21 +08:00
Glavo eea072cea5 更新微软账户登录对话框 (#5531) 2026-02-14 20:38:07 +08:00
3gf8jv4dv b0cd184e22 fix(l10n): replace mcedit with minecraft wiki's tutorial page (#5524)
resolves #5519
2026-02-13 22:33:48 +08:00
CiiLu 8b3e801a44 增加弹窗图标内容间距 (#5527) 2026-02-13 22:32:46 +08:00
CiiLu 04bb089f3a fix: 修复新建账号界面从微软账号切换到其他方式后下面按钮排版奇怪的问题 (#5523) 2026-02-12 20:41:17 +08:00
CiiLu 4bd0cbc88f fix: 下载游戏版本列表点击左侧图标无法触发水波纹效果 & 光标没有变成手型 (#5520) 2026-02-12 20:39:01 +08:00
CiiLu 9c4c7bd786 fix: 修复安装卡片部分区域点击无法触发水波纹效果的问题 (#5522) 2026-02-12 20:38:23 +08:00
Damon LuandGlavo f79d560605 阻止在 JFXCustomColorPickerDialog 输入非法颜色信息 (#5368)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-02-11 21:28:30 +08:00
Glavo 90e6782193 创建 SVGContainer 控件 (#5464) 2026-02-11 21:21:03 +08:00
Glavo ca071fd924 Add weak listener registration methods to WeakListenerHolder (#5509) 2026-02-10 22:42:57 +08:00
Glavo 2e26b14322 修复部分请求头中 Accept-Language 字段包含 Java 版本的问题 (#5508) 2026-02-10 22:39:22 +08:00
CiiLu b7fbebae43 feat: 使用授权代码流登录微软帐户 (#5330) 2026-02-10 02:52:52 +08:00
Glavo 33dc39e59e 修复陶瓦联机页面“导出联机日志”按钮填充异常的问题 (#5507) 2026-02-09 23:28:13 +08:00
Glavo a70b8d7baa 使用自定义 EasyTier 服务器节点 (#5504) 2026-02-09 22:54:18 +08:00
NoClassDefFoundErrorandGlavo 64821600c5 Fix #5326 修改在构建文件树时遇到重名文件夹导致读取失败的问题 (#5369)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-02-09 20:42:10 +08:00
dc993b871f feat: 无版本开始游戏不选择支持状态为“不支持”版本 (#5462)
Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-09 20:37:02 +08:00
3gf8jv4dv a0bd64ad1f docs: add HMCL_UI_SCALE to contribution guide [skip ci] (#5496) 2026-02-09 20:36:42 +08:00
mineDiamond 10df7a862b fix: 修复快速启动按钮出现在不支持的版本的bug (#5461) 2026-02-08 20:45:57 +08:00
3gf8jv4dv 9ea6a97aef fix(i18n): unify term WORLD in english (#5487) 2026-02-08 20:35:49 +08:00
Calboot dec2e4ec7a [Enhancement] NBTTreeView 自动展开 (#5467) 2026-02-08 20:29:24 +08:00
Glavo 8d504cd194 Fix #5469: 调整检查更新设置项填充大小 (#5481) 2026-02-08 20:28:05 +08:00
Glavo 2918f19482 修复 Java 管理页面中标签可能被截断的问题 (#5482) 2026-02-08 04:07:27 +08:00
CiiLuandGlavo 18f3e2870b chore(docs&l10n): highlight support for legacyfabric (#5425)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-02-07 21:58:17 +08:00
Glavo 9f2874c12b Fix #5476: 修复无法归一化版本号 "1.16.2 Pre-release 1" 的问题 (#5480) 2026-02-07 21:49:56 +08:00
Glavo 3fdf21fcd5 Fix #5457: 修复部分页面意外的显示滚动条的问题 (#5477) 2026-02-07 21:31:09 +08:00
Glavo c9e435c129 修复 ComponentSublistWrapper 被禁用时右侧图标颜色异常的问题 (#5478) 2026-02-07 21:30:42 +08:00
Glavo 26125f4002 Fix #5472: 修复无法正确识别 Minecraft 2.0 实例版本号的问题 (#5479) 2026-02-07 21:27:54 +08:00
3gf8jv4dv 93be558e84 docs: add tradchinese variant to localization guide (#5473) 2026-02-07 20:43:45 +08:00
3gf8jv4dv e1be52f0f0 docs: fix typos in simpchinese localization guide (#5475) 2026-02-07 20:43:06 +08:00
Glavo 359a90636a 避免 TwoLineListItem 中标签挤压标题 (#5474) 2026-02-07 18:58:56 +08:00
Glavo d9b31ffd18 优化 TwoLineListItem 中标签的展示方式 (#5465) 2026-02-07 18:02:20 +08:00
Glavo 4b72639969 清理 ToolbarListPageSkin (#5468) 2026-02-07 14:30:47 +08:00
CiiLuandGlavo aecbf2e476 修复 OfflineAccountSkinPane 中 cslApiField 的校验未生效的问题 (#5411)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-02-07 00:02:02 +08:00
Glavo 52d8c44320 更新 JFXSnackbar (#5451) 2026-02-07 00:01:20 +08:00
Burning_TNT 5d57052ea2 Fix #4977 Modrinth 依赖可以指向已经被删除的项目 (#5012) 2026-02-06 22:56:44 +08:00
Glavo 570e1e2029 优化 AdvancedListItem (#5460) 2026-02-06 22:48:41 +08:00
3gf8jv4dv 5b11202a9c fix(l10n): refine tradchinese text (#5410) 2026-02-06 22:40:39 +08:00
CiiLu c2d5d1b5f6 fix: 修复安装游戏时选择版本后返回已选择的版本会被清除的问题 (#5423) 2026-02-06 22:34:31 +08:00
CiiLu ef5849eba1 修复管理页面加载样式不统一的问题 (#5406) 2026-02-06 22:12:23 +08:00
CiiLu c2c542af3c 修复 1.5.2 等版本 LegacyFabric 无法安装的问题 (#5413) 2026-02-06 22:10:00 +08:00
Glavo 792412b4c7 优化 TwoLineListItem (#5447) 2026-02-06 21:47:30 +08:00
Glavo 2b8e8306d7 调整 ToolbarListPageSkin 中图标大小和填充 (#5456) 2026-02-06 17:01:03 +08:00
Glavo 84bcb01fe1 清理 SVG 相关代码 (#5450) 2026-02-06 04:21:14 +08:00
Glavo 253bdf974e 修复 SVG 图标对齐错误的问题 (#5449) 2026-02-06 03:46:03 +08:00
Glavo 5839382c64 重写 MappedObservableList (#5400) 2026-02-06 00:22:21 +08:00
Glavo ab9c45549b 使用 LineButton 代替 IconedTwoLineListItem (#5446) 2026-02-05 22:15:53 +08:00
CiiLu cb413fa40a 修复日志级别选择按钮背景颜色溢出的问题 (#5434) 2026-02-05 22:15:29 +08:00
Glavo 982c062a01 使用 LineComponent 简化代码 (#5408) 2026-02-05 21:37:51 +08:00
3gf8jv4dv 9f097149b2 docs: revise documents for tradchinese and lzh (#5402) 2026-02-05 20:44:16 +08:00
GIGN cb50c89fb9 Refine language in README_lzh.md (#5417) 2026-02-05 20:42:26 +08:00
CiiLu 7fb2b9b734 导出运行栈时附加 -e -l 参数 (#5426) 2026-02-05 20:41:03 +08:00
Glavo b933fbcd6b Fix #5437: 修复总任务数为 0 时 Task 进度计算错误的问题 (#5442) 2026-02-05 20:40:41 +08:00
辞庐 c880e4befb 移除 large-spinner-pane 样式 (#5405) 2026-02-04 22:24:47 +08:00
Glavo 575d908dba Fix #5338: 修复部分环境中进入下载页面时启动器崩溃的问题 (#5433) 2026-02-04 22:24:03 +08:00
3gf8jv4dv ecf188ad18 fix(i18n): clean up text for toolbar and related functions (#5409)
resolves #4772
2026-02-04 22:21:52 +08:00
辞庐 0b3cb9964a 修复 legacyfabricapi 可以和 forge 同时安装的问题 (#5414) 2026-02-04 22:13:30 +08:00
辞庐 c175f1c1b3 优化日志窗口无日志行背景 (#5422) 2026-02-04 22:11:12 +08:00
Glavo dbacf096c6 Fix #5431: 修复部分页面弹出菜单会被重复触发的问题 (#5432) 2026-02-04 21:56:41 +08:00
Glavo c7b9b73292 基于 HBox 重新实现 LineComponent (#5416) 2026-02-04 20:47:45 +08:00
Glavo 19079ac123 创建通用的 LineButton 控件并简化 TerracottaControllerPage (#5395) 2026-02-03 21:09:59 +08:00
3gf8jv4dv 2a66880a58 docs: fix incorrrect windows environment variable name (#5399) 2026-02-03 21:09:33 +08:00
3gf8jv4dv 8b91251337 docs: add tradchinese variant to release schedule document (#5403) 2026-02-03 21:09:01 +08:00
辞庐 0c2742a895 修复 #5292 中添加的图标错误的使用了 Rounded 图标的问题 (#5398) 2026-02-03 20:38:04 +08:00
3gf8jv4dv a8692c3893 docs: add tradchinese variant to contribution guide (#5401) 2026-02-03 20:34:15 +08:00
139 changed files with 4839 additions and 2592 deletions

No files matched your search

+1 -2
View File
@@ -28,7 +28,6 @@ val versionType = System.getenv("VERSION_TYPE") ?: if (isOfficial) "nightly" els
val versionRoot = System.getenv("VERSION_ROOT") ?: projectConfig.getProperty("versionRoot") ?: "3"
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE") ?: ""
@@ -60,6 +59,7 @@ dependencies {
implementation(libs.twelvemonkeys.imageio.webp)
implementation(libs.java.info)
implementation(libs.monet.fx)
implementation(libs.nayuki.qrcodegen)
if (launcherExe.isBlank()) {
implementation(libs.hmclauncher)
@@ -152,7 +152,6 @@ val hmclProperties = buildList {
}
add("hmcl.version.type" to versionType)
add("hmcl.microsoft.auth.id" to microsoftAuthId)
add("hmcl.microsoft.auth.secret" to microsoftAuthSecret)
add("hmcl.curseforge.apikey" to curseForgeApiKey)
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
}
@@ -127,6 +127,10 @@ public class JFXPopup extends PopupControl {
/// @param initOffsetX on the x-axis
/// @param initOffsetY on the y-axis
public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign, double initOffsetX, double initOffsetY) {
show(node, vAlign, hAlign, initOffsetX, initOffsetY, false);
}
public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign, double initOffsetX, double initOffsetY, boolean attachToNode) {
if (!isShowing()) {
Scene scene = node.getScene();
if (scene == null || scene.getWindow() == null) {
@@ -137,10 +141,13 @@ public class JFXPopup extends PopupControl {
boolean isRTL = node.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT;
this.show(node,
parent.getX() + scene.getX() + origin.getX() + (hAlign == PopupHPosition.RIGHT ? ((Region) node).getWidth() : 0),
parent.getY() + origin.getY() + scene.getY() + (vAlign == PopupVPosition.BOTTOM ? ((Region) node).getHeight() : 0)
);
double anchorX = parent.getX() + scene.getX() + origin.getX() + (hAlign == PopupHPosition.RIGHT ? ((Region) node).getWidth() : 0);
double anchorY = parent.getY() + origin.getY() + scene.getY() + (vAlign == PopupVPosition.BOTTOM ? ((Region) node).getHeight() : 0);
if (attachToNode)
this.show(node, anchorX, anchorY);
else
this.show(parent, anchorX, anchorY);
((JFXPopupSkin) getSkin()).reset(vAlign, isRTL ? hAlign.getOpposite() : hAlign, isRTL ? -initOffsetX : initOffsetX, initOffsetY);
Platform.runLater(() -> ((JFXPopupSkin) getSkin()).animate());
@@ -0,0 +1,421 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.PauseTransition;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.WeakChangeListener;
import javafx.css.PseudoClass;
import javafx.event.Event;
import javafx.event.EventType;
import javafx.geometry.Bounds;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.animation.Motion;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
/// "Snackbars provide brief messages about app processes at the bottom of the screen"
/// (<a href="https://material.io/design/components/snackbars.html#">Material Design Guidelines</a>).
///
/// To show a snackbar you need to
/// <ol>
/// - Have a [Pane] (snackbarContainer) to show the snackbar on top of. Register it in
/// [the JFXSnackbar constructor][#JFXSnackbar(Pane)] or using the [#registerSnackbarContainer(Pane)] method.
/// - Have or create a [JFXSnackbar]. - Having one snackbar where you pass all your
/// [SnackbarEvents][JFXSnackbar.SnackbarEvent] will ensure that the [enqueuemethod][JFXSnackbar#enqueue(SnackbarEvent)] works as intended.
///
/// - Have something to show in the snackbar. A [JFXSnackbarLayout] is nice and pretty,
/// but any arbitrary [Node] will do.
/// - Create a [SnackbarEvent][JFXSnackbar.SnackbarEvent] specifying the contents and the
/// duration.
/// </ol>
///
/// Finally, with all those things prepared, show your snackbar using
/// [snackbar.enqueue(snackbarEvent);][JFXSnackbar#enqueue(SnackbarEvent)].
///
/// It's most convenient to create functions to do most of this (creating the layout and event) with the default
/// settings; that way all you need to do to show a snackbar is specify the message or just the message and the duration.
///
/// @see <a href="https://material.io/design/components/snackbars.html#"> The Material Design Snackbar</a>
public class JFXSnackbar extends Group {
private static final String DEFAULT_STYLE_CLASS = "jfx-snackbar";
private Pane snackbarContainer;
private final ChangeListener<? super Number> sizeListener = (o, oldVal, newVal) -> refreshPopup();
private final WeakChangeListener<? super Number> weakSizeListener = new WeakChangeListener<>(sizeListener);
private final AtomicBoolean processingQueue = new AtomicBoolean(false);
private final ConcurrentLinkedQueue<SnackbarEvent> eventQueue = new ConcurrentLinkedQueue<>();
private final ConcurrentHashMap.KeySetView<Object, Boolean> eventsSet = ConcurrentHashMap.newKeySet();
private final Pane content;
private PseudoClass activePseudoClass = null;
private PauseTransition pauseTransition;
/// This constructor assumes that you will eventually call the [#registerSnackbarContainer(Pane)] method before
/// calling the [#enqueue(SnackbarEvent)] method. Otherwise, how will the snackbar know where to show itself?
///
///
/// "Snackbars provide brief messages about app processes at the bottom of the screen"
/// (<a href="https://material.io/design/components/snackbars.html#">Material Design Guidelines</a>).
///
/// To show a snackbar you need to
///
/// - Have a [Pane] (snackbarContainer) to show the snackbar on top of. Register it in
/// [the JFXSnackbar constructor][#JFXSnackbar(Pane)] or using the [#registerSnackbarContainer(Pane)] method.
/// - Have or create a [JFXSnackbar]. - Having one snackbar where you pass all your
/// [SnackbarEvents][JFXSnackbar.SnackbarEvent] will ensure that the [enqueuemethod][JFXSnackbar#enqueue(SnackbarEvent)] works as intended.
/// - Have something to show in the snackbar. A [JFXSnackbarLayout] is nice and pretty,
/// but any arbitrary [Node] will do.
/// - Create a [SnackbarEvent][JFXSnackbar.SnackbarEvent] specifying the contents and the
/// duration.
///
/// Finally, with all those things prepared, show your snackbar using
/// [snackbar.enqueue(snackbarEvent);][JFXSnackbar#enqueue(SnackbarEvent)].
///
public JFXSnackbar() {
this(null);
}
/// "Snackbars provide brief messages about app processes at the bottom of the screen"
/// (<a href="https://material.io/design/components/snackbars.html#">Material Design Guidelines</a>).
///
/// To show a snackbar you need to
///
/// - Have a [Pane] (snackbarContainer) to show the snackbar on top of. Register it in
/// [the JFXSnackbar constructor][#JFXSnackbar(Pane)] or using the [#registerSnackbarContainer(Pane)] method.
/// - Have or create a [JFXSnackbar]. - Having one snackbar where you pass all your
/// [SnackbarEvents][JFXSnackbar.SnackbarEvent] will ensure that the [enqueuemethod][JFXSnackbar#enqueue(SnackbarEvent)] works as intended.
/// - Have something to show in the snackbar. A [JFXSnackbarLayout] is nice and pretty,
/// but any arbitrary [Node] will do.
/// - Create a [SnackbarEvent][JFXSnackbar.SnackbarEvent] specifying the contents and the
/// duration.
///
/// Finally, with all those things prepared, show your snackbar using
/// [snackbar.enqueue(snackbarEvent);][JFXSnackbar#enqueue(SnackbarEvent)].
///
/// @param snackbarContainer where the snackbar will appear. Using a single snackbar instead of many, will ensure that
/// the [#enqueue(SnackbarEvent)] method works correctly.
public JFXSnackbar(Pane snackbarContainer) {
initialize();
content = new StackPane();
content.getStyleClass().add("jfx-snackbar-content");
//wrap the content in a group so that the content is managed inside its own container
//but the group is not managed in the snackbarContainer so it does not affect any layout calculations
getChildren().add(content);
setManaged(false);
setVisible(false);
// register the container before resizing it
registerSnackbarContainer(snackbarContainer);
// resize the popup if its layout has been changed
layoutBoundsProperty().addListener((o, oldVal, newVal) -> refreshPopup());
addEventHandler(SnackbarEvent.SNACKBAR, this::enqueue);
}
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
// Setters / Getters
public Pane getPopupContainer() {
return snackbarContainer;
}
public void setPrefWidth(double width) {
content.setPrefWidth(width);
}
public double getPrefWidth() {
return content.getPrefWidth();
}
// Public API
public void registerSnackbarContainer(Pane snackbarContainer) {
if (snackbarContainer != null) {
if (this.snackbarContainer != null) {
//since listeners are added the container should be properly registered/unregistered
throw new IllegalArgumentException("Snackbar Container already set");
}
this.snackbarContainer = snackbarContainer;
this.snackbarContainer.getChildren().add(this);
this.snackbarContainer.heightProperty().addListener(weakSizeListener);
this.snackbarContainer.widthProperty().addListener(weakSizeListener);
}
}
public void unregisterSnackbarContainer(Pane snackbarContainer) {
if (snackbarContainer != null) {
if (this.snackbarContainer == null) {
throw new IllegalArgumentException("Snackbar Container not set");
}
this.snackbarContainer.getChildren().remove(this);
this.snackbarContainer.heightProperty().removeListener(weakSizeListener);
this.snackbarContainer.widthProperty().removeListener(weakSizeListener);
this.snackbarContainer = null;
}
}
private void show(SnackbarEvent event) {
content.getChildren().setAll(event.getContent());
openAnimation = getTimeline(event.getTimeout());
if (event.getPseudoClass() != null) {
activePseudoClass = event.getPseudoClass();
content.pseudoClassStateChanged(activePseudoClass, true);
}
openAnimation.play();
}
private Timeline openAnimation = null;
private Timeline getTimeline(Duration timeout) {
Timeline animation;
animation = new Timeline(
new KeyFrame(
Duration.ZERO,
e -> {
this.toBack();
this.setVisible(false);
},
new KeyValue(this.translateYProperty(), this.getLayoutBounds().getHeight(), Motion.EASE),
new KeyValue(this.opacityProperty(), 0, Motion.EASE)
),
new KeyFrame(
Duration.millis(10),
e -> {
this.toFront();
this.setVisible(true);
}
),
new KeyFrame(Duration.millis(300),
new KeyValue(this.opacityProperty(), 1, Motion.EASE),
new KeyValue(this.translateYProperty(), 0, Motion.EASE)
)
);
animation.setCycleCount(1);
pauseTransition = Duration.INDEFINITE.equals(timeout) ? null : new PauseTransition(timeout);
if (pauseTransition != null) {
animation.setOnFinished(finish -> {
pauseTransition.setOnFinished(done -> {
pauseTransition = null;
eventsSet.remove(currentEvent);
currentEvent = eventQueue.peek();
close();
});
pauseTransition.play();
});
}
return animation;
}
public void close() {
if (openAnimation != null) {
openAnimation.stop();
}
if (this.isVisible()) {
Timeline closeAnimation = new Timeline(
new KeyFrame(
Duration.ZERO,
e -> this.toFront(),
new KeyValue(this.opacityProperty(), 1, Motion.EASE),
new KeyValue(this.translateYProperty(), 0, Motion.EASE)
),
new KeyFrame(
Duration.millis(290),
e -> this.setVisible(true)
),
new KeyFrame(Duration.millis(300),
e -> {
this.toBack();
this.setVisible(false);
},
new KeyValue(this.translateYProperty(), this.getLayoutBounds().getHeight(), Motion.EASE),
new KeyValue(this.opacityProperty(), 0, Motion.EASE)
)
);
closeAnimation.setCycleCount(1);
closeAnimation.setOnFinished(e -> {
resetPseudoClass();
processSnackbar();
});
closeAnimation.play();
}
}
private SnackbarEvent currentEvent = null;
public SnackbarEvent getCurrentEvent() {
return currentEvent;
}
/**
* Shows {@link SnackbarEvent SnackbarEvents} one by one. The next event will be shown after the current event's duration.
*
* @param event the {@link SnackbarEvent event} to put in the queue.
*/
public void enqueue(SnackbarEvent event) {
synchronized (this) {
if (!eventsSet.contains(event)) {
eventsSet.add(event);
eventQueue.offer(event);
} else if (currentEvent == event && pauseTransition != null) {
pauseTransition.playFromStart();
}
}
if (processingQueue.compareAndSet(false, true)) {
Platform.runLater(() -> {
currentEvent = eventQueue.poll();
if (currentEvent != null) {
show(currentEvent);
}
});
}
}
private void resetPseudoClass() {
if (activePseudoClass != null) {
content.pseudoClassStateChanged(activePseudoClass, false);
activePseudoClass = null;
}
}
private void processSnackbar() {
currentEvent = eventQueue.poll();
if (currentEvent != null) {
eventsSet.remove(currentEvent);
show(currentEvent);
} else {
//The enqueue method and this listener should be executed sequentially on the FX Thread so there
//should not be a race condition
processingQueue.getAndSet(false);
}
}
private void refreshPopup() {
if (snackbarContainer == null) {
return;
}
Bounds contentBound = this.getLayoutBounds();
double offsetX = Math.ceil(snackbarContainer.getWidth() / 2) - Math.ceil(contentBound.getWidth() / 2);
double offsetY = snackbarContainer.getHeight() - contentBound.getHeight();
this.setLayoutX(offsetX);
this.setLayoutY(offsetY);
}
///////////////////////////////////////////////////////////////////////////
// Event API
///////////////////////////////////////////////////////////////////////////
/// Specifies _what_ and _how long_ to show a [JFXSnackbar].
///
/// The _what_ can be any arbitrary [Node]; the [JFXSnackbarLayout] is a great choice.
///
/// The _how long_ is specified in the form of a [javafx.util.Duration][javafx.util.Duration], not to be
/// confused with the [java.time.Duration].
public static class SnackbarEvent extends Event {
public static final EventType<SnackbarEvent> SNACKBAR = new EventType<>(Event.ANY, "SNACKBAR");
/// The amount of time the snackbar will show for, if not otherwise specified.
///
/// It's 1.5 seconds.
public static Duration DEFAULT_DURATION = Duration.seconds(1.5);
private final Node content;
private final PseudoClass pseudoClass;
private final Duration timeout;
/// Creates a [SnackbarEvent] with the [default duration][#DEFAULT_DURATION] and no pseudoClass.
///
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
public SnackbarEvent(Node content) {
this(content, DEFAULT_DURATION, null);
}
/// Creates a [SnackbarEvent] with the [default duration][#DEFAULT_DURATION]; you specify the contents and
/// pseudoClass.
///
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
public SnackbarEvent(Node content, PseudoClass pseudoClass) {
this(content, DEFAULT_DURATION, pseudoClass);
}
/// Creates a SnackbarEvent with no pseudoClass; you specify the contents and duration.
/// pseudoClass.
///
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
/// @param timeout the amount of time you want the snackbar to show for.
public SnackbarEvent(Node content, Duration timeout) {
this(content, timeout, null);
}
/// Creates a SnackbarEvent; you specify the contents, duration and pseudoClass.
///
/// If you don't need so much customization, try one of the other constructors.
///
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
/// @param timeout the amount of time you want the snackbar to show for.
public SnackbarEvent(Node content, Duration timeout, PseudoClass pseudoClass) {
super(SNACKBAR);
this.content = content;
this.pseudoClass = pseudoClass;
this.timeout = timeout;
}
public Node getContent() {
return content;
}
public PseudoClass getPseudoClass() {
return pseudoClass;
}
public Duration getTimeout() {
return timeout;
}
@Override
@SuppressWarnings("unchecked")
public EventType<? extends SnackbarEvent> getEventType() {
return (EventType<? extends SnackbarEvent>) super.getEventType();
}
public boolean isPersistent() {
return Duration.INDEFINITE.equals(getTimeout());
}
}
}
@@ -0,0 +1,110 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import javafx.beans.binding.Bindings;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
/// JFXSnackbarLayout default layout for snackbar content
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2018-11-16
public class JFXSnackbarLayout extends BorderPane {
private final Label toast;
private JFXButton action;
private final StackPane actionContainer;
public JFXSnackbarLayout(String message) {
this(message, null, null);
}
public JFXSnackbarLayout(String message, String actionText, EventHandler<ActionEvent> actionHandler) {
initialize();
toast = new Label();
toast.setMinWidth(Control.USE_PREF_SIZE);
toast.getStyleClass().add("jfx-snackbar-toast");
toast.setWrapText(true);
toast.setText(message);
StackPane toastContainer = new StackPane(toast);
toastContainer.setPadding(new Insets(20));
actionContainer = new StackPane();
actionContainer.setPadding(new Insets(0, 10, 0, 0));
toast.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> {
if (getPrefWidth() == -1) {
return getPrefWidth();
}
double actionWidth = actionContainer.isVisible() ? actionContainer.getWidth() : 0.0;
return prefWidthProperty().get() - actionWidth;
}, prefWidthProperty(), actionContainer.widthProperty(), actionContainer.visibleProperty()));
setLeft(toastContainer);
setRight(actionContainer);
if (actionText != null) {
action = new JFXButton();
action.setText(actionText);
action.setOnAction(actionHandler);
action.setMinWidth(Control.USE_PREF_SIZE);
action.setButtonType(JFXButton.ButtonType.FLAT);
action.getStyleClass().add("jfx-snackbar-action");
// actions will be added upon showing the snackbar if needed
actionContainer.getChildren().add(action);
if (!actionText.isEmpty()) {
action.setVisible(true);
actionContainer.setVisible(true);
actionContainer.setManaged(true);
// to force updating the layout bounds
action.setText("");
action.setText(actionText);
action.setOnAction(actionHandler);
} else {
actionContainer.setVisible(false);
actionContainer.setManaged(false);
action.setVisible(false);
}
}
}
private static final String DEFAULT_STYLE_CLASS = "jfx-snackbar-layout";
public String getToast() {
return toast.getText();
}
public void setToast(String toast) {
this.toast.setText(toast);
}
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
}
@@ -34,14 +34,17 @@ import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.control.Tab;
import javafx.scene.control.TextFormatter;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.*;
import javafx.util.Duration;
import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
/**
* @author Shadi Shaheen
@@ -95,14 +98,17 @@ public class JFXCustomColorPickerDialog extends StackPane {
rgbField.getStyleClass().add("custom-color-field");
rgbField.setPromptText("RGB Color");
rgbField.setTextFormatter(colorCharFormatter());
rgbField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
hsbField.getStyleClass().add("custom-color-field");
hsbField.setPromptText("HSB Color");
hsbField.setTextFormatter(colorCharFormatter());
hsbField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
hexField.getStyleClass().add("custom-color-field");
hexField.setPromptText("#HEX Color");
hexField.setTextFormatter(colorCharFormatter());
hexField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
StackPane tabContent = new StackPane();
@@ -404,4 +410,20 @@ public class JFXCustomColorPickerDialog extends StackPane {
dialog.setMinWidth(minWidth);
dialog.setMinHeight(minHeight);
}
private static final Pattern COLOR_CHAR_PATTERN = Pattern.compile("[0-9a-zA-Z#(),%.\\s]*");
private static TextFormatter<String> colorCharFormatter() {
return new TextFormatter<>(change -> {
if (!change.isContentChange()) return change;
String ins = StringUtils.toHalfWidth(change.getText());
if (!COLOR_CHAR_PATTERN.matcher(ins).matches()) return null;
String full = StringUtils.toHalfWidth(change.getControlNewText());
long h = full.chars().filter(c -> c == '#').count();
if (h > 1 || (h == 1 && full.indexOf('#') != 0)) return null;
change.setText(ins);
return change;
});
}
}
@@ -22,27 +22,28 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.OAuth;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.security.SecureRandom;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
private final int port;
private final CompletableFuture<String> future = new CompletableFuture<>();
private final String codeVerifier;
private final String state;
public static String lastlyOpenedURL;
@@ -52,6 +53,34 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
super(port);
this.port = port;
var encoder = Base64.getUrlEncoder().withoutPadding();
var random = new SecureRandom();
{
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
// https://datatracker.ietf.org/doc/html/rfc6749#section-10.12
byte[] bytes = new byte[32];
random.nextBytes(bytes);
this.state = encoder.encodeToString(bytes);
}
{
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
byte[] bytes = new byte[64];
random.nextBytes(bytes);
this.codeVerifier = encoder.encodeToString(bytes);
}
}
@Override
public String getCodeVerifier() {
return codeVerifier;
}
@Override
public String getState() {
return state;
}
@Override
@@ -93,19 +122,32 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
String parameters = session.getQueryParameterString();
Map<String, String> query = mapOf(NetworkUtils.parseQuery(parameters));
if (query.containsKey("code")) {
idToken = query.get("id_token");
future.complete(query.get("code"));
String code = query.get("code");
if (code != null) {
if (this.state.equals(query.get("state"))) {
idToken = query.get("id_token");
future.complete(code);
} else if (query.containsKey("state")) {
LOG.warning("Failed to authenticate: invalid state in parameters");
future.completeExceptionally(new AuthenticationException("Failed to authenticate: invalid state"));
} else {
LOG.warning("Failed to authenticate: missing state in parameters");
future.completeExceptionally(new AuthenticationException("Failed to authenticate: missing state"));
}
} else {
LOG.warning("Error: " + parameters);
future.completeExceptionally(new AuthenticationException("failed to authenticate"));
LOG.warning("Failed to authenticate: missing authorization code in parameters");
future.completeExceptionally(new AuthenticationException("Failed to authenticate: missing authorization code"));
}
String html;
try {
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
.replace("%style%", Themes.getTheme().toColorScheme().toStyleSheet().replace("-monet", "--monet"))
.replace("%lang%", Locale.getDefault().toLanguageTag())
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
.replace("%success%", i18n("message.success"))
.replace("%ok%", i18n("button.ok"))
.replace("%close_page%", i18n("account.methods.microsoft.close_page"));
} catch (IOException e) {
LOG.error("Failed to load html", e);
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
@@ -123,7 +165,8 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
public static class Factory implements OAuth.Callback {
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowser = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowserAuthorizationCode = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowserDevice = new EventManager<>();
@Override
public OAuth.Session startServer() throws IOException, AuthenticationException {
@@ -150,17 +193,13 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
}
@Override
public void openBrowser(String url) throws IOException {
public void openBrowser(OAuth.GrantFlow grantFlow, String url) throws IOException {
lastlyOpenedURL = url;
try {
Thread.sleep(1500);
} catch (InterruptedException ignored) {
switch (grantFlow) {
case AUTHORIZATION_CODE -> onOpenBrowserAuthorizationCode.fireEvent(new OpenBrowserEvent(this, url));
case DEVICE -> onOpenBrowserDevice.fireEvent(new OpenBrowserEvent(this, url));
}
FXUtils.openLink(url);
onOpenBrowser.fireEvent(new OpenBrowserEvent(this, url));
}
@Override
@@ -168,17 +207,6 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
return System.getProperty("hmcl.microsoft.auth.id",
JarUtils.getAttribute("hmcl.microsoft.auth.id", ""));
}
@Override
public String getClientSecret() {
return System.getProperty("hmcl.microsoft.auth.secret",
JarUtils.getAttribute("hmcl.microsoft.auth.secret", ""));
}
@Override
public boolean isPublicClient() {
return true; // We have turned on the device auth flow.
}
}
public static class GrantDeviceCodeEvent extends Event {
@@ -33,6 +33,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.FXThread;
import org.jackhuang.hmcl.util.InvocationDispatcher;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.HttpRequest;
@@ -44,13 +45,15 @@ import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -310,11 +313,18 @@ public final class TerracottaManager {
public static TerracottaState.HostScanning setScanning() {
TerracottaState state = STATE_V.get();
if (state instanceof TerracottaState.PortSpecific portSpecific) {
String uri = NetworkUtils.withQuery(String.format("http://127.0.0.1:%d/state/scanning", portSpecific.port), Map.of(
"player", getPlayerName()
));
Task.supplyAsync(Schedulers.io(), TerracottaNodeList::fetch)
.thenComposeAsync(nodes -> {
List<Pair<String, String>> query = new ArrayList<>(nodes.size() + 1);
query.add(pair("player", getPlayerName()));
for (URI node : nodes) {
query.add(pair("public_nodes", node.toString()));
}
return new GetTask(NetworkUtils.withQuery(
"http://127.0.0.1:%d/state/scanning".formatted(portSpecific.port), query
)).setSignificance(Task.TaskSignificance.MINOR);
}).start();
new GetTask(uri).setSignificance(Task.TaskSignificance.MINOR).start();
return new TerracottaState.HostScanning(-1, -1, null);
}
return null;
@@ -323,15 +333,19 @@ public final class TerracottaManager {
public static Task<TerracottaState.GuestConnecting> setGuesting(String room) {
TerracottaState state = STATE_V.get();
if (state instanceof TerracottaState.PortSpecific portSpecific) {
String uri = NetworkUtils.withQuery(String.format("http://127.0.0.1:%d/state/guesting", portSpecific.port), Map.of(
"room", room,
"player", getPlayerName()
));
return new GetTask(uri)
.setSignificance(Task.TaskSignificance.MINOR)
.thenSupplyAsync(() -> new TerracottaState.GuestConnecting(-1, -1, null))
.setSignificance(Task.TaskSignificance.MINOR);
return Task.supplyAsync(Schedulers.io(), TerracottaNodeList::fetch)
.thenComposeAsync(nodes -> {
ArrayList<Pair<String, String>> query = new ArrayList<>(nodes.size() + 2);
query.add(pair("room", room));
query.add(pair("player", getPlayerName()));
for (URI node : nodes) {
query.add(pair("public_nodes", node.toString()));
}
return new GetTask(NetworkUtils.withQuery("http://127.0.0.1:%d/state/guesting".formatted(portSpecific.port), query))
.setSignificance(Task.TaskSignificance.MINOR)
.thenSupplyAsync(() -> new TerracottaState.GuestConnecting(-1, -1, null))
.setSignificance(Task.TaskSignificance.MINOR);
});
} else {
return null;
}
@@ -0,0 +1,103 @@
/*
* 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.terracotta;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jetbrains.annotations.Nullable;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// @author Glavo
public final class TerracottaNodeList {
private static final String NODE_LIST_URL = "https://terracotta.glavo.site/nodes";
@JsonSerializable
private record TerracottaNode(String url, @Nullable String region) implements Validation {
@Override
public void validate() throws JsonParseException, TolerableValidationException {
Validation.requireNonNull(url, "TerracottaNode.url cannot be null");
try {
new URI(url);
} catch (URISyntaxException e) {
throw new JsonParseException("Invalid URL: " + url, e);
}
}
}
private static volatile List<URI> list;
public static List<URI> fetch() {
List<URI> list = TerracottaNodeList.list;
if (list != null)
return list;
synchronized (TerracottaNodeList.class) {
list = TerracottaNodeList.list;
if (list != null)
return list;
try {
List<TerracottaNode> nodes = HttpRequest.GET(NODE_LIST_URL)
.getJson(JsonUtils.listTypeOf(TerracottaNode.class));
if (nodes == null) {
list = List.of();
LOG.info("No available Terracotta nodes found");
} else {
list = nodes.stream()
.filter(node -> {
if (node == null)
return false;
try {
node.validate();
} catch (Exception e) {
LOG.warning("Invalid terracotta node: " + node, e);
return false;
}
return StringUtils.isBlank(node.region) || LocaleUtils.IS_CHINA_MAINLAND == "CN".equalsIgnoreCase(node.region);
})
.map(it -> URI.create(it.url()))
.toList();
LOG.info("Terracotta node list: " + list);
}
} catch (Exception e) {
LOG.warning("Failed to fetch terracotta node list", e);
list = List.of();
}
TerracottaNodeList.list = list;
return list;
}
}
private TerracottaNodeList() {
}
}
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.ui;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.ui.account.ClassicAccountLoginDialog;
import org.jackhuang.hmcl.ui.account.OAuthAccountLoginDialog;
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginPane;
import java.util.Optional;
import java.util.concurrent.CancellationException;
@@ -49,10 +49,10 @@ public final class DialogController {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
runInFX(() -> {
OAuthAccountLoginDialog pane = new OAuthAccountLoginDialog((OAuthAccount) account, it -> {
MicrosoftAccountLoginPane pane = new MicrosoftAccountLoginPane(account, it -> {
res.set(it);
latch.countDown();
}, latch::countDown);
}, latch::countDown, false);
Controllers.dialog(pane);
});
latch.await();
@@ -312,14 +312,6 @@ public final class FXUtils {
});
}
public static Node wrap(Node node) {
return limitingSize(node, 30, 20);
}
public static Node wrap(SVG svg) {
return wrap(svg.createIcon(20));
}
private static class ListenerPair<T> {
private final ObservableValue<T> value;
private final ChangeListener<? super T> listener;
@@ -194,8 +194,9 @@ public class InstallerItem extends Control {
mutualIncompatible(incompatibleMap, forge, fabric, quilt, neoForge, cleanroom, legacyfabric);
addIncompatibles(incompatibleMap, liteLoader, fabric, quilt, neoForge, cleanroom, legacyfabric);
addIncompatibles(incompatibleMap, optiFine, fabric, quilt, neoForge, cleanroom, liteLoader, legacyfabric);
addIncompatibles(incompatibleMap, fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine, cleanroom, legacyfabricApi, legacyfabricApi);
addIncompatibles(incompatibleMap, fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine, cleanroom, legacyfabric, legacyfabricApi);
addIncompatibles(incompatibleMap, quiltApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine, cleanroom, legacyfabric, legacyfabricApi);
addIncompatibles(incompatibleMap, legacyfabricApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine, cleanroom, quilt, quiltApi);
for (Map.Entry<InstallerItem, Set<InstallerItem>> entry : incompatibleMap.entrySet()) {
InstallerItem item = entry.getKey();
@@ -342,6 +343,7 @@ public class InstallerItem extends Control {
BorderPane.setAlignment(statusLabel, Pos.CENTER_LEFT);
HBox buttonsContainer = new HBox();
buttonsContainer.setPickOnBounds(false);
buttonsContainer.setSpacing(8);
buttonsContainer.setAlignment(Pos.CENTER);
pane.getChildren().add(buttonsContainer);
@@ -18,22 +18,17 @@
package org.jackhuang.hmcl.ui;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Paint;
import javafx.scene.shape.SVGPath;
/**
* All vector icons used in the launcher.
* <p>
* Unless otherwise stated,
* these icons are from <a href="https://fonts.google.com/icons?icon.size=24&icon.color=%235f6368&icon.query=list&icon.set=Material+Symbols">Material Symbols</a>,
* with a style of outlined, a weight of 400, a grade of 0, and an optical size of 24 px.
* The view boxes of all icons are normalized to {@code 0 0 24 24}.
*/
/// All vector icons used in the launcher.
///
/// Unless otherwise stated,
/// these icons are from <a href="https://fonts.google.com/icons?icon.size=24&icon.color=%235f6368&icon.query=list&icon.set=Material+Symbols">Material Symbols</a>,
/// with a style of outlined, a weight of 400, a grade of 0, and an optical size of 24 px.
/// The view boxes of all icons are normalized to `0 0 24 24`.
public enum SVG {
NONE(""), // Empty Icon
ADD("M11 13H5V11H11V5H13V11H19V13H13V19H11V13Z"),
ADD_CIRCLE("M11 17H13V13H17V11H13V7H11V11H7V13H11V17ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
ALPHA_CIRCLE("M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,1 11,7M11,9V11H13V9H11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z"), // Not Material
@@ -50,9 +45,9 @@ public enum SVG {
CHECKROOM("M3 20Q2.575 20 2.2875 19.7125T2 19Q2 18.75 2.1 18.5375T2.4 18.2L11 11.75V10Q11 9.575 11.3 9.2875T12.025 9Q12.65 9 13.075 8.55T13.5 7.475Q13.5 6.85 13.0625 6.425T12 6Q11.375 6 10.9375 6.4375T10.5 7.5H8.5Q8.5 6.05 9.525 5.025T12 4Q13.45 4 14.475 5.0125T15.5 7.475Q15.5 8.65 14.8125 9.575T13 10.85V11.75L21.6 18.2Q21.8 18.325 21.9 18.5375T22 19Q22 19.425 21.7125 19.7125T21 20H3ZM6 18H18L12 13.5 6 18Z"),
CHECK_CIRCLE("M10.6 16.6 17.65 9.55 16.25 8.15 10.6 13.8 7.75 10.95 6.35 12.35 10.6 16.6ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
CLOSE("M6.4 19 5 17.6 10.6 12 5 6.4 6.4 5 12 10.6 17.6 5 19 6.4 13.4 12 19 17.6 17.6 19 12 13.4 6.4 19Z"),
CONTENT_CUT("m12 14l-2.35 2.35q.2.375.275.8T10 18q0 1.65-1.175 2.825T6 22t-2.825-1.175T2 18t1.175-2.825T6 14q.425 0 .85.075t.8.275L10 12L7.65 9.65q-.375.2-.8.275T6 10q-1.65 0-2.825-1.175T2 6t1.175-2.825T6 2t2.825 1.175T10 6q0 .425-.075.85t-.275.8L20.6 18.6q.675.675.3 1.538T19.575 21q-.275 0-.537-.112t-.463-.313zm3-3l-2-2l5.575-5.575q.2-.2.463-.312T19.574 3q.95 0 1.313.875t-.313 1.55zM6 8q.825 0 1.413-.587T8 6t-.587-1.412T6 4t-1.412.588T4 6t.588 1.413T6 8m6 4.5q.2 0 .35-.15t.15-.35t-.15-.35t-.35-.15t-.35.15t-.15.35t.15.35t.35.15M6 20q.825 0 1.413-.587T8 18t-.587-1.412T6 16t-1.412.588T4 18t.588 1.413T6 20"),
CONTENT_CUT("M19 21l-7-7-2.35 2.35q.2.375.275.8T10 18q0 1.65-1.175 2.825T6 22q-1.65 0-2.825-1.175T2 18t1.175-2.825T6 14q.425 0 .85.075t.8.275L10 12 7.65 9.65q-.375.2-.8.275T6 10q-1.65 0-2.825-1.175T2 6q0-1.65 1.175-2.825T6 2q1.65 0 2.825 1.175T10 6q0 .425-.075.85t-.275.8L22 20v1H19Zm-4-10-2-2 6-6h3v1l-7 7ZM7.4125 7.4125Q8 6.825 8 6t-.5875-1.4125Q6.825 4 6 4t-1.4125.5875Q4 5.175 4 6t.5875 1.4125T6 8t1.4125-.5875ZM12.35 12.35q.15-.15.15-.35t-.15-.35-.35-.15-.35.15-.15.35.15.35.35.15.35-.15ZM7.4125 19.4125Q8 18.825 8 18t-.5875-1.4125Q6.825 16 6 16t-1.4125.5875Q4 17.175 4 18t.5875 1.4125Q5.175 20 6 20t1.4125-.5875Z"),
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-2v2q0 .425-.288.713T16 8H8q-.425 0-.712-.288T7 7V5H5zm7-14q.425 0 .713-.288T13 4t-.288-.712T12 3t-.712.288T11 4t.288.713T12 5"),
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"),
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"),
@@ -92,8 +87,7 @@ public enum SVG {
LOCATION_CITY("M3 21V7H9V5l3-3 3 3v6h6V21H3Zm2-2H7V17H5v2Zm0-4H7V13H5v2Zm0-4H7V9H5v2Zm6 8h2V17H11v2Zm0-4h2V13H11v2Zm0-4h2V9H11v2Zm0-4h2V5H11V7Zm6 12h2V17H17v2Zm0-4h2V13H17v2Z"),
MENU("M3 18V16H21V18H3ZM3 13V11H21V13H3ZM3 8V6H21V8H3Z"),
MICROSOFT("M4 20H22v2H4V13H20v7h2V4H20v7H4V4h7V20h2V4h9V2H2V22H4"), // Not Material
MINIMIZE("M6 21V19H18V21H6Z"),
MOJANG("M13.9658 0C12.9552.828 12.7686 2.195 12.7007 3.418 12.7219 4.4201 14.0423 4.7174 14.6538 4.0082 15.0912 2.6579 14.3692 1.2739 13.9658 0ZM10.913 2.9297C10.7559 3.6983 10.6284 4.4669 10.4925 5.2355 8.9894 3.9913 7.1505 3.0825 5.142 3.2948 3.4944 3.4646.9429 2.6961.1404 4.6452-.1229 8.314.0722 12.0124.034 15.6896-.0891 16.9126.8957 18.1101 2.1781 17.9699 6.3989 18.0039 10.6283 18.0172 14.8491 17.9661 16.1145 18.102 16.8067 16.9554 16.9851 15.8768 13.2696 16.4374 9.1761 16.9296 5.7111 15.1292 2.5986 13.5836 2.246 8.3139 5.5581 6.798 9.3203 5.1759 13.8596 8.0383 14.9424 11.7877 15.6133 12.1105 16.2844 12.4247 16.9596 12.7432 16.6241 10.3483 16.6537 7.8005 15.5793 5.5882 15.0571 4.7474 14.0975 6.2714 13.3799 5.6217 12.416 4.8659 11.7495 3.8129 10.913 2.9297Z"), // Not Material
MINIMIZE_CENTER("M6 13v-2h12v2H6Z"), // Not Material
MORE_HORIZ("M6 14Q5.175 14 4.5875 13.4125T4 12Q4 11.175 4.5875 10.5875T6 10Q6.825 10 7.4125 10.5875T8 12Q8 12.825 7.4125 13.4125T6 14ZM12 14Q11.175 14 10.5875 13.4125T10 12Q10 11.175 10.5875 10.5875T12 10Q12.825 10 13.4125 10.5875T14 12Q14 12.825 13.4125 13.4125T12 14ZM18 14Q17.175 14 16.5875 13.4125T16 12Q16 11.175 16.5875 10.5875T18 10Q18.825 10 19.4125 10.5875T20 12Q20 12.825 19.4125 13.4125T18 14Z"),
MORE_VERT("M12 20Q11.175 20 10.5875 19.4125T10 18Q10 17.175 10.5875 16.5875T12 16Q12.825 16 13.4125 16.5875T14 18Q14 18.825 13.4125 19.4125T12 20ZM12 14Q11.175 14 10.5875 13.4125T10 12Q10 11.175 10.5875 10.5875T12 10Q12.825 10 13.4125 10.5875T14 12Q14 12.825 13.4125 13.4125T12 14ZM12 8Q11.175 8 10.5875 7.4125T10 6Q10 5.175 10.5875 4.5875T12 4Q12.825 4 13.4125 4.5875T14 6Q14 6.825 13.4125 7.4125T12 8Z"),
OPEN_IN_NEW("M5 21Q4.175 21 3.5875 20.4125T3 19V5Q3 4.175 3.5875 3.5875T5 3H12V5H5V19H19V12H21V19Q21 19.825 20.4125 20.4125T19 21H5ZM9.7 15.7 8.3 14.3 17.6 5H14V3H21V10H19V6.4L9.7 15.7Z"),
@@ -104,7 +98,7 @@ public enum SVG {
PERSON("M12 12Q10.35 12 9.175 10.825T8 8Q8 6.35 9.175 5.175T12 4Q13.65 4 14.825 5.175T16 8Q16 9.65 14.825 10.825T12 12ZM4 20V17.2Q4 16.35 4.4375 15.6375T5.6 14.55Q7.15 13.775 8.75 13.3875T12 13Q13.65 13 15.25 13.3875T18.4 14.55Q19.125 14.925 19.5625 15.6375T20 17.2V20H4ZM6 18H18V17.2Q18 16.925 17.8625 16.7T17.5 16.35Q16.15 15.675 14.775 15.3375T12 15Q10.6 15 9.225 15.3375T6.5 16.35Q6.275 16.475 6.1375 16.7T6 17.2V18ZM12 10Q12.825 10 13.4125 9.4125T14 8Q14 7.175 13.4125 6.5875T12 6Q11.175 6 10.5875 6.5875T10 8Q10 8.825 10.5875 9.4125T12 10ZM12 8ZM12 18Z"),
PUBLIC("M12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM11 19.95V18Q10.175 18 9.5875 17.4125T9 16V15L4.2 10.2Q4.125 10.65 4.0625 11.1T4 12Q4 15.025 5.9875 17.3T11 19.95ZM17.9 17.4Q18.925 16.275 19.4625 14.8875T20 12Q20 9.55 18.6375 7.525T15 4.6V5Q15 5.825 14.4125 6.4125T13 7H11V9Q11 9.425 10.7125 9.7125T10 10H8V12H14Q14.425 12 14.7125 12.2875T15 13V16H16Q16.65 16 17.175 16.3875T17.9 17.4Z"),
REFRESH("M12 20Q8.65 20 6.325 17.675T4 12Q4 8.65 6.325 6.325T12 4Q13.725 4 15.3 4.7125T18 6.75V4H20V11H13V9H17.2Q16.4 7.6 15.0125 6.8T12 6Q9.5 6 7.75 7.75T6 12Q6 14.5 7.75 16.25T12 18Q13.925 18 15.475 16.9T17.65 14H19.75Q19.05 16.65 16.9 18.325T12 20Z"),
REDO("M16.2 10H9.9q-1.575 0-2.738 1T6 13.5T7.163 16T9.9 17H16q.425 0 .713.288T17 18t-.288.713T16 19H9.9q-2.425 0-4.163-1.575T4 13.5t1.738-3.925T9.9 8h6.3l-1.9-1.9q-.275-.275-.275-.7t.275-.7t.7-.275t.7.275l3.6 3.6q.15.15.213.325t.062.375t-.062.375t-.213.325l-3.6 3.6q-.275.275-.7.275t-.7-.275t-.275-.7t.275-.7z"),
REDO("M9.9 19q-2.425 0-4.163-1.575T4 13.5t1.738-3.925T9.9 8h6.3l-2.6-2.6L15 4l5 5l-5 5l-1.4-1.4l2.6-2.6H9.9q-1.575 0-2.738 1T6 13.5T7.163 16T9.9 17H17v2z"),
RELEASE_CIRCLE("M9,7H13A2,2 0 0,1 15,9V11C15,11.84 14.5,12.55 13.76,12.85L15,17H13L11.8,13H11V17H9V7M11,9V11H13V9H11M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,16.41 7.58,20 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z"), // Not Material
RESTORE("M12 21Q8.55 21 5.9875 18.7125T3.05 13H5.1Q5.45 15.6 7.4125 17.3T12 19Q14.925 19 16.9625 16.9625T19 12Q19 9.075 16.9625 7.0375T12 5Q10.275 5 8.775 5.8T6.25 8H9V10H3V4H5V6.35Q6.275 4.75 8.1125 3.875T12 3Q13.875 3 15.5125 3.7125T18.3625 5.6375Q19.575 6.85 20.2875 8.4875T21 12Q21 13.875 20.2875 15.5125T18.3625 18.3625Q17.15 19.575 15.5125 20.2875T12 21Z"), // Not Material
ROCKET_LAUNCH("M5.65 10.025 7.6 10.85Q7.95 10.15 8.325 9.5T9.15 8.2L7.75 7.925 5.65 10.025ZM9.2 12.1 12.05 14.925Q13.1 14.525 14.3 13.7T16.55 11.825Q18.3 10.075 19.2875 7.9375T20.15 4Q18.35 3.875 16.2 4.8625T12.3 7.6Q11.25 8.65 10.425 9.85T9.2 12.1ZM13.65 10.475Q13.075 9.9 13.075 9.0625T13.65 7.65Q14.225 7.075 15.075 7.075T16.5 7.65Q17.075 8.225 17.075 9.0625T16.5 10.475Q15.925 11.05 15.075 11.05T13.65 10.475ZM14.125 18.5 16.225 16.4 15.95 15Q15.3 15.45 14.65 15.8125T13.3 16.525L14.125 18.5ZM21.95 2.175Q22.425 5.2 21.3625 8.0625T17.7 13.525L18.2 16Q18.3 16.5 18.15 16.975T17.65 17.8L13.45 22 11.35 17.075 7.075 12.8 2.15 10.7 6.325 6.5Q6.675 6.15 7.1625 6T8.15 5.95L10.625 6.45Q13.225 3.85 16.075 2.775T21.95 2.175ZM3.925 15.975Q4.8 15.1 6.0625 15.0875T8.2 15.95Q9.075 16.825 9.0625 18.0875T8.175 20.225Q7.55 20.85 6.0875 21.3T2.05 22.1Q2.4 19.525 2.85 18.0625T3.925 15.975ZM5.35 17.375Q5.1 17.625 4.85 18.2875T4.5 19.625Q5.175 19.525 5.8375 19.2875T6.75 18.8Q7.05 18.5 7.075 18.075T6.8 17.35Q6.5 17.05 6.075 17.0625T5.35 17.375Z"),
@@ -120,12 +114,12 @@ public enum SVG {
STADIA_CONTROLLER_FILL("M4.725 20q-1.5 0-2.5625-1.075T1.05 16.325q0-.225.025-.45t.075-.45l2.1-8.4q.35-1.35 1.425-2.1875T7.125 4h9.75q1.375 0 2.45.8375T20.75 7.025l2.1 8.4q.05.225.0875.4625t.0375.4625q0 1.525-1.0875 2.5875T19.275 20q-1.05 0-1.95-.55t-1.35-1.5l-.7-1.45q-.125-.25-.375-.375T14.375 16H9.625q-.275 0-.525.125t-.375.375l-.7 1.45q-.45.95-1.35 1.5T4.725 20ZM13.5 11q.425 0 .7125-.2875T14.5 10t-.2875-.7125T13.5 9t-.7125.2875T12.5 10t.2875.7125T13.5 11Zm2-2q.425 0 .7125-.2875T16.5 8q0-.425-.2875-.7125T15.5 7q-.425 0-.7125.2875T14.5 8t.2875.7125T15.5 9Zm0 4q.425 0 .7125-.2875T16.5 12q0-.425-.2875-.7125T15.5 11q-.425 0-.7125.2875T14.5 12t.2875.7125T15.5 13Zm2-2q.425 0 .7125-.2875T18.5 10q0-.425-.2875-.7125T17.5 9q-.425 0-.7125.2875T16.5 10q0 .425.2875.7125T17.5 11Zm-9 1.5q.325 0 .5375-.2125T9.25 11.75v-1h1q.325 0 .5375-.2125T11 10t-.2125-.5375T10.25 9.25h-1v-1q0-.325-.2125-.5375T8.5 7.5q-.325 0-.5375.2125T7.75 8.25v1h-1q-.325 0-.5375.2125T6 10q0 .325.2125.5375T6.75 10.75h1v1q0 .325.2125.5375T8.5 12.5Z"),
STYLE("M3.975 19.8 3.125 19.45Q2.35 19.125 2.0875 18.325T2.175 16.75L3.975 12.85V19.8ZM7.975 22Q7.15 22 6.5625 21.4125T5.975 20V14L8.625 21.35Q8.7 21.525 8.775 21.6875T8.975 22H7.975ZM13.125 21.9Q12.325 22.2 11.575 21.825T10.525 20.65L6.075 8.45Q5.775 7.65 6.125 6.8875T7.275 5.85L14.825 3.1Q15.625 2.8 16.375 3.175T17.425 4.35L21.875 16.55Q22.175 17.35 21.825 18.1125T20.675 19.15L13.125 21.9ZM10.975 10Q11.4 10 11.6875 9.7125T11.975 9Q11.975 8.575 11.6875 8.2875T10.975 8Q10.55 8 10.2625 8.2875T9.975 9Q9.975 9.425 10.2625 9.7125T10.975 10ZM12.425 20 19.975 17.25 15.525 5 7.975 7.75 12.425 20ZM7.975 7.75 15.525 5 7.975 7.75Z"),
STYLE_FILL("M3.975 19.8l-.85-.35q-.775-.325-1.0375-1.125T2.175 16.75l1.8-3.9V19.8Zm4 2.2q-.825 0-1.4125-.5875T5.975 20V14l2.65 7.35q.075.175.15.3375t.2.3125h-1Zm5.15-.1q-.8.3-1.55-.075t-1.05-1.175L6.075 8.45q-.3-.8.05-1.5625T7.275 5.85l7.55-2.75q.8-.3 1.55.075t1.05 1.175l4.45 12.2q.3.8-.05 1.5625T20.675 19.15l-7.55 2.75ZM10.975 10q.425 0 .7125-.2875T11.975 9q0-.425-.2875-.7125T10.975 8t-.7125.2875T9.975 9t.2875.7125T10.975 10Z"),
TEXTURE("M4.4-3Q3.925-3.1 3.5125-3.5125T3-4.4L19.6-21Q20.125-20.875 20.5-20.4875T21.025-19.6L4.4-3ZM3-9.3V-12.1L11.9-21H14.7L3-9.3ZM3-17V-19Q3-19.825 3.5875-20.4125T5-21H7L3-17ZM17-3 21-7V-5Q21-4.175 20.4125-3.5875T19-3H17ZM9.3-3 21-14.7V-11.9L12.1-3H9.3Z"),
TEXTURE("M4.4 21q-.475-.1-.8875-.5125T3 19.6L19.6 3q.525.125.9.5125t.525.8875L4.4 21ZM3 14.7v-2.8L11.9 3h2.8L3 14.7ZM3 7V5q0-.825.5875-1.4125T5 3h2L3 7Zm14 14 4-4v2q0 .825-.5875 1.4125T19 21h-2Zm-7.7 0L21 9.3v2.8L12.1 21H9.3Z"),
TRIP("M4 21Q3.175 21 2.5875 20.4125T2 19V8Q2 7.175 2.5875 6.5875T4 6H8V4Q8 3.175 8.5875 2.5875T10 2H14Q14.825 2 15.4125 2.5875T16 4V6H20Q20.825 6 21.4125 6.5875T22 8V19Q22 19.825 21.4125 20.4125T20 21H4ZM10 6H14V4H10V6ZM6 8H4V19H6V8ZM16 19V8H8V19H16ZM18 8V19H20V8H18ZM12 13.5Z"),
TUNE("M11 21V15H13V17H21V19H13V21H11ZM3 19V17H9V19H3ZM7 15V13H3V11H7V9H9V15H7ZM11 13V11H21V13H11ZM15 9V3H17V5H21V7H17V9H15ZM3 7V5H13V7H3Z"),
UNFOLD_MORE("M12 21 7.5 16.5l1.45-1.45L12 18.1l3.05-3.05 1.45 1.45L12 21ZM8.95 9.05 7.5 7.6 12 3.1l4.5 4.5-1.45 1.45L12 6 8.95 9.05Z"),
UPDATE("M12 21Q10.125 21 8.4875 20.2875T5.6375 18.3625Q4.425 17.15 3.7125 15.5125T3 12Q3 10.125 3.7125 8.4875T5.6375 5.6375Q6.85 4.425 8.4875 3.7125T12 3Q14.05 3 15.8875 3.875T19 6.35V4H21V10H15V8H17.75Q16.725 6.6 15.225 5.8T12 5Q9.075 5 7.0375 7.0375T5 12Q5 14.925 7.0375 16.9625T12 19Q14.625 19 16.5875 17.3T18.9 13H20.95Q20.575 16.425 18.0125 18.7125T12 21ZM14.8 16.2 11 12.4V7H13V11.6L16.2 14.8 14.8 16.2Z"),
UNDO("M8 19q-.425 0-.712-.288T7 18t.288-.712T8 17h6.1q1.575 0 2.738-1T18 13.5T16.838 11T14.1 10H7.8l1.9 1.9q.275.275.275.7t-.275.7t-.7.275t-.7-.275L4.7 9.7q-.15-.15-.213-.325T4.426 9t.063-.375T4.7 8.3l3.6-3.6q.275-.275.7-.275t.7.275t.275.7t-.275.7L7.8 8h6.3q2.425 0 4.163 1.575T20 13.5t-1.737 3.925T14.1 19z"),
UNDO("M7 19v-2h7.1q1.575 0 2.738-1T18 13.5T16.838 11T14.1 10H7.8l2.6 2.6L9 14L4 9l5-5l1.4 1.4L7.8 8h6.3q2.425 0 4.163 1.575T20 13.5t-1.737 3.925T14.1 19z"),
VISIBILITY("M12 16q1.875 0 3.1875-1.3125T16.5 11.5 15.1875 8.3125 12 7 8.8125 8.3125 7.5 11.5t1.3125 3.1875T12 16Zm0-1.8q-1.125 0-1.9125-.7875T9.3 11.5t.7875-1.9125T12 8.8q1.125 0 1.9125.7875T14.7 11.5q0 1.125-.7875 1.9125T12 14.2ZM12 19q-3.65 0-6.65-2.0375T1 11.5Q2.35 8.075 5.35 6.0375T12 4q3.65 0 6.65 2.0375T23 11.5q-1.35 3.425-4.35 5.4625T12 19Zm0-7.5ZM12 17q2.825 0 5.1875-1.4875T20.8 11.5q-1.25-2.525-3.6125-4.0125T12 6 6.8125 7.4875 3.2 11.5q1.25 2.525 3.6125 4.0125T12 17Z"),
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"),
@@ -135,48 +129,40 @@ public enum SVG {
public static final double DEFAULT_SIZE = 24;
private final String path;
static void setSize(SVGPath path, double size) {
double scale = size / DEFAULT_SIZE;
path.setScaleX(scale);
path.setScaleY(scale);
}
SVG(String path) {
this.path = path;
private final String rawPath;
private String path;
SVG(String rawPath) {
this.rawPath = rawPath;
}
public String getPath() {
if (path == null)
// We move the current point so that SVGPath will treat 0 0 24 24 as the layout bounds
path = "M24 24ZM0 0Z" + rawPath;
return path;
}
private static Node createIcon(SVGPath path, double size) {
if (size < 0) {
StackPane pane = new StackPane(path);
pane.setAlignment(Pos.CENTER);
return pane;
}
double scale = size / 24;
path.setScaleX(scale);
path.setScaleY(scale);
return new Group(path);
public SVGPath createIcon() {
var path = new SVGPath();
path.getStyleClass().add("svg");
path.setContent(getPath());
return path;
}
public Node createIcon(double size) {
SVGPath p = new SVGPath();
p.setContent(path);
p.getStyleClass().add("svg");
return createIcon(p, size);
public SVGContainer createIcon(double size) {
return new SVGContainer(this, size);
}
public Node createIcon() {
SVGPath p = new SVGPath();
p.setContent(path);
p.getStyleClass().add("svg");
return createIcon(p, -1);
}
public Node createIcon(ObservableValue<? extends Paint> color) {
SVGPath p = new SVGPath();
p.setContent(path);
p.getStyleClass().add("svg");
p.fillProperty().bind(color);
return createIcon(p, -1);
public SVGPath createIcon(ObservableValue<? extends Paint> color) {
SVGPath path = createIcon();
path.fillProperty().bind(color);
return path;
}
}
@@ -0,0 +1,175 @@
/*
* 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.ui;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.scene.Parent;
import javafx.scene.shape.SVGPath;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.animation.Motion;
/// A lightweight wrapper for displaying [SVG] icons.
///
/// @author Glavo
public final class SVGContainer extends Parent {
private static final String DEFAULT_STYLE_CLASS = "svg-container";
private final SVGPath path = new SVGPath();
private SVG icon = SVG.NONE;
private double iconSize = SVG.DEFAULT_SIZE;
private SVGPath tempPath;
private Timeline timeline;
{
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
this.path.getStyleClass().add("svg");
}
/// Creates an SVGContainer with the default icon and the default icon size.
public SVGContainer() {
this(SVG.NONE, SVG.DEFAULT_SIZE);
}
/// Creates an SVGContainer showing the given icon using the default icon size.
///
/// @param icon the [SVG] icon to display
public SVGContainer(SVG icon) {
this(icon, SVG.DEFAULT_SIZE);
}
/// Creates an SVGContainer with a custom icon size. The initial icon is
/// [SVG#NONE].
///
/// @param iconSize the icon size
public SVGContainer(double iconSize) {
this(SVG.NONE, iconSize);
}
/// Creates an SVGContainer with the specified icon and size.
///
/// @param icon the [SVG] icon to display
/// @param iconSize the icon size
public SVGContainer(SVG icon, double iconSize) {
setIconSizeImpl(iconSize);
setIcon(icon);
}
public double getIconSize() {
return iconSize;
}
private void setIconSizeImpl(double newSize) {
this.iconSize = newSize;
SVG.setSize(path, newSize);
if (tempPath != null)
SVG.setSize(tempPath, newSize);
}
public void setIconSize(double newSize) {
setIconSizeImpl(newSize);
requestLayout();
}
/// Gets the currently displayed icon.
public SVG getIcon() {
return icon;
}
/// Sets the icon to display without animation.
public void setIcon(SVG newIcon) {
setIcon(newIcon, Duration.ZERO);
}
/// Sets the icon to display with a cross-fade animation.
public void setIcon(SVG newIcon, Duration animationDuration) {
if (timeline != null) {
timeline.stop();
timeline = null;
}
SVG oldIcon = this.icon;
this.icon = newIcon;
if (animationDuration.equals(Duration.ZERO)) {
path.setContent(newIcon.getPath());
path.setOpacity(1);
if (getChildren().size() != 1)
getChildren().setAll(path);
} else {
if (tempPath == null) {
tempPath = new SVGPath();
tempPath.getStyleClass().add("svg");
SVG.setSize(tempPath, iconSize);
} else
tempPath.setOpacity(1);
tempPath.setContent(oldIcon.getPath());
getChildren().setAll(path, tempPath);
path.setOpacity(0);
path.setContent(newIcon.getPath());
timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(path.opacityProperty(), 0, Motion.LINEAR),
new KeyValue(tempPath.opacityProperty(), 1, Motion.LINEAR)
),
new KeyFrame(animationDuration,
new KeyValue(path.opacityProperty(), 1, Motion.LINEAR),
new KeyValue(tempPath.opacityProperty(), 0, Motion.LINEAR)
)
);
timeline.setOnFinished(e -> {
getChildren().setAll(path);
timeline = null;
});
timeline.play();
}
}
// Parent
@Override
public double prefWidth(double height) {
return iconSize;
}
@Override
public double prefHeight(double width) {
return iconSize;
}
@Override
public double minHeight(double width) {
return iconSize;
}
@Override
public double minWidth(double height) {
return iconSize;
}
@Override
protected void layoutChildren() {
// Do nothing
}
}
@@ -42,14 +42,11 @@ public abstract class ToolbarListPageSkin<E, P extends ListPageBase<E>> extends
public ToolbarListPageSkin(P skinnable) {
super(skinnable);
SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
spinnerPane.failedReasonProperty().bind(skinnable.failedReasonProperty());
spinnerPane.onFailedActionProperty().bind(skinnable.onFailedActionProperty());
spinnerPane.getStyleClass().add("large-spinner-pane");
ComponentList root = new ComponentList();
root.getStyleClass().add("no-padding");
StackPane container = new StackPane();
container.getChildren().add(root);
StackPane.setMargin(root, new Insets(10));
List<Node> toolbarButtons = initializeToolbar(skinnable);
@@ -61,32 +58,33 @@ public abstract class ToolbarListPageSkin<E, P extends ListPageBase<E>> extends
root.getContent().add(toolbar);
}
SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
spinnerPane.failedReasonProperty().bind(skinnable.failedReasonProperty());
spinnerPane.onFailedActionProperty().bind(skinnable.onFailedActionProperty());
ComponentList.setVgrow(spinnerPane, Priority.ALWAYS);
{
this.listView = new JFXListView<>();
this.listView.setPadding(Insets.EMPTY);
this.listView.setCellFactory(listView -> createListCell((JFXListView<E>) listView));
ComponentList.setVgrow(listView, Priority.ALWAYS);
this.listView.getStyleClass().add("no-horizontal-scrollbar");
Bindings.bindContent(this.listView.getItems(), skinnable.itemsProperty());
FXUtils.ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
root.getContent().add(listView);
spinnerPane.setContent(listView);
}
spinnerPane.setContent(root);
root.getContent().add(spinnerPane);
getChildren().setAll(spinnerPane);
}
public static Node wrap(Node node) {
StackPane stackPane = new StackPane();
stackPane.setPadding(new Insets(0, 5, 0, 2));
stackPane.getChildren().setAll(node);
return stackPane;
getChildren().setAll(container);
}
public static JFXButton createToolbarButton2(String text, SVG svg, Runnable onClick) {
JFXButton ret = new JFXButton();
ret.getStyleClass().add("jfx-tool-bar-button");
ret.setGraphic(wrap(svg.createIcon()));
ret.setGraphic(svg.createIcon(20));
ret.setText(text);
ret.setOnAction(e -> onClick.run());
return ret;
@@ -95,7 +93,7 @@ public abstract class ToolbarListPageSkin<E, P extends ListPageBase<E>> extends
public static JFXButton createDecoratorButton(String tooltip, SVG svg, Runnable onClick) {
JFXButton ret = new JFXButton();
ret.getStyleClass().add("jfx-decorator-button");
ret.setGraphic(wrap(svg.createIcon()));
ret.setGraphic(svg.createIcon(20));
FXUtils.installFastTooltip(ret, tooltip);
ret.setOnAction(e -> onClick.run());
return ret;
@@ -20,14 +20,19 @@ package org.jackhuang.hmcl.ui;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.collections.ListChangeListener;
import javafx.collections.WeakListChangeListener;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.event.EventPriority;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public class WeakListenerHolder {
public final class WeakListenerHolder {
private final List<Object> refs = new ArrayList<>(0);
public WeakListenerHolder() {
@@ -48,6 +53,22 @@ public class WeakListenerHolder {
return new WeakListChangeListener<>(listener);
}
public <T extends Event> void registerWeak(EventManager<T> manager, Consumer<T> consumer) {
refs.add(manager.registerWeak(consumer));
}
public <T extends Event> void registerWeak(EventManager<T> manager, Consumer<T> consumer, EventPriority priority) {
refs.add(manager.registerWeak(consumer, priority));
}
public <T> void onWeakChange(ObservableValue<T> value, Consumer<T> consumer) {
refs.add(FXUtils.onWeakChange(value, consumer));
}
public <T> void onWeakChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
refs.add(FXUtils.onWeakChangeAndOperate(value, consumer));
}
public void add(Object obj) {
refs.add(obj);
}
@@ -21,6 +21,7 @@ import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Tooltip;
import org.jackhuang.hmcl.auth.Account;
@@ -76,9 +77,10 @@ public class AccountAdvancedListItem extends AdvancedListItem {
FXUtils.installFastTooltip(this, tooltip);
canvas = new Canvas(32, 32);
setLeftGraphic(canvas);
canvas.setMouseTransparent(true);
AdvancedListItem.setAlignment(canvas, Pos.CENTER);
setActionButtonVisible(false);
setLeftGraphic(canvas);
if (account != null) {
this.accountProperty().set(account);
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.JFXButton;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ListProperty;
@@ -54,7 +53,6 @@ import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.util.Locale;
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
import static org.jackhuang.hmcl.ui.FXUtils.wrap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -130,35 +128,25 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
AdvancedListItem microsoftItem = new AdvancedListItem();
microsoftItem.getStyleClass().add("navigation-drawer-item");
microsoftItem.setActionButtonVisible(false);
microsoftItem.setTitle(i18n("account.methods.microsoft"));
microsoftItem.setLeftGraphic(wrap(SVG.MICROSOFT));
microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
microsoftItem.setLeftIcon(SVG.MICROSOFT);
microsoftItem.setOnAction(e -> Controllers.dialog(new MicrosoftAccountLoginPane()));
AdvancedListItem offlineItem = new AdvancedListItem();
offlineItem.getStyleClass().add("navigation-drawer-item");
offlineItem.setActionButtonVisible(false);
offlineItem.setTitle(i18n("account.methods.offline"));
offlineItem.setLeftGraphic(wrap(SVG.PERSON));
offlineItem.setLeftIcon(SVG.PERSON);
offlineItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_OFFLINE)));
VBox boxAuthServers = new VBox();
authServerItems = MappedObservableList.create(skinnable.authServersProperty(), server -> {
AdvancedListItem item = new AdvancedListItem();
item.getStyleClass().add("navigation-drawer-item");
item.setLeftGraphic(wrap(SVG.DRESSER));
item.setLeftIcon(SVG.DRESSER);
item.setOnAction(e -> Controllers.dialog(new CreateAccountPane(server)));
JFXButton btnRemove = new JFXButton();
btnRemove.setOnAction(e -> {
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
skinnable.authServersProperty().remove(server);
}, null);
e.consume();
});
btnRemove.getStyleClass().add("toggle-icon4");
btnRemove.setGraphic(SVG.CLOSE.createIcon(14));
item.setRightGraphic(btnRemove);
item.setRightAction(SVG.CLOSE, () -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
skinnable.authServersProperty().remove(server);
}, null));
ObservableValue<String> title = BindingMapping.of(server, AuthlibInjectorServer::getName);
item.titleProperty().bind(title);
@@ -206,8 +194,7 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
addAuthServerItem.getStyleClass().add("navigation-drawer-item");
addAuthServerItem.setTitle(i18n("account.injector.add"));
addAuthServerItem.setSubtitle(i18n("account.methods.authlib_injector"));
addAuthServerItem.setActionButtonVisible(false);
addAuthServerItem.setLeftGraphic(wrap(SVG.ADD_CIRCLE));
addAuthServerItem.setLeftIcon(SVG.ADD_CIRCLE);
addAuthServerItem.setOnAction(e -> Controllers.dialog(new AddAuthlibInjectorServerPane()));
VBox.setMargin(addAuthServerItem, new Insets(0, 0, 12, 0));
}
@@ -23,9 +23,7 @@ import javafx.application.Platform;
import javafx.beans.NamedArg;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -45,7 +43,6 @@ import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory;
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.game.OAuthServer;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
@@ -54,7 +51,6 @@ import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
import org.jackhuang.hmcl.util.StringUtils;
@@ -85,13 +81,11 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
private final JFXButton btnAccept;
private final SpinnerPane spinner;
private final Node body;
private final HBox actions;
private Node detailsPane; // AccountDetailsInputPane for Offline / Mojang / authlib-injector, Label for Microsoft
private final Pane detailsContainer;
private final BooleanProperty logging = new SimpleBooleanProperty();
private final ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
private final WeakListenerHolder holder = new WeakListenerHolder();
private TaskExecutor loginTask;
@@ -146,10 +140,10 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
btnCancel.setOnAction(e -> onCancel());
onEscPressed(this, btnCancel::fire);
HBox hbox = new HBox(spinner, btnCancel);
hbox.setAlignment(Pos.CENTER_RIGHT);
actions = new HBox(spinner, btnCancel);
actions.setAlignment(Pos.CENTER_RIGHT);
setActions(lblErrorMessage, hbox);
setActions(lblErrorMessage, actions);
}
if (showMethodSwitcher) {
@@ -226,7 +220,6 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
Runnable doCreate = () -> {
logging.set(true);
deviceCode.set(null);
loginTask = Task.supplyAsync(() -> factory.create(new DialogCharacterSelector(), username, password, null, additionalData))
.whenComplete(Schedulers.javafx(), account -> {
@@ -280,93 +273,16 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
btnAccept.disableProperty().unbind();
detailsContainer.getChildren().remove(detailsPane);
lblErrorMessage.setText("");
lblErrorMessage.setVisible(true);
setActions(lblErrorMessage, actions);
}
if (factory == Accounts.FACTORY_MICROSOFT) {
VBox vbox = new VBox(8);
detailsPane = vbox;
if (Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
hintPane.setSegment(i18n("account.methods.microsoft.snapshot"));
vbox.getChildren().add(hintPane);
return;
}
if (!IntegrityChecker.isOfficial()) {
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
hintPane.setSegment(i18n("unofficial.hint"));
vbox.getChildren().add(hintPane);
}
VBox codeBox = new VBox(8);
Label hint = new Label(i18n("account.methods.microsoft.code"));
Label code = new Label();
code.setMouseTransparent(true);
code.setStyle("-fx-font-size: 24");
codeBox.getChildren().addAll(hint, code);
codeBox.setAlignment(Pos.CENTER);
vbox.getChildren().add(codeBox);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
HintPane errHintPane = new HintPane(MessageDialogPane.MessageType.ERROR);
errHintPane.setVisible(false);
errHintPane.setManaged(false);
codeBox.setVisible(false);
codeBox.setManaged(false);
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
if (deviceCode != null) {
FXUtils.copyText(deviceCode.getUserCode());
code.setText(deviceCode.getUserCode());
hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getVerificationUri()));
codeBox.setVisible(true);
codeBox.setManaged(true);
} else {
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
codeBox.setVisible(false);
codeBox.setManaged(false);
}
});
lblErrorMessage.setVisible(false);
lblErrorMessage.textProperty().addListener((obs, oldVal, newVal) -> {
boolean hasError = !newVal.isEmpty();
errHintPane.setSegment(newVal);
errHintPane.setVisible(hasError);
errHintPane.setManaged(hasError);
hintPane.setVisible(!hasError);
hintPane.setManaged(!hasError);
codeBox.setVisible(!hasError && deviceCode.get() != null);
codeBox.setManaged(!hasError && deviceCode.get() != null);
});
FXUtils.onClicked(codeBox, () -> {
if (deviceCode.get() != null) FXUtils.copyText(deviceCode.get().getUserCode());
});
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(value ->
runInFX(() -> deviceCode.set(value))
));
HBox linkBox = new HBox();
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
JFXHyperlink deauthorizeLink = new JFXHyperlink(i18n("account.methods.microsoft.deauthorize"));
deauthorizeLink.setExternalLink("https://account.live.com/consent/Edit?client_id=000000004C794E0A");
JFXHyperlink forgotpasswordLink = new JFXHyperlink(i18n("account.methods.forgot_password"));
forgotpasswordLink.setExternalLink("https://account.live.com/ResetPassword.aspx");
linkBox.getChildren().setAll(profileLink, purchaseLink, deauthorizeLink, forgotpasswordLink);
vbox.getChildren().addAll(hintPane, errHintPane, linkBox);
btnAccept.setDisable(false);
detailsPane = new MicrosoftAccountLoginPane(true);
setActions();
} else {
detailsPane = new AccountDetailsInputPane(factory, btnAccept::fire);
btnAccept.disableProperty().bind(((AccountDetailsInputPane) detailsPane).validProperty().not());
setActions(lblErrorMessage, actions);
}
detailsContainer.getChildren().add(detailsPane);
@@ -0,0 +1,303 @@
package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXSpinner;
import io.nayuki.qrcodegen.QrCode;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.SVGPath;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.OAuth;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.QrCodeUtils;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.concurrent.CancellationException;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class MicrosoftAccountLoginPane extends JFXDialogLayout implements DialogAware {
private final Account accountToRelogin;
private final Consumer<AuthInfo> loginCallback;
private final Runnable cancelCallback;
@SuppressWarnings("FieldCanBeLocal")
private final WeakListenerHolder holder = new WeakListenerHolder();
private final ObjectProperty<Step> step = new SimpleObjectProperty<>();
private TaskExecutor browserTaskExecutor;
private TaskExecutor deviceTaskExecutor;
private final JFXButton btnLogin;
private final SpinnerPane loginButtonSpinner;
public MicrosoftAccountLoginPane() {
this(false);
}
public MicrosoftAccountLoginPane(boolean bodyonly) {
this(null, null, null, bodyonly);
}
public MicrosoftAccountLoginPane(Account account, Consumer<AuthInfo> callback, Runnable onCancel, boolean bodyonly) {
this.accountToRelogin = account;
this.loginCallback = callback;
this.cancelCallback = onCancel;
getStyleClass().add("microsoft-login-dialog");
if (bodyonly) {
this.pseudoClassStateChanged(PseudoClass.getPseudoClass("bodyonly"), true);
} else {
Label heading = new Label(accountToRelogin != null ? i18n("account.login.refresh") : i18n("account.create.microsoft"));
heading.getStyleClass().add("header-label");
setHeading(heading);
}
this.setMaxWidth(650);
onEscPressed(this, this::onCancel);
btnLogin = new JFXButton(i18n("account.login"));
btnLogin.getStyleClass().add("dialog-accept");
loginButtonSpinner = new SpinnerPane();
loginButtonSpinner.getStyleClass().add("small-spinner-pane");
loginButtonSpinner.setContent(btnLogin);
JFXButton btnCancel = new JFXButton(i18n("button.cancel"));
btnCancel.getStyleClass().add("dialog-cancel");
btnCancel.setOnAction(e -> onCancel());
setActions(loginButtonSpinner, btnCancel);
holder.registerWeak(Accounts.OAUTH_CALLBACK.onOpenBrowserAuthorizationCode, event -> Platform.runLater(() -> {
if (step.get() instanceof Step.StartAuthorizationCodeLogin)
step.set(new Step.WaitForOpenBrowser(event.getUrl()));
}));
holder.registerWeak(Accounts.OAUTH_CALLBACK.onGrantDeviceCode, event -> Platform.runLater(() -> {
if (step.get() instanceof Step.StartDeviceCodeLogin)
step.set(new Step.WaitForScanQrCode(event.getUserCode(), event.getVerificationUri()));
}));
this.step.set(Accounts.OAUTH_CALLBACK.getClientId().isEmpty()
? new Step.Init()
: new Step.StartAuthorizationCodeLogin());
FXUtils.onChangeAndOperate(step, this::onStep);
}
private void onStep(Step currentStep) {
VBox rootContainer = new VBox(10);
setBody(rootContainer);
rootContainer.setAlignment(Pos.TOP_CENTER);
if (Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
var snapshotHint = new HintPane(MessageDialogPane.MessageType.WARNING);
snapshotHint.setSegment(i18n("account.methods.microsoft.snapshot"));
rootContainer.getChildren().add(snapshotHint);
btnLogin.setDisable(true);
loginButtonSpinner.setLoading(false);
return;
}
if (!IntegrityChecker.isOfficial()) {
var unofficialHintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
unofficialHintPane.setSegment(i18n("unofficial.hint"));
rootContainer.getChildren().add(unofficialHintPane);
}
if (currentStep instanceof Step.Init) {
btnLogin.setOnAction(e -> this.step.set(new Step.StartAuthorizationCodeLogin()));
loginButtonSpinner.setLoading(false);
var hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
hintPane.setText(i18n("account.methods.microsoft.hint"));
rootContainer.getChildren().add(hintPane);
} else if (currentStep instanceof Step.StartAuthorizationCodeLogin) {
loginButtonSpinner.setLoading(false);
cancelAllTasks();
rootContainer.getChildren().add(new JFXSpinner());
browserTaskExecutor = Task.supplyAsync(() -> Accounts.FACTORY_MICROSOFT.create(null, null, null, null, OAuth.GrantFlow.AUTHORIZATION_CODE))
.whenComplete(Schedulers.javafx(), this::onLoginCompleted)
.executor(true);
} else if (currentStep instanceof Step.StartDeviceCodeLogin) {
loginButtonSpinner.setLoading(true);
cancelAllTasks();
rootContainer.getChildren().add(new JFXSpinner());
deviceTaskExecutor = Task.supplyAsync(() -> Accounts.FACTORY_MICROSOFT.create(null, null, null, null, OAuth.GrantFlow.DEVICE))
.whenComplete(Schedulers.javafx(), this::onLoginCompleted)
.executor(true);
} else if (currentStep instanceof Step.WaitForOpenBrowser wait) {
btnLogin.setOnAction(e -> {
FXUtils.openLink(wait.url());
loginButtonSpinner.setLoading(true);
});
loginButtonSpinner.setLoading(false);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
hintPane.setSegment(
i18n("account.methods.microsoft.methods.browser.hint", StringUtils.escapeXmlAttribute(wait.url()), wait.url()),
FXUtils::copyText
);
rootContainer.getChildren().add(hintPane);
} else if (currentStep instanceof Step.WaitForScanQrCode wait) {
loginButtonSpinner.setLoading(true);
String scanUri = "https://www.microsoft.com/link".equals(wait.verificationUri())
? "https://www.microsoft.com/link?otc=" + wait.userCode()
: wait.verificationUri();
var deviceHint = new HintPane(MessageDialogPane.MessageType.INFO);
deviceHint.setSegment(i18n("account.methods.microsoft.methods.device.hint",
StringUtils.escapeXmlAttribute(scanUri),
wait.verificationUri(),
wait.userCode()
));
var qrCode = new SVGPath();
qrCode.fillProperty().bind(Themes.colorSchemeProperty().getPrimary());
qrCode.setContent(QrCodeUtils.toSVGPath(QrCode.encodeText(scanUri, QrCode.Ecc.MEDIUM)));
qrCode.setScaleX(3);
qrCode.setScaleY(3);
var lblCode = new Label(wait.userCode());
lblCode.getStyleClass().add("code-label");
lblCode.setStyle("-fx-font-family: \"" + Lang.requireNonNullElse(config().getFontFamily(), FXUtils.DEFAULT_MONOSPACE_FONT) + "\";");
var codeBox = new StackPane(lblCode);
codeBox.getStyleClass().add("code-box");
codeBox.setCursor(Cursor.HAND);
FXUtils.onClicked(codeBox, () -> FXUtils.copyText(wait.userCode()));
codeBox.setMaxWidth(USE_PREF_SIZE);
rootContainer.getChildren().addAll(deviceHint, new Group(qrCode), codeBox);
} else if (currentStep instanceof Step.LoginFailed failed) {
btnLogin.setOnAction(e -> this.step.set(new Step.StartAuthorizationCodeLogin()));
loginButtonSpinner.setLoading(false);
cancelAllTasks();
HintPane errHintPane = new HintPane(MessageDialogPane.MessageType.ERROR);
errHintPane.setSegment(failed.message());
rootContainer.getChildren().add(errHintPane);
}
var linkBox = new FlowPane(8, 8);
linkBox.setAlignment(Pos.CENTER_LEFT);
linkBox.setPrefWrapLength(500);
if (currentStep instanceof Step.Init || currentStep instanceof Step.StartAuthorizationCodeLogin || currentStep instanceof Step.WaitForOpenBrowser) {
JFXHyperlink useQrCode = new JFXHyperlink(i18n("account.methods.microsoft.methods.device"));
useQrCode.setOnAction(e -> this.step.set(new Step.StartDeviceCodeLogin()));
linkBox.getChildren().add(useQrCode);
} else if (currentStep instanceof Step.StartDeviceCodeLogin || currentStep instanceof Step.WaitForScanQrCode) {
JFXHyperlink userBrowser = new JFXHyperlink(i18n("account.methods.microsoft.methods.browser"));
userBrowser.setOnAction(e -> this.step.set(new Step.StartAuthorizationCodeLogin()));
linkBox.getChildren().add(userBrowser);
}
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
linkBox.getChildren().addAll(profileLink, purchaseLink);
rootContainer.getChildren().add(linkBox);
setBody(rootContainer);
}
private void cancelAllTasks() {
if (browserTaskExecutor != null) browserTaskExecutor.cancel();
if (deviceTaskExecutor != null) deviceTaskExecutor.cancel();
}
private void onCancel() {
cancelAllTasks();
if (cancelCallback != null) cancelCallback.run();
fireEvent(new DialogCloseEvent());
}
private void onLoginCompleted(MicrosoftAccount account, Exception exception) {
if (exception == null) {
if (accountToRelogin != null) Accounts.getAccounts().remove(accountToRelogin);
int oldIndex = Accounts.getAccounts().indexOf(account);
if (oldIndex == -1) {
Accounts.getAccounts().add(account);
} else {
Accounts.getAccounts().remove(oldIndex);
Accounts.getAccounts().add(oldIndex, account);
}
Accounts.setSelectedAccount(account);
if (loginCallback != null) {
try {
loginCallback.accept(account.logIn());
} catch (AuthenticationException e) {
this.step.set(new Step.LoginFailed(Accounts.localizeErrorMessage(e)));
return;
}
}
fireEvent(new DialogCloseEvent());
} else if (!(exception instanceof CancellationException)) {
this.step.set(new Step.LoginFailed(Accounts.localizeErrorMessage(exception)));
}
}
private sealed interface Step {
final class Init implements Step {
}
final class StartAuthorizationCodeLogin implements Step {
}
record WaitForOpenBrowser(String url) implements Step {
}
final class StartDeviceCodeLogin implements Step {
}
record WaitForScanQrCode(String userCode, String verificationUri) implements Step {
}
record LoginFailed(String message) implements Step {
}
}
}
@@ -1,109 +0,0 @@
package org.jackhuang.hmcl.ui.account;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.OAuthAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.game.OAuthServer;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.DialogPane;
import org.jackhuang.hmcl.ui.construct.HintPane;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class OAuthAccountLoginDialog extends DialogPane {
private final OAuthAccount account;
private final Consumer<AuthInfo> success;
private final Runnable failed;
private final ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
private final WeakListenerHolder holder = new WeakListenerHolder();
public OAuthAccountLoginDialog(OAuthAccount account, Consumer<AuthInfo> success, Runnable failed) {
this.account = account;
this.success = success;
this.failed = failed;
setTitle(i18n("account.login.refresh"));
VBox vbox = new VBox(8);
Label usernameLabel = new Label(account.getUsername());
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
if (deviceCode != null) {
FXUtils.copyText(deviceCode.getUserCode());
hintPane.setSegment(
"<b>" + i18n("account.login.refresh.microsoft.hint") + "</b>\n"
+ i18n("account.methods.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri())
);
} else {
hintPane.setSegment(
"<b>" + i18n("account.login.refresh.microsoft.hint") + "</b>\n"
+ i18n("account.methods.microsoft.hint")
);
}
});
FXUtils.onClicked(hintPane, () -> {
if (deviceCode.get() != null) {
FXUtils.copyText(deviceCode.get().getUserCode());
}
});
HBox box = new HBox(8);
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));
birthLink.setExternalLink("https://support.microsoft.com/account-billing/how-to-change-a-birth-date-on-a-microsoft-account-837badbc-999e-54d2-2617-d19206b9540a");
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
box.getChildren().setAll(profileLink, birthLink, purchaseLink);
GridPane.setColumnSpan(box, 2);
vbox.getChildren().setAll(usernameLabel, hintPane, box);
setBody(vbox);
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(this::onGrantDeviceCode));
}
private void onGrantDeviceCode(OAuthServer.GrantDeviceCodeEvent event) {
FXUtils.runInFX(() -> {
deviceCode.set(event);
});
}
@Override
protected void onAccept() {
setLoading();
Task.supplyAsync(account::logInWhenCredentialsExpired)
.whenComplete(Schedulers.javafx(), (authInfo, exception) -> {
if (exception == null) {
success.accept(authInfo);
onSuccess();
} else {
LOG.info("Failed to login when credentials expired: " + account, exception);
onFailure(Accounts.localizeErrorMessage(exception));
}
}).start();
}
@Override
protected void onCancel() {
failed.run();
super.onCancel();
}
}
@@ -29,9 +29,6 @@ import javafx.scene.control.Label;
import javafx.scene.input.DragEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.ui.skin.SkinCanvas;
import org.jackhuang.hmcl.ui.skin.animation.SkinAniRunning;
import org.jackhuang.hmcl.ui.skin.animation.SkinAniWavingArms;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.Skin;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
@@ -40,6 +37,9 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.skin.SkinCanvas;
import org.jackhuang.hmcl.ui.skin.animation.SkinAniRunning;
import org.jackhuang.hmcl.ui.skin.animation.SkinAniWavingArms;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.nio.file.Path;
@@ -48,8 +48,8 @@ import java.util.UUID;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class OfflineAccountSkinPane extends StackPane {
private final OfflineAccount account;
@@ -110,6 +110,7 @@ public class OfflineAccountSkinPane extends StackPane {
cslApiField.setPromptText(i18n("account.skin.type.csl_api.location.hint"));
cslApiField.setValidators(new URLValidator());
FXUtils.setValidateWhileTextChanged(cslApiField, true);
skinItem.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("message.default"), Skin.Type.DEFAULT),
@@ -218,6 +219,10 @@ public class OfflineAccountSkinPane extends StackPane {
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
onEscPressed(this, cancelButton::fire);
acceptButton.disableProperty().bind(
skinItem.selectedDataProperty().isEqualTo(Skin.Type.CUSTOM_SKIN_LOADER_API)
.and(cslApiField.activeValidatorProperty().isNotNull()));
layout.setActions(littleSkinLink, acceptButton, cancelButton);
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.construct;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
@@ -28,8 +27,8 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.SVGContainer;
import org.jackhuang.hmcl.ui.animation.Motion;
import java.util.function.Consumer;
@@ -71,10 +70,9 @@ public class AdvancedListBox extends ScrollPane {
private AdvancedListItem createNavigationDrawerItem(String title, SVG leftGraphic) {
AdvancedListItem item = new AdvancedListItem();
item.getStyleClass().add("navigation-drawer-item");
item.setActionButtonVisible(false);
item.setTitle(title);
if (leftGraphic != null) {
item.setLeftGraphic(FXUtils.wrap(leftGraphic));
item.setLeftIcon(leftGraphic);
}
return item;
}
@@ -107,18 +105,11 @@ public class AdvancedListBox extends ScrollPane {
item.activeProperty().bind(tabHeader.getSelectionModel().selectedItemProperty().isEqualTo(tab));
item.setOnAction(e -> tabHeader.select(tab));
Node unselectedIcon = unselectedGraphic.createIcon(20);
Node selectedIcon = selectedGraphic.createIcon(20);
TransitionPane leftGraphic = new TransitionPane();
leftGraphic.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(leftGraphic, 30);
FXUtils.setLimitHeight(leftGraphic, 20);
leftGraphic.setPadding(Insets.EMPTY);
leftGraphic.setContent(item.isActive() ? selectedIcon : unselectedIcon, ContainerAnimations.NONE);
FXUtils.onChange(item.activeProperty(), active ->
leftGraphic.setContent(active ? selectedIcon : unselectedIcon, ContainerAnimations.FADE));
var leftGraphic = new SVGContainer(item.isActive() ? selectedGraphic : unselectedGraphic, AdvancedListItem.LEFT_ICON_SIZE);
leftGraphic.setMouseTransparent(true);
AdvancedListItem.setAlignment(leftGraphic, Pos.CENTER);
AdvancedListItem.setMargin(leftGraphic, AdvancedListItem.LEFT_ICON_MARGIN);
FXUtils.onChange(item.activeProperty(), active -> leftGraphic.setIcon(active ? selectedGraphic : unselectedGraphic, Motion.SHORT4));
item.setLeftGraphic(leftGraphic);
return add(item);
}
@@ -17,105 +17,134 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import javafx.beans.property.*;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.Pair;
import static org.jackhuang.hmcl.util.Pair.pair;
import org.jackhuang.hmcl.ui.SVG;
public class AdvancedListItem extends Control {
private final ObjectProperty<Node> leftGraphic = new SimpleObjectProperty<>(this, "leftGraphic");
private final ObjectProperty<Node> rightGraphic = new SimpleObjectProperty<>(this, "rightGraphic");
private final StringProperty title = new SimpleStringProperty(this, "title");
private final BooleanProperty active = new SimpleBooleanProperty(this, "active");
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
private final BooleanProperty actionButtonVisible = new SimpleBooleanProperty(this, "actionButtonVisible", true);
public static final double LEFT_GRAPHIC_SIZE = 32;
public static final double LEFT_ICON_SIZE = 20;
public static final Insets LEFT_ICON_MARGIN = new Insets(0, 6, 0, 6);
public static void setMargin(Node graphic, Insets margin) {
BorderPane.setMargin(graphic, margin);
}
public static void setAlignment(Node graphic, Pos alignment) {
BorderPane.setAlignment(graphic, alignment);
}
public AdvancedListItem() {
getStyleClass().add("advanced-list-item");
FXUtils.onClicked(this, () -> fireEvent(new ActionEvent()));
}
public Node getLeftGraphic() {
return leftGraphic.get();
}
private final ObjectProperty<Node> leftGraphic = new SimpleObjectProperty<>(this, "leftGraphic");
public ObjectProperty<Node> leftGraphicProperty() {
return leftGraphic;
}
public Node getLeftGraphic() {
return leftGraphic.get();
}
public void setLeftGraphic(Node leftGraphic) {
this.leftGraphic.set(leftGraphic);
}
public Node getRightGraphic() {
return rightGraphic.get();
public void setLeftIcon(SVG svg) {
Node icon = svg.createIcon(LEFT_ICON_SIZE);
icon.setMouseTransparent(true);
BorderPane.setMargin(icon, LEFT_ICON_MARGIN);
BorderPane.setAlignment(icon, Pos.CENTER);
leftGraphicProperty().set(icon);
}
private final ObjectProperty<Node> rightGraphic = new SimpleObjectProperty<>(this, "rightGraphic");
public ObjectProperty<Node> rightGraphicProperty() {
return rightGraphic;
}
public Node getRightGraphic() {
return rightGraphic.get();
}
public void setRightGraphic(Node rightGraphic) {
this.rightGraphic.set(rightGraphic);
}
public String getTitle() {
return title.get();
public void setRightAction(SVG icon, Runnable action) {
var button = new JFXButton();
button.setOnAction(e -> {
action.run();
e.consume();
});
button.getStyleClass().add("toggle-icon4");
button.setGraphic(icon.createIcon(14));
setAlignment(button, Pos.CENTER);
setRightGraphic(button);
}
private final StringProperty title = new SimpleStringProperty(this, "title");
public StringProperty titleProperty() {
return title;
}
public String getTitle() {
return title.get();
}
public void setTitle(String title) {
this.title.set(title);
}
public boolean isActive() {
return active.get();
}
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
public BooleanProperty activeProperty() {
return active;
}
public void setActive(boolean active) {
this.active.set(active);
public StringProperty subtitleProperty() {
return subtitle;
}
public String getSubtitle() {
return subtitle.get();
}
public StringProperty subtitleProperty() {
return subtitle;
}
public void setSubtitle(String subtitle) {
this.subtitle.set(subtitle);
}
public boolean isActionButtonVisible() {
return actionButtonVisible.get();
private final BooleanProperty active = new SimpleBooleanProperty(this, "active");
public BooleanProperty activeProperty() {
return active;
}
public BooleanProperty actionButtonVisibleProperty() {
return actionButtonVisible;
public boolean isActive() {
return active.get();
}
public void setActionButtonVisible(boolean actionButtonVisible) {
this.actionButtonVisible.set(actionButtonVisible);
public void setActive(boolean active) {
this.active.set(active);
}
private final ObjectProperty<EventHandler<ActionEvent>> onAction = new SimpleObjectProperty<>(this, "onAction") {
@Override
protected void invalidated() {
setEventHandler(ActionEvent.ACTION, get());
}
};
public final ObjectProperty<EventHandler<ActionEvent>> onActionProperty() {
return onAction;
}
@@ -128,32 +157,9 @@ public class AdvancedListItem extends Control {
return onActionProperty().get();
}
private ObjectProperty<EventHandler<ActionEvent>> onAction = new SimpleObjectProperty<EventHandler<ActionEvent>>(this, "onAction") {
@Override
protected void invalidated() {
setEventHandler(ActionEvent.ACTION, get());
}
};
@Override
protected Skin<?> createDefaultSkin() {
return new AdvancedListItemSkin(this);
}
public static Pair<Node, ImageView> createImageView(Image image) {
return createImageView(image, 32, 32);
}
public static Pair<Node, ImageView> createImageView(Image image, double width, double height) {
StackPane imageViewContainer = new StackPane();
FXUtils.setLimitWidth(imageViewContainer, width);
FXUtils.setLimitHeight(imageViewContainer, height);
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, width, height);
imageView.setPreserveRatio(true);
imageView.setImage(image);
imageViewContainer.getChildren().setAll(imageView);
return pair(imageViewContainer, imageView);
}
}
@@ -18,10 +18,8 @@
package org.jackhuang.hmcl.ui.construct;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.ui.FXUtils;
public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
@@ -40,41 +38,14 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
RipplerContainer container = new RipplerContainer(root);
HBox left = new HBox();
left.setAlignment(Pos.CENTER_LEFT);
left.setMouseTransparent(true);
TwoLineListItem item = new TwoLineListItem();
root.setCenter(item);
item.setMouseTransparent(true);
item.titleProperty().bind(skinnable.titleProperty());
item.subtitleProperty().bind(skinnable.subtitleProperty());
FXUtils.onChangeAndOperate(skinnable.leftGraphicProperty(),
newGraphic -> {
if (newGraphic == null) {
left.getChildren().clear();
} else {
left.getChildren().setAll(newGraphic);
}
});
root.setLeft(left);
HBox right = new HBox();
right.setAlignment(Pos.CENTER);
right.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(right, 40);
FXUtils.onChangeAndOperate(skinnable.rightGraphicProperty(),
newGraphic -> {
if (newGraphic == null) {
right.getChildren().clear();
} else {
right.getChildren().setAll(newGraphic);
}
});
FXUtils.onChangeAndOperate(skinnable.actionButtonVisibleProperty(),
visible -> root.setRight(visible ? right : null));
root.leftProperty().bind(skinnable.leftGraphicProperty());
root.rightProperty().bind(skinnable.rightGraphicProperty());
getChildren().setAll(container);
}
@@ -45,9 +45,11 @@ final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
ComponentSublistWrapper(ComponentSublist sublist) {
boolean noPadding = !sublist.hasComponentPadding();
this.getStyleClass().add("options-sublist-wrapper");
Node expandIcon = SVG.KEYBOARD_ARROW_DOWN.createIcon(20);
expandIcon.getStyleClass().add("expand-icon");
expandIcon.setMouseTransparent(true);
HBox.setMargin(expandIcon, new Insets(0, 8, 0, 8));
VBox labelVBox = new VBox();
labelVBox.setMouseTransparent(true);
@@ -72,7 +74,7 @@ final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
}
HBox header = new HBox();
header.setSpacing(16);
header.setSpacing(12);
header.getChildren().add(labelVBox);
header.setPadding(new Insets(10, 16, 10, 16));
header.setAlignment(Pos.CENTER_LEFT);
@@ -1,162 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 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.ui.construct;
import com.jfoenix.controls.JFXButton;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.DirectoryChooser;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class FileItem extends BorderPane {
private final Label lblPath = new Label();
private final SimpleStringProperty name = new SimpleStringProperty(this, "name");
private final SimpleStringProperty title = new SimpleStringProperty(this, "title");
private final SimpleStringProperty path = new SimpleStringProperty(this, "path");
private final SimpleBooleanProperty convertToRelativePath = new SimpleBooleanProperty(this, "convertToRelativePath");
public FileItem() {
VBox left = new VBox();
Label name = new Label();
name.textProperty().bind(nameProperty());
lblPath.getStyleClass().addAll("subtitle-label");
lblPath.textProperty().bind(path);
left.getChildren().addAll(name, lblPath);
setLeft(left);
JFXButton right = new JFXButton();
right.setGraphic(SVG.EDIT.createIcon(16));
right.getStyleClass().add("toggle-icon4");
right.setOnAction(e -> onExplore());
FXUtils.installFastTooltip(right, i18n("button.edit"));
setRight(right);
convertToRelativePath.addListener(onInvalidating(() -> path.set(processPath(path.get()))));
}
/**
* Converts the given path to absolute/relative(if possible) path according to {@link #convertToRelativePathProperty()}.
*/
private String processPath(String path) {
Path given;
try {
given = Path.of(path).toAbsolutePath().normalize();
} catch (IllegalArgumentException e) {
return path;
}
if (isConvertToRelativePath()) {
try {
return Metadata.CURRENT_DIRECTORY.relativize(given).normalize().toString();
} catch (IllegalArgumentException e) {
// the given path can't be relativized against current path
}
}
return given.normalize().toString();
}
public void onExplore() {
DirectoryChooser chooser = new DirectoryChooser();
if (path.get() != null) {
Path file;
try {
file = Path.of(path.get());
if (Files.exists(file)) {
if (Files.isRegularFile(file))
file = file.toAbsolutePath().normalize().getParent();
else if (Files.isDirectory(file))
file = file.toAbsolutePath().normalize();
chooser.setInitialDirectory(file.toFile());
}
} catch (InvalidPathException e) {
LOG.warning("Failed to resolve path: " + path.get());
}
}
chooser.titleProperty().bind(titleProperty());
var selectedDir = chooser.showDialog(Controllers.getStage());
if (selectedDir != null) {
path.set(processPath(selectedDir.toString()));
}
chooser.titleProperty().unbind();
}
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
public void setName(String name) {
this.name.set(name);
}
public String getTitle() {
return title.get();
}
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public String getPath() {
return path.get();
}
public StringProperty pathProperty() {
return path;
}
public void setPath(String path) {
this.path.set(path);
}
public boolean isConvertToRelativePath() {
return convertToRelativePath.get();
}
public BooleanProperty convertToRelativePathProperty() {
return convertToRelativePath;
}
public void setConvertToRelativePath(boolean convertToRelativePath) {
this.convertToRelativePath.set(convertToRelativePath);
}
}
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.Locale;
import java.util.function.Consumer;
public class HintPane extends VBox {
private final Text label = new Text();
@@ -66,7 +67,11 @@ public class HintPane extends VBox {
}
public void setSegment(String segment) {
flow.getChildren().setAll(FXUtils.parseSegment(segment, Controllers::onHyperlinkAction));
this.setSegment(segment, Controllers::onHyperlinkAction);
}
public void setSegment(String segment, Consumer<String> hyperlinkAction) {
flow.getChildren().setAll(FXUtils.parseSegment(segment, hyperlinkAction));
}
public void setChildren(Node... children) {
@@ -24,7 +24,7 @@ import org.jackhuang.hmcl.ui.SVG;
public final class IconedMenuItem extends IconedItem {
public IconedMenuItem(SVG icon, String text, Runnable action, JFXPopup popup) {
super(icon != null ? FXUtils.limitingSize(icon.createIcon(14), 14, 14) : null, text);
super(icon != null ? icon.createIcon(14) : null, text);
getStyleClass().setAll("iconed-menu-item");
@@ -1,118 +0,0 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.StringUtils;
public class IconedTwoLineListItem extends HBox {
private final StringProperty title = new SimpleStringProperty(this, "title");
private final ObservableList<Label> tags = FXCollections.observableArrayList();
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
private final StringProperty externalLink = new SimpleStringProperty(this, "externalLink");
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
private final ImageView imageView = new ImageView();
private final TwoLineListItem twoLineListItem = new TwoLineListItem();
private JFXButton externalLinkButton;
@SuppressWarnings("FieldCanBeLocal")
private final InvalidationListener observer;
public IconedTwoLineListItem() {
setAlignment(Pos.CENTER);
setSpacing(16);
imageView.imageProperty().bind(image);
twoLineListItem.titleProperty().bind(title);
twoLineListItem.subtitleProperty().bind(subtitle);
HBox.setHgrow(twoLineListItem, Priority.ALWAYS);
Bindings.bindContent(twoLineListItem.getTags(), tags);
observer = FXUtils.observeWeak(() -> {
getChildren().clear();
if (image.get() != null) getChildren().add(imageView);
getChildren().add(twoLineListItem);
if (StringUtils.isNotBlank(externalLink.get())) getChildren().add(getExternalLinkButton());
}, image, externalLink);
}
public String getTitle() {
return title.get();
}
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public ObservableList<Label> getTags() {
return tags;
}
public String getSubtitle() {
return subtitle.get();
}
public StringProperty subtitleProperty() {
return subtitle;
}
public void setSubtitle(String subtitle) {
this.subtitle.set(subtitle);
}
public String getExternalLink() {
return externalLink.get();
}
public StringProperty externalLinkProperty() {
return externalLink;
}
public void setExternalLink(String externalLink) {
this.externalLink.set(externalLink);
}
public Image getImage() {
return image.get();
}
public ObjectProperty<Image> imageProperty() {
return image;
}
public void setImage(Image image) {
this.image.set(image);
}
public ImageView getImageView() {
return imageView;
}
public JFXButton getExternalLinkButton() {
if (externalLinkButton == null) {
externalLinkButton = new JFXButton();
externalLinkButton.getStyleClass().add("toggle-icon4");
externalLinkButton.setGraphic(SVG.OPEN_IN_NEW.createIcon());
externalLinkButton.setOnAction(e -> FXUtils.openLink(externalLink.get()));
}
return externalLinkButton;
}
}
@@ -0,0 +1,176 @@
/*
* 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.ui.construct;
import javafx.beans.property.*;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.OverrunStyle;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
/// @author Glavo
public class LineButton extends LineButtonBase {
private static final String DEFAULT_STYLE_CLASS = "line-button";
private static final int IDX_TRAILING_TEXT = IDX_TRAILING;
private static final int IDX_TRAILING_ICON = IDX_TRAILING + 1;
public static LineButton createNavigationButton() {
var button = new LineButton();
button.setTrailingIcon(SVG.ARROW_FORWARD);
return button;
}
public static LineButton createExternalLinkButton(String url) {
var button = new LineButton();
button.setTrailingIcon(SVG.OPEN_IN_NEW);
if (url != null) {
button.setOnAction(event -> FXUtils.openLink(url));
}
return button;
}
public LineButton() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
container.setMouseTransparent(true);
}
private ObjectProperty<EventHandler<ActionEvent>> onAction;
public ObjectProperty<EventHandler<ActionEvent>> onActionProperty() {
if (onAction == null) {
onAction = new ObjectPropertyBase<>() {
@Override
public Object getBean() {
return LineButton.this;
}
@Override
public String getName() {
return "onAction";
}
@Override
protected void invalidated() {
setEventHandler(ActionEvent.ACTION, get());
}
};
}
return onAction;
}
public EventHandler<ActionEvent> getOnAction() {
return onActionProperty().get();
}
public void setOnAction(EventHandler<ActionEvent> value) {
onActionProperty().set(value);
}
private StringProperty trailingText;
public StringProperty trailingTextProperty() {
if (trailingText == null) {
trailingText = new StringPropertyBase() {
private Label trailingTextLabel;
@Override
public Object getBean() {
return LineButton.this;
}
@Override
public String getName() {
return "trailingText";
}
@Override
protected void invalidated() {
String message = get();
if (message != null && !message.isEmpty()) {
if (trailingTextLabel == null) {
trailingTextLabel = new Label();
trailingTextLabel.getStyleClass().add("trailing-label");
trailingTextLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
}
trailingTextLabel.setText(message);
setNode(IDX_TRAILING_TEXT, trailingTextLabel);
} else if (trailingTextLabel != null) {
trailingTextLabel.setText("");
setNode(IDX_TRAILING_TEXT, null);
}
}
};
}
return trailingText;
}
public String getTrailingText() {
return trailingText != null ? trailingText.get() : null;
}
public void setTrailingText(String trailingText) {
trailingTextProperty().set(trailingText);
}
private ObjectProperty<Node> trailingIcon;
public ObjectProperty<Node> trailingIconProperty() {
if (trailingIcon == null)
trailingIcon = new ObjectPropertyBase<>() {
@Override
public Object getBean() {
return LineButton.this;
}
@Override
public String getName() {
return "trailingIcon";
}
@Override
protected void invalidated() {
setNode(IDX_TRAILING_ICON, get());
}
};
return trailingIcon;
}
public Node getTrailingIcon() {
return trailingIcon != null ? trailingIcon.get() : null;
}
public void setTrailingIcon(Node trailingIcon) {
trailingIconProperty().set(trailingIcon);
}
public void setTrailingIcon(SVG rightIcon) {
setTrailingIcon(rightIcon, 20);
}
public void setTrailingIcon(SVG rightIcon, double size) {
Node rightIconNode = rightIcon.createIcon(size);
rightIconNode.getStyleClass().add("trailing-icon");
setTrailingIcon(rightIconNode);
}
}
@@ -17,107 +17,25 @@
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.event.ActionEvent;
import org.jackhuang.hmcl.ui.FXUtils;
/// @author Glavo
public abstract class LineButtonBase extends StackPane implements NoPaddingComponent {
public abstract class LineButtonBase extends LineComponent {
private static final String DEFAULT_STYLE_CLASS = "line-button-base";
private static final Insets PADDING = new Insets(8, 8, 8, 16);
protected final BorderPane root;
protected final RipplerContainer container;
private final Label titleLabel;
protected final RipplerContainer ripplerContainer;
public LineButtonBase() {
this.root = new BorderPane();
root.setPadding(PADDING);
root.setMinHeight(48);
this.getStyleClass().addAll(LineButtonBase.DEFAULT_STYLE_CLASS);
this.container = new RipplerContainer(root);
this.getChildren().setAll(container);
this.ripplerContainer = new RipplerContainer(container);
FXUtils.onClicked(this, this::fire);
this.titleLabel = new Label();
root.setCenter(titleLabel);
BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT);
titleLabel.textProperty().bind(titleProperty());
titleLabel.getStyleClass().add("title");
this.getChildren().setAll(ripplerContainer);
}
private final StringProperty title = new SimpleStringProperty(this, "title");
public StringProperty titleProperty() {
return title;
}
public String getTitle() {
return titleProperty().get();
}
public void setTitle(String title) {
this.titleProperty().set(title);
}
private StringProperty subtitle;
public StringProperty subtitleProperty() {
if (subtitle == null) {
subtitle = new StringPropertyBase() {
private VBox left;
private Label subtitleLabel;
@Override
public String getName() {
return "subtitle";
}
@Override
public Object getBean() {
return LineButtonBase.this;
}
@Override
protected void invalidated() {
String subtitle = get();
if (subtitle != null && !subtitle.isEmpty()) {
if (left == null) {
left = new VBox();
left.setMouseTransparent(true);
left.setAlignment(Pos.CENTER_LEFT);
subtitleLabel = new Label();
subtitleLabel.setWrapText(true);
subtitleLabel.setMinHeight(Region.USE_PREF_SIZE);
subtitleLabel.getStyleClass().add("subtitle");
}
subtitleLabel.setText(subtitle);
left.getChildren().setAll(titleLabel, subtitleLabel);
root.setCenter(left);
} else if (left != null) {
subtitleLabel.setText(null);
root.setCenter(titleLabel);
}
}
};
}
return subtitle;
}
public String getSubtitle() {
return subtitleProperty().get();
}
public void setSubtitle(String subtitle) {
subtitleProperty().set(subtitle);
public void fire() {
fireEvent(new ActionEvent());
}
}
@@ -0,0 +1,238 @@
/*
* 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.ui.construct;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.ui.SVG;
import java.util.Arrays;
import java.util.Objects;
/// @author Glavo
public abstract class LineComponent extends StackPane implements NoPaddingComponent {
private static final String DEFAULT_STYLE_CLASS = "line-component";
private static final double MIN_HEIGHT = 48.0;
private static final PseudoClass PSEUDO_LARGER_TITLE = PseudoClass.getPseudoClass("large-title");
protected static final int IDX_LEADING = 0;
protected static final int IDX_TITLE = 1;
protected static final int IDX_TRAILING = 2;
public static final double SPACING = 12;
public static final double DEFAULT_ICON_SIZE = 20;
public static void setMargin(Node child, Insets value) {
HBox.setMargin(child, value);
}
protected final HBox container;
private final Label titleLabel;
private final VBox titleContainer;
public LineComponent() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
this.setMinHeight(MIN_HEIGHT);
this.container = new HBox(SPACING);
container.getStyleClass().add("line-component-container");
container.setAlignment(Pos.CENTER_LEFT);
this.titleLabel = new Label();
titleLabel.getStyleClass().add("title-label");
titleLabel.setMinWidth(Region.USE_PREF_SIZE);
this.titleContainer = new VBox(titleLabel);
titleContainer.getStyleClass().add("title-container");
titleContainer.setMouseTransparent(true);
titleContainer.setAlignment(Pos.CENTER_LEFT);
titleContainer.minWidthProperty().bind(titleLabel.prefWidthProperty());
HBox.setHgrow(titleContainer, Priority.ALWAYS);
this.setNode(IDX_TITLE, titleContainer);
this.getChildren().setAll(container);
}
private Node[] nodes = new Node[2];
protected void setNode(int idx, Node node) {
if (nodes.length <= idx)
nodes = Arrays.copyOf(nodes, idx + 1);
if (nodes[idx] != node) {
nodes[idx] = node;
container.getChildren().setAll(Arrays.stream(nodes).filter(Objects::nonNull).toArray(Node[]::new));
}
}
public void setLargeTitle(boolean largeTitle) {
pseudoClassStateChanged(PSEUDO_LARGER_TITLE, largeTitle);
}
private final StringProperty title = new StringPropertyBase() {
@Override
public Object getBean() {
return LineComponent.this;
}
@Override
public String getName() {
return "title";
}
@Override
protected void invalidated() {
titleLabel.setText(get());
}
};
public final StringProperty titleProperty() {
return title;
}
public String getTitle() {
return titleProperty().get();
}
public void setTitle(String title) {
titleProperty().set(title);
}
private StringProperty subtitle;
public final StringProperty subtitleProperty() {
if (subtitle == null) {
subtitle = new StringPropertyBase() {
private Label subtitleLabel;
@Override
public String getName() {
return "subtitle";
}
@Override
public Object getBean() {
return LineComponent.this;
}
@Override
protected void invalidated() {
String subtitle = get();
if (subtitle != null && !subtitle.isEmpty()) {
if (subtitleLabel == null) {
subtitleLabel = new Label();
subtitleLabel.setWrapText(true);
subtitleLabel.setMinHeight(Region.USE_PREF_SIZE);
subtitleLabel.getStyleClass().add("subtitle-label");
}
subtitleLabel.setText(subtitle);
if (titleContainer.getChildren().size() == 1)
titleContainer.getChildren().add(subtitleLabel);
} else if (subtitleLabel != null) {
subtitleLabel.setText(null);
if (titleContainer.getChildren().size() == 2)
titleContainer.getChildren().remove(1);
}
}
};
}
return subtitle;
}
public final String getSubtitle() {
return subtitle != null ? subtitle.get() : null;
}
public final void setSubtitle(String subtitle) {
subtitleProperty().set(subtitle);
}
private ObjectProperty<Node> leading;
public final ObjectProperty<Node> leadingProperty() {
if (leading == null) {
leading = new ObjectPropertyBase<>() {
@Override
public Object getBean() {
return LineComponent.this;
}
@Override
public String getName() {
return "leading";
}
@Override
protected void invalidated() {
setNode(IDX_LEADING, get());
}
};
}
return leading;
}
public final Node getLeading() {
return leadingProperty().get();
}
public final void setLeading(Node node) {
leadingProperty().set(node);
}
public void setLeading(Image icon) {
setLeading(icon, -1);
}
public void setLeading(Image icon, double size) {
var imageView = new ImageView(icon);
if (size > 0) {
imageView.setFitWidth(size);
imageView.setFitHeight(size);
imageView.setPreserveRatio(true);
imageView.setSmooth(true);
}
imageView.setMouseTransparent(true);
setNode(IDX_LEADING, imageView);
}
public void setLeading(SVG svg) {
setLeading(svg, DEFAULT_ICON_SIZE);
}
public void setLeading(SVG svg, double size) {
Node node = svg.createIcon(size);
node.setMouseTransparent(true);
setNode(IDX_LEADING, node);
}
}
@@ -0,0 +1,240 @@
/*
* 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.ui.construct;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class LineFileChooserButton extends LineButton {
private static final String DEFAULT_STYLE_CLASS = "line-file-select-button";
public LineFileChooserButton() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
setTrailingIcon(SVG.EDIT);
}
/// Converts the given path to absolute/relative(if possible) path according to [#convertToRelativePathProperty()].
private String processPath(Path path) {
if (isConvertToRelativePath() && path.isAbsolute()) {
try {
return Metadata.CURRENT_DIRECTORY.relativize(path).normalize().toString();
} catch (IllegalArgumentException e) {
// the given path can't be relativized against current path
}
}
return path.normalize().toString();
}
@Override
public void fire() {
super.fire();
Stage owner = Controllers.getStage(); // TODO: Allow user to set owner stage
String windowTitle = getFileChooserTitle();
Path initialDirectory = null;
if (getLocation() != null) {
Path file;
try {
file = FileUtils.toAbsolute(Path.of(getLocation()));
if (Files.exists(file)) {
if (Files.isRegularFile(file))
initialDirectory = file.getParent();
else if (Files.isDirectory(file))
initialDirectory = file;
}
} catch (IllegalArgumentException e) {
LOG.warning("Failed to resolve path: " + getLocation());
}
}
Path path;
Type type = getType();
if (type == Type.OPEN_DIRECTORY) {
var directoryChooser = new DirectoryChooser();
if (windowTitle != null)
directoryChooser.setTitle(windowTitle);
if (initialDirectory != null)
directoryChooser.setInitialDirectory(initialDirectory.toFile());
path = FileUtils.toPath(directoryChooser.showDialog(owner));
} else {
var fileChooser = new FileChooser();
if (windowTitle != null)
fileChooser.setTitle(windowTitle);
if (initialDirectory != null)
fileChooser.setInitialDirectory(initialDirectory.toFile());
if (extensionFilters != null)
fileChooser.getExtensionFilters().setAll(extensionFilters);
fileChooser.setInitialFileName(getInitialFileName());
path = FileUtils.toPath(switch (type) {
case OPEN_FILE -> fileChooser.showOpenDialog(owner);
case SAVE_FILE -> fileChooser.showSaveDialog(owner);
default -> throw new AssertionError("Unknown Type: " + type);
});
}
if (path != null) {
setLocation(processPath(path));
}
}
private final StringProperty location = new StringPropertyBase() {
@Override
public Object getBean() {
return LineFileChooserButton.this;
}
@Override
public String getName() {
return "location";
}
@Override
protected void invalidated() {
setTrailingText(get());
}
};
public StringProperty locationProperty() {
return location;
}
public String getLocation() {
return locationProperty().get();
}
public void setLocation(String location) {
locationProperty().set(location);
}
private final StringProperty fileChooserTitle = new SimpleStringProperty(this, "fileChooserTitle");
public StringProperty fileChooserTitleProperty() {
return fileChooserTitle;
}
public String getFileChooserTitle() {
return fileChooserTitleProperty().get();
}
public void setFileChooserTitle(String fileChooserTitle) {
fileChooserTitleProperty().set(fileChooserTitle);
}
private ObjectProperty<Type> type;
public ObjectProperty<Type> typeProperty() {
if (type == null) {
type = new SimpleObjectProperty<>(this, "type", Type.OPEN_FILE);
}
return type;
}
public Type getType() {
return type != null ? type.get() : Type.OPEN_FILE;
}
public void setType(Type type) {
typeProperty().set(type);
}
private ObjectProperty<String> initialFileName;
public final ObjectProperty<String> initialFileNameProperty() {
if (initialFileName == null)
initialFileName = new SimpleObjectProperty<>(this, "initialFileName");
return initialFileName;
}
public final String getInitialFileName() {
return initialFileName != null ? initialFileName.get() : null;
}
public final void setInitialFileName(String value) {
initialFileNameProperty().set(value);
}
private ObservableList<FileChooser.ExtensionFilter> extensionFilters;
public ObservableList<FileChooser.ExtensionFilter> getExtensionFilters() {
if (extensionFilters == null)
extensionFilters = FXCollections.observableArrayList();
return extensionFilters;
}
private BooleanProperty convertToRelativePath;
public BooleanProperty convertToRelativePathProperty() {
if (convertToRelativePath == null)
convertToRelativePath = new BooleanPropertyBase(false) {
@Override
public Object getBean() {
return LineFileChooserButton.this;
}
@Override
public String getName() {
return "convertToRelativePath";
}
@Override
protected void invalidated() {
String location = getLocation();
if (location == null)
return;
try {
setLocation(processPath(FileUtils.toAbsolute(Path.of(getLocation()))));
} catch (IllegalArgumentException ignored) {
}
}
};
return convertToRelativePath;
}
public boolean isConvertToRelativePath() {
return convertToRelativePath != null && convertToRelativePath.get();
}
public void setConvertToRelativePath(boolean convertToRelativePath) {
convertToRelativePathProperty().set(convertToRelativePath);
}
public enum Type {
OPEN_FILE,
OPEN_DIRECTORY,
SAVE_FILE
}
}
@@ -1,112 +0,0 @@
/*
* 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.ui.construct;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
/// @author Glavo
public final class LineNavigationButton extends LineButtonBase {
private static final String DEFAULT_STYLE_CLASS = "line-navigation-button";
public LineNavigationButton() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
root.setMouseTransparent(true);
HBox right = new HBox();
root.setRight(right);
{
right.setAlignment(Pos.CENTER_RIGHT);
Label valueLabel = new Label();
valueLabel.getStyleClass().add("subtitle");
valueLabel.textProperty().bind(messageProperty());
Node arrowIcon = SVG.ARROW_FORWARD.createIcon(24);
HBox.setMargin(arrowIcon, new Insets(0, 8, 0, 8));
disabledProperty().addListener((observable, oldValue, newValue) ->
arrowIcon.setOpacity(newValue ? 0.4 : 1.0));
right.getChildren().setAll(valueLabel, arrowIcon);
}
FXUtils.onClicked(container, this::fire);
}
public void fire() {
fireEvent(new ActionEvent());
}
private final ObjectProperty<EventHandler<ActionEvent>> onAction = new ObjectPropertyBase<>() {
@Override
protected void invalidated() {
setEventHandler(ActionEvent.ACTION, get());
}
@Override
public Object getBean() {
return LineNavigationButton.this;
}
@Override
public String getName() {
return "onAction";
}
};
public ObjectProperty<EventHandler<ActionEvent>> onActionProperty() {
return onAction;
}
public EventHandler<ActionEvent> getOnAction() {
return onActionProperty().get();
}
public void setOnAction(EventHandler<ActionEvent> value) {
onActionProperty().set(value);
}
private final StringProperty message = new SimpleStringProperty(this, "message", "");
public StringProperty messageProperty() {
return message;
}
public String getMessage() {
return messageProperty().get();
}
public void setMessage(String message) {
messageProperty().set(message);
}
}
@@ -17,99 +17,47 @@
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.scene.Node;
/// @author Glavo
public class LinePane extends BorderPane implements NoPaddingComponent {
private static final Insets PADDING = new Insets(8, 8, 8, 16);
private final Label titleLabel;
public class LinePane extends LineComponent {
private static final String DEFAULT_STYLE_CLASS = "line-pane";
public LinePane() {
this.setPadding(PADDING);
this.setMinHeight(48);
this.titleLabel = new Label();
this.setCenter(titleLabel);
BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT);
titleLabel.textProperty().bind(titleProperty());
titleLabel.getStyleClass().add("title");
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
private final StringProperty title = new SimpleStringProperty(this, "title");
public StringProperty titleProperty() {
return title;
}
public String getTitle() {
return titleProperty().get();
}
public void setTitle(String title) {
this.titleProperty().set(title);
}
private StringProperty subtitle;
public StringProperty subtitleProperty() {
if (subtitle == null) {
subtitle = new StringPropertyBase() {
private VBox left;
private Label subtitleLabel;
@Override
public String getName() {
return "subtitle";
}
private ObjectProperty<Node> right;
public ObjectProperty<Node> rightProperty() {
if (right == null) {
right = new ObjectPropertyBase<>() {
@Override
public Object getBean() {
return LinePane.this;
}
@Override
protected void invalidated() {
String subtitle = get();
if (subtitle != null && !subtitle.isEmpty()) {
if (left == null) {
left = new VBox();
left.setMouseTransparent(true);
left.setAlignment(Pos.CENTER_LEFT);
public String getName() {
return "right";
}
subtitleLabel = new Label();
subtitleLabel.setWrapText(true);
subtitleLabel.setMinHeight(Region.USE_PREF_SIZE);
subtitleLabel.getStyleClass().add("subtitle");
}
subtitleLabel.setText(subtitle);
left.getChildren().setAll(titleLabel, subtitleLabel);
LinePane.this.setCenter(left);
} else if (left != null) {
subtitleLabel.setText(null);
LinePane.this.setCenter(titleLabel);
}
@Override
protected void invalidated() {
setNode(IDX_TRAILING, get());
}
};
}
return subtitle;
return right;
}
public String getSubtitle() {
return subtitleProperty().get();
public Node getRight() {
return rightProperty().get();
}
public void setSubtitle(String subtitle) {
subtitleProperty().set(subtitle);
public void setRight(Node right) {
rightProperty().set(right);
}
}
@@ -24,9 +24,7 @@ import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
@@ -43,7 +41,7 @@ import java.util.function.Function;
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
/// @author Glavo
public final class LineSelectButton<T> extends LineButtonBase {
public final class LineSelectButton<T> extends LineButton {
private static final String DEFAULT_STYLE_CLASS = "line-select-button";
private static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected");
@@ -53,109 +51,97 @@ public final class LineSelectButton<T> extends LineButtonBase {
public LineSelectButton() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
root.setMouseTransparent(true);
InvalidationListener updateTrailingText = observable -> {
T value = getValue();
if (value != null) {
Function<T, String> converter = getConverter();
setTrailingText(converter != null ? converter.apply(value) : value.toString());
} else {
setTrailingText(null);
}
};
converterProperty().addListener(updateTrailingText);
valueProperty().addListener(updateTrailingText);
HBox right = new HBox();
root.setRight(right);
{
right.setAlignment(Pos.CENTER_RIGHT);
setTrailingIcon(SVG.UNFOLD_MORE);
Label valueLabel = new Label();
valueLabel.getStyleClass().add("subtitle");
InvalidationListener updateValue = observable -> {
T value = getValue();
if (value == null)
valueLabel.setText("");
else {
Function<T, String> converter = getConverter();
valueLabel.setText(converter != null ? converter.apply(value) : value.toString());
}
};
converterProperty().addListener(updateValue);
valueProperty().addListener(updateValue);
Node arrowIcon = SVG.UNFOLD_MORE.createIcon(24);
HBox.setMargin(arrowIcon, new Insets(0, 8, 0, 8));
right.getChildren().setAll(valueLabel, arrowIcon);
}
container.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (event.getButton() == MouseButton.PRIMARY) {
if (popup == null) {
PopupMenu popupMenu = new PopupMenu();
this.popup = new JFXPopup(popupMenu);
container.addEventFilter(ScrollEvent.ANY, ignored -> popup.hide());
Bindings.bindContent(popupMenu.getContent(), MappedObservableList.create(itemsProperty(), item -> {
VBox vbox = new VBox();
var itemTitleLabel = new Label();
itemTitleLabel.getStyleClass().add("title");
itemTitleLabel.textProperty().bind(Bindings.createStringBinding(() -> {
if (item == null)
return "";
Function<T, String> converter = getConverter();
return converter != null ? converter.apply(item) : Objects.toString(item, "");
}, converterProperty()));
var itemSubtitleLabel = new Label();
itemSubtitleLabel.getStyleClass().add("subtitle");
itemSubtitleLabel.textProperty().bind(Bindings.createStringBinding(() -> {
Function<T, String> descriptionConverter = getDescriptionConverter();
return descriptionConverter != null ? descriptionConverter.apply(item) : "";
}, descriptionConverterProperty()));
FXUtils.onChangeAndOperate(itemSubtitleLabel.textProperty(), text -> {
if (text == null || text.isEmpty()) {
vbox.getChildren().setAll(itemTitleLabel);
} else {
vbox.getChildren().setAll(itemTitleLabel, itemSubtitleLabel);
}
});
var wrapper = new StackPane(vbox);
wrapper.setAlignment(Pos.CENTER_LEFT);
wrapper.getStyleClass().add("menu-container");
wrapper.setMouseTransparent(true);
RipplerContainer ripplerContainer = new RipplerContainer(wrapper);
FXUtils.onClicked(ripplerContainer, () -> {
setValue(item);
popup.hide();
});
FXUtils.onChangeAndOperate(valueProperty(),
value -> wrapper.pseudoClassStateChanged(SELECTED_PSEUDO_CLASS, Objects.equals(value, item)));
return ripplerContainer;
}));
popup.showingProperty().addListener((observable, oldValue, newValue) ->
container.getRippler().setRipplerDisabled(newValue));
}
if (popup.isShowing()) {
popup.hide();
} else {
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(this, popup);
popup.show(this, vPosition, JFXPopup.PopupHPosition.RIGHT,
0,
vPosition == JFXPopup.PopupVPosition.TOP ? this.getHeight() : -this.getHeight());
}
event.consume();
} else if (event.getButton() == MouseButton.SECONDARY) {
ripplerContainer.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (event.getButton() == MouseButton.SECONDARY) {
if (popup != null)
popup.hide();
event.consume();
}
});
}
@Override
public void fire() {
super.fire();
if (popup == null) {
PopupMenu popupMenu = new PopupMenu();
this.popup = new JFXPopup(popupMenu);
ripplerContainer.addEventFilter(ScrollEvent.ANY, ignored -> popup.hide());
Bindings.bindContent(popupMenu.getContent(), MappedObservableList.create(itemsProperty(), item -> {
VBox vbox = new VBox();
var itemTitleLabel = new Label();
itemTitleLabel.getStyleClass().add("title-label");
itemTitleLabel.textProperty().bind(Bindings.createStringBinding(() -> {
if (item == null)
return "";
Function<T, String> converter = getConverter();
return converter != null ? converter.apply(item) : Objects.toString(item, "");
}, converterProperty()));
var itemSubtitleLabel = new Label();
itemSubtitleLabel.getStyleClass().add("subtitle-label");
itemSubtitleLabel.textProperty().bind(Bindings.createStringBinding(() -> {
Function<T, String> descriptionConverter = getDescriptionConverter();
return descriptionConverter != null ? descriptionConverter.apply(item) : "";
}, descriptionConverterProperty()));
FXUtils.onChangeAndOperate(itemSubtitleLabel.textProperty(), text -> {
if (text == null || text.isEmpty()) {
vbox.getChildren().setAll(itemTitleLabel);
} else {
vbox.getChildren().setAll(itemTitleLabel, itemSubtitleLabel);
}
});
var wrapper = new StackPane(vbox);
wrapper.setAlignment(Pos.CENTER_LEFT);
wrapper.getStyleClass().add("menu-container");
wrapper.setMouseTransparent(true);
RipplerContainer ripplerContainer = new RipplerContainer(wrapper);
FXUtils.onClicked(ripplerContainer, () -> {
setValue(item);
popup.hide();
});
FXUtils.onChangeAndOperate(valueProperty(),
value -> wrapper.pseudoClassStateChanged(SELECTED_PSEUDO_CLASS, Objects.equals(value, item)));
return ripplerContainer;
}));
popup.showingProperty().addListener((observable, oldValue, newValue) ->
ripplerContainer.getRippler().setRipplerDisabled(newValue));
}
if (popup.isShowing()) {
popup.hide();
} else {
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(this, popup);
popup.show(this, vPosition, JFXPopup.PopupHPosition.RIGHT,
0,
vPosition == JFXPopup.PopupVPosition.TOP ? this.getHeight() : -this.getHeight(),
true);
}
}
private final ObjectProperty<T> value = new SimpleObjectProperty<>(this, "value");
public ObjectProperty<T> valueProperty() {
@@ -187,9 +173,8 @@ public final class LineSelectButton<T> extends LineButtonBase {
private ObjectProperty<Function<T, String>> descriptionConverter;
public ObjectProperty<Function<T, String>> descriptionConverterProperty() {
if (descriptionConverter == null) {
if (descriptionConverter == null)
descriptionConverter = new SimpleObjectProperty<>(this, "descriptionConverter");
}
return descriptionConverter;
}
@@ -19,19 +19,16 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.ui.FXUtils;
/// @author Glavo
public final class LineTextPane extends LinePane {
public final class LineTextPane extends LineComponent {
private static final String DEFAULT_STYLE_CLASS = "line-label-pane";
private static final String DEFAULT_STYLE_CLASS = "line-text-pane";
public LineTextPane() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
this.getStyleClass().addAll(DEFAULT_STYLE_CLASS);
}
private StringProperty text;
@@ -39,8 +36,6 @@ public final class LineTextPane extends LinePane {
public StringProperty textProperty() {
if (text == null) {
text = new StringPropertyBase() {
private static final Insets LABEL_MARGIN = new Insets(0, 8, 0, 16);
private Label rightLabel;
@Override
@@ -56,19 +51,18 @@ public final class LineTextPane extends LinePane {
@Override
protected void invalidated() {
String text = get();
if (text == null || text.isEmpty()) {
if (rightLabel != null)
rightLabel.setText(null);
LineTextPane.this.setRight(null);
} else {
if (text != null && !text.isEmpty()) {
if (rightLabel == null) {
rightLabel = FXUtils.newSafeTruncatedLabel();
FXUtils.copyOnDoubleClick(rightLabel);
BorderPane.setMargin(rightLabel, LABEL_MARGIN);
BorderPane.setAlignment(rightLabel, Pos.CENTER_RIGHT);
}
rightLabel.setText(text);
LineTextPane.this.setRight(rightLabel);
setNode(IDX_TRAILING, rightLabel);
} else {
if (rightLabel != null)
rightLabel.setText(null);
setNode(IDX_TRAILING, null);
}
}
};
@@ -20,25 +20,27 @@ package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXToggleButton;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Pos;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.ui.FXUtils;
public final class LineToggleButton extends LineButtonBase {
private static final String DEFAULT_STYLE_CLASS = "line-toggle-button";
private final JFXToggleButton toggleButton;
public LineToggleButton() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
JFXToggleButton toggleButton = new JFXToggleButton();
this.toggleButton = new JFXToggleButton();
toggleButton.selectedProperty().bindBidirectional(selectedProperty());
toggleButton.setSize(8);
FXUtils.setLimitHeight(toggleButton, 30);
setNode(IDX_TRAILING, toggleButton);
}
BorderPane.setAlignment(toggleButton, Pos.CENTER);
root.setRight(toggleButton);
FXUtils.onClicked(container, toggleButton::fire);
@Override
public void fire() {
toggleButton.fire();
super.fire();
}
private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected");
@@ -31,16 +31,12 @@ import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.Lang;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MessageDialogPane extends HBox {
@@ -72,7 +68,7 @@ public final class MessageDialogPane extends HBox {
private @Nullable ButtonBase cancelButton;
public MessageDialogPane(@NotNull String text, @Nullable String title, @NotNull MessageType type) {
this.setSpacing(8);
this.setSpacing(16);
this.getStyleClass().add("jfx-dialog-layout");
Label graphic = new Label();
@@ -219,36 +215,6 @@ public final class MessageDialogPane extends HBox {
return this;
}
public Builder cancelOnTimeout(long timeoutMs) {
if (dialog.getCancelButton() == null) {
throw new IllegalStateException("Call ok/yesOrNo/actionOrCancel before calling cancelOnTimeout");
}
ButtonBase cancelButton = dialog.getCancelButton();
String originalText = cancelButton.getText();
Timer timer = Lang.getTimer();
timer.scheduleAtFixedRate(new TimerTask() {
long timeout = timeoutMs;
@Override
public void run() {
if (timeout <= 0) {
cancel();
runInFX(() -> {
cancelButton.fire();
});
return;
}
timeout -= 1000;
long currentTimeout = timeout;
runInFX(() -> cancelButton.setText(originalText + " (" + (currentTimeout / 1000) + ")"));
}
}, 1000, 1000);
return this;
}
public MessageDialogPane build() {
return dialog;
}
@@ -18,8 +18,6 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXSpinner;
import javafx.beans.DefaultProperty;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*;
import javafx.event.Event;
import javafx.event.EventHandler;
@@ -33,14 +31,12 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
@DefaultProperty("content")
/// A spinner pane that can show spinner, failed reason, or content.
public class SpinnerPane extends Control {
private final ObjectProperty<Node> content = new SimpleObjectProperty<>(this, "content");
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading");
private final StringProperty failedReason = new SimpleStringProperty(this, "failedReason");
private static final String DEFAULT_STYLE_CLASS = "spinner-pane";
public SpinnerPane() {
getStyleClass().add("spinner-pane");
getStyleClass().add(DEFAULT_STYLE_CLASS);
}
public void showSpinner() {
@@ -52,108 +48,195 @@ public class SpinnerPane extends Control {
setLoading(false);
}
public Node getContent() {
return content.get();
private void updateContent() {
if (getSkin() instanceof Skin skin) {
skin.updateContent();
}
}
private ObjectProperty<Node> content;
public ObjectProperty<Node> contentProperty() {
if (content == null)
content = new ObjectPropertyBase<>() {
@Override
public Object getBean() {
return SpinnerPane.this;
}
@Override
public String getName() {
return "content";
}
@Override
protected void invalidated() {
updateContent();
}
};
return content;
}
public void setContent(Node content) {
this.content.set(content);
public Node getContent() {
return contentProperty().get();
}
public boolean isLoading() {
return loading.get();
public void setContent(Node content) {
contentProperty().set(content);
}
private BooleanProperty loading;
public BooleanProperty loadingProperty() {
if (loading == null)
loading = new BooleanPropertyBase() {
@Override
public Object getBean() {
return SpinnerPane.this;
}
@Override
public String getName() {
return "loading";
}
@Override
protected void invalidated() {
updateContent();
}
};
return loading;
}
public void setLoading(boolean loading) {
this.loading.set(loading);
public boolean isLoading() {
return loading != null && loading.get();
}
public String getFailedReason() {
return failedReason.get();
public void setLoading(boolean loading) {
loadingProperty().set(loading);
}
private StringProperty failedReason;
public StringProperty failedReasonProperty() {
if (failedReason == null)
failedReason = new StringPropertyBase() {
@Override
public Object getBean() {
return SpinnerPane.this;
}
@Override
public String getName() {
return "failedReason";
}
@Override
protected void invalidated() {
updateContent();
}
};
return failedReason;
}
public void setFailedReason(String failedReason) {
this.failedReason.set(failedReason);
public String getFailedReason() {
return failedReason != null ? failedReason.get() : null;
}
public void setFailedReason(String failedReason) {
failedReasonProperty().set(failedReason);
}
private ObjectProperty<EventHandler<Event>> onFailedAction;
public final ObjectProperty<EventHandler<Event>> onFailedActionProperty() {
if (onFailedAction == null) {
onFailedAction = new ObjectPropertyBase<>() {
@Override
public Object getBean() {
return SpinnerPane.this;
}
@Override
public String getName() {
return "onFailedAction";
}
@Override
protected void invalidated() {
setEventHandler(FAILED_ACTION, get());
}
};
}
return onFailedAction;
}
public final EventHandler<Event> getOnFailedAction() {
return onFailedAction != null ? onFailedAction.get() : null;
}
public final void setOnFailedAction(EventHandler<Event> value) {
onFailedActionProperty().set(value);
}
public final EventHandler<Event> getOnFailedAction() {
return onFailedActionProperty().get();
}
private final ObjectProperty<EventHandler<Event>> onFailedAction = new SimpleObjectProperty<EventHandler<Event>>(this, "onFailedAction") {
@Override
protected void invalidated() {
setEventHandler(FAILED_ACTION, get());
}
};
@Override
protected SkinBase<SpinnerPane> createDefaultSkin() {
return new Skin(this);
}
private static final class Skin extends SkinBase<SpinnerPane> {
private final JFXSpinner spinner = new JFXSpinner();
private final StackPane contentPane = new StackPane();
private final StackPane topPane = new StackPane();
private final TransitionPane root = new TransitionPane();
private final StackPane failedPane = new StackPane();
private final Label failedReasonLabel = new Label();
@SuppressWarnings("FieldCanBeLocal") // prevent from gc.
private final InvalidationListener observer;
Skin(SpinnerPane control) {
super(control);
topPane.getChildren().setAll(spinner);
topPane.getStyleClass().add("notice-pane");
failedPane.getStyleClass().add("notice-pane");
failedPane.getChildren().setAll(failedReasonLabel);
FXUtils.onClicked(failedPane, () -> {
EventHandler<Event> action = control.getOnFailedAction();
if (action != null)
action.handle(new Event(FAILED_ACTION));
});
updateContent();
this.getChildren().setAll(root);
}
FXUtils.onChangeAndOperate(getSkinnable().content, newValue -> {
if (newValue == null) {
private StackPane contentPane;
private StackPane spinnerPane;
private StackPane failedPane;
private Label failedReasonLabel;
void updateContent() {
SpinnerPane control = getSkinnable();
Node nextContent;
if (control.isLoading()) {
if (spinnerPane == null) {
spinnerPane = new StackPane(new JFXSpinner());
spinnerPane.getStyleClass().add("notice-pane");
}
nextContent = spinnerPane;
} else if (control.getFailedReason() != null) {
if (failedPane == null) {
failedReasonLabel = new Label();
failedPane = new StackPane(failedReasonLabel);
failedPane.getStyleClass().add("notice-pane");
FXUtils.onClicked(failedPane, () -> control.fireEvent(new Event(SpinnerPane.FAILED_ACTION)));
}
failedReasonLabel.setText(control.getFailedReason());
nextContent = failedPane;
} else {
if (contentPane == null) {
contentPane = new StackPane();
}
Node content = control.getContent();
if (content != null)
contentPane.getChildren().setAll(content);
else
contentPane.getChildren().clear();
} else {
contentPane.getChildren().setAll(newValue);
}
});
getChildren().setAll(root);
observer = FXUtils.observeWeak(() -> {
if (getSkinnable().getFailedReason() != null) {
root.setContent(failedPane, ContainerAnimations.FADE);
failedReasonLabel.setText(getSkinnable().getFailedReason());
} else if (getSkinnable().isLoading()) {
root.setContent(topPane, ContainerAnimations.FADE);
} else {
root.setContent(contentPane, ContainerAnimations.FADE);
}
}, getSkinnable().loadingProperty(), getSkinnable().failedReasonProperty());
nextContent = contentPane;
}
if (nextContent != failedPane && failedReasonLabel != null) {
failedReasonLabel.setText(null);
}
root.setContent(nextContent, ContainerAnimations.FADE);
}
}
@@ -18,126 +18,185 @@
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.AggregatedObservableList;
public class TwoLineListItem extends VBox {
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
private static Label createTagLabel(String tag) {
Label tagLabel = new Label();
tagLabel.setText(tag);
HBox.setMargin(tagLabel, new Insets(0, 8, 0, 0));
return tagLabel;
}
private final HBox firstLine;
private HBox secondLine;
private final StringProperty title = new SimpleStringProperty(this, "title");
private final ObservableList<Label> tags = FXCollections.observableArrayList();
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
private final Label lblSubtitle;
private final Label lblTitle;
private Label lblSubtitle;
private final AggregatedObservableList<Node> firstLineChildren;
public TwoLineListItem() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
setMouseTransparent(true);
lblTitle = new Label();
lblTitle.getStyleClass().add("title");
this.firstLine = new HBox(lblTitle);
firstLine.getStyleClass().add("first-line");
firstLine.setAlignment(Pos.CENTER_LEFT);
this.getChildren().setAll(firstLine);
}
public TwoLineListItem(String titleString, String subtitleString) {
this();
title.set(titleString);
subtitle.set(subtitleString);
setTitle(titleString);
setSubtitle(subtitleString);
}
public TwoLineListItem() {
setMouseTransparent(true);
private void initSecondLine() {
if (secondLine == null) {
lblSubtitle = new Label();
lblSubtitle.getStyleClass().add("subtitle");
HBox firstLine = new HBox();
firstLine.getStyleClass().add("first-line");
secondLine = new HBox(lblSubtitle);
}
}
lblTitle = new Label();
lblTitle.getStyleClass().add("title");
lblTitle.textProperty().bind(title);
private final StringProperty title = new StringPropertyBase() {
@Override
public Object getBean() {
return TwoLineListItem.this;
}
firstLineChildren = new AggregatedObservableList<>();
firstLineChildren.appendList(FXCollections.singletonObservableList(lblTitle));
firstLineChildren.appendList(tags);
Bindings.bindContent(firstLine.getChildren(), firstLineChildren.getAggregatedList());
@Override
public String getName() {
return "title";
}
lblSubtitle = new Label();
lblSubtitle.getStyleClass().add("subtitle");
lblSubtitle.textProperty().bind(subtitle);
@Override
protected void invalidated() {
lblTitle.setText(get());
}
};
HBox secondLine = new HBox();
secondLine.getChildren().setAll(lblSubtitle);
getChildren().setAll(firstLine, secondLine);
FXUtils.onChangeAndOperate(subtitle, subtitleString -> {
if (subtitleString == null) getChildren().setAll(firstLine);
else getChildren().setAll(firstLine, secondLine);
});
getStyleClass().add(DEFAULT_STYLE_CLASS);
public StringProperty titleProperty() {
return title;
}
public String getTitle() {
return title.get();
}
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public String getSubtitle() {
return subtitle.get();
}
private StringProperty subtitle;
public StringProperty subtitleProperty() {
if (subtitle == null) {
subtitle = new StringPropertyBase() {
@Override
public Object getBean() {
return TwoLineListItem.this;
}
@Override
public String getName() {
return "subtitle";
}
@Override
protected void invalidated() {
String subtitle = get();
if (subtitle != null) {
initSecondLine();
lblSubtitle.setText(subtitle);
if (getChildren().size() == 1)
getChildren().add(secondLine);
} else if (secondLine != null) {
lblSubtitle.setText(null);
if (getChildren().size() > 1)
getChildren().setAll(firstLine);
}
}
};
}
return subtitle;
}
public void setSubtitle(String subtitle) {
this.subtitle.set(subtitle);
public String getSubtitle() {
return subtitle != null ? subtitleProperty().get() : null;
}
public Label getSubtitleLabel() {
return lblSubtitle;
public void setSubtitle(String subtitle) {
if (this.subtitle == null && subtitle == null)
return;
subtitleProperty().set(subtitle);
}
public Label getTitleLabel() {
return lblTitle;
}
public void addTag(String tag) {
Label tagLabel = createTagLabel(tag);
tagLabel.getStyleClass().add("tag");
getTags().add(tagLabel);
public Label getSubtitleLabel() {
initSecondLine();
return lblSubtitle;
}
public void addTagWarning(String tag) {
Label tagLabel = createTagLabel(tag);
tagLabel.getStyleClass().add("tag-warning");
getTags().add(tagLabel);
}
private ObservableList<Label> tags;
public ObservableList<Label> getTags() {
if (tags == null) {
tags = FXCollections.observableArrayList();
var tagsBox = new HBox(8);
tagsBox.getStyleClass().add("tags");
tagsBox.setAlignment(Pos.CENTER_LEFT);
Bindings.bindContent(tagsBox.getChildren(), tags);
var scrollPane = new ScrollPane(tagsBox);
HBox.setHgrow(scrollPane, Priority.ALWAYS);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
FXUtils.onChangeAndOperate(tagsBox.heightProperty(), height -> FXUtils.setLimitHeight(scrollPane, height.doubleValue()));
scrollPane.setPrefWidth(50);
firstLine.getChildren().setAll(lblTitle, scrollPane);
}
return tags;
}
public void addTag(String tag, PseudoClass pseudoClass) {
var tagLabel = new Label(tag);
tagLabel.getStyleClass().add("tag");
if (pseudoClass != null)
tagLabel.pseudoClassStateChanged(pseudoClass, true);
getTags().add(tagLabel);
}
public void addTag(String tag) {
addTag(tag, null);
}
private static final PseudoClass WARNING_PSEUDO_CLASS = PseudoClass.getPseudoClass("warning");
public void addTagWarning(String tag) {
addTag(tag, WARNING_PSEUDO_CLASS);
}
@Override
public String toString() {
return getTitle();
return "TwoLineListItem[title=%s, subtitle=%s, tags=%s]".formatted(getTitle(), getSubtitle(), tags);
}
}
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXSnackbar;
import com.jfoenix.controls.JFXSnackbarLayout;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
@@ -454,7 +455,7 @@ public class DecoratorController {
// ==== Toast ====
public void showToast(String content) {
decorator.getSnackbar().fireEvent(new JFXSnackbar.SnackbarEvent(content, null, 2000L, false, null));
decorator.getSnackbar().fireEvent(new JFXSnackbar.SnackbarEvent(new JFXSnackbarLayout(content)));
}
// ==== Wizard ====
@@ -239,7 +239,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
JFXButton btnMin = new JFXButton();
btnMin.setFocusTraversable(false);
btnMin.setGraphic(SVG.MINIMIZE.createIcon(Themes.titleFillProperty()));
btnMin.setGraphic(SVG.MINIMIZE_CENTER.createIcon(Themes.titleFillProperty()));
btnMin.getStyleClass().add("jfx-decorator-button");
btnMin.setOnAction(e -> skinnable.minimize());
@@ -107,7 +107,7 @@ public final class LocalModpackPage extends ModpackPage {
.whenComplete(Schedulers.javafx(), (manifest, exception) -> {
if (exception instanceof ManuallyCreatedModpackException) {
hideSpinner();
lblName.setText(FileUtils.getName(selectedFile));
nameProperty.set(FileUtils.getName(selectedFile));
installAsVersion.set(false);
if (name == null) {
@@ -127,9 +127,9 @@ public final class LocalModpackPage extends ModpackPage {
} else {
hideSpinner();
controller.getSettings().put(MODPACK_MANIFEST, manifest);
lblName.setText(manifest.getName());
lblVersion.setText(manifest.getVersion());
lblAuthor.setText(manifest.getAuthor());
nameProperty.set(manifest.getName());
versionProperty.set(manifest.getVersion());
authorProperty.set(manifest.getAuthor());
if (name == null) {
// trim: https://github.com/HMCL-dev/HMCL/issues/962
@@ -2,14 +2,15 @@ package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.geometry.Insets;
import javafx.beans.property.StringProperty;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.LinePane;
import org.jackhuang.hmcl.ui.construct.LineTextPane;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
@@ -23,9 +24,9 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
protected final WizardController controller;
protected final Label lblName;
protected final Label lblVersion;
protected final Label lblAuthor;
protected final StringProperty nameProperty;
protected final StringProperty versionProperty;
protected final StringProperty authorProperty;
protected final JFXTextField txtModpackName;
protected final JFXButton btnInstall;
protected final JFXButton btnDescription;
@@ -33,54 +34,43 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
protected ModpackPage(WizardController controller) {
this.controller = controller;
this.getStyleClass().add("large-spinner-pane");
VBox borderPane = new VBox();
borderPane.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(borderPane, 500);
ComponentList componentList = new ComponentList();
{
BorderPane archiveNamePane = new BorderPane();
var archiveNamePane = new LinePane();
{
Label label = new Label(i18n("archive.file.name"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
archiveNamePane.setLeft(label);
archiveNamePane.setTitle(i18n("archive.file.name"));
txtModpackName = new JFXTextField();
BorderPane.setMargin(txtModpackName, new Insets(0, 0, 8, 32));
txtModpackName.setPrefWidth(300);
// FIXME: Validator are not shown properly
// BorderPane.setMargin(txtModpackName, new Insets(0, 0, 8, 32));
BorderPane.setAlignment(txtModpackName, Pos.CENTER_RIGHT);
archiveNamePane.setCenter(txtModpackName);
archiveNamePane.setRight(txtModpackName);
}
BorderPane modpackNamePane = new BorderPane();
var modpackNamePane = new LineTextPane();
{
modpackNamePane.setLeft(new Label(i18n("modpack.name")));
lblName = new Label();
BorderPane.setAlignment(lblName, Pos.CENTER_RIGHT);
modpackNamePane.setCenter(lblName);
modpackNamePane.setTitle(i18n("modpack.name"));
nameProperty = modpackNamePane.textProperty();
}
BorderPane versionPane = new BorderPane();
var versionPane = new LineTextPane();
{
versionPane.setLeft(new Label(i18n("archive.version")));
lblVersion = new Label();
BorderPane.setAlignment(lblVersion, Pos.CENTER_RIGHT);
versionPane.setCenter(lblVersion);
versionPane.setTitle(i18n("archive.version"));
versionProperty = versionPane.textProperty();
}
BorderPane authorPane = new BorderPane();
var authorPane = new LineTextPane();
{
authorPane.setLeft(new Label(i18n("archive.author")));
lblAuthor = new Label();
BorderPane.setAlignment(lblAuthor, Pos.CENTER_RIGHT);
authorPane.setCenter(lblAuthor);
authorPane.setTitle(i18n("archive.author"));
authorProperty = authorPane.textProperty();
}
BorderPane descriptionPane = new BorderPane();
var descriptionPane = new BorderPane();
{
btnDescription = FXUtils.newBorderButton(i18n("modpack.description"));
btnDescription.setOnAction(e -> onDescribe());
@@ -139,7 +139,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
} else {
handler.reject(e.getMessage());
}
}).executor(true), i18n("message.downloading"), TaskCancellationAction.NORMAL);
}), i18n("message.downloading"), TaskCancellationAction.NORMAL);
} else {
// otherwise we still consider the file as modpack zip file
// since casually the url may not ends with ".zip"
@@ -156,7 +156,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
} else {
handler.reject(e.getMessage());
}
}).executor(true),
}),
i18n("message.downloading"),
TaskCancellationAction.NORMAL
);
@@ -53,9 +53,9 @@ public final class RemoteModpackPage extends ModpackPage {
return;
}
lblName.setText(manifest.getName());
lblVersion.setText(manifest.getVersion());
lblAuthor.setText(manifest.getAuthor());
nameProperty.set(manifest.getName());
versionProperty.set(manifest.getVersion());
authorProperty.set(manifest.getAuthor());
Profile profile = controller.getSettings().get(ModpackPage.PROFILE);
String name = controller.getSettings().get(MODPACK_NAME);
@@ -60,7 +60,6 @@ import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.NativePatcher;
import org.jackhuang.hmcl.util.SettingsMap;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -131,11 +130,6 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
return title;
}
@Override
public void cleanup(SettingsMap settings) {
settings.remove(libraryId);
}
private void onRefresh() {
refresh();
}
@@ -168,6 +162,8 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
RemoteVersionListCell(VersionsPage control) {
this.control = control;
imageView.setMouseTransparent(true);
HBox hbox = new HBox(16);
HBox.setHgrow(twoLineListItem, Priority.ALWAYS);
hbox.setAlignment(Pos.CENTER);
@@ -17,7 +17,11 @@
*/
package org.jackhuang.hmcl.ui.export;
import com.jfoenix.controls.*;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXSlider;
import com.jfoenix.controls.JFXTextArea;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.ObservableList;
@@ -53,8 +57,6 @@ import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE;
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_MODRINTH;
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_SERVER;
@@ -127,7 +129,6 @@ public final class ModpackInfoPage extends Control implements WizardPage {
exportInfo.setVersion(version.get());
exportInfo.setAuthor(author.get());
exportInfo.setDescription(description.get());
exportInfo.setPackWithLauncher(packWithLauncher.get());
exportInfo.setUrl(url.get());
exportInfo.setForceUpdate(forceUpdate.get());
exportInfo.setPackWithLauncher(packWithLauncher.get());
@@ -171,10 +172,11 @@ public final class ModpackInfoPage extends Control implements WizardPage {
public static class ModpackInfoPageSkin extends SkinBase<ModpackInfoPage> {
private ObservableList<Node> originList;
private final List<JFXTextField> validatingFields = new ArrayList<>();
public ModpackInfoPageSkin(ModpackInfoPage skinnable) {
super(skinnable);
Insets insets = new Insets(5, 0, 12, 0);
Insets componentListMargin = new Insets(16, 0, 16, 0);
ScrollPane scroll = new ScrollPane();
@@ -182,7 +184,6 @@ public final class ModpackInfoPage extends Control implements WizardPage {
scroll.setFitToHeight(true);
getChildren().setAll(scroll);
List<JFXTextField> validatingFields = new ArrayList<>();
{
BorderPane borderPane = new BorderPane();
@@ -209,83 +210,52 @@ public final class ModpackInfoPage extends Control implements WizardPage {
BorderPane.setMargin(list, componentListMargin);
borderPane.setCenter(list);
var instanceNamePane = new LineTextPane();
{
BorderPane borderPane1 = new BorderPane();
borderPane1.setLeft(new Label(i18n("modpack.wizard.step.initialization.exported_version")));
instanceNamePane.setTitle(i18n("modpack.wizard.step.initialization.exported_version"));
instanceNamePane.setText(skinnable.versionName);
Label versionNameLabel = new Label();
versionNameLabel.setText(skinnable.versionName);
borderPane1.setRight(versionNameLabel);
list.getContent().add(borderPane1);
list.getContent().add(instanceNamePane);
}
{
GridPane pane = new GridPane();
list.getContent().add(pane);
pane.setHgap(16);
pane.setVgap(8);
pane.getColumnConstraints().setAll(new ColumnConstraints(), FXUtils.getColumnHgrowing());
int rowIndex = 0;
list.getContent().addAll(
createTextFieldLinePane(i18n("modpack.name"), skinnable.name, new RequiredValidator()),
createTextFieldLinePane(i18n("archive.author"), skinnable.author, new RequiredValidator()),
createTextFieldLinePane(i18n("archive.version"), skinnable.version, new RequiredValidator())
);
JFXTextField txtModpackName = new JFXTextField();
txtModpackName.textProperty().bindBidirectional(skinnable.name);
txtModpackName.getValidators().add(new RequiredValidator());
validatingFields.add(txtModpackName);
pane.addRow(rowIndex++, new Label(i18n("modpack.name")), txtModpackName);
if (skinnable.options.isRequireFileApi()) {
list.getContent().add(createTextFieldLinePane(
i18n("modpack.file_api"), skinnable.fileApi,
skinnable.options.isValidateFileApi() ? new RequiredValidator() : null,
new URLValidator(true)
));
}
JFXTextField txtModpackAuthor = new JFXTextField();
txtModpackAuthor.textProperty().bindBidirectional(skinnable.author);
txtModpackAuthor.getValidators().add(new RequiredValidator());
validatingFields.add(txtModpackAuthor);
pane.addRow(rowIndex++, new Label(i18n("archive.author")), txtModpackAuthor);
if (skinnable.options.isRequireLaunchArguments()) {
list.getContent().add(createTextFieldLinePane(
i18n("settings.advanced.minecraft_arguments"), skinnable.launchArguments
));
}
JFXTextField txtModpackVersion = new JFXTextField();
txtModpackVersion.textProperty().bindBidirectional(skinnable.version);
txtModpackVersion.getValidators().add(new RequiredValidator());
validatingFields.add(txtModpackVersion);
pane.addRow(rowIndex++, new Label(i18n("archive.version")), txtModpackVersion);
if (skinnable.options.isRequireJavaArguments()) {
list.getContent().add(createTextFieldLinePane(
i18n("settings.advanced.jvm_args"), skinnable.javaArguments
));
}
if (skinnable.options.isRequireFileApi()) {
JFXTextField txtModpackFileApi = new JFXTextField();
txtModpackFileApi.textProperty().bindBidirectional(skinnable.fileApi);
validatingFields.add(txtModpackFileApi);
if (skinnable.options.isValidateFileApi()) {
txtModpackFileApi.getValidators().add(new RequiredValidator());
}
txtModpackFileApi.getValidators().add(new URLValidator(true));
pane.addRow(rowIndex++, new Label(i18n("modpack.file_api")), txtModpackFileApi);
}
if (skinnable.options.isRequireLaunchArguments()) {
JFXTextField txtLaunchArguments = new JFXTextField();
txtLaunchArguments.textProperty().bindBidirectional(skinnable.launchArguments);
pane.addRow(rowIndex++, new Label(i18n("settings.advanced.minecraft_arguments")), txtLaunchArguments);
}
if (skinnable.options.isRequireJavaArguments()) {
JFXTextField txtJavaArguments = new JFXTextField();
txtJavaArguments.textProperty().bindBidirectional(skinnable.javaArguments);
pane.addRow(rowIndex++, new Label(i18n("settings.advanced.jvm_args")), txtJavaArguments);
}
if (skinnable.options.isRequireUrl()) {
JFXTextField txtModpackUrl = new JFXTextField();
txtModpackUrl.textProperty().bindBidirectional(skinnable.url);
pane.addRow(rowIndex++, new Label(i18n("modpack.origin.url")), txtModpackUrl);
}
if (skinnable.options.isRequireOrigins()) {
JFXTextField txtMcbbs = new JFXTextField();
FXUtils.setValidateWhileTextChanged(txtMcbbs, true);
txtMcbbs.getValidators().add(new NumberValidator(i18n("input.number"), true));
txtMcbbs.textProperty().bindBidirectional(skinnable.mcbbsThreadId);
validatingFields.add(txtMcbbs);
pane.addRow(rowIndex++, new Label(i18n("modpack.origin.mcbbs")), txtMcbbs);
}
if (skinnable.options.isRequireUrl()) {
list.getContent().add(createTextFieldLinePane(
i18n("modpack.origin.url"), skinnable.url
));
}
if (skinnable.options.isRequireOrigins()) {
list.getContent().add(createTextFieldLinePane(
i18n("modpack.origin.mcbbs"), skinnable.mcbbsThreadId,
new NumberValidator(i18n("input.number"), true)
));
}
if (skinnable.options.isRequireMinMemory()) {
@@ -339,78 +309,51 @@ public final class ModpackInfoPage extends Control implements WizardPage {
}
if (skinnable.options.isRequireAuthlibInjectorServer()) {
JFXComboBox<AuthlibInjectorServer> cboServers = new JFXComboBox<>();
cboServers.setMaxWidth(Double.MAX_VALUE);
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
Bindings.bindContent(cboServers.getItems(), config().getAuthlibInjectorServers());
var serversSelectButton = new LineSelectButton<AuthlibInjectorServer>();
serversSelectButton.setTitle(i18n("account.injector.server"));
serversSelectButton.setConverter(AuthlibInjectorServer::getName);
serversSelectButton.setDescriptionConverter(AuthlibInjectorServer::getUrl);
serversSelectButton.itemsProperty().set(config().getAuthlibInjectorServers());
skinnable.authlibInjectorServer.bind(Bindings.createStringBinding(() ->
Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem())
.map(AuthlibInjectorServer::getUrl)
.orElse(null)));
skinnable.authlibInjectorServer.bind(Bindings.createStringBinding(() -> {
AuthlibInjectorServer selected = serversSelectButton.getValue();
return selected != null ? selected.getUrl() : null;
}, serversSelectButton.valueProperty()));
BorderPane pane = new BorderPane();
Label left = new Label(i18n("account.injector.server"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
pane.setLeft(left);
pane.setRight(cboServers);
list.getContent().add(pane);
list.getContent().add(serversSelectButton);
}
if (skinnable.options.isRequireForceUpdate()) {
BorderPane pane = new BorderPane();
pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.force_update")));
list.getContent().add(pane);
var requireForceUpdateButton = new LineToggleButton();
requireForceUpdateButton.setTitle(i18n("modpack.wizard.step.initialization.force_update"));
requireForceUpdateButton.selectedProperty().bindBidirectional(skinnable.forceUpdate);
JFXToggleButton button = new JFXToggleButton();
button.selectedProperty().bindBidirectional(skinnable.forceUpdate);
button.setSize(8);
button.setMinHeight(16);
button.setMaxHeight(16);
pane.setRight(button);
list.getContent().add(requireForceUpdateButton);
}
{
BorderPane pane = new BorderPane();
pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.include_launcher")));
list.getContent().add(pane);
var canIncludeLauncherButton = new LineToggleButton();
canIncludeLauncherButton.setTitle(i18n("modpack.wizard.step.initialization.include_launcher"));
canIncludeLauncherButton.setDisable(!skinnable.canIncludeLauncher);
canIncludeLauncherButton.selectedProperty().bindBidirectional(skinnable.packWithLauncher);
JFXToggleButton button = new JFXToggleButton();
button.setDisable(!skinnable.canIncludeLauncher);
button.selectedProperty().bindBidirectional(skinnable.packWithLauncher);
button.setSize(8);
button.setMinHeight(16);
button.setMaxHeight(16);
pane.setRight(button);
list.getContent().add(canIncludeLauncherButton);
}
if (skinnable.options.isRequireNoCreateRemoteFiles()) {
BorderPane noCreateRemoteFiles = new BorderPane();
noCreateRemoteFiles.setLeft(new Label(i18n("modpack.wizard.step.initialization.no_create_remote_files")));
list.getContent().add(noCreateRemoteFiles);
var requireNoCreateRemoteFilesButton = new LineToggleButton();
requireNoCreateRemoteFilesButton.setTitle(i18n("modpack.wizard.step.initialization.no_create_remote_files"));
requireNoCreateRemoteFilesButton.selectedProperty().bindBidirectional(skinnable.noCreateRemoteFiles);
JFXToggleButton noCreateRemoteFilesButton = new JFXToggleButton();
noCreateRemoteFilesButton.selectedProperty().bindBidirectional(skinnable.noCreateRemoteFiles);
noCreateRemoteFilesButton.setSize(8);
noCreateRemoteFilesButton.setMinHeight(16);
noCreateRemoteFilesButton.setMaxHeight(16);
noCreateRemoteFiles.setRight(noCreateRemoteFilesButton);
list.getContent().add(requireNoCreateRemoteFilesButton);
}
if (skinnable.options.isRequireSkipCurseForgeRemoteFiles()) {
BorderPane skipCurseForgeRemoteFiles = new BorderPane();
skipCurseForgeRemoteFiles.setLeft(new Label(i18n("modpack.wizard.step.initialization.skip_curseforge_remote_files")));
list.getContent().add(skipCurseForgeRemoteFiles);
JFXToggleButton skipCurseForgeRemoteFilesButton = new JFXToggleButton();
var skipCurseForgeRemoteFilesButton = new LineToggleButton();
skipCurseForgeRemoteFilesButton.setTitle(i18n("modpack.wizard.step.initialization.skip_curseforge_remote_files"));
skipCurseForgeRemoteFilesButton.selectedProperty().bindBidirectional(skinnable.skipCurseForgeRemoteFiles);
skipCurseForgeRemoteFilesButton.setSize(8);
skipCurseForgeRemoteFilesButton.setMinHeight(16);
skipCurseForgeRemoteFilesButton.setMaxHeight(16);
skipCurseForgeRemoteFiles.setRight(skipCurseForgeRemoteFilesButton);
list.getContent().add(skipCurseForgeRemoteFilesButton);
}
}
@@ -436,5 +379,31 @@ public final class ModpackInfoPage extends Control implements WizardPage {
FXUtils.smoothScrolling(scroll);
}
private LinePane createTextFieldLinePane(String title, StringProperty property, ValidatorBase... validators) {
LinePane linePane = new LinePane();
JFXTextField textField = new JFXTextField();
textField.setMinWidth(500);
linePane.setTitle(title);
linePane.setRight(textField);
textField.textProperty().bindBidirectional(property);
boolean needValidation = false;
if (validators != null) {
for (ValidatorBase validator : validators) {
if (validator != null) {
needValidation = true;
textField.getValidators().add(validator);
}
}
}
if (needValidation) {
FXUtils.setValidateWhileTextChanged(textField, true);
validatingFields.add(textField);
}
return linePane;
}
}
}
@@ -17,11 +17,7 @@
*/
package org.jackhuang.hmcl.ui.main;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import javafx.beans.binding.Bindings;
import com.google.gson.*;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
@@ -30,8 +26,10 @@ import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
import org.jackhuang.hmcl.ui.construct.LineButton;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.IOException;
@@ -42,20 +40,22 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class AboutPage extends StackPane {
private final WeakListenerHolder holder = new WeakListenerHolder();
public AboutPage() {
ComponentList about = new ComponentList();
{
IconedTwoLineListItem launcher = new IconedTwoLineListItem();
launcher.setImage(FXUtils.newBuiltinImage("/assets/img/icon.png"));
var launcher = LineButton.createExternalLinkButton(Metadata.PUBLISH_URL);
launcher.setLargeTitle(true);
launcher.setLeading(FXUtils.newBuiltinImage("/assets/img/icon.png"));
launcher.setTitle("Hello Minecraft! Launcher");
launcher.setSubtitle(Metadata.VERSION);
launcher.setExternalLink(Metadata.PUBLISH_URL);
IconedTwoLineListItem author = new IconedTwoLineListItem();
author.setImage(FXUtils.newBuiltinImage("/assets/img/yellow_fish.png"));
var author = LineButton.createExternalLinkButton("https://space.bilibili.com/1445341");
author.setLargeTitle(true);
author.setLeading(FXUtils.newBuiltinImage("/assets/img/yellow_fish.png"));
author.setTitle("huanghongxun");
author.setSubtitle(i18n("about.author.statement"));
author.setExternalLink("https://space.bilibili.com/1445341");
about.getContent().setAll(launcher, author);
}
@@ -66,20 +66,20 @@ public final class AboutPage extends StackPane {
ComponentList legal = new ComponentList();
{
IconedTwoLineListItem copyright = new IconedTwoLineListItem();
var copyright = LineButton.createExternalLinkButton(Metadata.ABOUT_URL);
copyright.setLargeTitle(true);
copyright.setTitle(i18n("about.copyright"));
copyright.setSubtitle(i18n("about.copyright.statement"));
copyright.setExternalLink(Metadata.ABOUT_URL);
IconedTwoLineListItem claim = new IconedTwoLineListItem();
var claim = LineButton.createExternalLinkButton(Metadata.EULA_URL);
claim.setLargeTitle(true);
claim.setTitle(i18n("about.claim"));
claim.setSubtitle(i18n("about.claim.statement"));
claim.setExternalLink(Metadata.EULA_URL);
IconedTwoLineListItem openSource = new IconedTwoLineListItem();
var openSource = LineButton.createExternalLinkButton("https://github.com/HMCL-dev/HMCL");
openSource.setLargeTitle(true);
openSource.setTitle(i18n("about.open_source"));
openSource.setSubtitle(i18n("about.open_source.statement"));
openSource.setExternalLink("https://github.com/HMCL-dev/HMCL");
legal.getContent().setAll(copyright, claim, openSource);
}
@@ -113,7 +113,7 @@ public final class AboutPage extends StackPane {
: new Image(url);
}
private static ComponentList loadIconedTwoLineList(String path) {
private ComponentList loadIconedTwoLineList(String path) {
ComponentList componentList = new ComponentList();
InputStream input = FXUtils.class.getResourceAsStream(path);
@@ -127,36 +127,42 @@ public final class AboutPage extends StackPane {
for (JsonElement element : array) {
JsonObject obj = element.getAsJsonObject();
IconedTwoLineListItem item = new IconedTwoLineListItem();
var button = new LineButton();
button.setLargeTitle(true);
if (obj.get("externalLink") instanceof JsonPrimitive externalLink) {
button.setTrailingIcon(SVG.OPEN_IN_NEW);
String link = externalLink.getAsString();
button.setOnAction(event -> FXUtils.openLink(link));
}
if (obj.has("image")) {
JsonElement image = obj.get("image");
if (image.isJsonPrimitive()) {
item.setImage(loadImage(image.getAsString()));
button.setLeading(loadImage(image.getAsString()));
} else if (image.isJsonObject()) {
item.imageProperty().bind(Bindings.when(Themes.darkModeProperty())
.then(loadImage(image.getAsJsonObject().get("dark").getAsString()))
.otherwise(loadImage(image.getAsJsonObject().get("light").getAsString())));
holder.add(FXUtils.onWeakChangeAndOperate(Themes.darkModeProperty(), darkMode -> {
button.setLeading(darkMode
? loadImage(image.getAsJsonObject().get("dark").getAsString())
: loadImage(image.getAsJsonObject().get("light").getAsString())
);
}));
}
}
if (obj.has("title"))
item.setTitle(obj.get("title").getAsString());
else if (obj.has("titleLocalized"))
item.setTitle(i18n(obj.get("titleLocalized").getAsString()));
if (obj.get("title") instanceof JsonPrimitive title)
button.setTitle(title.getAsString());
else if (obj.get("titleLocalized") instanceof JsonPrimitive titleLocalized)
button.setTitle(i18n(titleLocalized.getAsString()));
if (obj.has("subtitle"))
item.setSubtitle(obj.get("subtitle").getAsString());
else if (obj.has("subtitleLocalized"))
item.setSubtitle(i18n(obj.get("subtitleLocalized").getAsString()));
if (obj.get("subtitle") instanceof JsonPrimitive subtitle)
button.setSubtitle(subtitle.getAsString());
else if (obj.get("subtitleLocalized") instanceof JsonPrimitive subtitleLocalized)
button.setSubtitle(i18n(subtitleLocalized.getAsString()));
if (obj.has("externalLink")) {
String link = obj.get("externalLink").getAsString();
item.setExternalLink(link);
FXUtils.installFastTooltip(item.getExternalLinkButton(), link);
}
componentList.getContent().add(item);
componentList.getContent().add(button);
}
} catch (IOException | JsonParseException e) {
LOG.warning("Failed to load list: " + path, e);
@@ -17,14 +17,14 @@
*/
package org.jackhuang.hmcl.ui.main;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
import org.jackhuang.hmcl.ui.construct.LineButton;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -33,6 +33,8 @@ import org.jackhuang.hmcl.Metadata;
public class FeedbackPage extends SpinnerPane {
private final WeakListenerHolder holder = new WeakListenerHolder();
public FeedbackPage() {
VBox content = new VBox();
content.setPadding(new Insets(10));
@@ -45,30 +47,33 @@ public class FeedbackPage extends SpinnerPane {
ComponentList groups = new ComponentList();
{
IconedTwoLineListItem users = new IconedTwoLineListItem();
users.setImage(FXUtils.newBuiltinImage("/assets/img/icon.png"));
var users = LineButton.createExternalLinkButton(Metadata.GROUPS_URL);
users.setLargeTitle(true);
users.setLeading(FXUtils.newBuiltinImage("/assets/img/icon.png"));
users.setTitle(i18n("contact.chat.qq_group"));
users.setSubtitle(i18n("contact.chat.qq_group.statement"));
users.setExternalLink(Metadata.GROUPS_URL);
IconedTwoLineListItem discord = new IconedTwoLineListItem();
discord.setImage(FXUtils.newBuiltinImage("/assets/img/discord.png"));
var discord = LineButton.createExternalLinkButton("https://discord.gg/jVvC7HfM6U");
discord.setLargeTitle(true);
discord.setLeading(FXUtils.newBuiltinImage("/assets/img/discord.png"));
discord.setTitle(i18n("contact.chat.discord"));
discord.setSubtitle(i18n("contact.chat.discord.statement"));
discord.setExternalLink("https://discord.gg/jVvC7HfM6U");
groups.getContent().setAll(users, discord);
}
ComponentList feedback = new ComponentList();
{
IconedTwoLineListItem github = new IconedTwoLineListItem();
github.imageProperty().bind(Bindings.when(Themes.darkModeProperty())
.then(FXUtils.newBuiltinImage("/assets/img/github-white.png"))
.otherwise(FXUtils.newBuiltinImage("/assets/img/github.png")));
var github = LineButton.createExternalLinkButton("https://github.com/HMCL-dev/HMCL/issues/new/choose");
github.setLargeTitle(true);
github.setTitle(i18n("contact.feedback.github"));
github.setSubtitle(i18n("contact.feedback.github.statement"));
github.setExternalLink("https://github.com/HMCL-dev/HMCL/issues/new/choose");
holder.add(FXUtils.onWeakChangeAndOperate(Themes.darkModeProperty(), darkMode -> {
github.setLeading(darkMode
? FXUtils.newBuiltinImage("/assets/img/github-white.png")
: FXUtils.newBuiltinImage("/assets/img/github.png"));
}));
feedback.getContent().setAll(github);
}
@@ -26,11 +26,11 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
import org.jackhuang.hmcl.ui.construct.LineButton;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jackhuang.hmcl.util.io.HttpRequest;
import java.util.Collections;
import java.util.List;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
@@ -50,10 +50,11 @@ public class HelpPage extends SpinnerPane {
FXUtils.smoothScrolling(scrollPane);
setContent(scrollPane);
IconedTwoLineListItem docPane = new IconedTwoLineListItem();
var docPane = LineButton.createExternalLinkButton(Metadata.DOCS_URL);
docPane.setLargeTitle(true);
docPane.setTitle(i18n("help.doc"));
docPane.setSubtitle(i18n("help.detail"));
docPane.setExternalLink(Metadata.DOCS_URL);
ComponentList doc = new ComponentList();
doc.getContent().setAll(docPane);
content.getChildren().add(doc);
@@ -68,77 +69,32 @@ public class HelpPage extends SpinnerPane {
for (HelpCategory category : helpCategories) {
ComponentList categoryPane = new ComponentList();
for (Help help : category.getItems()) {
IconedTwoLineListItem item = new IconedTwoLineListItem();
item.setTitle(help.getTitle());
item.setSubtitle(help.getSubtitle());
item.setExternalLink(help.getUrl());
for (Help help : category.items()) {
var item = LineButton.createExternalLinkButton(help.url());
item.setLargeTitle(true);
item.setTitle(help.title());
item.setSubtitle(help.subtitle());
categoryPane.getContent().add(item);
}
content.getChildren().add(ComponentList.createComponentListTitle(category.title));
content.getChildren().add(ComponentList.createComponentListTitle(category.title()));
content.getChildren().add(categoryPane);
}
hideSpinner();
}).start();
}
private static class HelpCategory {
@SerializedName("title")
private final String title;
@SerializedName("items")
private final List<Help> items;
public HelpCategory() {
this("", Collections.emptyList());
}
public HelpCategory(String title, List<Help> items) {
this.title = title;
this.items = items;
}
public String getTitle() {
return title;
}
public List<Help> getItems() {
return items;
}
@JsonSerializable
private record HelpCategory(
@SerializedName("title") String title,
@SerializedName("items") List<Help> items) {
}
private static class Help {
@SerializedName("title")
private final String title;
@SerializedName("subtitle")
private final String subtitle;
@SerializedName("url")
private final String url;
public Help() {
this("", "", "");
}
public Help(String title, String subtitle, String url) {
this.title = title;
this.subtitle = subtitle;
this.url = url;
}
public String getTitle() {
return title;
}
public String getSubtitle() {
return subtitle;
}
public String getUrl() {
return url;
}
@JsonSerializable
private record Help(
@SerializedName("title") String title,
@SerializedName("subtitle") String subtitle,
@SerializedName("url") String url) {
}
}
@@ -18,13 +18,13 @@
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.*;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
@@ -48,6 +48,8 @@ import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.DialogPane;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Result;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.gson.JsonUtils;
@@ -108,23 +110,39 @@ public final class JavaDownloadDialog extends StackPane {
}
private final class DownloadMojangJava extends DialogPane {
private final ToggleGroup toggleGroup = new ToggleGroup();
private final List<JFXCheckBox> checkboxes = new ArrayList<>();
DownloadMojangJava() {
setTitle(i18n("java.download"));
validProperty().bind(toggleGroup.selectedToggleProperty().isNotNull());
setTitle(i18n("java.download.title"));
VBox vbox = new VBox(16);
Label prompt = new Label(i18n("java.download.prompt"));
vbox.getChildren().add(prompt);
setValid(false);
InvalidationListener updateValidStatus = e -> {
for (JFXCheckBox box : checkboxes) {
if (!box.isDisabled() && box.isSelected()) {
setValid(true);
return;
}
}
setValid(false);
};
for (GameJavaVersion version : supportedGameJavaVersions) {
JFXRadioButton button = new JFXRadioButton("Java " + version.majorVersion());
JFXCheckBox button = new JFXCheckBox("Java " + version.majorVersion());
button.setUserData(version);
vbox.getChildren().add(button);
toggleGroup.getToggles().add(button);
if (JavaManager.REPOSITORY.isInstalled(platform, version))
if (JavaManager.REPOSITORY.isInstalled(platform, version)) {
button.setDisable(true);
button.setSelected(true);
} else {
button.selectedProperty().addListener(updateValidStatus);
}
vbox.getChildren().add(button);
checkboxes.add(button);
}
setBody(vbox);
@@ -137,38 +155,52 @@ public final class JavaDownloadDialog extends StackPane {
setActions(warningLabel, acceptPane, cancelButton);
}
private Task<Void> downloadTask(GameJavaVersion javaVersion) {
return JavaManager.getDownloadJavaTask(downloadProvider, platform, javaVersion).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception != null) {
Throwable resolvedException = resolveException(exception);
LOG.warning("Failed to download java", exception);
if (!(resolvedException instanceof CancellationException)) {
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
}
}
});
}
@Override
protected void onAccept() {
fireEvent(new DialogCloseEvent());
GameJavaVersion javaVersion = (GameJavaVersion) toggleGroup.getSelectedToggle().getUserData();
if (javaVersion == null)
List<GameJavaVersion> selectedVersions = new ArrayList<>();
for (JFXCheckBox box : checkboxes) {
if (!box.isDisabled() && box.isSelected()) {
selectedVersions.add((GameJavaVersion) box.getUserData());
}
}
if (selectedVersions.isEmpty())
return;
if (JavaManager.REPOSITORY.isInstalled(platform, javaVersion))
Controllers.confirm(i18n("download.java.override"), null, () -> {
Controllers.taskDialog(Task.supplyAsync(() -> JavaManager.REPOSITORY.getJavaExecutable(platform, javaVersion))
.thenComposeAsync(Schedulers.javafx(), realPath -> {
if (realPath != null) {
JavaManager.removeJava(realPath);
Task<Void> task = Task.allOf(selectedVersions.stream().map(javaVersion -> JavaManager.getDownloadJavaTask(downloadProvider, platform, javaVersion).wrapResult()).toList())
.whenComplete(Schedulers.javafx(), (results, exception) -> {
Throwable exceptionToDisplay;
if (exception == null) {
List<Throwable> exceptions = results.stream()
.filter(Result::isFailure)
.map(Result::getException)
.map(Lang::resolveException)
.filter(it -> !(it instanceof CancellationException))
.toList();
if (!exceptions.isEmpty()) {
if (exceptions.size() == 1) {
exceptionToDisplay = exceptions.get(0);
} else {
exceptionToDisplay = new IOException("Failed to download Java");
for (Throwable e : exceptions)
exceptionToDisplay.addSuppressed(e);
}
return downloadTask(javaVersion);
}), i18n("download.java"), TaskCancellationAction.NORMAL);
}, null);
else
Controllers.taskDialog(downloadTask(javaVersion), i18n("download.java.process"), TaskCancellationAction.NORMAL);
} else {
exceptionToDisplay = null;
}
} else {
exceptionToDisplay = exception;
}
if (exceptionToDisplay != null) {
LOG.warning("Failed to download java", exceptionToDisplay);
Controllers.dialog(DownloadProviders.localizeErrorMessage(exceptionToDisplay), i18n("install.failed"));
}
});
Controllers.taskDialog(task, i18n("download.java.process"), TaskCancellationAction.NORMAL);
}
}
@@ -311,7 +343,7 @@ public final class JavaDownloadDialog extends StackPane {
FXUtils.onChange(distributionBox.getSelectionModel().selectedItemProperty(),
it -> currentJavaVersionList.set(getJavaVersionList(it)));
setHeading(new Label(i18n("java.download")));
setHeading(new Label(i18n("java.download.title")));
setBody(body);
setActions(warningLabel, downloadButtonPane, cancelButton);
if (platform.getOperatingSystem() == OperatingSystem.LINUX && platform.getArchitecture() == Architecture.RISCV64) {
@@ -115,7 +115,6 @@ public final class JavaInstallPage extends WizardSinglePage {
nameField = new JFXTextField();
nameField.textProperty().bindBidirectional(control.nameProperty);
FXUtils.setLimitWidth(nameField, 200);
BorderPane.setAlignment(nameField, Pos.CENTER_RIGHT);
namePane.setRight(nameField);
nameField.setValidators(
new RequiredValidator(),
@@ -30,6 +30,7 @@ import javafx.scene.control.Skin;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.java.JavaInfo;
@@ -189,7 +190,6 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
JavaPageSkin(JavaManagementPage skinnable) {
super(skinnable);
this.listView.getStyleClass().add("no-horizontal-scrollbar");
}
@Override
@@ -232,6 +232,7 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
center.setAlignment(Pos.CENTER_LEFT);
this.content = new TwoLineListItem();
HBox.setHgrow(content, Priority.ALWAYS);
BorderPane.setAlignment(content, Pos.CENTER);
center.getChildren().setAll(content);
@@ -241,7 +242,7 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
right.setAlignment(Pos.CENTER_RIGHT);
{
JFXButton revealButton = new JFXButton();
revealButton.setGraphic(FXUtils.limitingSize(SVG.FOLDER_OPEN.createIcon(24), 24, 24));
revealButton.setGraphic(SVG.FOLDER_OPEN.createIcon());
revealButton.getStyleClass().add("toggle-icon4");
revealButton.setOnAction(e -> {
JavaRuntime java = getItem();
@@ -159,7 +159,7 @@ public final class JavaRestorePage extends ListPageBase<JavaRestorePage.Disabled
{
JFXButton revealButton = new JFXButton();
revealButton.getStyleClass().add("toggle-icon4");
revealButton.setGraphic(FXUtils.limitingSize(SVG.FOLDER_OPEN.createIcon(24), 24, 24));
revealButton.setGraphic(SVG.FOLDER_OPEN.createIcon());
revealButton.setOnAction(e -> skinnable.onReveal());
FXUtils.installFastTooltip(revealButton, i18n("reveal.in_file_manager"));
@@ -168,7 +168,7 @@ public final class JavaRestorePage extends ListPageBase<JavaRestorePage.Disabled
JFXButton removeButton = new JFXButton();
removeButton.getStyleClass().add("toggle-icon4");
removeButton.setGraphic(FXUtils.limitingSize(SVG.DELETE.createIcon(24), 24, 24));
removeButton.setGraphic(SVG.DELETE.createIcon());
removeButton.setOnAction(e -> skinnable.onRemove());
FXUtils.installFastTooltip(removeButton, i18n("java.disabled.management.remove"));
@@ -176,7 +176,7 @@ public final class JavaRestorePage extends ListPageBase<JavaRestorePage.Disabled
} else {
JFXButton restoreButton = new JFXButton();
restoreButton.getStyleClass().add("toggle-icon4");
restoreButton.setGraphic(FXUtils.limitingSize(SVG.RESTORE.createIcon(24), 24, 24));
restoreButton.setGraphic(SVG.RESTORE.createIcon());
restoreButton.setOnAction(e -> skinnable.onRestore());
FXUtils.installFastTooltip(restoreButton, i18n("java.disabled.management.restore"));
@@ -64,12 +64,12 @@ import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.IOException;
import java.util.List;
@@ -320,6 +320,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
Task<?> task = versionList.refreshAsync("")
.thenSupplyAsync(() -> versionList.getVersions("").stream()
.filter(it -> it.getVersionType() == RELEASE)
.filter(it -> NativePatcher.checkSupportedStatus(GameVersionNumber.asGameVersion(it.getGameVersion()), Platform.SYSTEM_PLATFORM, OperatingSystem.SYSTEM_VERSION) != NativePatcher.SupportStatus.UNSUPPORTED)
.sorted()
.findFirst()
.orElseThrow(() -> new IOException("No versions found")))
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXPopup;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.scene.layout.Region;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
@@ -65,7 +66,6 @@ import java.util.Locale;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.ui.FXUtils.wrap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -171,16 +171,14 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
// third item in left sidebar
AdvancedListItem gameItem = new AdvancedListItem();
gameItem.setLeftGraphic(wrap(SVG.FORMAT_LIST_BULLETED));
gameItem.setActionButtonVisible(false);
gameItem.setLeftIcon(SVG.FORMAT_LIST_BULLETED);
gameItem.setTitle(i18n("version.manage"));
gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage()));
FXUtils.onSecondaryButtonClicked(gameItem, () -> showGameListPopupMenu(gameItem));
// forth item in left sidebar
AdvancedListItem downloadItem = new AdvancedListItem();
downloadItem.setLeftGraphic(wrap(SVG.DOWNLOAD));
downloadItem.setActionButtonVisible(false);
downloadItem.setLeftIcon(SVG.DOWNLOAD);
downloadItem.setTitle(i18n("download"));
downloadItem.setOnAction(e -> {
Controllers.getDownloadPage().showGameDownloads();
@@ -193,8 +191,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
// fifth item in left sidebar
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
launcherSettingsItem.setLeftGraphic(wrap(SVG.SETTINGS));
launcherSettingsItem.setActionButtonVisible(false);
launcherSettingsItem.setLeftIcon(SVG.SETTINGS);
launcherSettingsItem.setTitle(i18n("settings"));
launcherSettingsItem.setOnAction(e -> {
Controllers.getSettingsPage().showGameSettings(Profiles.getSelectedProfile());
@@ -206,8 +203,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
// sixth item in left sidebar
AdvancedListItem terracottaItem = new AdvancedListItem();
terracottaItem.setLeftGraphic(wrap(SVG.GRAPH2));
terracottaItem.setActionButtonVisible(false);
terracottaItem.setLeftIcon(SVG.GRAPH2);
terracottaItem.setTitle(i18n("terracotta"));
terracottaItem.setOnAction(e -> {
if (TerracottaMetadata.PROVIDER != null) {
@@ -249,7 +245,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
setCenter(getSkinnable().getMainPage());
}
public void showGameListPopupMenu(AdvancedListItem gameListItem) {
public void showGameListPopupMenu(Region gameListItem) {
GameListPopupMenu.show(gameListItem,
JFXPopup.PopupVPosition.TOP,
JFXPopup.PopupHPosition.LEFT,
@@ -178,20 +178,21 @@ public final class SettingsPage extends ScrollPane {
}
{
VBox content = new VBox();
content.setSpacing(8);
JFXRadioButton chkUpdateStable = new JFXRadioButton(i18n("update.channel.stable"));
JFXRadioButton chkUpdateDev = new JFXRadioButton(i18n("update.channel.dev"));
VBox content = new VBox(12);
content.setPadding(new Insets(8, 0, 0, 0));
updateChannelGroup = new ToggleGroup();
chkUpdateDev.setToggleGroup(updateChannelGroup);
chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT);
chkUpdateStable.setToggleGroup(updateChannelGroup);
JFXRadioButton chkUpdateStable = new JFXRadioButton(i18n("update.channel.stable"));
chkUpdateStable.setUserData(UpdateChannel.STABLE);
chkUpdateStable.setToggleGroup(updateChannelGroup);
JFXRadioButton chkUpdateDev = new JFXRadioButton(i18n("update.channel.dev"));
chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT);
chkUpdateDev.setToggleGroup(updateChannelGroup);
Label noteWrapper = new Label(i18n("update.note"));
VBox.setMargin(noteWrapper, new Insets(10, 0, 0, 0));
VBox.setMargin(noteWrapper, new Insets(8, 0, 0, 0));
content.getChildren().setAll(chkUpdateStable, chkUpdateDev, noteWrapper);
@@ -41,6 +41,7 @@ public final class NBTTreeView extends TreeView<Tag> {
public NBTTreeView(NBTTreeView.Item tree) {
this.setRoot(tree);
if (tree != null) tree.setExpanded(true);
this.setCellFactory(cellFactory());
}
@@ -141,6 +142,9 @@ public final class NBTTreeView extends TreeView<Tag> {
item.getChildren().add(subTree);
}
}
FXUtils.onChangeAndOperate(item.expandedProperty(), expanded -> {
if (expanded && item.getChildren().size() == 1) item.getChildren().get(0).setExpanded(true);
});
return item;
}
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.profile;
import com.jfoenix.controls.JFXButton;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.SkinBase;
@@ -35,7 +36,6 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
public ProfileListItemSkin(ProfileListItem skinnable) {
super(skinnable);
BorderPane root = new BorderPane();
root.setPickOnBounds(false);
RipplerContainer container = new RipplerContainer(root);
@@ -46,7 +46,9 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
FXUtils.onClicked(getSkinnable(), () -> getSkinnable().setSelected(true));
Node left = FXUtils.wrap(SVG.FOLDER);
Node left = SVG.FOLDER.createIcon(20);
left.setMouseTransparent(true);
BorderPane.setMargin(left, new Insets(0, 6, 0, 6));
root.setLeft(left);
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
@@ -35,10 +35,7 @@ import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.FileItem;
import org.jackhuang.hmcl.ui.construct.LineToggleButton;
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -54,7 +51,7 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
private final StringProperty location;
private final Profile profile;
private final JFXTextField txtProfileName;
private final FileItem gameDir;
private final LineFileChooserButton gameDir;
private final LineToggleButton toggleUseRelativePath;
/**
@@ -107,10 +104,11 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
});
}
gameDir = new FileItem();
gameDir.setName(i18n("profile.instance_directory"));
gameDir.setTitle(i18n("profile.instance_directory.choose"));
gameDir.pathProperty().bindBidirectional(location);
gameDir = new LineFileChooserButton();
gameDir.setTitle(i18n("profile.instance_directory"));
gameDir.setFileChooserTitle(i18n("profile.instance_directory.choose"));
gameDir.setType(LineFileChooserButton.Type.OPEN_DIRECTORY);
gameDir.locationProperty().bindBidirectional(location);
toggleUseRelativePath = new LineToggleButton();
toggleUseRelativePath.setTitle(i18n("profile.use_relative_path"));
@@ -188,7 +186,7 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
}
} else {
if (StringUtils.isBlank(getLocation())) {
gameDir.onExplore();
gameDir.fire();
}
Profile newProfile = new Profile(txtProfileName.getText(), Path.of(getLocation()));
newProfile.setUseRelativePath(toggleUseRelativePath.isSelected());
@@ -22,8 +22,6 @@ import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.WeakChangeListener;
import javafx.collections.FXCollections;
@@ -34,13 +32,8 @@ import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.setting.Profile;
@@ -57,13 +50,7 @@ import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.ComponentSublist;
import org.jackhuang.hmcl.ui.construct.HintPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
@@ -163,12 +150,12 @@ public class TerracottaControllerPage extends StackPane {
body.getStyleClass().add("terracotta-hint");
body.setLineSpacing(4);
LineButton download = LineButton.of();
download.setLeftImage(FXUtils.newBuiltinImage("/assets/img/terracotta.png"));
var download = createLargeTitleLineButton();
download.setLeading(FXUtils.newBuiltinImage("/assets/img/terracotta.png"));
download.setTitle(i18n(String.format("terracotta.status.uninitialized.%s.title", fork)));
download.setSubtitle(i18n("terracotta.status.uninitialized.desc"));
download.setRightIcon(SVG.ARROW_FORWARD);
FXUtils.onClicked(download, () -> {
download.setTrailingIcon(SVG.ARROW_FORWARD, ICON_SIZE);
download.setOnAction(event -> {
TerracottaState.Preparing s = TerracottaManager.download();
if (s != null) {
UI_STATE.set(s);
@@ -207,12 +194,12 @@ public class TerracottaControllerPage extends StackPane {
flow.getStyleClass().add("terracotta-hint");
flow.setLineSpacing(4);
LineButton host = LineButton.of();
host.setLeftIcon(SVG.HOST);
var host = createLargeTitleLineButton();
host.setLeading(SVG.HOST, ICON_SIZE);
host.setTitle(i18n("terracotta.status.waiting.host.title"));
host.setSubtitle(i18n("terracotta.status.waiting.host.desc"));
host.setRightIcon(SVG.ARROW_FORWARD);
FXUtils.onClicked(host, () -> {
host.setTrailingIcon(SVG.ARROW_FORWARD, ICON_SIZE);
host.setOnAction(event -> {
if (LauncherHelper.countMangedProcesses() >= 1) {
TerracottaState.HostScanning s1 = TerracottaManager.setScanning();
if (s1 != null) {
@@ -239,12 +226,12 @@ public class TerracottaControllerPage extends StackPane {
}
});
LineButton guest = LineButton.of();
guest.setLeftIcon(SVG.ADD_CIRCLE);
var guest = createLargeTitleLineButton();
guest.setLeading(SVG.ADD_CIRCLE, ICON_SIZE);
guest.setTitle(i18n("terracotta.status.waiting.guest.title"));
guest.setSubtitle(i18n("terracotta.status.waiting.guest.desc"));
guest.setRightIcon(SVG.ARROW_FORWARD);
FXUtils.onClicked(guest, () -> {
guest.setTrailingIcon(SVG.ARROW_FORWARD, ICON_SIZE);
guest.setOnAction(event -> {
Controllers.prompt(i18n("terracotta.status.waiting.guest.prompt.title"), (code, handler) -> {
Task<TerracottaState.GuestConnecting> task = TerracottaManager.setGuesting(code);
if (task != null) {
@@ -263,11 +250,11 @@ public class TerracottaControllerPage extends StackPane {
});
if (ThreadLocalRandom.current().nextDouble() < 0.02D) {
LineButton feedback = LineButton.of();
feedback.setLeftIcon(SVG.FEEDBACK);
var feedback = createLargeTitleLineButton();
feedback.setLeading(SVG.FEEDBACK, ICON_SIZE);
feedback.setTitle(i18n("terracotta.feedback.title"));
feedback.setSubtitle(i18n("terracotta.feedback.desc"));
feedback.setRightIcon(SVG.OPEN_IN_NEW);
feedback.setTrailingIcon(SVG.OPEN_IN_NEW, ICON_SIZE);
FXUtils.onClicked(feedback, () -> FXUtils.openLink(TerracottaMetadata.FEEDBACK_LINK));
nodesProperty.setAll(flow, host, guest, feedback);
@@ -282,11 +269,11 @@ public class TerracottaControllerPage extends StackPane {
body.getStyleClass().add("terracotta-hint");
body.setLineSpacing(4);
LineButton room = LineButton.of();
room.setLeftIcon(SVG.ARROW_BACK);
var room = createLargeTitleLineButton();
room.setLeading(SVG.ARROW_BACK, ICON_SIZE);
room.setTitle(i18n("terracotta.back"));
room.setSubtitle(i18n("terracotta.status.scanning.back"));
FXUtils.onClicked(room, () -> {
room.setOnAction(event -> {
TerracottaState.Waiting s = TerracottaManager.setWaiting();
if (s != null) {
UI_STATE.set(s);
@@ -298,11 +285,11 @@ public class TerracottaControllerPage extends StackPane {
statusProperty.set(i18n("terracotta.status.host_starting"));
progressProperty.set(-1);
LineButton room = LineButton.of();
room.setLeftIcon(SVG.ARROW_BACK);
var room = createLargeTitleLineButton();
room.setLeading(SVG.ARROW_BACK, ICON_SIZE);
room.setTitle(i18n("terracotta.back"));
room.setSubtitle(i18n("terracotta.status.host_starting.back"));
FXUtils.onClicked(room, () -> {
room.setOnAction(event -> {
TerracottaState.Waiting s = TerracottaManager.setWaiting();
if (s != null) {
UI_STATE.set(s);
@@ -342,17 +329,17 @@ public class TerracottaControllerPage extends StackPane {
code.setCursor(Cursor.HAND);
FXUtils.onClicked(code, () -> copyCode(cs));
LineButton copy = LineButton.of();
copy.setLeftIcon(SVG.CONTENT_COPY);
var copy = createLargeTitleLineButton();
copy.setLeading(SVG.CONTENT_COPY, ICON_SIZE);
copy.setTitle(i18n("terracotta.status.host_ok.code.copy"));
copy.setSubtitle(i18n("terracotta.status.host_ok.code.desc"));
FXUtils.onClicked(copy, () -> copyCode(cs));
LineButton back = LineButton.of();
back.setLeftIcon(SVG.ARROW_BACK);
var back = createLargeTitleLineButton();
back.setLeading(SVG.ARROW_BACK, ICON_SIZE);
back.setTitle(i18n("terracotta.back"));
back.setSubtitle(i18n("terracotta.status.host_ok.back"));
FXUtils.onClicked(back, () -> {
back.setOnAction(event -> {
TerracottaState.Waiting s = TerracottaManager.setWaiting();
if (s != null) {
UI_STATE.set(s);
@@ -369,11 +356,11 @@ public class TerracottaControllerPage extends StackPane {
statusProperty.set(i18n("terracotta.status.guest_starting"));
progressProperty.set(-1);
LineButton room = LineButton.of();
room.setLeftIcon(SVG.ARROW_BACK);
var room = createLargeTitleLineButton();
room.setLeading(SVG.ARROW_BACK, ICON_SIZE);
room.setTitle(i18n("terracotta.back"));
room.setSubtitle(i18n("terracotta.status.guest_starting.back"));
FXUtils.onClicked(room, () -> {
room.setOnAction(event -> {
TerracottaState.Waiting s = TerracottaManager.setWaiting();
if (s != null) {
UI_STATE.set(s);
@@ -384,12 +371,12 @@ public class TerracottaControllerPage extends StackPane {
if (state instanceof TerracottaState.GuestStarting) {
TerracottaState.GuestStarting.Difficulty difficulty = ((TerracottaState.GuestStarting) state).getDifficulty();
if (difficulty != null && difficulty != TerracottaState.GuestStarting.Difficulty.UNKNOWN) {
LineButton info = LineButton.of();
info.setLeftIcon(switch (difficulty) {
var info = createLargeTitleLineButton();
info.setLeading(switch (difficulty) {
case UNKNOWN -> throw new AssertionError();
case EASIEST, SIMPLE -> SVG.INFO;
case MEDIUM, TOUGH -> SVG.WARNING;
});
}, ICON_SIZE);
String difficultyID = difficulty.name().toLowerCase(Locale.ROOT);
info.setTitle(i18n(String.format("terracotta.difficulty.%s", difficultyID)));
@@ -412,15 +399,15 @@ public class TerracottaControllerPage extends StackPane {
statusProperty.set(i18n("terracotta.status.guest_ok"));
progressProperty.set(1);
LineButton tutorial = LineButton.of();
var tutorial = createLargeTitleLineButton();
tutorial.setTitle(i18n("terracotta.status.guest_ok.title"));
tutorial.setSubtitle(i18n("terracotta.status.guest_ok.desc", guestOK.getUrl()));
LineButton back = LineButton.of();
back.setLeftIcon(SVG.ARROW_BACK);
var back = createLargeTitleLineButton();
back.setLeading(SVG.ARROW_BACK, ICON_SIZE);
back.setTitle(i18n("terracotta.back"));
back.setSubtitle(i18n("terracotta.status.guest_ok.back"));
FXUtils.onClicked(back, () -> {
back.setOnAction(event -> {
TerracottaState.Waiting s = TerracottaManager.setWaiting();
if (s != null) {
UI_STATE.set(s);
@@ -438,11 +425,11 @@ public class TerracottaControllerPage extends StackPane {
progressProperty.set(1);
nodesProperty.setAll();
LineButton back = LineButton.of();
back.setLeftIcon(SVG.ARROW_BACK);
var back = createLargeTitleLineButton();
back.setLeading(SVG.ARROW_BACK, ICON_SIZE);
back.setTitle(i18n("terracotta.back"));
back.setSubtitle(i18n("terracotta.status.exception.back"));
FXUtils.onClicked(back, () -> {
back.setOnAction(event -> {
TerracottaState.Waiting s = TerracottaManager.setWaiting();
if (s != null) {
UI_STATE.set(s);
@@ -450,8 +437,8 @@ public class TerracottaControllerPage extends StackPane {
});
SpinnerPane exportLog = new SpinnerPane();
LineButton exportLogInner = LineButton.of();
exportLogInner.setLeftIcon(SVG.OUTPUT);
var exportLogInner = createLargeTitleLineButton();
exportLogInner.setLeading(SVG.OUTPUT, ICON_SIZE);
exportLogInner.setTitle(i18n("terracotta.export_log"));
exportLogInner.setSubtitle(i18n("terracotta.export_log.desc"));
exportLog.setContent(exportLogInner);
@@ -459,7 +446,7 @@ public class TerracottaControllerPage extends StackPane {
// FIXME: SpinnerPane loses its content width in loading state.
exportLog.minHeightProperty().bind(back.heightProperty());
FXUtils.onClicked(exportLogInner, () -> {
exportLogInner.setOnAction(event -> {
exportLog.setLoading(true);
TerracottaManager.exportLogs().thenAcceptAsync(Schedulers.io(), data -> {
@@ -493,11 +480,11 @@ public class TerracottaControllerPage extends StackPane {
progressProperty.set(1);
if (fatal.isRecoverable()) {
LineButton retry = LineButton.of();
retry.setLeftIcon(SVG.RESTORE);
var retry = createLargeTitleLineButton();
retry.setLeading(SVG.RESTORE, ICON_SIZE);
retry.setTitle(i18n("terracotta.status.fatal.retry"));
retry.setSubtitle(message);
FXUtils.onClicked(retry, () -> {
retry.setOnAction(event -> {
TerracottaState s = TerracottaManager.recover();
if (s != null) {
UI_STATE.set(s);
@@ -534,7 +521,8 @@ public class TerracottaControllerPage extends StackPane {
children.add(statusPane);
children.addAll(nodesProperty);
}
// Prevent the shadow of components from being clipped
StackPane.setMargin(components, new Insets(0, 0, 5, 0));
transition.setContent(components, ContainerAnimations.SLIDE_UP_FADE_IN);
};
listener.changed(UI_STATE, null, UI_STATE.get());
@@ -561,34 +549,27 @@ public class TerracottaControllerPage extends StackPane {
private ComponentSublist getThirdPartyDownloadNodes() {
ComponentSublist locals = new ComponentSublist();
locals.setComponentPadding(false);
LineButton header = LineButton.of(false);
header.setLeftImage(FXUtils.newBuiltinImage("/assets/img/terracotta.png"));
var header = new LinePane();
header.getStyleClass().add("no-padding");
header.setLargeTitle(true);
header.setMinHeight(LinePane.USE_COMPUTED_SIZE);
header.setMouseTransparent(true);
header.setLeading(FXUtils.newBuiltinImage("/assets/img/terracotta.png"));
header.setTitle(i18n("terracotta.from_local.title"));
header.setSubtitle(i18n("terracotta.from_local.desc"));
locals.setHeaderLeft(header);
for (TerracottaMetadata.Link link : TerracottaMetadata.PACKAGE_LINKS) {
HBox node = new HBox();
node.setAlignment(Pos.CENTER_LEFT);
node.setPadding(new Insets(10, 16, 10, 16));
Label description = new Label(link.description().getText(I18n.getLocale().getCandidateLocales()));
HBox placeholder = new HBox();
HBox.setHgrow(placeholder, Priority.ALWAYS);
Node icon = SVG.OPEN_IN_NEW.createIcon(16);
node.getChildren().setAll(description, placeholder, icon);
String url = link.link();
RipplerContainer container = new RipplerContainer(node);
container.setOnMouseClicked(ev -> Controllers.dialog(
LineButton item = new LineButton();
item.setTrailingIcon(SVG.OPEN_IN_NEW);
item.setTitle(link.description().getText(I18n.getLocale().getCandidateLocales()));
item.setOnAction(event -> Controllers.dialog(
i18n("terracotta.from_local.guide", TerracottaMetadata.PACKAGE_NAME),
i18n("message.info"), MessageDialogPane.MessageType.INFO,
() -> FXUtils.openLink(url)
() -> FXUtils.openLink(link.link())
));
ComponentList.setNoPadding(container);
locals.getContent().add(container);
locals.getContent().add(item);
}
return locals;
}
@@ -597,103 +578,12 @@ public class TerracottaControllerPage extends StackPane {
FXUtils.copyText(code, i18n("terracotta.status.host_ok.code.copy.toast"));
}
private static final class LineButton extends RipplerContainer {
private final WeakListenerHolder holder = new WeakListenerHolder();
private static final double ICON_SIZE = 28;
private final ObjectProperty<Node> left = new SimpleObjectProperty<>(this, "left");
private final ObjectProperty<Node> right = new SimpleObjectProperty<>(this, "right");
private final StringProperty title = new SimpleStringProperty(this, "title", "");
private final StringProperty subTitle = new SimpleStringProperty(this, "subTitle", "");
public static LineButton of() {
return of(true);
}
public static LineButton of(boolean padding) {
HBox container = new HBox();
if (padding) {
container.setPadding(new Insets(10, 16, 10, 16));
}
container.setAlignment(Pos.CENTER_LEFT);
container.setCursor(Cursor.HAND);
container.setSpacing(16);
LineButton button = new LineButton(container);
VBox spacing = new VBox();
HBox.setHgrow(spacing, Priority.ALWAYS);
button.holder.add(FXUtils.observeWeak(() -> {
List<Node> nodes = new ArrayList<>(4);
Node left = button.left.get();
if (left != null) {
nodes.add(left);
}
{
// FIXME: It's sucked to have the following TwoLineListItem-liked logic whose subtitle is a TextFlow.
VBox middle = new VBox();
middle.getStyleClass().add("two-line-list-item");
middle.setMouseTransparent(true);
{
HBox firstLine = new HBox();
firstLine.getStyleClass().add("first-line");
{
Label lblTitle = new Label(button.title.get());
lblTitle.getStyleClass().add("title");
firstLine.getChildren().setAll(lblTitle);
}
HBox secondLine = new HBox();
secondLine.getStyleClass().add("second-line");
{
Text text = new Text(button.subTitle.get());
TextFlow lblSubtitle = new TextFlow(text);
lblSubtitle.getStyleClass().add("subtitle");
secondLine.getChildren().setAll(lblSubtitle);
}
middle.getChildren().setAll(firstLine, secondLine);
}
nodes.add(middle);
}
nodes.add(spacing);
Node right = button.right.get();
if (right != null) {
nodes.add(right);
}
container.getChildren().setAll(nodes);
}, button.title, button.subTitle, button.left, button.right));
ComponentList.setNoPadding(button);
return button;
}
private LineButton(Node container) {
super(container);
}
public void setTitle(String title) {
this.title.set(title);
}
public void setSubtitle(String subtitle) {
this.subTitle.set(subtitle);
}
public void setLeftImage(Image left) {
this.left.set(new ImageView(left));
}
public void setLeftIcon(SVG left) {
this.left.set(left.createIcon(28));
}
public void setRightIcon(SVG right) {
this.right.set(right.createIcon(28));
}
private static LineButton createLargeTitleLineButton() {
var lineButton = new LineButton();
lineButton.setLargeTitle(true);
return lineButton;
}
private static final class PlayerProfileUI extends VBox {
@@ -88,8 +88,8 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
public void add() {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("datapack.choose_datapack"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("datapack.extension"), "*.zip"));
chooser.setTitle(i18n("datapack.add.title"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.datapack"), "*.zip"));
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
if (res != null) {
@@ -183,7 +183,6 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
{
SpinnerPane center = new SpinnerPane();
ComponentList.setVgrow(center, Priority.ALWAYS);
center.getStyleClass().add("large-spinner-pane");
center.loadingProperty().bind(skinnable.loadingProperty());
listView.setCellFactory(x -> new DatapackInfoListCell(listView, getSkinnable().readOnly));
@@ -48,6 +48,7 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
@@ -244,6 +245,8 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
protected ModDownloadListPageSkin(DownloadListPage control) {
super(control);
listView.getStyleClass().add("no-horizontal-scrollbar");
BorderPane pane = new BorderPane();
GridPane searchPane = new GridPane();
@@ -521,7 +524,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
spinnerPane.setContent(listView);
Bindings.bindContent(listView.getItems(), getSkinnable().items);
listView.setSelectionModel(null);
listView.setSelectionModel(new NoneMultipleSelectionModel<>());
// 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);
@@ -27,9 +27,18 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.HMCLGameRepository;
@@ -46,7 +55,11 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.SimpleMultimap;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
@@ -54,7 +67,14 @@ import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -518,8 +538,10 @@ public class DownloadPage extends Control implements DecoratorPage {
private void loadDependencies(RemoteMod.Version version, DownloadPage selfPage, SpinnerPane spinnerPane, ComponentList dependenciesList) {
spinnerPane.setLoading(true);
Task.supplyAsync(() -> {
Task.composeAsync(() -> {
// TODO: Massive tasks may cause OOM.
EnumMap<RemoteMod.DependencyType, List<Node>> dependencies = new EnumMap<>(RemoteMod.DependencyType.class);
List<Task<?>> queue = new ArrayList<>(version.getDependencies().size());
for (RemoteMod.Dependency dependency : version.getDependencies()) {
if (dependency.getType() == RemoteMod.DependencyType.INCOMPATIBLE || dependency.getType() == RemoteMod.DependencyType.BROKEN) {
continue;
@@ -532,11 +554,22 @@ public class DownloadPage extends Control implements DecoratorPage {
list.add(title);
dependencies.put(dependency.getType(), list);
}
DependencyModItem dependencyModItem = new DependencyModItem(selfPage.page, dependency.load(), selfPage.version, selfPage.callback);
dependencies.get(dependency.getType()).add(dependencyModItem);
queue.add(Task.supplyAsync(Schedulers.io(), dependency::load)
.setSignificance(Task.TaskSignificance.MINOR)
.thenAcceptAsync(Schedulers.javafx(), dep -> {
if (dep == RemoteMod.BROKEN) {
return;
}
DependencyModItem dependencyModItem = new DependencyModItem(selfPage.page, dep, selfPage.version, selfPage.callback);
dependencies.get(dependency.getType()).add(dependencyModItem);
})
.setSignificance(Task.TaskSignificance.MINOR));
}
return dependencies.values().stream().flatMap(Collection::stream).collect(Collectors.toList());
return Task.allOf(queue).thenSupplyAsync(() ->
dependencies.values().stream().flatMap(Collection::stream).collect(Collectors.toList())
);
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
spinnerPane.setLoading(false);
if (exception == null) {
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.image.ImageView;
import org.jackhuang.hmcl.event.Event;
@@ -26,7 +27,6 @@ import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.util.Pair;
import java.util.function.Consumer;
@@ -39,14 +39,20 @@ public class GameAdvancedListItem extends AdvancedListItem {
@SuppressWarnings("unused")
private Consumer<Event> onVersionIconChangedListener;
@SuppressWarnings("SuspiciousNameCombination")
public GameAdvancedListItem() {
Pair<Node, ImageView> view = createImageView(null);
setLeftGraphic(view.getKey());
imageView = view.getValue();
this.imageView = new ImageView();
FXUtils.limitSize(imageView, LEFT_GRAPHIC_SIZE, LEFT_GRAPHIC_SIZE);
imageView.setPreserveRatio(true);
imageView.setImage(null);
Node imageViewWrapper = FXUtils.limitingSize(imageView, LEFT_GRAPHIC_SIZE, LEFT_GRAPHIC_SIZE);
imageView.setMouseTransparent(true);
AdvancedListItem.setAlignment(imageViewWrapper, Pos.CENTER);
setLeftGraphic(imageViewWrapper);
holder.add(FXUtils.onWeakChangeAndOperate(Profiles.selectedVersionProperty(), this::loadVersion));
setActionButtonVisible(false);
}
private void loadVersion(String version) {
@@ -115,7 +115,7 @@ public final class GameListCell extends ListCell<GameListItem> {
item.update();
});
btnUpgrade.getStyleClass().add("toggle-icon4");
btnUpgrade.setGraphic(FXUtils.limitingSize(SVG.UPDATE.createIcon(24), 24, 24));
btnUpgrade.setGraphic(SVG.UPDATE.createIcon());
FXUtils.installFastTooltip(btnUpgrade, i18n("version.update"));
right.getChildren().add(btnUpgrade);
@@ -127,7 +127,7 @@ public final class GameListCell extends ListCell<GameListItem> {
});
btnLaunch.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnLaunch, Pos.CENTER);
btnLaunch.setGraphic(FXUtils.limitingSize(SVG.ROCKET_LAUNCH.createIcon(24), 24, 24));
btnLaunch.setGraphic(SVG.ROCKET_LAUNCH.createIcon());
FXUtils.installFastTooltip(btnLaunch, i18n("version.launch.test"));
right.getChildren().add(btnLaunch);
@@ -143,7 +143,7 @@ public final class GameListCell extends ListCell<GameListItem> {
});
btnManage.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnManage, Pos.CENTER);
btnManage.setGraphic(FXUtils.limitingSize(SVG.MORE_VERT.createIcon(24), 24, 24));
btnManage.setGraphic(SVG.MORE_VERT.createIcon());
FXUtils.installFastTooltip(btnManage, i18n("settings.game.management"));
right.getChildren().add(btnManage);
}
@@ -69,8 +69,7 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
AdvancedListItem addProfileItem = new AdvancedListItem();
addProfileItem.getStyleClass().add("navigation-drawer-item");
addProfileItem.setTitle(i18n("profile.new"));
addProfileItem.setActionButtonVisible(false);
addProfileItem.setLeftGraphic(FXUtils.wrap(SVG.ADD_CIRCLE));
addProfileItem.setLeftIcon(SVG.ADD_CIRCLE);
addProfileItem.setOnAction(e -> Controllers.navigate(new ProfilePage(null)));
pane.setFitToWidth(true);
@@ -136,7 +136,7 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
public void installOffline() {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("install.installer.install_offline.extension"), "*.jar", "*.exe"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.modloader.installer"), "*.jar", "*.exe"));
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
if (file != null) doInstallOffline(file);
}
@@ -170,7 +170,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
public void add() {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("mods.choose_mod"));
chooser.setTitle(i18n("mods.add.title"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.mod"), "*.jar", "*.zip", "*.litemod"));
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
@@ -102,6 +102,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
ComponentList root = new ComponentList();
root.getStyleClass().add("no-padding");
listView = new JFXListView<>();
listView.getStyleClass().add("no-horizontal-scrollbar");
{
toolbarPane = new TransitionPane();
@@ -199,7 +200,6 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
{
SpinnerPane center = new SpinnerPane();
ComponentList.setVgrow(center, Priority.ALWAYS);
center.getStyleClass().add("large-spinner-pane");
center.loadingProperty().bind(skinnable.loadingProperty());
listView.setCellFactory(x -> new ModInfoListCell(listView));
@@ -581,15 +581,15 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
imageView.setImage(VersionIconType.COMMAND.getIcon());
restoreButton.getStyleClass().add("toggle-icon4");
restoreButton.setGraphic(FXUtils.limitingSize(SVG.RESTORE.createIcon(24), 24, 24));
restoreButton.setGraphic(SVG.RESTORE.createIcon());
FXUtils.installFastTooltip(restoreButton, i18n("mods.restore"));
revealButton.getStyleClass().add("toggle-icon4");
revealButton.setGraphic(FXUtils.limitingSize(SVG.FOLDER.createIcon(24), 24, 24));
revealButton.setGraphic(SVG.FOLDER.createIcon());
infoButton.getStyleClass().add("toggle-icon4");
infoButton.setGraphic(FXUtils.limitingSize(SVG.INFO.createIcon(24), 24, 24));
infoButton.setGraphic(SVG.INFO.createIcon());
container.getChildren().setAll(checkBox, imageView, content, restoreButton, revealButton, infoButton);
@@ -2,30 +2,27 @@ package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
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.SkinBase;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.resourcepack.ResourcepackFile;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.ListPageBase;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.MDListCell;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.ByteArrayInputStream;
@@ -38,7 +35,6 @@ import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
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;
@@ -115,8 +111,8 @@ public final class ResourcepackListPage extends ListPageBase<ResourcepackListPag
public void onAddFiles() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(i18n("resourcepack.add"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("resourcepack"), "*.zip"));
fileChooser.setTitle(i18n("resourcepack.add.title"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.resourcepack"), "*.zip"));
List<Path> files = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage()));
if (files != null && !files.isEmpty()) {
addFiles(files);
@@ -128,45 +124,24 @@ public final class ResourcepackListPage extends ListPageBase<ResourcepackListPag
Controllers.navigate(Controllers.getDownloadPage());
}
private static final class ResourcepackListPageSkin extends SkinBase<ResourcepackListPage> {
private final JFXListView<ResourcepackInfoObject> listView;
private static final class ResourcepackListPageSkin extends ToolbarListPageSkin<ResourcepackInfoObject, ResourcepackListPage> {
private ResourcepackListPageSkin(ResourcepackListPage control) {
public ResourcepackListPageSkin(ResourcepackListPage control) {
super(control);
}
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<>();
FXUtils.ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
HBox toolbar = new HBox();
toolbar.setAlignment(Pos.CENTER_LEFT);
toolbar.setPickOnBounds(false);
toolbar.getChildren().setAll(
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, control::refresh),
createToolbarButton2(i18n("resourcepack.add"), SVG.ADD, control::onAddFiles),
createToolbarButton2(i18n("resourcepack.download"), SVG.DOWNLOAD, control::onDownload)
@Override
protected List<Node> initializeToolbar(ResourcepackListPage skinnable) {
return List.of(
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
createToolbarButton2(i18n("resourcepack.add"), SVG.ADD, skinnable::onAddFiles),
createToolbarButton2(i18n("resourcepack.download"), SVG.DOWNLOAD, skinnable::onDownload)
);
root.getContent().add(toolbar);
}
SpinnerPane center = new SpinnerPane();
ComponentList.setVgrow(center, Priority.ALWAYS);
center.getStyleClass().add("large-spinner-pane");
center.loadingProperty().bind(control.loadingProperty());
listView.setCellFactory(x -> new ResourcepackListCell(listView, control));
Bindings.bindContent(listView.getItems(), control.getItems());
center.setContent(listView);
root.getContent().add(center);
pane.getChildren().setAll(root);
getChildren().setAll(pane);
@Override
protected ListCell<ResourcepackInfoObject> createListCell(JFXListView<ResourcepackInfoObject> listView) {
return new ResourcepackListCell(listView, getSkinnable());
}
}
@@ -22,7 +22,6 @@ import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXListView;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
@@ -32,7 +31,6 @@ import javafx.scene.image.WritableImage;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.SVGPath;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.schematic.LitematicFile;
import org.jackhuang.hmcl.setting.Profile;
@@ -146,9 +144,9 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
public void onAddFiles() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(i18n("schematics.add"));
fileChooser.setTitle(i18n("schematics.add.title"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(
i18n("schematics"), "*.litematic"));
i18n("extension.schematic"), "*.litematic"));
List<Path> files = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage()));
if (files != null && !files.isEmpty()) {
addFiles(files);
@@ -551,8 +549,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
private final HBox right;
private final ImageView iconImageView;
private final SVGPath iconSVG;
private final StackPane iconSVGWrapper;
private final SVGContainer iconSVGView;
private final Tooltip tooltip = new Tooltip();
@@ -568,15 +565,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
this.iconImageView = new ImageView();
FXUtils.limitSize(iconImageView, 32, 32);
this.iconSVG = new SVGPath();
iconSVG.getStyleClass().add("svg");
iconSVG.setScaleX(32.0 / SVG.DEFAULT_SIZE);
iconSVG.setScaleY(32.0 / SVG.DEFAULT_SIZE);
this.iconSVGWrapper = new StackPane(new Group(iconSVG));
iconSVGWrapper.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(iconSVGWrapper, 32);
FXUtils.setLimitHeight(iconSVGWrapper, 32);
this.iconSVGView = new SVGContainer(32);
BorderPane.setAlignment(left, Pos.CENTER);
root.setLeft(left);
@@ -638,8 +627,8 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
iconImageView.setImage(fileItem.getImage());
left.getChildren().setAll(iconImageView);
} else {
iconSVG.setContent(item.getIcon().getPath());
left.getChildren().setAll(iconSVGWrapper);
iconSVGView.setIcon(item.getIcon());
left.getChildren().setAll(iconSVGView);
}
center.setTitle(item.getName());
@@ -175,27 +175,19 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
componentList = new ComponentList();
if (!globalSetting) {
BorderPane copyGlobalPane = new BorderPane();
{
Label label = new Label(i18n("settings.game.copy_global"));
copyGlobalPane.setLeft(label);
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
var copyGlobalButton = LineButton.createNavigationButton();
copyGlobalButton.setTitle(i18n("settings.game.copy_global"));
copyGlobalButton.setOnAction(event ->
Controllers.confirm(i18n("settings.game.copy_global.copy_all.confirm"), null, () -> {
Set<String> ignored = new HashSet<>(Arrays.asList(
"usesGlobal",
"versionIcon"
));
JFXButton copyAll = FXUtils.newBorderButton(i18n("settings.game.copy_global.copy_all"));
copyAll.disableProperty().bind(modpack);
copyGlobalPane.setRight(copyAll);
copyAll.setOnAction(e -> Controllers.confirm(i18n("settings.game.copy_global.copy_all.confirm"), null, () -> {
Set<String> ignored = new HashSet<>(Arrays.asList(
"usesGlobal",
"versionIcon"
));
PropertyUtils.copyProperties(profile.getGlobal(), lastVersionSetting, name -> !ignored.contains(name));
}, null));
PropertyUtils.copyProperties(profile.getGlobal(), lastVersionSetting, name -> !ignored.contains(name));
}, null));
BorderPane.setAlignment(copyAll, Pos.CENTER_RIGHT);
}
componentList.getContent().add(copyGlobalPane);
componentList.getContent().add(copyGlobalButton);
}
javaItem = new MultiFileItem<>();
@@ -424,7 +416,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
serverPane.addRow(0, new Label(i18n("settings.advanced.server_ip")), txtServerIP);
}
LineNavigationButton showAdvancedSettingPane = new LineNavigationButton();
var showAdvancedSettingPane = LineButton.createNavigationButton();
showAdvancedSettingPane.setTitle(i18n("settings.advanced"));
showAdvancedSettingPane.setOnAction(event -> {
if (lastVersionSetting != null) {
@@ -103,7 +103,7 @@ public final class Versions {
i18n("install.failed.downloading.detail", file.getFile().getUrl()) + "\n" + StringUtils.getStackTrace(e),
i18n("download.failed.no_code"), MessageDialogPane.MessageType.ERROR);
}
}).executor(true),
}),
i18n("message.downloading"),
TaskCancellationAction.NORMAL
);
@@ -20,17 +20,16 @@ package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.FileItem;
import org.jackhuang.hmcl.ui.construct.LineFileChooserButton;
import org.jackhuang.hmcl.ui.construct.LinePane;
import org.jackhuang.hmcl.ui.construct.LineTextPane;
import java.nio.file.Files;
import java.nio.file.Paths;
@@ -42,47 +41,40 @@ public class WorldExportPageSkin extends SkinBase<WorldExportPage> {
public WorldExportPageSkin(WorldExportPage skinnable) {
super(skinnable);
Insets insets = new Insets(0, 0, 12, 0);
VBox container = new VBox();
container.setSpacing(16);
container.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(container, 500);
{
HBox labelContainer = new HBox();
labelContainer.setPadding(new Insets(0, 0, 0, 5));
Label label = new Label(i18n("world.export"));
labelContainer.getChildren().setAll(label);
container.getChildren().add(labelContainer);
}
ComponentList list = new ComponentList();
FileItem fileItem = new FileItem();
fileItem.setName(i18n("world.export.location"));
fileItem.pathProperty().bindBidirectional(skinnable.pathProperty());
list.getContent().add(fileItem);
var chooseFileButton = new LineFileChooserButton();
chooseFileButton.setTitle(i18n("world.export.location"));
chooseFileButton.setType(LineFileChooserButton.Type.SAVE_FILE);
chooseFileButton.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("world"), "*.zip"));
chooseFileButton.locationProperty().bindBidirectional(skinnable.pathProperty());
var worldNamePane = new LinePane();
worldNamePane.setTitle(i18n("world.name"));
JFXTextField txtWorldName = new JFXTextField();
txtWorldName.textProperty().bindBidirectional(skinnable.worldNameProperty());
txtWorldName.setLabelFloat(true);
txtWorldName.setPromptText(i18n("world.name"));
StackPane.setMargin(txtWorldName, insets);
list.getContent().add(txtWorldName);
txtWorldName.setPrefWidth(300);
worldNamePane.setRight(txtWorldName);
Label lblGameVersionTitle = new Label(i18n("world.game_version"));
Label lblGameVersion = new Label();
lblGameVersion.textProperty().bind(skinnable.gameVersionProperty());
BorderPane gameVersionPane = new BorderPane();
gameVersionPane.setPadding(new Insets(4, 0, 4, 0));
gameVersionPane.setLeft(lblGameVersionTitle);
gameVersionPane.setRight(lblGameVersion);
list.getContent().add(gameVersionPane);
LineTextPane gameVersionPane = new LineTextPane();
gameVersionPane.setTitle(i18n("world.game_version"));
gameVersionPane.textProperty().bind(skinnable.gameVersionProperty());
container.getChildren().add(list);
list.getContent().setAll(chooseFileButton, worldNamePane, gameVersionPane);
container.getChildren().setAll(
ComponentList.createComponentListTitle(i18n("world.export")),
list
);
JFXButton btnExport = FXUtils.newRaisedButton(i18n("button.export"));
btnExport.disableProperty().bind(Bindings.createBooleanBinding(() -> txtWorldName.getText().isEmpty() || Files.exists(Paths.get(fileItem.getPath())),
txtWorldName.textProperty().isEmpty(), fileItem.pathProperty()));
btnExport.disableProperty().bind(Bindings.createBooleanBinding(() -> txtWorldName.getText().isEmpty() || Files.exists(Paths.get(chooseFileButton.getLocation())),
txtWorldName.textProperty().isEmpty(), chooseFileButton.locationProperty()));
btnExport.setOnAction(e -> skinnable.export());
HBox bottom = new HBox();
bottom.setAlignment(Pos.CENTER_RIGHT);
@@ -31,7 +31,6 @@ import javafx.scene.control.ScrollPane;
import javafx.scene.effect.BoxBlur;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
@@ -202,7 +201,6 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
HBox right = new HBox(8);
{
right.setAlignment(Pos.CENTER_RIGHT);
BorderPane.setAlignment(right, Pos.CENTER_RIGHT);
right.getChildren().setAll(visibilityButton, seedLabel);
seedPane.setRight(right);
}
@@ -410,9 +408,9 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
}
}
private void setRightTextField(BorderPane borderPane, int perfWidth, Tag tag) {
private void setRightTextField(LinePane linePane, int perfWidth, Tag tag) {
JFXTextField textField = new JFXTextField();
setRightTextField(borderPane, textField, perfWidth);
setRightTextField(linePane, textField, perfWidth);
if (tag instanceof IntTag intTag) {
bindTagAndTextField(intTag, textField);
} else if (tag instanceof FloatTag floatTag) {
@@ -422,11 +420,10 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
}
}
private void setRightTextField(BorderPane borderPane, JFXTextField textField, int perfWidth) {
private void setRightTextField(LinePane linePane, JFXTextField textField, int perfWidth) {
textField.setDisable(isReadOnly);
textField.setPrefWidth(perfWidth);
BorderPane.setAlignment(textField, Pos.CENTER_RIGHT);
borderPane.setRight(textField);
linePane.setRight(textField);
}
private void bindTagAndToggleButton(Tag tag, LineToggleButton toggleButton) {
@@ -22,6 +22,7 @@ import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXPopup;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -53,6 +54,7 @@ import java.nio.file.Path;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
import static org.jackhuang.hmcl.util.StringUtils.parseColorEscapes;
@@ -67,6 +69,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
private List<World> worlds;
private Profile profile;
private String instanceId;
private final BooleanProperty supportQuickPlay = new SimpleBooleanProperty(this, "supportQuickPlay", false);
private int refreshCount = 0;
@@ -121,6 +124,9 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
return;
}
Optional<String> gameVersion = profile.getRepository().getGameVersion(instanceId);
supportQuickPlay.set(World.supportQuickPlay(GameVersionNumber.asGameVersion(gameVersion)));
worlds = result;
updateWorldList();
@@ -133,8 +139,8 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
public void add() {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("world.import.choose"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("world.extension"), "*.zip"));
chooser.setTitle(i18n("world.add.title"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.world"), "*.zip"));
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
if (res == null || res.isEmpty()) return;
@@ -158,16 +164,16 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
refresh();
}, e -> {
if (e instanceof FileAlreadyExistsException)
handler.reject(i18n("world.import.failed", i18n("world.import.already_exists")));
handler.reject(i18n("world.add.failed", i18n("world.add.already_exists")));
else if (e instanceof IOException && e.getCause() instanceof InvalidPathException)
handler.reject(i18n("world.import.failed", i18n("install.new_game.malformed")));
handler.reject(i18n("world.add.failed", i18n("install.new_game.malformed")));
else
handler.reject(i18n("world.import.failed", e.getClass().getName() + ": " + e.getLocalizedMessage()));
handler.reject(i18n("world.add.failed", e.getClass().getName() + ": " + e.getLocalizedMessage()));
}).start();
}, world.getWorldName(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid));
}, e -> {
LOG.warning("Unable to parse world file " + zipFile, e);
Controllers.dialog(i18n("world.import.invalid"));
Controllers.dialog(i18n("world.add.invalid"));
}).start();
}
@@ -203,6 +209,10 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
return showAll;
}
public ReadOnlyBooleanProperty supportQuickPlayProperty() {
return supportQuickPlay;
}
private final class WorldListPageSkin extends ToolbarListPageSkin<World, WorldListPage> {
WorldListPageSkin() {
@@ -269,6 +279,8 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
right.setAlignment(Pos.CENTER_RIGHT);
btnLaunch = new JFXButton();
btnLaunch.visibleProperty().bind(page.supportQuickPlayProperty());
btnLaunch.managedProperty().bind(btnLaunch.visibleProperty());
right.getChildren().add(btnLaunch);
btnLaunch.getStyleClass().add("toggle-icon4");
btnLaunch.setGraphic(SVG.ROCKET_LAUNCH.createIcon());
@@ -286,7 +298,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
btnMore.setOnAction(event -> {
World world = getItem();
if (world != null)
showPopupMenu(world, JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight());
showPopupMenu(world, page.supportQuickPlayProperty().get(), JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight());
});
}
@@ -302,7 +314,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
if (event.getButton() == MouseButton.PRIMARY)
page.showManagePage(world);
else if (event.getButton() == MouseButton.SECONDARY)
showPopupMenu(world, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
showPopupMenu(world, page.supportQuickPlayProperty().get(), JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
});
}
@@ -340,19 +352,19 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
// Popup Menu
public void showPopupMenu(World world, JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) {
public void showPopupMenu(World world, boolean supportQuickPlay, JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) {
boolean worldLocked = world.isLocked();
PopupMenu popupMenu = new PopupMenu();
JFXPopup popup = new JFXPopup(popupMenu);
if (world.supportQuickPlay()) {
if (supportQuickPlay) {
IconedMenuItem launchItem = new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch_and_enter_world"), () -> page.launch(world), popup);
launchItem.setDisable(worldLocked);
popupMenu.getContent().add(launchItem);
popupMenu.getContent().addAll(
launchItem,
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> page.generateLaunchScript(world), popup),
new MenuSeparator()
);
@@ -35,11 +35,13 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.ChunkBaseApp;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.util.Optional;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -52,7 +54,8 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
private final World world;
private final Path backupsDir;
private final Profile profile;
private final String versionId;
private final String instanceId;
private final boolean supportQuickPlay;
private FileChannel sessionLockChannel;
private final ObjectProperty<State> state;
@@ -66,11 +69,11 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
private final TabHeader.Tab<WorldBackupsPage> worldBackupsTab = new TabHeader.Tab<>("worldBackupsPage");
private final TabHeader.Tab<DatapackListPage> datapackTab = new TabHeader.Tab<>("datapackListPage");
public WorldManagePage(World world, Profile profile, String versionId) {
public WorldManagePage(World world, Profile profile, String instanceId) {
this.world = world;
this.backupsDir = profile.getRepository().getBackupsDirectory(versionId);
this.backupsDir = profile.getRepository().getBackupsDirectory(instanceId);
this.profile = profile;
this.versionId = versionId;
this.instanceId = instanceId;
updateSessionLockChannel();
@@ -87,6 +90,9 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
this.state = new SimpleObjectProperty<>(new State(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName())), null, true, true, true));
Optional<String> gameVersion = profile.getRepository().getGameVersion(instanceId);
supportQuickPlay = World.supportQuickPlay(GameVersionNumber.asGameVersion(gameVersion));
this.addEventHandler(Navigator.NavigationEvent.EXITED, this::onExited);
this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
}
@@ -144,11 +150,11 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
public void launch() {
fireEvent(new PageCloseEvent());
Versions.launchAndEnterWorld(profile, versionId, world.getFileName());
Versions.launchAndEnterWorld(profile, instanceId, world.getFileName());
}
public void generateLaunchScript() {
Versions.generateLaunchScriptForQuickEnterWorld(profile, versionId, world.getFileName());
Versions.generateLaunchScriptForQuickEnterWorld(profile, instanceId, world.getFileName());
}
@Override
@@ -225,7 +231,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
AdvancedListBox toolbar = new AdvancedListBox();
BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0));
{
if (getSkinnable().world.supportQuickPlay()) {
if (getSkinnable().supportQuickPlay) {
toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, () -> getSkinnable().launch(), advancedListItem -> advancedListItem.disableProperty().bind(getSkinnable().readOnlyProperty()));
}
@@ -257,7 +263,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
PopupMenu managePopupMenu = new PopupMenu();
JFXPopup managePopup = new JFXPopup(managePopupMenu);
if (getSkinnable().world.supportQuickPlay()) {
if (getSkinnable().supportQuickPlay) {
managePopupMenu.getContent().addAll(
new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch"), () -> getSkinnable().launch(), managePopup),
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> getSkinnable().generateLaunchScript(), managePopup),
@@ -72,7 +72,7 @@ public final class WorldManageUIUtils {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(i18n("world.export.title"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("world"), "*.zip"));
fileChooser.setInitialFileName(world.getWorldName());
fileChooser.setInitialFileName(world.getWorldName() + ".zip");
Path file = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage()));
if (file == null) {
return;
@@ -0,0 +1,48 @@
/*
* 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.util;
import io.nayuki.qrcodegen.QrCode;
/// @author Glavo
public final class QrCodeUtils {
public static String toSVGPath(QrCode qr) {
return toSVGPath(qr, 0);
}
public static String toSVGPath(QrCode qr, int border) {
int actualSize = qr.size + border * 2;
var builder = new StringBuilder(qr.size * qr.size * 12);
builder.append('M').append(actualSize).append(' ').append(actualSize).append("ZM0 0Z");
for (int y = 0; y < qr.size; y++) {
for (int x = 0; x < qr.size; x++) {
if (qr.getModule(x, y)) {
if (x != 0 || y != 0)
builder.append(' ');
builder.append("M").append(x + border).append(',').append(y + border).append("h1v1h-1z");
}
}
}
return builder.toString();
}
private QrCodeUtils() {
}
}
@@ -76,7 +76,7 @@
},
{
"title": "EasyTier",
"subtitle": "Copyright 2024-present Easytier Programme within The Commons Conservancy",
"subtitle": "Copyright 2024-present Easytier Programme within The Commons Conservancy.\nLicensed under the LGPL 3.0 License.",
"externalLink": "https://github.com/EasyTier/EasyTier"
},
{
+105 -32
View File
@@ -324,20 +324,25 @@
-fx-fill: -monet-on-surface-variant;
}
.two-line-list-item > .first-line > .tag {
.two-line-list-item > .first-line .scroll-bar {
-fx-opacity: 0;
-fx-max-height: 0;
-fx-min-height: 0;
-fx-pref-height: 0;
}
.two-line-list-item > .first-line .tag {
-fx-text-fill: -monet-on-secondary-container;
-fx-background-color: -monet-secondary-container;
-fx-background-radius: 2;
-fx-padding: 2;
-fx-font-weight: normal;
-fx-font-size: 12px;
}
.two-line-list-item > .first-line > .tag-warning {
.two-line-list-item > .first-line .tag:warning {
-fx-text-fill: -monet-on-error-container;
-fx-background-color: -fixed-warning-tag-background;
-fx-padding: 2;
-fx-font-weight: normal;
-fx-font-size: 12px;
}
.two-line-item-second-large {
@@ -567,6 +572,41 @@
-fx-text-fill: -monet-on-surface;
}
/*******************************************************************************
* *
* Microsoft Login Dialog *
* *
******************************************************************************/
.microsoft-login-dialog:bodyonly {
-fx-padding: 0px 0px 0px 0px;
}
.method-title {
-fx-text-fill: -monet-on-surface;
-fx-font-weight: bold;
}
.method-desc {
-fx-text-fill: -monet-outline;
-fx-font-size: 0.9em;
-fx-line-spacing: 2;
-fx-wrap-text: true;
-fx-text-alignment: center;
}
.code-box {
-fx-background-color: -monet-surface-variant;
-fx-background-radius: 6;
-fx-padding: 0 10 0 10;
-fx-alignment: center;
}
.code-box .code-label {
-fx-font-size: 22px;
-fx-font-weight: bold;
-fx-text-fill: -monet-primary;
}
/*******************************************************************************
* *
@@ -587,8 +627,16 @@
-fx-pref-width: 150.0px;
}
/*******************************************************************************
* *
* JFX Snack Bar *
* *
******************************************************************************/
.jfx-snackbar-content {
-fx-background-color: -monet-inverse-surface;
-fx-padding: 5;
-fx-spacing: 5;
}
.jfx-snackbar-toast {
@@ -1076,6 +1124,14 @@
-fx-border-width: 0;
}
.options-sublist-wrapper .expand-icon .svg {
-fx-fill: -monet-on-surface-variant;
}
.options-sublist-wrapper .expand-icon .svg:disabled {
-fx-opacity: 0.4;
}
/*******************************************************************************
* *
* JFX Toggle Button *
@@ -1103,15 +1159,16 @@
.jfx-toggle-button Line {
-fx-stroke: -jfx-untoggle-line-color;
}
.jfx-toggle-button:selected Line{
.jfx-toggle-button:selected Line {
-fx-stroke: -jfx-toggle-line-color;
}
.jfx-toggle-button Circle{
.jfx-toggle-button Circle {
-fx-fill: -jfx-untoggle-color;
}
.jfx-toggle-button:selected Circle{
.jfx-toggle-button:selected Circle {
-fx-fill: -jfx-toggle-color;
}
@@ -1268,6 +1325,7 @@
.log-toggle {
-fx-border: 1px;
-fx-background-insets: 0;
-fx-border-color: -fixed-log-toggle-unselected;
-fx-text-fill: -fixed-log-toggle-unselected;
}
@@ -1348,6 +1406,10 @@
-fx-background-color: -fixed-log-info;
}
.log-window-list-cell {
-fx-background-color: -fixed-log-info;
}
.log-window-list-cell:debug {
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-debug;
@@ -1403,14 +1465,6 @@
-fx-stroke-width: 3.0;
}
.large-spinner-pane .jfx-spinner {
-jfx-radius: 20;
}
.large-spinner-pane .jfx-spinner {
-fx-stroke-width: 5.0;
}
.second-spinner {
-jfx-radius: 30;
}
@@ -1860,32 +1914,51 @@
/*******************************************************************************
* *
* Line Button *
* Line Component *
* *
******************************************************************************/
.line-select-button .svg {
-fx-opacity: 1;
.line-component .line-component-container {
-fx-padding: 10 16 10 16;
}
.line-select-button .svg:disabled {
.line-component.no-padding .line-component-container {
-fx-padding: 0;
}
.line-component:large-title .title-label {
-fx-font-size: 15px;
}
.line-button .svg:disabled {
-fx-opacity: 0.4;
}
.line-select-button .menu-container .title {
-fx-font-size: 13px;
-fx-text-fill: -monet-on-surface;
}
.line-select-button .menu-container:selected .title {
-fx-text-fill: -monet-primary;
}
.line-select-button .menu-container .subtitle {
.line-button .trailing-label {
-fx-text-fill: -monet-on-surface-variant;
}
.line-select-button .menu-container:selected .subtitle {
.line-button .trailing-icon .svg {
-fx-fill: -monet-on-surface-variant;
-fx-opacity: 1;
}
.line-button .trailing-icon .svg:disabled {
-fx-opacity: 0.4;
}
.line-select-button .menu-container .title-label {
-fx-font-size: 13px;
}
.line-select-button .menu-container:selected .title-label {
-fx-text-fill: -monet-primary;
}
.line-select-button .menu-container:selected .subtitle-label {
-fx-text-fill: -monet-primary;
}
.line-toggle-button .jfx-toggle-button {
-fx-padding: 0;
}
@@ -118,11 +118,11 @@ account.methods.microsoft.error.wrong_verify_method=Failed to log in. Please try
account.methods.microsoft.logging_in=Logging in...
account.methods.microsoft.makegameidsettings=Create Profile / Edit Profile Name
account.methods.microsoft.hint=Click the "Log in" button to start adding your Microsoft account.
account.methods.microsoft.manual=Please enter the code shown above on the pop-up webpage to complete the login.\n\
\n\
If the website fails to load, please open %s manually in your browser.\n\
\n\
<b>If your internet connection is bad, it may cause web pages to load slowly or fail to load altogether. You may try again later or switch to a different internet connection.</b>
account.methods.microsoft.methods.device=Log In with QR Code
account.methods.microsoft.methods.device.hint=Scan QR code or visit <a href="%s">%s</a> to complete login, enter <b>%s</b> in the opened page.
account.methods.microsoft.methods.browser=Log In via Browser
account.methods.microsoft.methods.browser.hint=Click the "Log in" button or <a href="%s">copy the link</a> and paste it into the browser to log in.
account.methods.microsoft.manual=<b>If your internet connection is bad, it may cause web pages to load slowly or fail to load altogether.\nYou may try again later or switch to a different internet connection.</b>
account.methods.microsoft.profile=Account Profile
account.methods.microsoft.purchase=Buy Minecraft
account.methods.microsoft.snapshot=You are using an unofficial build of HMCL. Please download the <a href="https://hmcl.huangyuhui.net/download">official build</a> to log in.
@@ -141,7 +141,6 @@ account.methods.offline.uuid.hint=UUID is a unique identifier for Minecraft play
\n\
This option is for advanced users only. We do not recommend changing this option unless you know what you are doing.
account.methods.offline.uuid.malformed=Invalid format.
account.methods.forgot_password=Forgot Password
account.methods.ban_query=Ban Query
account.missing=No Accounts
account.missing.add=Click here to add one.
@@ -368,7 +367,6 @@ download.provider.balanced.desc=Balanced, but may not be the latest
download.provider.mirror=From Mirror
download.provider.mirror.desc=Fast, but may not be the latest
download.java=Downloading Java
download.java.override=This Java version already exists. Do you want to uninstall and reinstall it?
download.java.process=Java Download Process
download.javafx=Downloading dependencies for the launcher...
download.javafx.notes=We are currently downloading dependencies for HMCL from the Internet.\n\
@@ -390,12 +388,18 @@ exception.ssl_handshake=Failed to establish SSL connection because the SSL certi
exception.dns.pollution=Failed to establish an SSL connection. DNS resolution may be incorrect. Please try changing your DNS server or using a proxy service.
extension.bat=Windows Batch File
extension.mod=Mod File
extension.png=Image File
extension.ps1=Windows PowerShell Script
extension.sh=Shell Script
extension.command=macOS Shell Script
extension.datapack=Datapack Archive
extension.mod=Mod File
extension.modloader.installer=Mod Loader Installer
extension.resourcepack=Resource Pack Archive
extension.schematic=Schematic File
extension.world=World Archive
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher cannot create the HMCL directory (%s). Please move HMCL to another location and reopen it.
fatal.javafx.incompatible=Missing JavaFX environment.\n\
Hello Minecraft! Launcher cannot automatically install JavaFX on Java <11.\n\
@@ -459,7 +463,7 @@ folder.logs=Logs
folder.mod=Mods
folder.resourcepacks=Resource Packs
folder.shaderpacks=Shader Packs
folder.saves=Saves
folder.saves=Worlds
folder.schematics=Schematics
folder.screenshots=Screenshots
folder.world=World Directory
@@ -472,7 +476,7 @@ game.crash.reason.analyzing=Analyzing...
game.crash.reason.multiple=Several reasons detected:\n\n
game.crash.reason.block=The game crashed because of a block in the world.\n\
\n\
You can try removing this block using MCEdit or deleting the mod that added it.\n\
You can try removing this block using the <a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">map editors</a> or deleting the mod that added it.\n\
\n\
Block Type: %1$s\n\
Location: %2$s
@@ -488,7 +492,7 @@ game.crash.reason.duplicated_mod=The game cannot continue to run because of dupl
Each mod can only be installed once. Please delete the duplicate mod and try again.
game.crash.reason.entity=The game crashed because of an entity in the world.\n\
\n\
You can try removing the entity using MCEdit or deleting the mod that added it.\n\
You can try removing the entity using the <a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">map editors</a> or deleting the mod that added it.\n\
\n\
Entity Type: %1$s\n\
Location: %2$s
@@ -733,10 +737,9 @@ install.installer.game=Minecraft
install.installer.incompatible=Incompatible with %s
install.installer.install=Installing %s
install.installer.install_offline=Install/Update from Local File
install.installer.install_offline.extension=(Neo)Forge/Cleanroom/OptiFine installer
install.installer.install_offline.tooltip=We support using the local (Neo)Forge, Cleanroom, and OptiFine installer.
install.installer.install_online=Online Install
install.installer.install_online.tooltip=We currently support Forge, NeoForge, Cleanroom, OptiFine, Fabric, Quilt, and LiteLoader.
install.installer.install_online.tooltip=We currently support Forge, NeoForge, Cleanroom, OptiFine, Fabric, Legacy Fabric, Quilt, and LiteLoader.
install.installer.liteloader=LiteLoader
install.installer.not_installed=Not installed
install.installer.optifine=OptiFine
@@ -756,17 +759,18 @@ install.new_game.malformed=Invalid name.
install.select=Choose operation
install.success=Successfully installed.
java.add=Add Java
java.add=Add
java.add.failed=This Java is invalid or incompatible with the current platform.
java.disable=Disable Java
java.disable.confirm=Are you sure you want to disable this Java?
java.disabled.management=Disabled Java
java.disabled.management.remove=Remove this Java from the list
java.disabled.management.restore=Re-enable this Java
java.download=Download Java
java.download=Download
java.download.banshanjdk-8=Download Banshan JDK 8
java.download.load_list.failed=Failed to load version list
java.download.more=More Java distributions
java.download.title=Download Java
java.download.prompt=Please choose the Java version you want to download:
java.download.distribution=Distribution
java.download.version=Version
@@ -1063,9 +1067,10 @@ modrinth.category.256x=256x
modrinth.category.512x+=512x+
mods=Mods
mods.add=Add Mod
mods.add=Add
mods.add.failed=Failed to add mod %s.
mods.add.success=%s was successfully added.
mods.add.title=Choose mod file you want to add
mods.broken_dependency.title=Broken dependency
mods.broken_dependency.desc=This dependency existed before, but it does not exist anymore. Try using another download source.
mods.category=Category
@@ -1082,7 +1087,6 @@ mods.check_updates.failed_download=Failed to download some files.
mods.check_updates.file=File
mods.check_updates.source=Source
mods.check_updates.target_version=Target Version
mods.choose_mod=Choose mod
mods.curseforge=CurseForge
mods.dependency.embedded=Built-in Dependencies (Already packaged in the mod file by the author. No need to download separately)
mods.dependency.optional=Optional Dependencies (If missing, the game will run normally, but the mod features may be missing)
@@ -1104,7 +1108,7 @@ mods.mcmod.page=MCMod Page
mods.mcmod.search=Search in MCMod
mods.modrinth=Modrinth
mods.name=Name
mods.not_modded=You must install a modloader (Forge, NeoForge, Fabric, Quilt, or LiteLoader) first to manage your mods!
mods.not_modded=You must install a modloader (Forge, NeoForge, Fabric, Legacy Fabric, Quilt, or LiteLoader) first to manage your mods!
mods.restore=Restore
mods.url=Official Page
mods.update_modpack_mod.warning=Updating mods in a modpack can lead to irreparable results, possibly corrupting the modpack so that it cannot launch. Are you sure you want to update?
@@ -1127,9 +1131,8 @@ nbt.save.failed=Failed to save file
nbt.title=View File - %s
datapack=Datapacks
datapack.add=Install Datapack
datapack.choose_datapack=Choose datapack to import
datapack.extension=Datapack
datapack.add=Add
datapack.add.title=Choose datapack archive you want to add
datapack.reload.toast=Minecraft is running, please use the /reload command to reload the data pack
datapack.title=World [%s] - Datapacks
@@ -1138,9 +1141,13 @@ web.open_in_browser=Do you want to open this address in a browser:\n%s
web.view_in_browser=View all in browser
world=Worlds
world.add=Add World
world.add=Add
world.add.already_exists=This world already exists.
world.add.failed=Failed to add this world: %s
world.add.invalid=Failed to parse the world.
world.add.title=Choose world archive you want to add
world.backup=World Backup
world.backup.create.new_one=Create New Backup
world.backup.create.new_one=New Backup
world.backup.create.failed=Failed to create backup.\n%s
world.backup.create.success=Successfully created a new backup: %s
world.backup.delete=Delete this backup
@@ -1161,13 +1168,12 @@ world.datapack=Datapacks
world.datetime=Last played on %s
world.delete=Delete the World
world.delete.failed=Failed to delete world.\n%s
world.download=Download World
world.download=Download
world.download.title=Download World - %1s
world.export=Export the World
world.export.title=Choose the directory for this exported world
world.export.location=Save As
world.export.wizard=Export World "%s"
world.extension=World Archive
world.game_version=Game Version
world.icon=World Icon
world.icon.change=Change world icon
@@ -1178,10 +1184,6 @@ world.icon.change.fail.not_64x64.text=The image resolution is %d×%d instead of
world.icon.change.succeed.toast=Successfully updated the world icon.
world.icon.change.tip=A 64×64 PNG image is required. Images with an incorrect resolution cannot be parsed by Minecraft.
world.icon.choose.title=Select world icon
world.import.already_exists=This world already exists.
world.import.choose=Choose world archive you want to import
world.import.failed=Failed to import this world: %s
world.import.invalid=Failed to parse the world.
world.info=World Information
world.info.basic=Basic Information
world.info.allow_cheats=Allow Commands/Cheats
@@ -1250,6 +1252,7 @@ repositories.chooser.title=Choose download source for JavaFX
resourcepack=Resource Packs
resourcepack.add=Add
resourcepack.add.title=Choose resource pack archive you want to add
resourcepack.manage=Resource Packs
resourcepack.download=Download
resourcepack.add.failed=Failed to add resource pack
@@ -1259,10 +1262,11 @@ resourcepack.download.title=Download Resource Pack - %1s
reveal.in_file_manager=Reveal in File Manager
schematics=Schematics
schematics.add=Add Schematic Files
schematics.add=Add
schematics.add.failed=Failed to add schematic files
schematics.add.title=Choose schematic file you want to import
schematics.back_to=Back to "%s"
schematics.create_directory=Create Directory
schematics.create_directory=New Directory
schematics.create_directory.prompt=Please enter the new directory name
schematics.create_directory.failed=Failed to create directory
schematics.create_directory.failed.already_exists=Directory already exists
@@ -1398,9 +1402,9 @@ settings.game.java_directory.template=%1$s (%2$s)
settings.game.management=Manage
settings.game.working_directory=Working Directory
settings.game.working_directory.choose=Choose the working directory
settings.game.working_directory.hint=Enable the "Isolated" option in "Working Directory" to allow the current instance to store its settings, saves, and mods in a separate directory.\n\
settings.game.working_directory.hint=Enable the "Isolated" option in "Working Directory" to allow the current instance to store its settings, worlds, and mods in a separate directory.\n\
\n\
It is recommended to enable this option to avoid mod conflicts, but you will need to move your saves manually.
It is recommended to enable this option to avoid mod conflicts, but you will need to move your worlds manually.
settings.icon=Icon
@@ -1627,7 +1631,7 @@ version.manage=All Instances
version.manage.clean=Delete Log Files
version.manage.clean.tooltip=Delete the files in "logs" and "crash-reports" directories.
version.manage.duplicate=Duplicate Instance
version.manage.duplicate.duplicate_save=Duplicate Saves
version.manage.duplicate.duplicate_save=Duplicate Worlds
version.manage.duplicate.prompt=Enter New Instance Name
version.manage.duplicate.confirm=The duplicated instance will have a copy of all files in the instance directory (".minecraft/versions/<instance name>"), with an isolated working directory and settings.
version.manage.manage=Edit Instance
@@ -1635,7 +1639,7 @@ version.manage.manage.title=Edit Instance - %1s
version.manage.redownload_assets_index=Update Game Assets
version.manage.remove=Delete Instance
version.manage.remove.confirm.trash=Are you sure you want to remove the instance "%1$s"? You can still find its files in your recycle bin by the name of "%2$s".
version.manage.remove.confirm.independent=Since this instance is stored in an isolated directory, deleting it will also delete its saves and other data. Do you still want to delete the instance "%s"?
version.manage.remove.confirm.independent=Since this instance is stored in an isolated directory, deleting it will also delete its worlds and other data. Do you still want to delete the instance "%s"?
version.manage.remove.failed=Failed to delete the instance. Some files might be in use.
version.manage.remove_assets=Delete All Assets
version.manage.remove_libraries=Delete All Libraries
@@ -120,7 +120,6 @@ account.methods.offline.uuid.hint=UUID هو معرف فريد للاعبي Minec
\n\
هذا الخيار للمستخدمين المتقدمين فقط. لا نوصي بتغيير هذا الخيار ما لم تعرف ما تفعله.
account.methods.offline.uuid.malformed=تنسيق غير صالح.
account.methods.forgot_password=نسيت كلمة المرور
account.methods.ban_query=استعلام الحظر
account.missing=لا توجد حسابات
account.missing.add=انقر هنا لإضافة واحد.
@@ -341,7 +340,6 @@ download.provider.official=من المصادر الرسمية
download.provider.balanced=من الأسرع المتاح
download.provider.mirror=من المرآة
download.java=تنزيل Java
download.java.override=هذا الإصدار من Java موجود بالفعل. هل تريد إلغاء التثبيت وإعادة التثبيت؟
download.java.process=عملية تنزيل Java
download.javafx=تنزيل التبعيات للمشغل...
download.javafx.notes=نحن نقوم حالياً بتنزيل تبعيات HMCL من الإنترنت.\n\
@@ -363,11 +361,14 @@ exception.ssl_handshake=فشل إنشاء اتصال SSL لأن شهادة SSL
exception.dns.pollution=فشل إنشاء اتصال SSL. قد تكون هناك مشكلة في تحليل أسماء النطاقات (DNS). يرجى محاولة تغيير خادم DNS أو استخدام خدمة وكيل.
extension.bat=ملف Batch لـ Windows
extension.mod=ملف التعديل
extension.png=ملف صورة
extension.ps1=نص PowerShell لـ Windows
extension.sh=نص Shell
extension.datapack=حزمة بيانات
extension.mod=ملف التعديل
extension.world=أرشيف العالم
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher لا يمكنه إنشاء دليل HMCL (%s). يرجى نقل HMCL إلى موقع آخر وإعادة فتحه.
fatal.javafx.incompatible=بيئة JavaFX مفقودة.\n\
Hello Minecraft! Launcher لا يمكنه تثبيت JavaFX تلقائياً على Java <11.\n\
@@ -431,7 +432,6 @@ folder.logs=السجلات
folder.mod=التعديلات
folder.resourcepacks=حزم الموارد
folder.shaderpacks=حزم الظلال
folder.saves=الحفظ
folder.schematics=المخططات
folder.screenshots=لقطات الشاشة
folder.world=دليل العالم
@@ -442,12 +442,6 @@ game.crash.info=معلومات التعطل
game.crash.reason=سبب التعطل
game.crash.reason.analyzing=جاري التحليل...
game.crash.reason.multiple=تم اكتشاف عدة أسباب:\n\n
game.crash.reason.block=تعطلت اللعبة بسبب كتلة في العالم.\n\
\n\
يمكنك محاولة إزالة هذه الكتلة باستخدام MCEdit أو حذف التعديل الذي أضافها.\n\
\n\
نوع الكتلة: %1$s\n\
الموقع: %2$s
game.crash.reason.bootstrap_failed=تعطلت اللعبة بسبب التعديل "%1$s".\n\
\n\
يمكنك محاولة حذفه أو تحديثه.
@@ -458,12 +452,6 @@ game.crash.reason.duplicated_mod=لا يمكن للعبة الاستمرار ف
%2$s\n\
\n\
يمكن تثبيت كل تعديل مرة واحدة فقط. يرجى حذف التعديل المكرر والمحاولة مرة أخرى.
game.crash.reason.entity=تعطلت اللعبة بسبب كيان في العالم.\n\
\n\
يمكنك محاولة إزالة الكيان باستخدام MCEdit أو حذف التعديل الذي أضافه.\n\
\n\
نوع الكيان: %1$s\n\
الموقع: %2$s
game.crash.reason.modmixin_failure=تعطلت اللعبة لأن بعض التعديلات فشلت في الحقن.\n\
\n\
هذا يعني عموماً أن التعديل به خطأ أو غير متوافق مع البيئة الحالية.\n\
@@ -703,7 +691,6 @@ install.installer.game=Minecraft
install.installer.incompatible=غير متوافق مع %s
install.installer.install=تثبيت %s
install.installer.install_offline=التثبيت/التحديث من ملف محلي
install.installer.install_offline.extension=مثبت (Neo)Forge/Cleanroom/OptiFine
install.installer.install_offline.tooltip=ندعم استخدام مثبت (Neo)Forge و Cleanroom و OptiFine المحلي.
install.installer.install_online=التثبيت عبر الإنترنت
install.installer.install_online.tooltip=ندعم حالياً Forge و NeoForge و Cleanroom و OptiFine و Fabric و Quilt و LiteLoader.
@@ -726,14 +713,12 @@ install.new_game.malformed=اسم غير صالح.
install.select=اختر العملية
install.success=تم التثبيت بنجاح.
java.add=إضافة Java
java.add.failed=هذا Java غير صالح أو غير متوافق مع المنصة الحالية.
java.disable=تعطيل Java
java.disable.confirm=هل أنت متأكد من أنك تريد تعطيل هذا Java؟
java.disabled.management=Java المعطل
java.disabled.management.remove=إزالة هذا Java من القائمة
java.disabled.management.restore=إعادة تفعيل هذا Java
java.download=تنزيل Java
java.download.banshanjdk-8=تنزيل Banshan JDK 8
java.download.load_list.failed=فشل تحميل قائمة الإصدارات
java.download.more=المزيد من توزيعات Java
@@ -1032,7 +1017,6 @@ modrinth.category.256x=256x
modrinth.category.512x+=512x+
mods=التعديلات
mods.add=إضافة تعديل
mods.add.failed=فشل إضافة التعديل %s.
mods.add.success=تمت إضافة %s بنجاح.
mods.broken_dependency.title=تبعية معطلة
@@ -1050,7 +1034,6 @@ mods.check_updates.file=ملف
mods.check_updates.source=المصدر
mods.check_updates.target_version=الإصدار المستهدف
mods.check_updates.update=تحديث
mods.choose_mod=اختر تعديلاً
mods.curseforge=CurseForge
mods.dependency.embedded=تبعيات مدمجة (معبأة بالفعل في ملف التعديل من قبل المطور. لا حاجة للتنزيل بشكل منفصل)
mods.dependency.optional=تبعيات اختيارية (إذا كانت مفقودة، ستعمل اللعبة بشكل طبيعي، ولكن قد تكون ميزات التعديل مفقودة)
@@ -1087,9 +1070,6 @@ nbt.save.failed=فشل حفظ الملف
nbt.title=عرض الملف - %s
datapack=حزم البيانات
datapack.add=تثبيت حزمة بيانات
datapack.choose_datapack=اختر حزمة بيانات للاستيراد
datapack.extension=حزمة بيانات
datapack.title=عالم [%s] - حزم البيانات
web.failed=فشل تحميل الصفحة
@@ -1097,9 +1077,11 @@ web.open_in_browser=هل تريد فتح هذا العنوان في المتصف
web.view_in_browser=عرض الكل في المتصفح
world=العوالم
world.add=إضافة عالم
world.add.already_exists=هذا العالم موجود بالفعل.
world.add.title=اختر أرشيف العالم الذي تريد استيراده
world.add.failed=فشل استيراد هذا العالم: %s
world.add.invalid=فشل تحليل العالم.
world.backup=نسخ احتياطي للعالم
world.backup.create.new_one=إنشاء نسخة احتياطية جديدة
world.backup.create.failed=فشل إنشاء النسخة الاحتياطية.\n%s
world.backup.create.success=تم إنشاء نسخة احتياطية جديدة بنجاح: %s
world.backup.delete=حذف هذه النسخة الاحتياطية
@@ -1113,18 +1095,12 @@ world.datapack=حزم البيانات
world.datetime=آخر لعب في %s
world.delete=حذف العالم
world.delete.failed=فشل حذف العالم.\n%s
world.download=تنزيل عالم
world.download.title=تنزيل عالم - %1s
world.export=تصدير العالم
world.export.title=اختر الدليل لهذا العالم المُصدر
world.export.location=حفظ باسم
world.export.wizard=تصدير العالم "%s"
world.extension=أرشيف العالم
world.game_version=إصدار اللعبة
world.import.already_exists=هذا العالم موجود بالفعل.
world.import.choose=اختر أرشيف العالم الذي تريد استيراده
world.import.failed=فشل استيراد هذا العالم: %s
world.import.invalid=فشل تحليل العالم.
world.info=معلومات العالم
world.info.basic=معلومات أساسية
world.info.allow_cheats=السماح بالأوامر/الغش
@@ -1191,10 +1167,8 @@ resourcepack.download.title=تنزيل حزمة موارد - %1s
reveal.in_file_manager=إظهار في مدير الملفات
schematics=المخططات
schematics.add=إضافة ملفات مخططات
schematics.add.failed=فشل إضافة ملفات المخططات
schematics.back_to=العودة إلى "%s"
schematics.create_directory=إنشاء دليل
schematics.create_directory.prompt=يرجى إدخال اسم الدليل الجديد
schematics.create_directory.failed=فشل إنشاء الدليل
schematics.create_directory.failed.already_exists=الدليل موجود بالفعل
@@ -1324,9 +1298,6 @@ settings.game.java_directory.template=%1$s (%2$s)
settings.game.management=إدارة
settings.game.working_directory=دليل العمل
settings.game.working_directory.choose=اختر دليل العمل
settings.game.working_directory.hint=قم بتفعيل خيار "معزول" في "دليل العمل" للسماح للنسخة الحالية بتخزين إعداداتها وحفظها وتعديلاتها في دليل منفصل.\n\
\n\
يوصى بتفعيل هذا الخيار لتجنب تعارضات التعديلات، ولكن ستحتاج إلى نقل حفظك يدويًا.
settings.icon=الأيقونة
@@ -1539,7 +1510,6 @@ version.manage=جميع النسخ
version.manage.clean=حذف ملفات السجل
version.manage.clean.tooltip=حذف الملفات في أدلة "logs" و "crash-reports".
version.manage.duplicate=تكرار النسخة
version.manage.duplicate.duplicate_save=تكرار الحفظ
version.manage.duplicate.prompt=أدخل اسم النسخة الجديد
version.manage.duplicate.confirm=ستحتوي النسخة المكررة على نسخة من جميع الملفات في دليل النسخة (".minecraft/versions/<اسم النسخة>")، مع دليل عمل وإعدادات معزولة.
version.manage.manage=تحرير النسخة
@@ -1547,7 +1517,6 @@ version.manage.manage.title=تحرير النسخة - %1s
version.manage.redownload_assets_index=تحديث أصول اللعبة
version.manage.remove=حذف النسخة
version.manage.remove.confirm.trash=هل أنت متأكد من أنك تريد إزالة النسخة "%1$s"؟ لا يزال بإمكانك العثور على ملفاتها في سلة المحذوفات باسم "%2$s".
version.manage.remove.confirm.independent=نظرًا لأن هذه النسخة مخزنة في دليل معزول، فإن حذفها سيحذف أيضًا حفظها وبيانات أخرى. هل لا تزال تريد حذف النسخة "%s"؟
version.manage.remove.failed=خطأ في حذف النسخة. قد تكون بعض الملفات لازالت في الإستخدام.
version.manage.remove_assets=حذف جميع الأصول
version.manage.remove_libraries=حذف جميع المكتبات
@@ -123,7 +123,6 @@ account.methods.offline.uuid.hint=UUID es un identificador único para los jugad
\n\
Esta opción es sólo para usuarios avanzados. No recomendamos editar esta opción a menos que sepas lo que estás haciendo.
account.methods.offline.uuid.malformed=Formato no válido.
account.methods.forgot_password=Olvidé mi contraseña
account.methods.ban_query=Consulta de bloqueo de cuentas
account.missing=No hay cuentas
account.missing.add=Haga clic aquí para añadir una.
@@ -346,7 +345,6 @@ download.provider.official=De fuentes oficiales
download.provider.balanced=De la fuente más rápida disponible
download.provider.mirror=Desde espejo
download.java=Descargando Java
download.java.override=Esta versión de Java ya existe. ¿Desea desinstalarla y volver a instalarla?
download.java.process=Proceso de descarga Java
download.javafx=Descargando dependencias para el launcher...
download.javafx.notes=Estamos descargando dependencias para HMCL desde Internet.\n\
@@ -365,11 +363,14 @@ exception.ssl_handshake=No se pudo establecer una conexión SSL porque falta el
exception.dns.pollution=No se pudo establecer una conexión SSL. Es posible que la resolución DNS sea incorrecta. Por favor, intente cambiar el servidor DNS o usar un servicio proxy.
extension.bat=Archivo por lotes de Windows
extension.mod=Archivo mod
extension.png=Archivo de imagen
extension.ps1=Windows PowerShell Script
extension.sh=Shell Script
extension.datapack=Paquete de datos
extension.mod=Archivo mod
extension.world=Archivo del mundo
fatal.create_hmcl_current_directory_failure=El lanzador no puede crear el directorio HMCL (%s). Por favor, mueva el lanzador a otra ubicación y vuelva a abrirlo.
fatal.javafx.incompatible=No se encontró un entorno JavaFX.\n\
Hello Minecraft! Launcher no puede instalar automáticamente JavaFX con versiones de Java inferiores a la 11.\n\
@@ -447,12 +448,6 @@ game.crash.info=Información de colgado
game.crash.reason=Motivo del colgado
game.crash.reason.analyzing=Analizando...
game.crash.reason.multiple=Se han detectado varias razones:\n\n
game.crash.reason.block=El juego se ha colgado debido a un bloqueo en el mundo.\n\
\n\
Puedes intentar eliminar este bloque usando MCEdit o borrando el mod que lo añadió.\n\
\n\
Tipo de bloque: %1$s\n\
Ubicación: %2$s
game.crash.reason.bootstrap_failed=El juego se ha colgado debido al mod «%1$s».\n\
\n\
Puedes intentar borrarlo o actualizarlo.
@@ -466,12 +461,6 @@ game.crash.reason.duplicated_mod=El juego no puede iniciarse debido a la existen
%2$s\n\
\n\
Cada mod sólo puede instalarse una vez, por favor, elimine el mod duplicado e inténtelo de nuevo.
game.crash.reason.entity=El juego se ha colgado debido a una entidad en el mundo.\n\
\n\
Puedes intentar eliminar la entidad usando MCEdit o borrando el mod que la añadió.\n\
\n\
Tipo de entidad: %1$s\n\
Ubicación: %2$s
game.crash.reason.fabric_version_0_12=La versión de Fabric Loader 0.12 o posterior es incompatible con los mods instalados actualmente. Necesitas bajarlo a la versión 0.11.7.
game.crash.reason.fabric_warnings=El cargador de Fabric Loader ha advertido:\n\
\n\
@@ -710,10 +699,9 @@ install.installer.game=Minecraft
install.installer.incompatible=Incompatible con %s
install.installer.install=Instalando %s
install.installer.install_offline=Instalar/actualizar desde archivo local
install.installer.install_offline.extension=Instalador de (Neo)Forge/Cleanroom/OptiFine
install.installer.install_offline.tooltip=Apoyamos el uso del instalador local de (Neo)Forge/Cleanroom/OptiFine.
install.installer.install_online=Instalación en línea
install.installer.install_online.tooltip=Actualmente soportamos Forge, NeoForge, Cleanroom, Fabric, Quilt, LiteLoader y OptiFine.
install.installer.install_online.tooltip=Actualmente soportamos Forge, NeoForge, Cleanroom, Fabric, Legacy Fabric, Quilt, LiteLoader y OptiFine.
install.installer.liteloader=LiteLoader
install.installer.not_installed=No está instalado
install.installer.optifine=OptiFine
@@ -733,14 +721,12 @@ install.new_game.malformed=Nombre no válido.
install.select=Elegir una operación
install.success=Instalado con éxito.
java.add=Añadir Java
java.add.failed=Este Java no es válido o es incompatible con la plataforma actual.
java.disable=Deshabilitar este Java
java.disable.confirm=¿Está seguro de que desea desactivar este Java?
java.disabled.management=Java desactivado
java.disabled.management.remove=Eliminar este Java de la lista
java.disabled.management.restore=Activar este Java
java.download=Descargar Java
java.download.banshanjdk-8=Descargar Banshan JDK 8
java.download.load_list.failed=No se ha podido cargar la lista de versiones
java.download.more=Más distribuciones de Java
@@ -1039,7 +1025,6 @@ modrinth.category.256x=256x
modrinth.category.512x+=512x+
mods=Mods
mods.add=Añadir mods
mods.add.failed=No se ha podido añadir el mod %s.
mods.add.success=%s se ha añadido correctamente.
mods.broken_dependency.title=Dependencia rota
@@ -1058,7 +1043,6 @@ mods.check_updates.failed_download=No se han podido descargar algunos de los arc
mods.check_updates.file=Archivo
mods.check_updates.source=Fuente
mods.check_updates.target_version=Versión de destino
mods.choose_mod=Elige un mod
mods.curseforge=CurseForge
mods.dependency.embedded=Dependencias incorporadas (Already packaged in the mod file by the author. No need to download separately)
mods.dependency.optional=Dependencias opcionales (If missing, the game will run normally, but the mod features may be missing)
@@ -1080,7 +1064,7 @@ mods.mcmod.page=Página de MCMod
mods.mcmod.search=Búsqueda en MCMod
mods.modrinth=Modrinth
mods.name=Nombre
mods.not_modded=¡Debes instalar primero un cargador de mods (Forge, NeoForge, Fabric, Quilt o LiteLoader) para gestionar tus mods!
mods.not_modded=¡Debes instalar primero un cargador de mods (Forge, NeoForge, Fabric, Legacy Fabric, Quilt o LiteLoader) para gestionar tus mods!
mods.restore=Restaurar
mods.url=Página oficial
mods.update_modpack_mod.warning=Actualizar mods en un modpack puede generar resultados irreparables, posiblemente corrompiendo el modpack para que no pueda iniciarse. ¿Seguro que quieres actualizar?
@@ -1093,9 +1077,6 @@ nbt.save.failed=No se ha podido guardar el archivo
nbt.title=Ver archivo - %s
datapack=Paquetes de datos
datapack.add=Instalar paquetes de datos
datapack.choose_datapack=Elija el paquete de datos que desea importar
datapack.extension=Paquete de datos
datapack.title=Mundo [%s] - Paquetes de datos
web.failed=No se ha podido cargar la página
@@ -1103,9 +1084,11 @@ web.open_in_browser=Desea abrir esta dirección en un navegador:\n%s
web.view_in_browser=Ver en navegador
world=Mundos
world.add=Añadir mundo
world.add.already_exists=Este mundo ya existe.
world.add.title=Elija el archivo de mundo que desea importar
world.add.failed=No se ha podido importar este mundo: %s
world.add.invalid=No se ha podido analizar el mundo.
world.backup=Copia de seguridad
world.backup.create.new_one=Crear uno nuevo
world.backup.create.failed=No se ha podido crear la copia de seguridad.\n%s
world.backup.create.success=Creada con éxito una nueva copia de seguridad: %s
world.backup.delete=Eliminar esta copia de seguridad
@@ -1122,18 +1105,12 @@ world.datapack=Paquetes de datos
world.datetime=Jugado por última vez en %s
world.delete=Eliminar este mundo
world.delete.failed=No se pudo eliminar el mundo.\n%s
world.download=Descargar Mundo
world.download.title=Descargar mundo - %1s
world.export=Exportar el mundo
world.export.title=Elija el directorio para este mundo exportado
world.export.location=Guardar como
world.export.wizard=Exportar Mundo %s
world.extension=Archivo del mundo
world.game_version=Versión del juego
world.import.already_exists=Este mundo ya existe.
world.import.choose=Elija el archivo de mundo que desea importar
world.import.failed=No se ha podido importar este mundo: %s
world.import.invalid=No se ha podido analizar el mundo.
world.info=Información del mundo
world.info.basic=Información básica
world.info.allow_cheats=Permitir comandos/trucos
@@ -1200,10 +1177,8 @@ resourcepack.download.title=Descargar paquete de recursos - %1s
reveal.in_file_manager=Mostrar en el administrador de archivos
schematics=Esquemas
schematics.add=Añadir archivos de esquema
schematics.add.failed=No se han podido añadir archivos de esquema
schematics.back_to=Atrás a «%s»
schematics.create_directory=Crear directorio
schematics.create_directory.prompt=Introduzca el nuevo nombre del directorio
schematics.create_directory.failed=No se ha podido crear el directorio
schematics.create_directory.failed.already_exists=El directorio ya existe
@@ -1332,9 +1307,6 @@ settings.game.java_directory.version=Especifique la versión de Java
settings.game.management=Gestionar
settings.game.working_directory=Directorio de trabajo
settings.game.working_directory.choose=Elija el directorio de trabajo
settings.game.working_directory.hint=Activar la opción «Aislar» en «Directorio de trabajo» para permitir que la instancia actual almacene sus configuraciones, mundos y mods en un directorio separado.\n\
\n\
Se recomienda activar esta opción para evitar conflictos con los mods, pero tendrás que mover tus guardados manualmente.
settings.icon=Icono
@@ -1472,7 +1444,6 @@ version.manage.manage.title=Editar instancia - %1s
version.manage.redownload_assets_index=Actualizar activos del juego
version.manage.remove=Eliminar instancia
version.manage.remove.confirm.trash=¿Estás seguro de que quieres eliminar la instancia «%s»? Todavía puedes encontrar sus archivos en tu papelera de reciclaje con el nombre de «%s».
version.manage.remove.confirm.independent=Dado que esta instancia está almacenada en un directorio aislado, al eliminarla también se eliminarán sus guardados y otros datos. ¿Aún quieres borrar la instancia %s?
version.manage.remove_assets=Borrar todas las activos del juego
version.manage.remove_libraries=Borrar todas las bibliotecas
version.manage.rename=Renombrar instancia
@@ -91,7 +91,6 @@ account.methods.microsoft.birth=誕生日の設定を編集する方法...
account.methods.microsoft.deauthorize=アカウントのバインドを解除
account.methods.microsoft.close_page=Microsoftアカウントの認証が終了しました。後で終了するログイン手順がいくつか残っています。このページを今すぐ閉じることができます。
account.methods.microsoft.logging_in=ログイン...
account.methods.forgot_password=パスワードをお忘れの方
account.methods.microsoft.makegameidsettings=プロファイルを作成/プロフィール名を編集する
account.methods.microsoft.profile=アカウントプロファイル..
account.methods.microsoft.purchase=Minecraftを購入する
@@ -315,11 +314,14 @@ exception.ssl_handshake=現在のJava仮想マシンに該当するSSL証明書
exception.dns.pollution=SSL 接続を確立できませんでした。DNS 解決が正しくない可能性があります。DNS サーバーを変更するか、プロキシサービスを使用してみてください。
extension.bat=WindowsBatファイル
extension.mod=Modファイル
extension.png=画像ファイル
extension.ps1=PowerShellスクリプト
extension.sh=Bashスクリプト
extension.datapack=Datapack
extension.mod=Modファイル
extension.world=World zip
fatal.javafx.incompatible=アプリケーションは11未満の現在のJava環境でJavaFXにパッチを適用できません。\nこのアプリは、JDK11以降またはJavaFXがバンドルされたJDKを使用して実行してください。
fatal.javafx.missing=JavaFXがありません。\nJava11以降を使用している場合は、Oracle JRE 8にダウングレードするか、BellSoft Liberica FullJREをインストールしてください。\n他のOpenJDKビルドを使用している場合は、OpenJFXが含まれていることを確認してください。
fatal.config_loading_failure=構成にアクセスできません。\nHelloMinecraftを確認してください。Launcherには、「%s」とその中のファイルへの読み取りおよび書き込みアクセス権があります。
@@ -338,7 +340,6 @@ folder.game=ゲームディレクトリ
folder.mod=Mod
folder.resourcepacks=リソースパック
folder.shaderpacks=シェーダーパックフォルダー
folder.saves=マップ
folder.screenshots=スクリーンショット
game=ゲーム
@@ -347,13 +348,11 @@ game.crash.info=ゲームステータス
game.crash.reason=クラッシュアナライザー
game.crash.reason.analyzing=分析中..
game.crash.reason.multiple=複数の理由が検出されました:\n\n
game.crash.reason.block=ブロックが原因でゲームを実行できません。\nMCEditでこのブロックを削除するか、関連するModを直接削除できます。\nブロックタイプ:%1$s\nブロックの場所:%2$s
game.crash.reason.bootstrap_failed=mod %1$s がクラッシュしたため、ゲームを実行できません。\n削除または更新を試みることができます。
game.crash.reason.config=modが構成ファイルを解析できないため、ゲームを実行できません。\nMod %1$s は構成ファイル %2$s を解析できません。
game.crash.reason.debug_crash=クラッシュが手動でトリガーされるため、ゲームを実行できません。\n実際、ゲームは無実であり、すべてあなたの責任です。
game.crash.reason.duplicated_mod=Mod %1$s が重複してインストールされているため、現在のゲームを継続して実行できません。\n%2$s\n各Modは1つしかインストールできませんので、余分なModを削除して再度お試しください。
game.crash.reason.mixin_apply_mod_failed=Mixin が %1$s モジュールを適用できないため、現在のゲームを続行できません。\n問題を解決するために、このモジュールを削除または更新してみてください。
game.crash.reason.entity=エンティティが原因でゲームを実行できません。\nMCEditでこのエンティティを削除するか、関連するModを直接削除できます。\nエンティティタイプ:%1$s\nエンティティの場所:%2$s
game.crash.reason.fabric_version_0_12=Fabric 0.12以降は、現在インストールされているmodと互換性がありません。ファブリックを0.11.7にダウングレードする必要があります。
game.crash.reason.fabric_warnings=Fabricはいくつかの警告を出します:\n%1$s
game.crash.reason.modmixin_failure=mod インジェクションの失敗により、現在のゲームを続行できません。\nこれは通常、MOD にバグがあるか、現在の環境と互換性がないことを意味します。\n間違った mod のログを確認できます。
@@ -440,7 +439,6 @@ install.installer.game=Minecraft
install.installer.incompatible=%s と互換性がありません
install.installer.install=インストール %s
install.installer.install_offline=ファイルからのインストール/更新
install.installer.install_offline.extension=Forge / OptiFineインストーラー
install.installer.install_offline.tooltip=Forge / OptiFineインストールファイルのインポートをサポートします
install.installer.install_online=オンラインでインストール
install.installer.install_online.tooltip=Fabric、Forge、OptiFine、LiteLoaderのインストールをサポートします。
@@ -458,14 +456,12 @@ install.new_game.malformed=無効な名前
install.select=操作を選択します
install.success=正常にインストールされました
java.add=Javaの追加
java.add.failed=このJavaは無効であるか、現在のプラットフォームと互換性がない。
java.disable=無効化
java.disable.confirm=本当にこのJavaを無効にしますか?
java.disabled.management=無効なJava
java.disabled.management.remove=このJavaをリストから削除する
java.disabled.management.restore=有効化
java.download=Javaをダウンロード
java.download.load_list.failed=バージョンリストの読み込みに失敗しました
java.download.more=その他のJavaディストリビューション
java.download.prompt=ダウンロードしたいJavaのバージョンを選択してください:
@@ -660,7 +656,6 @@ modrinth.category.datapack=Datapack
modrinth.category.folia=Folia
mods=Mods
mods.add=modを追加
mods.add.failed=mods %s の追加に失敗しました。
mods.add.success=mods %s が正常に追加されました。
mods.category=Category
@@ -673,7 +668,6 @@ mods.check_updates.failed_download=一部のファイルのダウンロードに
mods.check_updates.file=ファイル
mods.check_updates.source=Source
mods.check_updates.target_version=Target
mods.choose_mod=modを選択してください
mods.curseforge=CurseForge
mods.disable=無効にする
mods.download=Modのダウンロード
@@ -692,28 +686,22 @@ mods.url=公式ページ
mods.update_modpack_mod.warning=modpack 内の mod を更新すると、修復不可能な結果につながる可能性があり、modpack が破損して起動できなくなる可能性があります。更新してもよろしいですか?
datapack=Datapacks
datapack.add=データパックをインストールします
datapack.choose_datapack=インポートするデータパックのzipを選択してください
datapack.extension=Datapack
datapack.title=World %s -データパック
world=マップ
world.add=マップを追加(.zip)
world.add.already_exists=このマップはすでに存在しています。
world.add.title=インポートするzipファイルを選択してください
world.add.failed=このマップをインポートできません:%s
world.add.invalid=無効なワールドzipファイル
world.datapack=データパックの管理
world.datetime=最終ゲーム時刻:%s
world.delete=このマップを削除
world.delete.failed=マップの削除に失敗しました\n%s
world.download=ダウンロード
world.export=このマップをエクスポートする
world.export.title=保存する場所を選択してください
world.export.location=エクスポート先
world.export.wizard=エクスポートワールド: %s
world.extension=World zip
world.game_version=ゲームバージョン
world.import.already_exists=このマップはすでに存在しています。
world.import.choose=インポートするzipファイルを選択してください
world.import.failed=このマップをインポートできません:%s
world.import.invalid=無効なワールドzipファイル
world.manage=マップ
world.manage.title=マップ - %s
world.name=マップ名
@@ -955,7 +943,6 @@ version.manage.manage.title=バージョンの管理 - %1s
version.manage.redownload_assets_index=ゲームアセットファイルの更新
version.manage.remove=このバージョンを削除します
version.manage.remove.confirm.trash=このバージョン %s を削除してもよろしいですか?このバージョンは、システムのゴミ箱に %s という名前で復元できます。
version.manage.remove.confirm.independent=このバージョンは独立モードであるため、このバージョンを削除すると、このバージョンに属するすべての保存済みワールドも削除されます。このバージョン %s を削除しますか?
version.manage.remove_assets=すべてのゲームリソースファイルの削除
version.manage.remove_libraries=ライブラリファイルを削除します
version.manage.rename=このバージョンの名前を変更します
@@ -110,7 +110,6 @@ account.methods.microsoft.logging_in=方登入……
account.methods.microsoft.makegameidsettings=添檔 / 書檔之名
account.methods.microsoft.profile=纂戶簿訊息
account.methods.microsoft.purchase=買礦藝
account.methods.forgot_password=亡符節
account.methods.ban_query=檢戶簿羈否
account.methods.microsoft.snapshot.website=官網
account.methods.offline=離綫之式
@@ -345,7 +344,6 @@ download.provider.balanced.desc=均,容有舊
download.provider.mirror=先別源
download.provider.mirror.desc=至快,容有舊
download.java=引爪哇
download.java.override=爪哇是版既現,除而復裝諸?
download.java.process=引爪哇
download.javafx=甫引行需諸案……
download.javafx.notes=方引 HMCL 所需諸案。\n擊「迭引源」以閲詳,并迭之。擊「罷」以止而退。\n誡:引之允滯,宜迭其源。
@@ -361,11 +359,14 @@ exception.ssl_handshake=無築 SSL 鏈。爪哇缺證。宜改爪哇,抑制廢
exception.dns.pollution=無築 SSL 鏈。或由 DNS 污染、解析有誤也。宜更 DNS 伺服,抑用代理以訪。\n君可求助於右上之鈕。
extension.bat=視窗角本
extension.mod=改囊案
extension.png=圖案
extension.ps1=PowerShell 角本
extension.sh=Bash 角本
extension.datapack=錄囊
extension.mod=改囊案
extension.world=生界緊囊
fatal.create_hmcl_current_directory_failure=HMCL 不能立其案夾 (%s),宜稍遷而再啟之。\n君可求助於 https://docs.hmcl.net/help.html。
fatal.javafx.incompatible=JavaFX 闕。\n既行於爪哇之次乎爪哇十一者,HMCL 不能自補之也。宜晉爪哇於十一抑後之者。\n君可求助於 https://docs.hmcl.net/help.html。
fatal.javafx.incomplete=JavaFX 損。宜更爪哇,抑復置 OpenJFX。
@@ -407,14 +408,14 @@ game.crash.feedback=<b>無錄是頁以示人!</b>宜擊<b>「導崩記」</b>
game.crash.info=戲訊
game.crash.reason=潰退之由
game.crash.reason.analyzing=方求之焉……
game.crash.reason.block=塊謬而崩。\n或操 <a href="https://www.mcedit.net">MCEdit</a> 修檔以去之,抑徑去改囊之有干者。\n塊類:%1$s\n塊位:%2$s
game.crash.reason.block=塊謬而崩。\n或操輿圖纂具修檔以去之,抑徑去改囊之有干者。\n塊類:%1$s\n塊位:%2$s\n\n輿圖纂具薦:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英吉利語大典</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-tw#地图编辑工具">漢語大典</a>
game.crash.reason.bootstrap_failed=改囊「%1$s」謬而崩。\n或抑廢之。
game.crash.reason.mixin_apply_mod_failed=「%1$s」不能旁嵌而崩。\n或廢之,或新之。
game.crash.reason.config=置設之案謬而崩。\n改囊「%1$s」之置設「%2$s」有謬。
game.crash.reason.multiple=其所由也衆:\n\n
game.crash.reason.debug_crash=手崩也。\n戲事無誤,乃君致之!
game.crash.reason.duplicated_mod=改囊「%1$s」復而崩。\n%2$s\n改囊不能再載。宜廢冗者。
game.crash.reason.entity=實體謬而崩。\n或操 <a href="https://www.mcedit.net">MCEdit</a> 修檔以去之,抑徑去改囊之有干者。\n實體類:%1$s\n實體位:%2$s
game.crash.reason.entity=實體謬而崩。\n或操輿圖纂具修檔以去之,抑徑去改囊之有干者。\n實體類:%1$s\n實體位:%2$s\n\n輿圖纂具薦:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英吉利語大典</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-tw#地图编辑工具">漢語大典</a>
game.crash.reason.fabric_version_0_12=改囊或弗見容於緞載之〇點一二與愈晉者。宜退乎〇點一一點七。
game.crash.reason.fabric_warnings=緞報曰:\n%1$s
game.crash.reason.file_already_exists=案「%1$s」既存而謬。\n誠以爲可去,宜另備而去之,然後復行。
@@ -511,7 +512,6 @@ install.installer.game=礦藝
install.installer.incompatible=與 %s 不兼也
install.installer.install=裝 %s
install.installer.install_offline=取私案而置
install.installer.install_offline.extension= (新) 鍛/Cleanroom/OptiFine 裝者
install.installer.install_offline.tooltip=可錄 (新) 鍛/Cleanroom/OptiFine 裝者
install.installer.install_online=網取而置
install.installer.install_online.tooltip=可置鍛、新鍛、Cleanroom、緞、褥、輕載與 OptiFine
@@ -833,6 +833,7 @@ mods=改囊
mods.add=增改囊
mods.add.failed=增改囊「%s」未成。\n君可求助於右上之鈕。
mods.add.success=增改囊 %s 畢。
mods.add.title=擇改囊
mods.broken_dependency.title=所依之壞者
mods.broken_dependency.desc=夫改囊素存於改囊庫,今闕矣,宜易他源。
mods.category=類
@@ -849,7 +850,6 @@ mods.check_updates.failed_download=有引案未成
mods.check_updates.file=案
mods.check_updates.source=源
mods.check_updates.target_version=將至之版
mods.choose_mod=擇改囊
mods.curseforge=CurseForge
mods.dependency.embedded=既存之相依改囊 (既以內於改囊案,無須他引)
mods.dependency.optional=可選之相依改囊 (设若阙如,戲亦能行)
@@ -885,8 +885,7 @@ nbt.title=覽案 - %s
datapack=錄囊
datapack.add=增錄囊
datapack.choose_datapack=擇須導入之錄囊緊囊
datapack.extension=錄囊
datapack.add.title=擇須導入之錄囊緊囊
datapack.title=生界 [%s] - 錄囊
web.failed=載網葉未成
@@ -895,6 +894,10 @@ web.view_in_browser=覽全誌于瀏網器
world=生界
world.add=增生界
world.add.already_exists=生界既存
world.add.title=擇須導入之生界緊囊
world.add.failed=不能錄生界:「%s」
world.add.invalid=無可辨此緊囊
world.backup=司備
world.backup.create.new_one=建新備
world.backup.create.failed=建備未成。\n%s
@@ -917,12 +920,7 @@ world.export=錄出生界
world.export.title=擇是生界之存處
world.export.location=存至
world.export.wizard=錄出生界「%s」
world.extension=生界緊囊
world.game_version=戲版
world.import.already_exists=生界既存
world.import.choose=擇須導入之生界緊囊
world.import.failed=不能錄生界:「%s」
world.import.invalid=無可辨此緊囊
world.info=生界之訊
world.info.basic=基訊
world.info.allow_cheats=允令 (舞弊)
@@ -122,7 +122,6 @@ account.methods.offline.name.invalid=Для имени пользователя
account.methods.offline.uuid=UUID
account.methods.offline.uuid.hint=UUID - это уникальный идентификатор игрового персонажа в Minecraft. Способ генерации UUID различается в разных лаунчерах игр. Изменение UUID на тот, который генерируется другим лаунчером гарантирует, что игровые блоки/предметы в рюкзаке вашего офлайн аккаунта останутся. Этот параметр предназначен для экспертов. Если вы не знаете что делаете, мы не советуем вам изменять этот параметр. Эта опция не требуется для присоединения к серверам.
account.methods.offline.uuid.malformed=Недопустимый формат.
account.methods.forgot_password=Забыли пароль
account.methods.ban_query=Просмотреть аккаунт заблокирован
account.missing=Нет аккаунтов
account.missing.add=Нажмите здесь, чтобы добавить.
@@ -343,7 +342,6 @@ download.provider.official=Из официальных источников
download.provider.balanced=Из самых быстрых доступных
download.provider.mirror=Из зеркала
download.java=Скачивание Java
download.java.override=Эта версия Java уже существует. Вы хотите удалить и установить ее заново?
download.java.process=Процесс скачивания Java
download.javafx=Скачивание зависимостей лаунчера...
download.javafx.notes=Скачивание зависимостей для лаунчер из Интернета.\n\
@@ -365,11 +363,14 @@ exception.ssl_handshake=Не удалось установить SSL-соеди
exception.dns.pollution=Не удалось установить SSL‑соединение. Возможно, некорректно разрешаются DNS‑имена. Попробуйте сменить DNS‑сервер или воспользоваться прокси-службой.
extension.bat=Пакетный файл Windows
extension.mod=Файл мода
extension.png=Файл изображения
extension.ps1=Сценарий Windows PowerShell
extension.sh=Сценарий оболочки Bash
extension.datapack=Набор данных
extension.mod=Файл мода
extension.world=Архив мира
fatal.create_hmcl_current_directory_failure=Лаунчер не может создать папку HMCL (%s). Пожалуйста, переместите лаунчер в другое место и откройте его снова.
fatal.javafx.incompatible=Отсутствует среда JavaFX.\n\
Лаунчер не может автоматически установить JavaFX на Java <11.\n\
@@ -441,12 +442,6 @@ game.crash.info=Сведения об отказе
game.crash.reason=Анализатор сбоев
game.crash.reason.analyzing=Анализирование...
game.crash.reason.multiple=Обнаружено несколько причин:\n\n
game.crash.reason.block=Игра отказала из-за блока в мире.\n\
\n\
Удалите этот блок с помощью MCEdit или удалите мод связанный с ним напрямую.\n\
\n\
Тип блока: %1$s\n\
Расположение блока: %2$s
game.crash.reason.bootstrap_failed=Игра отказала из-за мода «%1$s».\n\
\n\
Попробуйте удалить или обновить его.
@@ -457,12 +452,6 @@ game.crash.reason.duplicated_mod=Не удалось продолжить зап
%2$s\n\
\n\
Каждый мод должен быть установлен только один, удалите дублирующий мод и попробуйте снова.
game.crash.reason.entity=Игра отказала из-за сущность в мире.\n\
\n\
Удалите эту сущность с помощью MCEdit или удалите мод связанный с ним напрямую.\n\
\n\
Тип сущность: %1$s\n\
Расположение сущность: %2$s
game.crash.reason.fabric_version_0_12=Fabric 0.12 (и выше) несовместим с установленными модами. Следует понизить версию fabric до 0.11.7.
game.crash.reason.mixin_apply_mod_failed=Игра отказала из-за невозможности применить mixin к моду «%1$s».\n\
\n\
@@ -701,10 +690,9 @@ install.installer.game=Minecraft
install.installer.incompatible=Несовместимо с %s
install.installer.install=Установить %s
install.installer.install_offline=Установить/обновить из локального файла
install.installer.install_offline.extension=Установщик (Neo)Forge/Cleanroom/OptiFine
install.installer.install_offline.tooltip=Поддерживаются установщики (Neo)Forge/Cleanroom/OptiFine.
install.installer.install_online=Установить с интернета
install.installer.install_online.tooltip=Поддерживаются Forge, NeoForge, Cleanroom, Fabric, Quilt, LiteLoader и OptiFine.
install.installer.install_online.tooltip=Поддерживаются Forge, NeoForge, Cleanroom, Fabric, Legacy Fabric, Quilt, LiteLoader и OptiFine.
install.installer.liteloader=LiteLoader
install.installer.not_installed=Не установлено
install.installer.optifine=OptiFine
@@ -724,14 +712,12 @@ install.new_game.malformed=Недопустимое имя
install.select=Выберите операцию
install.success=Успешно установлено.
java.add=Добавить Java
java.add.failed=Этот Java недопустим или несовместим с текущей платформой.
java.disable=Отключить Java
java.disable.confirm=Вы уверены, что хотите отключить эту Java?
java.disabled.management=Отключенная Java
java.disabled.management.remove=Удалить эту Java из списка
java.disabled.management.restore=Включить эту Java
java.download=Скачать Java
java.download.banshanjdk-8=Скачать Banshan JDK 8
java.download.load_list.failed=Не удалось загрузить список версий
java.download.more=Больше дистрибутивов Java
@@ -1032,7 +1018,6 @@ modrinth.category.256x=256x
modrinth.category.512x+=512x+
mods=Моды
mods.add=Добавить мод
mods.add.failed=Не удалось установить мод %s.
mods.add.success=%s был успешно добавлен.
mods.broken_dependency.title=Сломанная зависимость
@@ -1051,7 +1036,6 @@ mods.check_updates.failed_download=Не удалось скачать некот
mods.check_updates.file=Файл
mods.check_updates.source=Источник
mods.check_updates.target_version=Целевая версия
mods.choose_mod=Выберите мод
mods.curseforge=CurseForge
mods.dependency.embedded=Встроенные зависимости (Уже упакован в файл мода автором. Нет необходимости скачивать отдельно.)
mods.dependency.optional=Необязательные зависимости (Если он отсутствует, игра будет работать нормально, но функции мода могут отсутствовать.)
@@ -1073,7 +1057,7 @@ mods.mcmod.page=Страница MCMod
mods.mcmod.search=Искать на MCMod
mods.modrinth=Modrinth
mods.name=Название
mods.not_modded=Для управления модами необходимо сначала установить загрузчик (Forge, NeoForge, Fabric, Quilt или LiteLoader)!
mods.not_modded=Для управления модами необходимо сначала установить загрузчик (Forge, NeoForge, Fabric, Legacy Fabric, Quilt или LiteLoader)!
mods.restore=Восстановить
mods.url=Официальная страница
mods.update_modpack_mod.warning=Обновление модов в пакете модов может привести к непоправимым результатам, возможно, повредить пакет модов так, что он не сможет запуститься. Вы уверены, что хотите обновить?
@@ -1086,9 +1070,6 @@ nbt.save.failed=Не удалось сохранить файл
nbt.title=Смотреть файл - %s
datapack=Наборы данных
datapack.add=Установить
datapack.choose_datapack=Выберите набор данных для импорта
datapack.extension=Набор данных
datapack.title=Мир [%s] - Наборы данных
web.failed=Не удалось загрузить страницу
@@ -1096,9 +1077,11 @@ web.open_in_browser=Хотите ли вы открыть этот адрес в
web.view_in_browser=Смотреть в браузере
world=Миры
world.add=Добавить мир
world.add.already_exists=Мир уже существует.
world.add.title=Выберите архив мира, который хотите импортировать
world.add.failed=Не удалось импортировать этот мир\: %s
world.add.invalid=Не удалось разобрать мир.
world.backup=Резервный мир
world.backup.create.new_one=Создать новый
world.backup.create.failed=Не удалось создать резервную копию.\n%s
world.backup.create.success=Успешно создано новое резервное копирование: %s
world.backup.delete=Удалить эту резервную копию
@@ -1115,18 +1098,12 @@ world.delete=Удалить этот мир
world.delete.failed=Не удалось удалить мир.\n%s
world.datapack=Наборы данных
world.datetime=Последний запуск игры %s
world.download=Скачать мир
world.download.title=Скачать мир - %1s
world.export=Экспорт мира
world.export.title=Выберите папку для экспорта мира
world.export.location=Экспорт в
world.export.wizard=Экспорт мира %s
world.extension=Архив мира
world.game_version=Версия игры
world.import.already_exists=Мир уже существует.
world.import.choose=Выберите архив мира, который хотите импортировать
world.import.failed=Не удалось импортировать этот мир\: %s
world.import.invalid=Не удалось разобрать мир.
world.info=Сведения о мире
world.info.basic=Основные сведения
world.info.allow_cheats=Разрешить команды/читы
@@ -1192,10 +1169,8 @@ resourcepack.download.title=Скачать пакет ресурсов - %1s
reveal.in_file_manager=Открыть в файловый менеджер
schematics=Схемы
schematics.add=Добавить файлы схем
schematics.add.failed=Не удалось добавить файлы схем
schematics.back_to=Назад на «%s»
schematics.create_directory=Создать папку
schematics.create_directory.prompt=Введите новое имя папки
schematics.create_directory.failed=Не удалось создать папку
schematics.create_directory.failed.already_exists=Папка уже существует
@@ -1323,9 +1298,6 @@ settings.game.java_directory.template=%s (%s)
settings.game.management=Инструменты
settings.game.working_directory=Рабочая папка
settings.game.working_directory.choose=Выберите рабочую папку
settings.game.working_directory.hint=Включите параметр «Раздельно» в разделе «Рабочая папка», чтобы позволить текущему экземпляру хранить свои настройки, сохранения и моды в отдельной папке.\n\
\n\
Рекомендуется включить этот параметр, чтобы избежать конфликтов с модами, но нужно будет переместить свои сохранения вручную.
settings.icon=Значок
@@ -120,7 +120,6 @@ account.methods.offline.uuid.hint=UUID - це унікальний іденти
\n\
Ця опція призначена лише для досвідчених користувачів. Ми не рекомендуємо змінювати цю опцію, якщо ви не знаєте, що робите.
account.methods.offline.uuid.malformed=Недійсний формат.
account.methods.forgot_password=Забули пароль
account.methods.ban_query=Запит блокування
account.missing=Немає облікових записів
account.missing.add=Натисніть тут, щоб додати.
@@ -342,7 +341,6 @@ download.provider.official=З офіційних джерел
download.provider.balanced=З найшвидшого доступного
download.provider.mirror=З дзеркала
download.java=Завантаження Java
download.java.override=Ця версія Java вже існує. Бажаєте видалити та перевстановити її?
download.java.process=Процес завантаження Java
download.javafx=Завантаження залежностей для лаунчера...
download.javafx.notes=Ми зараз завантажуємо залежності для HMCL з Інтернету. \n\
@@ -363,11 +361,14 @@ exception.ssl_handshake=Не вдалося встановити SSL-з'єдна
exception.dns.pollution=Не вдалося встановити SSL-з'єднання. Можливо, DNS неправильно розв'язується. Спробуйте змінити DNS-сервер або використовувати проксі-сервіс.
extension.bat=Пакетний файл Windows
extension.mod=Файл мода
extension.png=Файл зображення
extension.ps1=Скрипт PowerShell Windows
extension.sh=Скрипт оболонки
extension.datapack=Datapack
extension.mod=Файл мода
extension.world=Архів світу
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Лаунчер не може створити каталог HMCL (%s). Перемістіть HMCL в інше місце та відкрийте його знову.
fatal.javafx.incompatible=Відсутнє середовище JavaFX. \n\
Hello Minecraft! Лаунчер не може автоматично встановити JavaFX на Java <11. \n\
@@ -426,12 +427,6 @@ game.crash.info=Інформація про збій
game.crash.reason=Причина збою
game.crash.reason.analyzing=Аналіз...
game.crash.reason.multiple=Виявлено кілька причин:\n\n
game.crash.reason.block=Гра зазнала збою через блок у світі. \n\
\n\
Ви можете спробувати видалити цей блок за допомогою MCEdit або видалити мод, який його додав. \n\
\n\
Тип блоку: %1$s \n\
Місцезнаходження: %2$s
game.crash.reason.bootstrap_failed=Гра зазнала збою через мод "%1$s". \n\
\n\
Ви можете спробувати видалити або оновити його.
@@ -440,10 +435,6 @@ game.crash.reason.debug_crash=Гра зазнала збою, оскільки
game.crash.reason.duplicated_mod=Гра не може продовжувати роботу через дублікати модів "%1$s". \n\
%2$s \n\
Кожен мод можна встановити лише один раз. Видаліть дублікат мода та спробуйте знову.
game.crash.reason.entity=Гра зазнала збою через сутність у світі. \n\
Ви можете спробувати видалити сутність за допомогою MCEdit або видалити мод, який її додав. \n\
Тип сутності: %1$s \n\
Місцезнаходження: %2$s
game.crash.reason.modmixin_failure=Гра зазнала збою, оскільки деякі моди не вдалося ввести. \n\
Це, як правило, означає, що мод має помилку або несумісний з поточним середовищем. \n\
Ви можете перевірити журнал, щоб знайти помилковий мод.
@@ -650,10 +641,9 @@ install.installer.game=Minecraft
install.installer.incompatible=Несумісний з %s
install.installer.install=Встановлення %s
install.installer.install_offline=Встановити/Оновити з локального файлу
install.installer.install_offline.extension=Інсталятор (Neo)Forge/Cleanroom/OptiFine
install.installer.install_offline.tooltip=Ми підтримуємо використання локального інсталятора (Neo)Forge/Cleanroom/OptiFine.
install.installer.install_online=Онлайн встановлення
install.installer.install_online.tooltip=Наразі ми підтримуємо Forge, NeoForge, Cleanroom, OptiFine, Fabric, Quilt та LiteLoader.
install.installer.install_online.tooltip=Наразі ми підтримуємо Forge, NeoForge, Cleanroom, OptiFine, Fabric, Legacy Fabric, Quilt та LiteLoader.
install.installer.liteloader=LiteLoader
install.installer.not_installed=Не встановлено
install.installer.optifine=OptiFine
@@ -673,14 +663,12 @@ install.new_game.malformed=Недійсна назва.
install.select=Виберіть операцію
install.success=Успішно встановлено.
java.add=Додати Java
java.add.failed=Ця Java недійсна або несумісна з поточною платформою.
java.disable=Вимкнути Java
java.disable.confirm=Ви впевнені, що хочете вимкнути цю Java?
java.disabled.management=Вимкнена Java
java.disabled.management.remove=Видалити цю Java зі списку
java.disabled.management.restore=Повторно ввімкнути цю Java
java.download=Завантажити Java
java.download.banshanjdk-8=Завантажити Banshan JDK 8
java.download.load_list.failed=Не вдалося завантажити список версій
java.download.more=Більше дистрибутивів Java
@@ -977,7 +965,6 @@ modrinth.category.256x=256x
modrinth.category.512x+=512x+
mods=Моди
mods.add=Додати мод
mods.add.failed=Не вдалося додати мод %s.
mods.add.success=%s успішно додано.
mods.broken_dependency.title=Зламана залежність
@@ -996,7 +983,6 @@ mods.check_updates.failed_download=Не вдалося завантажити д
mods.check_updates.file=Файл
mods.check_updates.source=Джерело
mods.check_updates.target_version=Цільова версія
mods.choose_mod=Вибрати мод
mods.curseforge=CurseForge
mods.dependency.embedded=Вбудовані залежності (Вже запаковані в файл мода автором. Не потрібно завантажувати окремо)
mods.dependency.optional=Необов'язкові залежності (Якщо відсутні, гра буде працювати нормально, але функції мода можуть бути відсутні)
@@ -1018,7 +1004,7 @@ mods.mcmod.page=Сторінка MCMod
mods.mcmod.search=Пошук в MCMod
mods.modrinth=Modrinth
mods.name=Назва
mods.not_modded=Ви повинні спочатку встановити завантажувач модів (Forge, NeoForge, Fabric, Quilt або LiteLoader), щоб керувати своїми модами!
mods.not_modded=Ви повинні спочатку встановити завантажувач модів (Forge, NeoForge, Fabric, Legacy Fabric, Quilt або LiteLoader), щоб керувати своїми модами!
mods.restore=Відновити
mods.url=Офіційна сторінка
mods.update_modpack_mod.warning=Оновлення модів у модпаку може призвести до непоправних наслідків, можливо, пошкодивши модпак так, що він не зможе запуститися. Ви впевнені, що хочете оновити?
@@ -1031,9 +1017,6 @@ nbt.save.failed=Не вдалося зберегти файл
nbt.title=Перегляд файлу - %s
datapack=Datapacks
datapack.add=Встановити Datapack
datapack.choose_datapack=Виберіть datapack для імпорту
datapack.extension=Datapack
datapack.title=Світ [%s] - Datapacks
web.failed=Не вдалося завантажити сторінку
@@ -1041,9 +1024,11 @@ web.open_in_browser=Бажаєте відкрити цю адресу в бра
web.view_in_browser=Переглянути все в браузері
world=Світи
world.add=Додати світ
world.add.already_exists=Цей світ вже існує.
world.add.title=Виберіть архів світу, який ви хочете імпортувати
world.add.failed=Не вдалося імпортувати цей світ: %s
world.add.invalid=Не вдалося розібрати світ.
world.backup=Резервне копіювання світу
world.backup.create.new_one=Створити нову резервну копію
world.backup.create.failed=Не вдалося створити резервну копію. \n%s
world.backup.create.success=Успішно створено нову резервну копію: %s
world.backup.delete=Видалити цю резервну копію
@@ -1060,18 +1045,12 @@ world.datapack=Datapacks
world.datetime=Останній раз грали %s
world.delete=Видалити цей світ
world.delete.failed=Не вдалося видалити світ.\n%s
world.download=Завантажити світ
world.download.title=Завантажити світ - %1s
world.export=Експортувати світ
world.export.title=Виберіть каталог для цього експортованого світу
world.export.location=Зберегти як
world.export.wizard=Експортувати світ "%s"
world.extension=Архів світу
world.game_version=Версія гри
world.import.already_exists=Цей світ вже існує.
world.import.choose=Виберіть архів світу, який ви хочете імпортувати
world.import.failed=Не вдалося імпортувати цей світ: %s
world.import.invalid=Не вдалося розібрати світ.
world.info=Інформація про світ
world.info.basic=Базова інформація
world.info.allow_cheats=Дозволити команди/чіти
@@ -1138,10 +1117,8 @@ resourcepack.download.title=Завантажити пакет ресурсів -
reveal.in_file_manager=Показати в менеджері файлів
schematics=Схематики
schematics.add=Додати файли схематик
schematics.add.failed=Не вдалося додати файли схематик
schematics.back_to=Назад до "%s"
schematics.create_directory=Створити каталог
schematics.create_directory.prompt=Введіть нову назву каталогу
schematics.create_directory.failed=Не вдалося створити каталог
schematics.create_directory.failed.already_exists=Каталог вже існує
@@ -1268,9 +1245,6 @@ settings.game.java_directory.template=%s (%s)
settings.game.management=Керувати
settings.game.working_directory=Робочий каталог
settings.game.working_directory.choose=Вибрати робочий каталог
settings.game.working_directory.hint=Увімкніть опцію "Ізольований" у "Робочий каталог", щоб дозволити поточному екземпляру зберігати свої налаштування, збереження та моди в окремому каталозі. \n\
\n\
Рекомендується увімкнути цю опцію, щоб уникнути конфліктів модів, але вам доведеться перемістити свої збереження вручну.
settings.icon=Іконка
@@ -1401,7 +1375,6 @@ version.manage=Усі екземпляри
version.manage.clean=Видалити файли журналів
version.manage.clean.tooltip=Видалити файли в каталогах "logs" та "crash-reports".
version.manage.duplicate=Дублювати екземпляр
version.manage.duplicate.duplicate_save=Дублювати збереження
version.manage.duplicate.prompt=Введіть нову назву екземпляра
version.manage.duplicate.confirm=Дубльований екземпляр матиме копію всіх файлів у каталозі екземпляра (".minecraft/versions/<назва екземпляра>"), з ізольованим робочим каталогом та налаштуваннями.
version.manage.manage=Редагувати екземпляр
@@ -1409,7 +1382,6 @@ version.manage.manage.title=Редагувати екземпляр - %1s
version.manage.redownload_assets_index=Оновити ресурси гри
version.manage.remove=Видалити екземпляр
version.manage.remove.confirm.trash=Ви впевнені, що хочете видалити екземпляр "%s"? Ви все ще можете знайти його файли у своєму кошику за назвою "%s".
version.manage.remove.confirm.independent=Оскільки цей екземпляр зберігається в ізольованому каталозі, його видалення також видалить його збереження та інші дані. Ви все ще хочете видалити екземпляр "%s"?
version.manage.remove_assets=Видалити всі ресурси
version.manage.remove_libraries=Видалити всі бібліотеки
version.manage.rename=Перейменувати екземпляр
@@ -117,12 +117,13 @@ account.methods.microsoft.error.wrong_verify_method=登入失敗。請在 Micros
account.methods.microsoft.logging_in=登入中……
account.methods.microsoft.makegameidsettings=建立檔案 / 編輯檔案名稱
account.methods.microsoft.hint=點擊「登入」按鈕開始新增 Microsoft 帳戶。
account.methods.microsoft.manual=請在彈出的網頁中輸入上方顯示的代碼以完成登入。\n\
若網站未能顯示,請手動在瀏覽器中開啟:%s\n\
<b>若網路環境不佳,可能會導致網頁載入緩慢甚至無法載入,請稍後再試或更換網路環境後再試。</b>\n
account.methods.microsoft.profile=編輯帳戶個人信息
account.methods.microsoft.methods.device=掃描 QR Code 登入
account.methods.microsoft.methods.device.hint=掃描 QR Code 或訪問 <a href="%s">%s</a>,在開啟的頁面中輸入 <b>%s</b> 完成登入。
account.methods.microsoft.methods.browser=在瀏覽器中登入
account.methods.microsoft.methods.browser.hint=點擊「登入」按鈕或者<a href="%s">複製連結</a>並在瀏覽器中貼上以登入。
account.methods.microsoft.manual=<b>若網路環境不佳,可能會導致網頁載入緩慢甚至無法載入,請稍後再試或更換網路環境後再試。</b>
account.methods.microsoft.profile=編輯帳戶個人資訊
account.methods.microsoft.purchase=購買 Minecraft
account.methods.forgot_password=忘記密碼
account.methods.ban_query=查詢帳戶是否被封禁
account.methods.microsoft.snapshot=你正在使用第三方提供的 HMCL。請下載 <a href="https://hmcl.huangyuhui.net/download">官方版本</a> 進行登入。
account.methods.microsoft.snapshot.tooltip=你正在使用第三方提供的 HMCL。請下載官方版本來重新整理帳戶。
@@ -362,7 +363,6 @@ download.provider.balanced.desc=平衡,但可能不是最新
download.provider.mirror=盡量使用鏡像源
download.provider.mirror.desc=載入快,但可能不是最新
download.java=下載 Java
download.java.override=此 Java 版本已經存在,是否移除並重新安裝?
download.java.process=下載 Java
download.javafx=正在下載必要的執行時元件
download.javafx.notes=正在透過網路下載 HMCL 必要的執行時元件。\n點擊「切換下載源」按鈕查看詳情以及選取下載源。點擊「取消」按鈕停止並退出。\n注意:如果下載速度過慢,請嘗試切換下載源。
@@ -380,12 +380,18 @@ exception.ssl_handshake=無法建立 SSL 連線。目前 Java 缺少相關的 SS
exception.dns.pollution=無法建立 SSL 連線。可能是 DNS 解析有誤。請嘗試更換 DNS 伺服器或使用代理服務。
extension.bat=Windows 批次檔
extension.mod=模組檔案
extension.png=圖片檔案
extension.ps1=PowerShell 指令碼
extension.sh=Bash 指令碼
extension.command=macOS Shell 指令碼
extension.datapack=資料包壓縮檔
extension.mod=模組檔案
extension.modloader.installer=模組載入器安裝檔
extension.resourcepack=資源包壓縮檔
extension.schematic=原理圖檔案
extension.world=世界壓縮檔
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher 無法建立 HMCL 資料夾 (%s),請將 HMCL 移動至其他位置再開啟。
fatal.javafx.incompatible=缺少 JavaFX 執行環境。\nHMCL 無法在低於 Java 11 的 Java 環境上自行補全 JavaFX 執行環境。請更新到 Java 11 或更高版本。
fatal.javafx.incomplete=JavaFX 執行環境不完整。請嘗試更換你的 Java 或者重新安裝 OpenJFX。
@@ -394,7 +400,7 @@ fatal.config_change_owner_root=你正在使用 root 帳戶開啟 Hello Minecraft
fatal.config_in_temp_dir=你正在暫存目錄中開啟 Hello Minecraft! Launcher,你的設定和遊戲資料可能會遺失。建議將 HMCL 移動至其他位置再開啟。\n是否繼續開啟?
fatal.config_loading_failure=Hello Minecraft! Launcher 無法載入設定檔案。\n請確保 HMCL 對「%s」目錄及該目錄下的檔案擁有讀寫權限。
fatal.config_loading_failure.unix=Hello Minecraft! Launcher 無法載入設定檔案,因為設定檔案是由使用者「%1$s」建立的。\n請使用 root 帳戶開啟 HMCL (不推薦),或在終端中執行以下指令將設定檔案的所有權變更為目前使用者:\n%2$s
fatal.config_unsupported_version=目前設定檔案是由更高版本的 Hello Minecraft! Launcher 建立的,目前版本的 HMCL 無法正常載入。請更新並重新啟動 HMCL。\n在更新啟動器之前,你所做的所有設定更改都不會被保存。
fatal.config_unsupported_version=目前設定檔案是由更高版本的 Hello Minecraft! Launcher 建立的,目前版本的 HMCL 無法正常載入。請更新並重新啟動 HMCL。\n在更新啟動器之前,你所做的所有設定更改都不會被儲存。
fatal.mac_app_translocation=由於 macOS 的安全機制,Hello Minecraft! Launcher 被系統隔離至暫存目錄中。\n請將 HMCL 移動到其他目錄後再嘗試開啟,否則你的設定和遊戲資料可能會在重啟後遺失。\n是否繼續開啟?
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即將升級完成,請重新開啟 HMCL。
fatal.apply_update_failure=我們很抱歉 Hello Minecraft! Launcher 無法自動完成升級程式,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 HMCL 來完成升級。
@@ -429,13 +435,13 @@ game.crash.info=遊戲訊息
game.crash.reason=崩潰原因
game.crash.reason.analyzing=分析中……
game.crash.reason.multiple=檢測到多個原因:\n\n
game.crash.reason.block=目前遊戲由於某個方塊不能正常工作,無法繼續執行。\n你可以嘗試使用 MCEdit 工具編輯世界刪除該方塊,或者直接刪除對應的模組。\n方塊類型:%1$s\n方塊坐標:%2$s
game.crash.reason.block=目前遊戲由於某個方塊不能正常工作,無法繼續執行。\n你可以嘗試使用地圖編輯工具編輯世界刪除該方塊,或者直接刪除對應的模組。\n方塊類型:%1$s\n方塊坐標:%2$s\n\n相關工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-tw#地图编辑工具">中文 Wiki</a>
game.crash.reason.bootstrap_failed=目前遊戲由於模組「%1$s」出現問題,無法繼續執行。\n你可以嘗試刪除或更新該模組以解決問題。
game.crash.reason.mixin_apply_mod_failed=目前遊戲由於 Mixin 無法應用於「%1$s」模組,無法繼續執行。\n你可以嘗試刪除或更新該模組以解決問題。
game.crash.reason.config=目前遊戲由於無法解析模組配置檔案,無法繼續執行\n模組「%1$s」的配置檔案「%2$s」無法被解析。
game.crash.reason.debug_crash=目前遊戲由於手動觸發崩潰,無法繼續執行。\n事實上遊戲並沒有問題,問題都是你造成的!
game.crash.reason.duplicated_mod=目前遊戲由於模組重複安裝,無法繼續執行。\n%s\n每種模組只能安裝一個,請你刪除多餘的模組再試。
game.crash.reason.entity=目前遊戲由於某個實體不能正常工作,無法繼續執行。\n你可以嘗試使用 MCEdit 工具編輯世界刪除該實體,或者直接刪除對應的模組。\n實體類型:%1$s\n實體坐標:%2$s
game.crash.reason.entity=目前遊戲由於某個實體不能正常工作,無法繼續執行。\n你可以嘗試使用地圖編輯工具編輯世界刪除該實體,或者直接刪除對應的模組。\n實體類型:%1$s\n實體坐標:%2$s\n\n相關工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-tw#地图编辑工具">中文 Wiki</a>
game.crash.reason.fabric_version_0_12=Fabric Loader 0.12 及更高版本與目前已經安裝的模組可能不相容,你需要將 Fabric Loader 降級至 0.11.7。
game.crash.reason.fabric_warnings=Fabric 提供了一些警告訊息:\n%1$s
game.crash.reason.modmixin_failure=目前遊戲由於某些模組注入失敗,無法繼續執行。\n這一般代表著該模組存在問題,或與目前環境不相容。\n你可以查看日誌尋找出錯模組。
@@ -533,10 +539,9 @@ install.installer.game=Minecraft
install.installer.incompatible=與 %s 不相容
install.installer.install=安裝 %s
install.installer.install_offline=從本機檔案安裝或升級
install.installer.install_offline.extension=(Neo)Forge/Cleanroom/OptiFine 安裝器
install.installer.install_offline.tooltip=支援匯入已經下載好的 (Neo)Forge、Cleanroom 及 OptiFine 安裝器
install.installer.install_online=線上安裝
install.installer.install_online.tooltip=支援安裝 Forge、NeoForge、Cleanroom、Fabric、Quilt、LiteLoader 和 OptiFine
install.installer.install_online.tooltip=支援安裝 Forge、NeoForge、Cleanroom、Fabric、Legacy Fabric、Quilt、LiteLoader 和 OptiFine
install.installer.liteloader=LiteLoader
install.installer.not_installed=未安裝
install.installer.optifine=OptiFine
@@ -556,7 +561,7 @@ install.new_game.malformed=名稱無效
install.select=請選取安裝方式
install.success=安裝成功
java.add=添加 Java
java.add=新增 Java
java.add.failed=Java 無效或與目前平臺不相容
java.disable=停用此 Java
java.disable.confirm=你確定要停用此 Java 嗎?
@@ -567,6 +572,7 @@ java.download=下載 Java
java.download.banshanjdk-8=下載 Banshan JDK 8
java.download.load_list.failed=載入版本清單失敗
java.download.more=更多發行版
java.download.title=下載 Java
java.download.prompt=請選擇你要下載的 Java 版本:
java.download.distribution=發行版
java.download.version=版本
@@ -855,6 +861,7 @@ mods=模組
mods.add=新增模組
mods.add.failed=新增模組「%s」失敗。
mods.add.success=成功新增模組「%s」。
mods.add.title=選取要新增的模組檔案
mods.broken_dependency.title=損壞的相依模組
mods.broken_dependency.desc=該相依模組曾經存在於模組倉庫中,但現在已被刪除,請嘗試其他下載源。
mods.category=類別
@@ -871,7 +878,6 @@ mods.check_updates.failed_download=部分檔案下載失敗
mods.check_updates.file=檔案
mods.check_updates.source=來源
mods.check_updates.target_version=目標版本
mods.choose_mod=選取模組
mods.curseforge=CurseForge
mods.dependency.embedded=內建相依模組 (作者已經打包在模組檔中,無需單獨下載)
mods.dependency.optional=可選相依模組 (如果不安裝,遊戲可以執行,但模組功能可能缺失)
@@ -893,7 +899,7 @@ mods.mcmod.page=MC 百科頁面
mods.mcmod.search=MC 百科檢索
mods.modrinth=Modrinth
mods.name=名稱
mods.not_modded=你需要先在「自動安裝」頁面安裝 Forge、NeoForge、Fabric、Quilt 或 LiteLoader 才能管理模組。
mods.not_modded=你需要先在「自動安裝」頁面安裝 Forge、NeoForge、Fabric、Legacy Fabric、Quilt 或 LiteLoader 才能管理模組。
mods.restore=回退
mods.url=官方頁面
mods.update_modpack_mod.warning=更新模組包中的模組可能導致模組包損壞,使模組包無法正常啟動。該操作不可逆,確定要更新嗎?
@@ -917,8 +923,7 @@ nbt.title=查看檔案 - %s
datapack=資料包
datapack.add=新增資料包
datapack.choose_datapack=選取要匯入的資料包壓縮檔
datapack.extension=資料包
datapack.add.title=選取要新增的資料包壓縮檔
datapack.reload.toast=Minecraft 正在執行,請使用 /reload 指令重新載入資料包
datapack.title=世界 [%s] - 資料包
@@ -928,6 +933,10 @@ web.view_in_browser=在瀏覽器中查看完整日誌
world=世界
world.add=新增世界
world.add.already_exists=此世界已經存在
world.add.failed=無法新增此世界: %s
world.add.invalid=無法識別的世界壓縮檔
world.add.title=選取要新增的世界壓縮檔
world.backup=世界備份
world.backup.create.new_one=建立新備份
world.backup.create.failed=建立備份失敗。\n%s
@@ -956,7 +965,6 @@ world.export=匯出此世界
world.export.title=選取該世界的儲存位置
world.export.location=儲存到
world.export.wizard=匯出世界「%s」
world.extension=世界壓縮檔
world.icon=世界圖示
world.icon.change=修改世界圖示
world.icon.change.fail.load.title=圖片解析失敗
@@ -966,10 +974,6 @@ world.icon.change.fail.not_64x64.text=該圖片的解析度為 %d×%d,而不
world.icon.change.succeed.toast=世界圖示修改成功
world.icon.change.tip=請提供一張 64×64 PNG 格式的圖片。錯誤解析度的圖片將無法被 Minecraft 解析。
world.icon.choose.title=選擇世界圖示
world.import.already_exists=此世界已經存在
world.import.choose=選取要匯入的世界壓縮檔
world.import.failed=無法匯入此世界: %s
world.import.invalid=無法識別的世界壓縮檔
world.info=世界資訊
world.info.basic=基本資訊
world.info.allow_cheats=允許指令(作弊)
@@ -1035,6 +1039,7 @@ repositories.chooser.title=選取 JavaFX 下載源
resourcepack=資源包
resourcepack.add=新增資源包
resourcepack.add.title=選取要新增的資源包壓縮檔
resourcepack.manage=資源包管理
resourcepack.download=下載資源包
resourcepack.add.failed=新增資源包失敗
@@ -1044,8 +1049,9 @@ resourcepack.download.title=資源包下載 - %1s
reveal.in_file_manager=在檔案管理員中查看
schematics=原理圖
schematics.add=添加原理圖
schematics.add.failed=添加原理圖失敗
schematics.add=新增原理圖
schematics.add.failed=新增原理圖失敗
schematics.add.title=選取要新增的原理圖檔案
schematics.back_to=返回到「%s」
schematics.create_directory=建立目錄
schematics.create_directory.prompt=請輸入新目錄名稱
@@ -1267,7 +1273,7 @@ terracotta.back=退出
terracotta.feedback.title=填寫回饋表
terracotta.feedback.desc=在 HMCL 更新聯機核心時,我們歡迎您用 10 秒時間填寫聯機品質回饋收集表。
terracotta.sudo_installing=HMCL 需要驗證您的密碼才能安裝線上核心
terracotta.difficulty.easiest=當前網路狀況極佳:稍等一下就成功!
terracotta.difficulty.easiest=目前網路狀況極佳:稍等一下就成功!
terracotta.difficulty.simple=目前網路狀況較好:建立連線需要一段時間…
terracotta.difficulty.medium=目前網路狀態中等:已啟用抗干擾備用線路,連線可能失敗
terracotta.difficulty.tough=目前網路狀態極差:已啟用抗干擾備用線路,連線可能失敗
@@ -1277,7 +1283,7 @@ terracotta.from_local.desc=在部分地區,內建的預設下載管道可能
terracotta.from_local.guide=您應下載名為 %s 的線上核心套件。下載完成後,請將檔案拖曳到目前介面來安裝。
terracotta.from_local.file_name_mismatch=您應該下載名為 %1$s 的線上核心包,而非 %2$s
terracotta.export_log=匯出線上核心日誌
terracotta.export_log.desc=為分析錯誤提供更多信息
terracotta.export_log.desc=為分析錯誤提供更多資訊
terracotta.status.bootstrap=正在收集資訊
terracotta.status.uninitialized.not_exist=未下載聯機核心
terracotta.status.uninitialized.not_exist.title=下載聯機核心 (約 8MiB)
@@ -1285,8 +1291,8 @@ terracotta.status.uninitialized.update=需更新聯機核心
terracotta.status.uninitialized.update.title=更新聯機核心 (約 8MiB)
terracotta.status.uninitialized.desc=您承諾,在多人聯機全過程中,您將嚴格遵守您所在國家或地區的全部法律法規
terracotta.confirm.title=使用者須知
terracotta.confirm.desc=陶瓦聯機是第三方開源自由軟體,您在使用過程中所遇到的問題請通過相關管道進行回饋。\n\
陶瓦聯機使用 P2P 技術,聯機成功後房間內用戶之間將直接連接,不會使用第三方伺服器對您的流量進行轉發。最終聯機體驗和參與聯機者的網路情況有較大關係。\n\
terracotta.confirm.desc=陶瓦聯機是第三方開源自由軟體,您在使用過程中所遇到的問題請透過相關管道進行回饋。\n\
陶瓦聯機使用 P2P 技術,連線成功後房間內使用者之間將直接連接,不會使用第三方伺服器對您的流量進行轉發。最終聯機體驗和參與連線者的網路情況有較大關係。\n\
在多人聯機全過程中,您必須嚴格遵守您所在國家與地區的全部法律法規。
terracotta.status.preparing=正在下載聯機核心 (請勿退出啟動器)
terracotta.status.launching=正在初始化聯機核心
@@ -118,13 +118,14 @@ account.methods.microsoft.error.wrong_verify_method=登录失败。请在微软
account.methods.microsoft.logging_in=登录中……
account.methods.microsoft.makegameidsettings=创建档案 / 编辑档案名称
account.methods.microsoft.hint=点击“登录”按钮开始添加微软账户。
account.methods.microsoft.manual=请在弹出的网页中输入上方显示的代码以完成登录。\n\
若网站未能显示,请手动在浏览器中打开:%s\n\
<b>若网络环境不佳,可能会导致网页加载缓慢甚至无法加载,请使用网络代理并重试。</b>\n\
account.methods.microsoft.methods.device=扫描二维码登录
account.methods.microsoft.methods.device.hint=扫描二维码或访问 <a href="%s">%s</a>,在打开的页面中输入 <b>%s</b> 完成登录。
account.methods.microsoft.methods.browser=在浏览器登录
account.methods.microsoft.methods.browser.hint=点击“登录”按钮或者<a href="%s">复制链接</a>并在浏览器中粘贴以登录。
account.methods.microsoft.manual=<b>若网络环境不佳,可能会导致网页加载缓慢甚至无法加载,请使用网络代理并重试。</b>\n\
如遇到问题,你可以点击右上角帮助按钮进行求助。
account.methods.microsoft.profile=编辑账户个人信息
account.methods.microsoft.purchase=购买 Minecraft
account.methods.forgot_password=忘记密码
account.methods.ban_query=检测账户是否被封禁
account.methods.microsoft.snapshot=你正在使用第三方提供的 HMCL。请下载 <a href="https://hmcl.huangyuhui.net/download">官方版本</a> 来登录微软账户。
account.methods.microsoft.snapshot.tooltip=你正在使用第三方提供的 HMCL。请下载官方版本来刷新账户。
@@ -364,7 +365,6 @@ download.provider.balanced.desc=平衡,但可能不是最新
download.provider.mirror=尽量使用镜像源
download.provider.mirror.desc=加载快,但可能不是最新
download.java=下载 Java
download.java.override=此 Java 版本已经存在,是否卸载并重新安装?
download.java.process=下载 Java
download.javafx=正在下载必要的运行时组件……
download.javafx.notes=正在通过网络下载 HMCL 必要的运行时组件。\n点击“切换下载源”按钮查看详情以及选择下载源。点击“取消”按钮停止并退出。\n注意:若下载速度过慢,请尝试切换下载源。
@@ -383,12 +383,18 @@ exception.ssl_handshake=无法建立 SSL 连接。当前 Java 缺少相关的 SS
exception.dns.pollution=无法建立 SSL 连接。可能是 DNS 解析有误。请尝试更换 DNS 服务器或使用代理服务。\n你可以点击右上角帮助按钮进行求助。
extension.bat=Windows 脚本
extension.mod=模组文件
extension.png=图片文件
extension.ps1=PowerShell 脚本
extension.sh=Bash 脚本
extension.command=macOS Shell 脚本
extension.datapack=数据包压缩包
extension.mod=模组文件
extension.modloader.installer=模组加载器安装包
extension.resourcepack=资源包压缩包
extension.schematic=原理图文件
extension.world=世界压缩包
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher 无法创建 HMCL 文件夹 (%s),请将 HMCL 移动至其他位置再启动。\n如遇到问题,你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.javafx.incompatible=缺少 JavaFX 运行环境。\nHello Minecraft! Launcher 无法在低于 Java 11 的 Java 环境上自行补全 JavaFX 运行环境。请更新到 Java 11 或更高版本。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.javafx.incomplete=JavaFX 运行环境不完整。请尝试更换你的 Java 或者重新安装 OpenJFX。
@@ -432,14 +438,14 @@ game.crash.feedback=<b>请不要将本界面截图或拍照给他人!</b>如
game.crash.info=游戏信息
game.crash.reason=崩溃原因
game.crash.reason.analyzing=分析中……
game.crash.reason.block=当前游戏由于某个方块不能正常工作,无法继续运行。\n你可以尝试使用 <a href="https://www.mcedit.net">MCEdit</a> 工具编辑世界删除该方块,或者直接删除对应的模组。\n方块类型:%1$s\n方块坐标:%2$s
game.crash.reason.block=当前游戏由于某个方块不能正常工作,无法继续运行。\n你可以尝试使用地图编辑工具编辑世界删除该方块,或者直接删除对应的模组。\n方块类型:%1$s\n方块坐标:%2$s\n\n相关工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-cn#地图编辑工具">中文 Wiki</a>
game.crash.reason.bootstrap_failed=当前游戏由于模组“%1$s”出现问题,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
game.crash.reason.mixin_apply_mod_failed=当前游戏由于 Mixin 无法应用于“%1$s”模组,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
game.crash.reason.config=当前游戏由于无法解析模组配置文件,无法继续运行。\n无法解析模组“%1$s”的配置文件“%2$s”。
game.crash.reason.multiple=检测到多个原因:\n\n
game.crash.reason.debug_crash=当前游戏由于手动触发崩溃,无法继续运行。\n事实上游戏并没有问题,问题都是你造成的!
game.crash.reason.duplicated_mod=当前游戏由于模组“%1$s”重复安装,无法继续运行。\n%2$s\n每种模组只能安装一个,请你删除多余的模组再试。
game.crash.reason.entity=当前游戏由于某个实体不能正常工作,无法继续运行。\n你可以尝试使用 <a href="https://www.mcedit.net">MCEdit</a> 工具编辑世界删除该实体,或者直接删除对应的模组。\n实体类型:%1$s\n实体坐标:%2$s
game.crash.reason.entity=当前游戏由于某个实体不能正常工作,无法继续运行。\n你可以尝试使用地图编辑工具编辑世界删除该实体,或者直接删除对应的模组。\n实体类型:%1$s\n实体坐标:%2$s\n\n相关工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-cn#地图编辑工具">中文 Wiki</a>
game.crash.reason.fabric_version_0_12=Fabric Loader 0.12 及更高版本与当前已经安装的模组可能不兼容,你需要将 Fabric Loader 降级至 0.11.7。
game.crash.reason.fabric_warnings=Fabric 提供了一些警告信息:\n%1$s
game.crash.reason.file_already_exists=当前游戏由于文件“%1$s”已经存在,无法继续运行。\n如果你认为这个文件可以删除,你可以在备份这个文件后尝试删除它,并重新启动游戏。
@@ -538,10 +544,9 @@ install.installer.game=Minecraft
install.installer.incompatible=与 %s 不兼容
install.installer.install=安装 %s
install.installer.install_offline=从本地文件安装/升级
install.installer.install_offline.extension=(Neo)Forge/Cleanroom/OptiFine 安装器
install.installer.install_offline.tooltip=支持导入已经下载好的 (Neo)Forge、Cleanroom 和 OptiFine 安装器
install.installer.install_online=在线安装
install.installer.install_online.tooltip=支持安装 Forge、NeoForge、Cleanroom、Fabric、Quilt、LiteLoader 和 OptiFine
install.installer.install_online.tooltip=支持安装 Forge、NeoForge、Cleanroom、Fabric、Legacy Fabric、Quilt、LiteLoader 和 OptiFine
install.installer.liteloader=LiteLoader
install.installer.not_installed=未安装
install.installer.optifine=OptiFine
@@ -572,6 +577,7 @@ java.download=下载 Java
java.download.banshanjdk-8=下载 Banshan JDK 8
java.download.load_list.failed=加载版本列表失败
java.download.more=更多发行版
java.download.title=下载 Java
java.download.prompt=请选择你要下载的 Java 版本:
java.download.distribution=发行版
java.download.version=版本
@@ -860,6 +866,7 @@ mods=模组
mods.add=添加模组
mods.add.failed=添加模组“%s”失败。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
mods.add.success=成功添加模组 %s。
mods.add.title=选择要添加的模组文件
mods.broken_dependency.title=损坏的前置模组
mods.broken_dependency.desc=该前置模组曾经在该模组仓库上存在过,但现在被删除了。换个下载源试试吧。
mods.category=类别
@@ -876,7 +883,6 @@ mods.check_updates.failed_download=部分文件下载失败
mods.check_updates.file=文件
mods.check_updates.source=来源
mods.check_updates.target_version=目标版本
mods.choose_mod=选择模组
mods.curseforge=CurseForge
mods.dependency.embedded=内置的前置模组 (已经由作者打包在模组文件中,无需另外下载)
mods.dependency.optional=可选的前置模组 (若缺失游戏能够正常运行,但模组功能可能缺失)
@@ -898,7 +904,7 @@ mods.mcmod.page=MC 百科页面
mods.mcmod.search=MC 百科搜索
mods.modrinth=Modrinth
mods.name=名称
mods.not_modded=你需要先在“自动安装”页面安装 Forge、NeoForge、Fabric、Quilt 或 LiteLoader 才能管理模组。
mods.not_modded=你需要先在“自动安装”页面安装 Forge、NeoForge、Fabric、Legacy Fabric、Quilt 或 LiteLoader 才能管理模组。
mods.restore=回退
mods.url=官方页面
mods.update_modpack_mod.warning=更新整合包中的模组可能导致整合包损坏,使整合包无法正常启动。该操作不可逆,确定要更新吗?
@@ -922,8 +928,7 @@ nbt.title=查看文件 - %s
datapack=数据包
datapack.add=添加数据包
datapack.choose_datapack=选择要导入的数据包压缩包
datapack.extension=数据包
datapack.add.title=选择要添加的数据包压缩包
datapack.reload.toast=Minecraft 正在运行,请使用 /reload 命令重新加载数据包
datapack.title=世界 [%s] - 数据包
@@ -933,6 +938,10 @@ web.view_in_browser=在浏览器中查看完整日志
world=世界
world.add=添加世界
world.add.already_exists=此世界已经存在
world.add.failed=无法添加此世界:%s
world.add.invalid=无法识别该世界压缩包
world.add.title=选择要添加的世界压缩包
world.backup=备份管理
world.backup.create.new_one=创建新备份
world.backup.create.failed=创建备份失败。\n%s
@@ -961,7 +970,6 @@ world.export=导出此世界
world.export.title=选择该世界的存储位置
world.export.location=保存到
world.export.wizard=导出世界“%s”
world.extension=世界压缩包
world.game_version=游戏版本
world.icon=世界图标
world.icon.change=修改世界图标
@@ -972,10 +980,6 @@ world.icon.change.fail.not_64x64.text=该图片的分辨率为 %d×%d,而不
world.icon.change.succeed.toast=世界图标修改成功
world.icon.change.tip=请提供一张 64×64 PNG 格式的图片。错误分辨率的图片将无法被 Minecraft 解析。
world.icon.choose.title=选择世界图标
world.import.already_exists=此世界已经存在
world.import.choose=选择要导入的世界压缩包
world.import.failed=无法导入此世界:%s
world.import.invalid=无法识别该世界压缩包
world.info=世界信息
world.info.basic=基本信息
world.info.allow_cheats=允许命令(作弊)
@@ -1040,6 +1044,7 @@ repositories.chooser.title=选择 JavaFX 下载源
resourcepack=资源包
resourcepack.add=添加资源包
resourcepack.add.title=选择要添加的资源包压缩包
resourcepack.manage=资源包管理
resourcepack.download=下载资源包
resourcepack.add.failed=添加资源包失败
@@ -1051,6 +1056,7 @@ reveal.in_file_manager=在文件管理器中查看
schematics=原理图
schematics.add=添加原理图
schematics.add.failed=添加原理图失败
schematics.add.title=选择要添加的原理图文件
schematics.back_to=返回到“%s”
schematics.create_directory=创建文件夹
schematics.create_directory.prompt=请输入新文件夹名称
@@ -19,20 +19,135 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<html lang="%lang%">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello Minecraft! Launcher</title>
<style>
%style%
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: var(--monet-surface);
color: var(--monet-on-surface);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
user-select: none;
cursor: default;
}
.dialog-card {
background-color: var(--monet-surface-container-high);
padding: 24px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.14), 0 0 0 1px rgba(0, 0, 0, 0.02);
max-width: 560px;
width: 90%;
display: flex;
flex-direction: row;
gap: 16px;
}
.icon-column {
flex-shrink: 0;
padding-top: 4px;
}
.icon-column svg {
width: 40px;
height: 40px;
display: block;
fill: var(--monet-on-surface);
}
.content-column {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.title {
font-size: 20px;
line-height: 1.4;
font-weight: 500;
color: var(--monet-on-surface);
margin-bottom: 8px;
}
.subtitle {
font-size: 14px;
line-height: 1.5;
color: var(--monet-on-surface-variant);
word-wrap: break-word;
margin-bottom: 24px;
}
.actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.btn-confirm {
background-color: transparent;
color: var(--monet-primary);
border: none;
padding: 0 16px;
height: 40px;
font-size: 14px;
font-weight: 500;
font-family: inherit;
cursor: pointer;
transition: background-color 0.2s;
outline: none;
min-width: 64px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-confirm:hover {
background-color: rgba(128, 128, 128, 0.08);
}
.btn-confirm:active {
background-color: rgba(128, 128, 128, 0.12);
}
</style>
</head>
<body>
<div>
%close-page%
<div class="dialog-card">
<div class="icon-column">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="m10.6 16.6l7.05-7.05l-1.4-1.4l-5.65 5.65l-2.85-2.85l-1.4 1.4zM12 22q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8"/>
</svg>
</div>
<script>
setTimeout(function() {
open("about:blank","_self").close();
}, 5000);
</script>
</body>
<div class="content-column">
<div class="title">%success%</div>
<div class="subtitle">%close_page%</div>
<div class="actions">
<button class="btn-confirm" onclick="closeWindow()">%ok%</button>
</div>
</div>
</div>
<script>
function closeWindow() {
window.close();
window.open("about:blank", "_self").close();
}
setTimeout(closeWindow, 3000);
</script>
</body>
</html>
+21 -21
View File
@@ -1,63 +1,63 @@
{
"__comment__": "THIS FILE IS MACHINE GENERATED! DO NOT EDIT!",
"version_latest": "0.4.1",
"version_latest": "0.4.2",
"packages": {
"windows-x86_64": {
"hash": "4693fec29bb54a0bb1a1a8a263a935f1673b8f76e84d125974cf259f1912a195beab4dfd04c23cae592cf71b116e82ecd48828b1445ab75c980c8cd79c777d21",
"hash": "6a98f524d4f00373696517306af8aa50d01d55ce4eadb27e9e4bc2f882707a0b5f20d5d4c33371d1459dcf5bf144ffed9beb414202d9ccf32b11dbbfcf19d650",
"files": {
"VCRUNTIME140.DLL": "3d4b24061f72c0e957c7b04a0c4098c94c8f1afb4a7e159850b9939c7210d73398be6f27b5ab85073b4e8c999816e7804fef0f6115c39cd061f4aaeb4dcda8cf",
"terracotta-0.4.1-windows-x86_64.exe": "3d29f7deb61b8b87e14032baecad71042340f6259d7ff2822336129714470e1a951cd96ee5d6a8c9312e7cf1bb9bb9d1cbf757cfa44d2b2f214362c32ea03b5b"
"terracotta-0.4.2-windows-x86_64.exe": "6e98d1f2380ed22fb5a2dd4aafce6c773e9cf69100c8bb8e49e7d6983756bdb9a31f80e06bcfbe5a2742144fe806d3d687dec54d8f09d87c659341f99dd9fd80"
}
},
"windows-arm64": {
"hash": "6c68da53142cc92598a9d3b803f110c77927ee7b481e2f623dfab17cd97fee4a58378e796030529713d3e394355135cc01d5f5d86cef7dbd31bbf8e913993d4c",
"hash": "fc1077247014ac0c712469498bde2ef7f6d881d5fcb7bdd5e11ebe20218fed365be19afdb8d453a79d77b729f866058522b910741767f4df947faa891434b463",
"files": {
"VCRUNTIME140.DLL": "5cb5ce114614101d260f4754c09e8a0dd57e4da885ebb96b91e274326f3e1dd95ed0ade9f542f1922fad0ed025e88a1f368e791e1d01fae69718f0ec3c7b98c8",
"terracotta-0.4.1-windows-arm64.exe": "d2cf0f29aac752d322e594da6144bbe2874aabde52b5890a6f04a33b77a377464cbf3367e40de6b046c7226517f48e23fd40e611198fcaa1f30503c36d99b20c"
"terracotta-0.4.2-windows-arm64.exe": "30a15c5c53e5817c5a3634532172559327474741d3b2c7ef4e8a30acc6f59cdcf3570bf5f583e3cbe9e2abc8253e977c1abda1e9f36c88c4e99240da257347d0"
}
},
"macos-x86_64": {
"hash": "c247ab9023cf47231f3a056ddf70fe823e504c19ce241bf264c0a3cf2981c044b236dc239f430afb2541445d1b61d18898f8af5e1c063f31fb952bdfbea4aff5",
"hash": "a762e4b2d6f84e899292b9e3856d009411a516d3c47f54575f843ce082f63dff2baa68ba0faa844b8b64fb12e91017386f15f5e7f975f8ee605bf8d4217cb091",
"files": {
"terracotta-0.4.1-macos-x86_64": "dd2cde70a950849498425b8b00c17fb80edb8dd9bc0312da4885d97a28a4959d87782bd2202ef7f71bdbf2a92609089585c9e3faf24c10df6150e221e111a764",
"terracotta-0.4.1-macos-x86_64.pkg": "6057d5b4ea93da45796277a20ddaea7850e8c66326ded769f20fff74e132b453d6d615d937fc8f1d62b0b36b26961df5afb76535aecf85f25d2160889225ac6d"
"terracotta-0.4.2-macos-x86_64": "24efb85390eff88a538ed7e503fb1488e5e622730ca30c741a0e8b4c8f4e8d4868a2f9f38da8de540aeb535af2fd1e41c7081dae9c700e8a1a03b6c540218164",
"terracotta-0.4.2-macos-x86_64.pkg": "0f80437061231018ec0f860f875aba02cae9e0b36d21f2db8e99d25948806e1119f7844a4d4acca01d6124d7079718762cdebe3756b005a55632de7029fe0d24"
}
},
"macos-arm64": {
"hash": "0ddf48a44ea2563c37707caea8381ad3c69da73dd443d07dd98fe630f4e24ccda6f1fcdc9207a66bf61758b887a7b47186883ccd12bcb6b68387d8d756873f44",
"hash": "09e444fea2d9fd19f3e5cb62e29055228345be163924cbd408d947646fafed1012cf48508ee6a155ede3d571e2ffaa72d09ceeb1493c8a60feb05e0699f19ba3",
"files": {
"terracotta-0.4.1-macos-arm64": "ddc335ee082b4c0323314d482933810fc2d377cfc131a9aa978b54f245f1bed8973139edf7cf94f7ae6c04660dfe18d275b1a81638781e482b4ff585f351eed9",
"terracotta-0.4.1-macos-arm64.pkg": "5cbb41a450f080234b66fa264351bd594e3f6ef1599c96f277baa308e34dd26caefa3a34b3d65e972bc20868f2d4a661e8b3910d4b0311a374c6ac680bdccf8f"
"terracotta-0.4.2-macos-arm64": "8e59a9d78acd57702dc044d6f2799c6af586b075a262f7d4dbbf0876e1af8d8271e04783c24ff820b801e3b14cd0190ab8403d097f3f2d98b6d911f95ed1e972",
"terracotta-0.4.2-macos-arm64.pkg": "b0b72f8883767c359f0700e958bd859dd5932c4674fbdf2a32f7de189fb9f54822d4825a754dc856416c8760338eb8ffca80b40c0a57c608fbe6ae9928888993"
}
},
"linux-x86_64": {
"hash": "c00b0622203c1610a8e72bde5230fca6fe56cf1f45dc9bc7e857350f3a050d442c814084758090799b9a5b875730fa666539ee75bec706c05d9338ea208301eb",
"hash": "d326ad95815d04568d485b5038e40ffc47ca54292fa0925eee6f5cea014024f901d661708aac2a743037b990882ad82b4d0b7bb03dc3b2fe720dbf0f3efe1c98",
"files": {
"terracotta-0.4.1-linux-x86_64": "e53a9d8ec085ef7a7816b3da374e5c88fced2cf8319d447be3226422d01f9d7ee2019e403eafe21082135050652b2788b7d9520cc432c8d08931930b99595ed7"
"terracotta-0.4.2-linux-x86_64": "fac328ba8957a711b03557bb913940f22d61b76608cd203fdf51024b6f94b19f5bc91c9b8a9fa80baf6968e1e6873c1880fd4cf54a2f8e3c6cf1e6ac161f8d0c"
}
},
"linux-arm64": {
"hash": "4d661e1123ca050980960fe2d32b8d93a2c3ba807748569a45f16050fb1b52089bfc64c2dd64aba652dfed0e4ac3fba1ff9518cc3f95598c99fc71d93552b589",
"hash": "57c08f48d9535e93ad547d2dfc852d267992cc164a7208b42a2da0a6cbc2f21862f610e02a746b4b67150f4dec26b86a4f96eb9bd2f58d124d5b40ba50c6d55e",
"files": {
"terracotta-0.4.1-linux-arm64": "d4ccf6ff8c2fac060fecaa3c8368677f9342e283f2435493b3e259e922ee0bb0b08f15a292bf91898800a462336c64d0dee7b73f03c1752e5b0988a05adb3b52"
"terracotta-0.4.2-linux-arm64": "d807744c2041c98686e4b505324713badea7a0f31e8810be49ae053a63fb6dfc474ac58d678fb93eea0dd5cccff7372d9ec6135a1046f4b306cad35cd90ecacd"
}
},
"linux-loongarch64": {
"hash": "8297bb37617e9e7ce282fc553c5b14c84a900bcff4d025be31fd4a4da8b3943d040afc6143aa17de9a88e5fa29af7254d38db8ae6418ee539c2301632448da09",
"hash": "a19f4ad4fa1fd2f3c1c867f8743178ab0524d1634d63e122ffc080623e4f14207ecab58830fa0d888696ef28ce4cfa189e6ffbe92c6e1aa1836652617a96a801",
"files": {
"terracotta-0.4.1-linux-loongarch64": "ffdf7582d095307b91ddfc3e5d0daa78d434e4e34916d0cdf1520ae74b188fe5a48307047bf2da9a526eb725fe80cf230a93001bc8199d236b9cf28a1beaa6e9"
"terracotta-0.4.2-linux-loongarch64": "7d4896b275207def0861e544cf2afc9501c1946f1bc8bb2bad9ef4510dd915eabb63eeedff725e69565cdf5c27c146b52dd75d24115d5141909c0865f89cd7b9"
}
},
"linux-riscv64": {
"hash": "092f863885205107525e7ccb0e18977f6fd3018910ca5819772ec741dd8cffee52cc352a44b928bd2ba99ab881adaadff9e3bf4bf283f7384a35fea14becb0b4",
"hash": "ef3dfcf3c64dfcaf94adc518f0184d017a32a924f67ef33ffb8128f20669cf177cd345df3be8c00225a275bd44f70657b94fb43c60afeb21238000ac7a7f1721",
"files": {
"terracotta-0.4.1-linux-riscv64": "a6e5d70ddc433bf804764b69e8c204a6a428ece22b9d8ab713ed339fb81bfa1d29daeb6bdfd62c85ff193396315f96172f4a28925e5a4efc45f7d6fa868782a9"
"terracotta-0.4.2-linux-riscv64": "afddc55f16f5cf3103548d13f7a3238964c8899dba8eb0e1fdc9e354da65e91fa4486de4b8680a7a3d9326eacf18c1ddb6b5b1f9459908f35f77b36f769172ea"
}
},
"freebsd-x86_64": {
"hash": "288bd7a97b2e2c5fb3c7d8107ed1311bc881fb74cf92da40b6c84156239d0832d2306b74b1e93a683188a5db896a6506326c6a7a4ac0ab2e798fa4f1f00787f0",
"hash": "a4d02ebbb1419f4e9265f5b24060432799151a27e0d1871690d2f3a51f40bcfd6d92c176001a15a62caa9bf31ea1b67a7b2da10e3351bfcdd36bfac1377d08d9",
"files": {
"terracotta-0.4.1-freebsd-x86_64": "c90e7db3c5e5cc8d53d8d10a8bf88899e2c418758c14c3d14bc24efc32a711f76882bb5b5a49db5b0256ead4f22b666139bd2a212bf09036d7e7e64ddec4ec3c"
"terracotta-0.4.2-freebsd-x86_64": "a50da02752cef73d21804b26eb02261568ed3053c8fef1074014d50226843810f71e6eda39a53af3bad8e173cec40826631a4c8877af460d14a307f1b0a62519"
}
}
},
@@ -0,0 +1,47 @@
/*
* 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.ui;
import javafx.geometry.Bounds;
import javafx.scene.shape.SVGPath;
import org.junit.jupiter.api.condition.EnabledIf;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
/// @author Glavo
public final class SVGTest {
@ParameterizedTest
@EnumSource(SVG.class)
@EnabledIf("org.jackhuang.hmcl.JavaFXLauncher#isStarted")
public void testViewWindow(SVG svg) {
SVGPath path = new SVGPath();
path.setContent(svg.getPath());
Bounds layoutBounds = path.getLayoutBounds();
assertEquals(
new ViewBox(0, 0, 24, 24),
new ViewBox(layoutBounds.getMinX(), layoutBounds.getMinY(), layoutBounds.getWidth(), layoutBounds.getHeight())
);
}
private record ViewBox(double minX, double minY, double width, double height) {
}
}
@@ -24,12 +24,16 @@ import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class OAuth {
public static final OAuth MICROSOFT = new OAuth(
@@ -77,17 +81,31 @@ public class OAuth {
private Result authenticateAuthorizationCode(Options options) throws IOException, InterruptedException, JsonParseException, ExecutionException, AuthenticationException {
Session session = options.callback.startServer();
options.callback.openBrowser(NetworkUtils.withQuery(authorizationURL,
mapOf(pair("client_id", options.callback.getClientId()), pair("response_type", "code"),
pair("redirect_uri", session.getRedirectURI()), pair("scope", options.scope),
pair("prompt", "select_account"))));
String codeVerifier = session.getCodeVerifier();
String state = session.getState();
String codeChallenge = generateCodeChallenge(codeVerifier);
options.callback.openBrowser(GrantFlow.AUTHORIZATION_CODE, NetworkUtils.withQuery(authorizationURL,
mapOf(pair("client_id", options.callback.getClientId()),
pair("response_type", "code"),
pair("redirect_uri", session.getRedirectURI()),
pair("scope", options.scope),
pair("prompt", "select_account"),
pair("code_challenge", codeChallenge),
pair("state", state),
pair("code_challenge_method", "S256")
)));
String code = session.waitFor();
// Authorization Code -> Token
AuthorizationResponse response = HttpRequest.POST(accessTokenURL)
.form(pair("client_id", options.callback.getClientId()), pair("code", code),
pair("grant_type", "authorization_code"), pair("client_secret", options.callback.getClientSecret()),
pair("redirect_uri", session.getRedirectURI()), pair("scope", options.scope))
.form(pair("client_id", options.callback.getClientId()),
pair("code", code),
pair("grant_type", "authorization_code"),
pair("code_verifier", codeVerifier),
pair("redirect_uri", session.getRedirectURI()),
pair("scope", options.scope))
.ignoreHttpCode()
.retry(5)
.getJson(AuthorizationResponse.class);
@@ -106,7 +124,7 @@ public class OAuth {
options.callback.grantDeviceCode(deviceTokenResponse.userCode, deviceTokenResponse.verificationURI);
// Microsoft OAuth Flow
options.callback.openBrowser(deviceTokenResponse.verificationURI);
options.callback.openBrowser(GrantFlow.DEVICE, deviceTokenResponse.verificationURI);
long startTime = System.nanoTime();
long interval = TimeUnit.MILLISECONDS.convert(deviceTokenResponse.interval, TimeUnit.SECONDS);
@@ -153,10 +171,6 @@ public class OAuth {
pair("grant_type", "refresh_token")
);
if (!options.callback.isPublicClient()) {
query.put("client_secret", options.callback.getClientSecret());
}
RefreshResponse response = HttpRequest.POST(tokenURL)
.form(query)
.accept("application/json")
@@ -174,6 +188,20 @@ public class OAuth {
}
}
private static String generateCodeChallenge(String codeVerifier) {
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.2
try {
byte[] bytes = codeVerifier.getBytes(StandardCharsets.US_ASCII);
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(bytes, 0, bytes.length);
byte[] digest = messageDigest.digest();
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
} catch (Exception e) {
LOG.warning("Failed to generate code challenge", e);
throw new RuntimeException(e);
}
}
private static void handleErrorResponse(ErrorResponse response) throws AuthenticationException {
if (response.error == null || response.errorDescription == null) {
return;
@@ -207,6 +235,9 @@ public class OAuth {
}
public interface Session {
String getState();
String getCodeVerifier();
String getRedirectURI();
@@ -237,15 +268,12 @@ public class OAuth {
/**
* Open browser
*
* @param url OAuth url.
* @param grantFlow the grant flow.
* @param url OAuth url.
*/
void openBrowser(String url) throws IOException;
void openBrowser(GrantFlow grantFlow, String url) throws IOException;
String getClientId();
String getClientSecret();
boolean isPublicClient();
}
public enum GrantFlow {
@@ -47,10 +47,10 @@ public final class MicrosoftAccount extends OAuthAccount {
this.characterUUID = requireNonNull(session.getProfile().getId());
}
protected MicrosoftAccount(MicrosoftService service, CharacterSelector characterSelector) throws AuthenticationException {
protected MicrosoftAccount(MicrosoftService service, OAuth.GrantFlow flow) throws AuthenticationException {
this.service = requireNonNull(service);
MicrosoftSession acquiredSession = service.authenticate();
MicrosoftSession acquiredSession = service.authenticate(flow);
if (acquiredSession.getProfile() == null) {
session = service.refresh(acquiredSession);
} else {
@@ -105,7 +105,7 @@ public final class MicrosoftAccount extends OAuthAccount {
@Override
public AuthInfo logInWhenCredentialsExpired() throws AuthenticationException {
MicrosoftSession acquiredSession = service.authenticate();
MicrosoftSession acquiredSession = service.authenticate(OAuth.GrantFlow.DEVICE);
if (!Objects.equals(characterUUID, acquiredSession.getProfile().getId())) {
throw new WrongAccountException(characterUUID, acquiredSession.getProfile().getId());
}
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.auth.microsoft;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.auth.OAuth;
import java.util.Map;
import java.util.Objects;
@@ -39,9 +40,7 @@ public class MicrosoftAccountFactory extends AccountFactory<MicrosoftAccount> {
@Override
public MicrosoftAccount create(CharacterSelector selector, String username, String password, ProgressCallback progressCallback, Object additionalData) throws AuthenticationException {
Objects.requireNonNull(selector);
return new MicrosoftAccount(service, selector);
return new MicrosoftAccount(service, (OAuth.GrantFlow) additionalData);
}
@Override
Loaded 100 of 139 files, more files were not shown because too many files have changed in this diff. Show more