Compare commits
56
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
662a82f21f | ||
|
|
3f0fcc1382 | ||
|
|
258556b22c | ||
|
|
d7e3cc4473 | ||
|
|
93d4489b94 | ||
|
|
77eee17d36 | ||
|
|
4118fa9729 | ||
|
|
83ffc91a88 | ||
|
|
fdf883d09d | ||
|
|
342bcc5438 | ||
|
|
59bcc7fe62 | ||
|
|
77b42b6f99 | ||
|
|
2f5594d8c1 | ||
|
|
13ad50208f | ||
|
|
13910c24b7 | ||
|
|
cc9bdb62ca | ||
|
|
baa7c816b7 | ||
|
|
090f0b9822 | ||
|
|
6d9ae24912 | ||
|
|
d03851b196 | ||
|
|
97cf9dcf16 | ||
|
|
7bacc2bb5a | ||
|
|
c7637079d7 | ||
|
|
8f3282e9a2 | ||
|
|
8e491862b9 | ||
|
|
e0c276075c | ||
|
|
19ea64e14b | ||
|
|
5b26b8f4eb | ||
|
|
ed7ef92ad9 | ||
|
|
cd95891108 | ||
|
|
df52bc6e81 | ||
|
|
9d9fc78384 | ||
|
|
e287f672fd | ||
|
|
24702dc5a0 | ||
|
|
886a25a935 | ||
|
|
450504ceda | ||
|
|
8767cc0e98 | ||
|
|
7331f619cf | ||
|
|
7a8f931641 | ||
|
|
ce51dc4dd7 | ||
|
|
b97e7fbd55 | ||
|
|
7f578be08e | ||
|
|
513f53bc06 | ||
|
|
b2d4d36856 | ||
|
|
9c031c5ac1 | ||
|
|
e2d3ac847d | ||
|
|
0f4152406c | ||
|
|
1a258a2554 | ||
|
|
3742be0aa8 | ||
|
|
0e34554344 | ||
|
|
1acfc55004 | ||
|
|
1428183e36 | ||
|
|
cbc6daf3d5 | ||
|
|
0ad180c082 | ||
|
|
1da8c8d6ac | ||
|
|
ea232b0976 |
No files matched your search
@@ -4,6 +4,8 @@
|
||||
hs_err_pid*
|
||||
*.hprof
|
||||
|
||||
*.jfr
|
||||
|
||||
.gradle
|
||||
gradle.properties
|
||||
|
||||
|
||||
+12
-2
@@ -4,6 +4,7 @@ import org.jackhuang.hmcl.gradle.ci.JenkinsUtils
|
||||
import org.jackhuang.hmcl.gradle.l10n.CheckTranslations
|
||||
import org.jackhuang.hmcl.gradle.l10n.CreateLanguageList
|
||||
import org.jackhuang.hmcl.gradle.l10n.CreateLocaleNamesResourceBundle
|
||||
import org.jackhuang.hmcl.gradle.l10n.SyncTranslations
|
||||
import org.jackhuang.hmcl.gradle.l10n.UpsideDownTranslate
|
||||
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
|
||||
import org.jackhuang.hmcl.gradle.pack.CreateDeb
|
||||
@@ -430,13 +431,22 @@ tasks.register<CheckTranslations>("checkTranslations") {
|
||||
val dir = layout.projectDirectory.dir("src/main/resources/assets/lang")
|
||||
|
||||
englishFile.set(dir.file("I18N.properties"))
|
||||
simplifiedChineseFile.set(dir.file("I18N_zh_CN.properties"))
|
||||
traditionalChineseFile.set(dir.file("I18N_zh.properties"))
|
||||
simplifiedChineseFile.set(dir.file("I18N_zh_Hans.properties"))
|
||||
traditionalChineseFile.set(dir.file("I18N_zh_Hant.properties"))
|
||||
classicalChineseFile.set(dir.file("I18N_lzh.properties"))
|
||||
}
|
||||
|
||||
// l10n
|
||||
|
||||
tasks.register<SyncTranslations>("syncTranslations") {
|
||||
group = "localization"
|
||||
description = "Synchronizes translation keys and blank lines with I18N.properties."
|
||||
|
||||
val dir = layout.projectDirectory.dir("src/main/resources/assets/lang")
|
||||
sourceFile.set(dir.file("I18N.properties"))
|
||||
translationFiles.from(fileTree(dir) { include("I18N_*.properties") })
|
||||
}
|
||||
|
||||
val generatedDir = layout.buildDirectory.dir("generated")
|
||||
|
||||
val upsideDownTranslate = tasks.register<UpsideDownTranslate>("upsideDownTranslate") {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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 com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.skins.JFXCheckBoxSkin;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.CheckBoxTreeItem;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/// Displays a [CheckBoxTreeItem] with a material-design checkbox.
|
||||
///
|
||||
/// The checkbox's selected and indeterminate states are bidirectionally bound to
|
||||
/// the current tree item. Reusing or clearing the cell removes the old bindings.
|
||||
/// Installing a checkbox skin or switching tree items applies the selection state
|
||||
/// immediately, without playing a selection animation.
|
||||
/// Ordinary tree items retain the display provided by [JFXTreeCell].
|
||||
/// Empty cells and `null` values display neither content nor a checkbox.
|
||||
///
|
||||
/// @param <T> the type of value stored in each tree item
|
||||
@NotNullByDefault
|
||||
public class JFXCheckTreeCell<T> extends JFXTreeCell<T> {
|
||||
|
||||
/// The checkbox bound to the currently displayed checkable tree item.
|
||||
private final JFXCheckBox checkBox = new JFXCheckBox();
|
||||
|
||||
/// Places the checkbox before the graphic supplied by the superclass.
|
||||
private final HBox graphic = new HBox();
|
||||
|
||||
/// The tree item whose selection properties are currently bound, if any.
|
||||
private @Nullable CheckBoxTreeItem<?> boundTreeItem;
|
||||
|
||||
/// Creates an empty cell whose checkbox toggles the current tree item's selection.
|
||||
public JFXCheckTreeCell() {
|
||||
checkBox.setAllowIndeterminate(false);
|
||||
checkBox.setFocusTraversable(false);
|
||||
graphic.setAlignment(Pos.CENTER_LEFT);
|
||||
graphic.setPickOnBounds(false);
|
||||
checkBox.skinProperty().addListener(observable -> finishSelectionAnimation());
|
||||
treeItemProperty().addListener(observable -> updateDisplay(getItem(), isEmpty()));
|
||||
}
|
||||
|
||||
/// Finishes animations from the previous item and immediately displays the current state.
|
||||
private void finishSelectionAnimation() {
|
||||
if (checkBox.getSkin() instanceof JFXCheckBoxSkin skin) {
|
||||
skin.finishSelectionAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the checkbox to the inherited display and updates its selection bindings.
|
||||
@Override
|
||||
protected void updateDisplay(@Nullable T item, boolean empty) {
|
||||
// Release graphics before the superclass reuses or reparents them.
|
||||
graphic.getChildren().clear();
|
||||
super.updateDisplay(item, empty);
|
||||
|
||||
@Nullable CheckBoxTreeItem<?> treeItem = !empty && item != null
|
||||
&& getTreeItem() instanceof CheckBoxTreeItem<?> checkTreeItem ? checkTreeItem : null;
|
||||
if (boundTreeItem != treeItem) {
|
||||
if (boundTreeItem != null) {
|
||||
checkBox.selectedProperty().unbindBidirectional(boundTreeItem.selectedProperty());
|
||||
checkBox.indeterminateProperty().unbindBidirectional(boundTreeItem.indeterminateProperty());
|
||||
}
|
||||
boundTreeItem = treeItem;
|
||||
if (treeItem != null) {
|
||||
checkBox.selectedProperty().bindBidirectional(treeItem.selectedProperty());
|
||||
checkBox.indeterminateProperty().bindBidirectional(treeItem.indeterminateProperty());
|
||||
} else {
|
||||
checkBox.setSelected(false);
|
||||
checkBox.setIndeterminate(false);
|
||||
}
|
||||
finishSelectionAnimation();
|
||||
}
|
||||
|
||||
if (treeItem != null) {
|
||||
@Nullable Node itemGraphic = getGraphic();
|
||||
graphic.getChildren().add(checkBox);
|
||||
if (itemGraphic != null) {
|
||||
graphic.getChildren().add(itemGraphic);
|
||||
}
|
||||
setGraphic(graphic);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import javafx.scene.control.TreeCell;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
@@ -83,8 +84,7 @@ public class JFXTreeCell<T> extends TreeCell<T> {
|
||||
treeItemRef = new WeakReference<>(newTreeItem);
|
||||
}
|
||||
};
|
||||
final WeakInvalidationListener weakTreeItemListener = new WeakInvalidationListener(treeItemInvalidationListener);
|
||||
treeItemProperty().addListener(weakTreeItemListener);
|
||||
treeItemProperty().addListener(treeItemInvalidationListener);
|
||||
if (getTreeItem() != null) {
|
||||
getTreeItem().graphicProperty().addListener(weakTreeItemGraphicListener);
|
||||
}
|
||||
@@ -104,13 +104,20 @@ public class JFXTreeCell<T> extends TreeCell<T> {
|
||||
selectedPane.setVisible(isSelected());
|
||||
}
|
||||
|
||||
private void updateDisplay(T item, boolean empty) {
|
||||
/// Updates the text and graphic for the current value and tree item.
|
||||
///
|
||||
/// @implSpec Called when the cell value or the current tree item's graphic changes.
|
||||
/// Subclasses may override this method to decorate the default display.
|
||||
///
|
||||
/// @param item the cell value, or `null` to clear the display
|
||||
/// @param empty whether the cell has no item
|
||||
protected void updateDisplay(@Nullable T item, boolean empty) {
|
||||
if (item == null || empty) {
|
||||
hbox = null;
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
TreeItem<T> treeItem = getTreeItem();
|
||||
@Nullable TreeItem<T> treeItem = getTreeItem();
|
||||
if (treeItem != null && treeItem.getGraphic() != null) {
|
||||
if (item instanceof Node) {
|
||||
setText(null);
|
||||
|
||||
@@ -169,11 +169,8 @@ public class JFXCheckBoxSkin extends CheckBoxSkin {
|
||||
};
|
||||
}
|
||||
|
||||
private void playSelectAnimation(Boolean selection) {
|
||||
if (selection == null) {
|
||||
selection = false;
|
||||
}
|
||||
|
||||
/// Animates a selection change, or applies it immediately when animations are disabled.
|
||||
private void playSelectAnimation(boolean selection) {
|
||||
JFXCheckBox control = (JFXCheckBox) this.getSkinnable();
|
||||
|
||||
this.box.setBorder(new Border(new BorderStroke(
|
||||
@@ -183,17 +180,7 @@ public class JFXCheckBoxSkin extends CheckBoxSkin {
|
||||
new BorderWidths(this.lineThick))));
|
||||
|
||||
if (!AnimationUtils.isAnimationEnabled()) {
|
||||
if (selection) {
|
||||
this.mark.setVisible(true);
|
||||
this.mark.setScaleX(1.0);
|
||||
this.mark.setScaleY(1.0);
|
||||
JFXNodeUtils.updateBackground(box.getBackground(), box, control.getCheckedColor());
|
||||
} else {
|
||||
this.mark.setVisible(false);
|
||||
this.mark.setScaleX(0.0);
|
||||
this.mark.setScaleY(0.0);
|
||||
JFXNodeUtils.updateBackground(box.getBackground(), box, Color.TRANSPARENT);
|
||||
}
|
||||
finishSelectionAnimation();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -203,6 +190,22 @@ public class JFXCheckBoxSkin extends CheckBoxSkin {
|
||||
this.select.play();
|
||||
}
|
||||
|
||||
/// Stops selection animations and immediately displays the checkbox's current selected state.
|
||||
///
|
||||
/// The check mark, fill, and border are updated even if no animation is running.
|
||||
/// This also suppresses the initial selection animation on the first layout.
|
||||
/// The skin must not have been disposed.
|
||||
public void finishSelectionAnimation() {
|
||||
transition.stop();
|
||||
select.stop();
|
||||
invalid = false;
|
||||
boolean selected = getSkinnable().isSelected();
|
||||
mark.setVisible(selected);
|
||||
mark.setScaleX(selected ? 1.0 : 0.0);
|
||||
mark.setScaleY(selected ? 1.0 : 0.0);
|
||||
updateColors();
|
||||
}
|
||||
|
||||
private void createFillTransition() {
|
||||
this.select = new JFXFillTransition(Duration.millis(120.0), this.box, Color.TRANSPARENT,
|
||||
(Color) ((JFXCheckBox) this.getSkinnable()).getCheckedColor());
|
||||
|
||||
@@ -38,10 +38,8 @@ import org.jackhuang.hmcl.game.HMCLCacheRepository;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WindowsNativeUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
@@ -142,12 +140,8 @@ public final class Launcher extends Application {
|
||||
Platform.setImplicitExit(false);
|
||||
Controllers.initialize(primaryStage);
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
|
||||
Themes.applyNativeDarkMode(primaryStage);
|
||||
|
||||
UpdateChecker.init();
|
||||
|
||||
WindowsNativeUtils.installWindowsAppUserModelRelaunchProperties(primaryStage);
|
||||
primaryStage.show();
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
/*
|
||||
* 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.download;
|
||||
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.download.cleanroom.CleanroomRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.neoforge.NeoForgeRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
|
||||
import org.jackhuang.hmcl.game.AssetObject;
|
||||
import org.jackhuang.hmcl.game.GameComponentType;
|
||||
import org.jackhuang.hmcl.game.HMCLCacheRepository;
|
||||
import org.jackhuang.hmcl.setting.DownloadSource;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/// @author Glavo
|
||||
@NotNullByDefault
|
||||
public final class HMCLDownloadProvider extends DownloadProvider {
|
||||
private volatile DownloadSource versionListSource = DownloadSource.DEFAULT;
|
||||
private volatile DownloadSource fileSource = DownloadSource.DEFAULT;
|
||||
|
||||
private static DownloadCandidates getCandidates(
|
||||
DownloadSource source,
|
||||
WebURL defaultUrl, WebURL bmclapiUrl
|
||||
) {
|
||||
if (LocaleUtils.IS_CHINA_MAINLAND) {
|
||||
return switch (source) {
|
||||
case DEFAULT, OFFICIAL -> DownloadCandidates.of(defaultUrl, bmclapiUrl);
|
||||
case MIRROR -> DownloadCandidates.of(bmclapiUrl, defaultUrl);
|
||||
};
|
||||
} else {
|
||||
return switch (source) {
|
||||
case DEFAULT, OFFICIAL -> DownloadCandidates.of(defaultUrl);
|
||||
case MIRROR -> DownloadCandidates.of(bmclapiUrl, defaultUrl);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static DownloadCandidates getCandidates(DownloadSource source, List<MirrorRule> rules, List<String> urls) {
|
||||
if (urls.isEmpty()) {
|
||||
throw new IllegalArgumentException("urls cannot be empty");
|
||||
}
|
||||
|
||||
// true means prefer mirror, false means prefer official, null means do not use mirror
|
||||
@Nullable Boolean mirrorFirst;
|
||||
if (LocaleUtils.IS_CHINA_MAINLAND) {
|
||||
mirrorFirst = switch (source) {
|
||||
case DEFAULT, MIRROR -> true;
|
||||
case OFFICIAL -> false;
|
||||
};
|
||||
} else {
|
||||
mirrorFirst = switch (source) {
|
||||
case DEFAULT, OFFICIAL -> null;
|
||||
case MIRROR -> true;
|
||||
};
|
||||
}
|
||||
|
||||
if (mirrorFirst == null) {
|
||||
return DownloadCandidates.of(urls.stream().map(DownloadCandidate::of).toArray(DownloadCandidate[]::new));
|
||||
}
|
||||
|
||||
var candidates = new ArrayList<DownloadCandidate>(urls.size() * 2);
|
||||
for (String url : urls) {
|
||||
DownloadCandidate candidate = DownloadCandidate.of(url);
|
||||
if (candidate.url() == null) {
|
||||
// Invalid URL, just add it to the list and let the download task handle it.
|
||||
candidates.add(candidate);
|
||||
continue;
|
||||
}
|
||||
|
||||
@Nullable DownloadCandidate mirrorCandidate = null;
|
||||
boolean fallback = false;
|
||||
for (MirrorRule rule : rules) {
|
||||
if (url.startsWith(rule.source)) {
|
||||
mirrorCandidate = DownloadCandidate.of(rule.target + url.substring(rule.source.length()));
|
||||
fallback = rule.fallback;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mirrorCandidate == null) {
|
||||
candidates.add(candidate);
|
||||
} else if (mirrorFirst && !fallback) {
|
||||
candidates.add(mirrorCandidate);
|
||||
candidates.add(candidate);
|
||||
} else {
|
||||
candidates.add(candidate);
|
||||
candidates.add(mirrorCandidate);
|
||||
}
|
||||
}
|
||||
|
||||
return DownloadCandidates.of(List.copyOf(candidates));
|
||||
}
|
||||
|
||||
private final String bmclRoot;
|
||||
private final List<MirrorRule> rules;
|
||||
|
||||
public HMCLDownloadProvider(String bmclRoot) {
|
||||
this.bmclRoot = bmclRoot;
|
||||
this.rules = List.of(
|
||||
new MirrorRule("https://bmclapi2.bangbang93.com", bmclRoot),
|
||||
new MirrorRule("https://launchermeta.mojang.com", bmclRoot),
|
||||
new MirrorRule("https://piston-meta.mojang.com", bmclRoot),
|
||||
new MirrorRule("https://piston-data.mojang.com", bmclRoot),
|
||||
new MirrorRule("https://launcher.mojang.com", bmclRoot),
|
||||
new MirrorRule("https://libraries.minecraft.net", bmclRoot + "/libraries"),
|
||||
new MirrorRule("http://files.minecraftforge.net/maven", bmclRoot + "/maven"),
|
||||
new MirrorRule("https://files.minecraftforge.net/maven", bmclRoot + "/maven"),
|
||||
new MirrorRule("https://maven.minecraftforge.net", bmclRoot + "/maven"),
|
||||
new MirrorRule("https://maven.neoforged.net/releases/", bmclRoot + "/maven/"),
|
||||
new MirrorRule("http://dl.liteloader.com/versions", bmclRoot + "/maven"),
|
||||
new MirrorRule("https://dl.liteloader.com/versions", bmclRoot + "/maven"),
|
||||
new MirrorRule("https://meta.fabricmc.net", bmclRoot + "/fabric-meta"),
|
||||
new MirrorRule("https://maven.fabricmc.net", bmclRoot + "/maven"),
|
||||
new MirrorRule("https://authlib-injector.yushi.moe", bmclRoot + "/mirrors/authlib-injector"),
|
||||
new MirrorRule("https://repo1.maven.org/maven2", "https://mirrors.cloud.tencent.com/nexus/repository/maven-public"),
|
||||
new MirrorRule("https://repo.maven.apache.org/maven2", "https://mirrors.cloud.tencent.com/nexus/repository/maven-public"),
|
||||
new MirrorRule("https://hmcl.glavo.site/metadata/cleanroom", "https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/cleanroom"),
|
||||
new MirrorRule("https://hmcl.glavo.site/metadata/fmllibs", "https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/fmllibs"),
|
||||
new MirrorRule("https://zkitefly.github.io/unlisted-versions-of-minecraft", "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto"),
|
||||
|
||||
// https://github.com/mcmod-info-mirror/mcim-rust-api
|
||||
new MirrorRule("https://api.modrinth.com", "https://mod.mcimirror.top/modrinth", true),
|
||||
new MirrorRule("https://cdn.modrinth.com", "https://mod.mcimirror.top", true),
|
||||
new MirrorRule("https://api.curseforge.com", "https://mod.mcimirror.top/curseforge", true),
|
||||
new MirrorRule("https://edge.forgecdn.net", "https://mod.mcimirror.top", true)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<? extends ComponentRemoteVersionList<?>> fetchVersionsAsync(GameComponentType type, @Nullable GameVersionNumber gameVersion) {
|
||||
assert (gameVersion == null) == (type == GameComponentType.GAME);
|
||||
switch (type) {
|
||||
case FORGE -> {
|
||||
Task<ComponentRemoteVersionList<ForgeRemoteVersion>> fetchOfficial = ForgeRemoteVersion.fetchAsync(this, gameVersion);
|
||||
|
||||
DownloadSource source = versionListSource;
|
||||
if (!LocaleUtils.IS_CHINA_MAINLAND && (source == DownloadSource.DEFAULT || source == DownloadSource.OFFICIAL)) {
|
||||
return fetchOfficial;
|
||||
}
|
||||
|
||||
Task<ComponentRemoteVersionList<ForgeRemoteVersion>> fetchBMCL = ForgeRemoteVersion.fetchBMCLAsync(this, bmclRoot, gameVersion);
|
||||
if (source == DownloadSource.MIRROR) {
|
||||
return new FallbackTask<>(fetchBMCL, fetchOfficial);
|
||||
} else {
|
||||
return new FallbackTask<>(fetchOfficial, fetchBMCL);
|
||||
}
|
||||
}
|
||||
case NEO_FORGE -> {
|
||||
Task<ComponentRemoteVersionList<NeoForgeRemoteVersion>> fetchOfficial = NeoForgeRemoteVersion.fetchAsync(this, gameVersion);
|
||||
|
||||
DownloadSource source = versionListSource;
|
||||
if (!LocaleUtils.IS_CHINA_MAINLAND && (source == DownloadSource.DEFAULT || source == DownloadSource.OFFICIAL)) {
|
||||
return fetchOfficial;
|
||||
}
|
||||
Task<ComponentRemoteVersionList<NeoForgeRemoteVersion>> fetchBMCL = NeoForgeRemoteVersion.fetchBMCLAsync(bmclRoot, gameVersion);
|
||||
if (source == DownloadSource.MIRROR || source == DownloadSource.DEFAULT) {
|
||||
return new FallbackTask<>(fetchBMCL, fetchOfficial);
|
||||
} else {
|
||||
return new FallbackTask<>(fetchOfficial, fetchBMCL);
|
||||
}
|
||||
}
|
||||
case OPTIFINE -> {
|
||||
return OptiFineRemoteVersion.fetchBMCLAsync(this, bmclRoot, gameVersion);
|
||||
}
|
||||
}
|
||||
|
||||
return super.fetchVersionsAsync(type, gameVersion);
|
||||
}
|
||||
|
||||
public void setFileSource(DownloadSource fileSource) {
|
||||
this.fileSource = Objects.requireNonNull(fileSource);
|
||||
}
|
||||
|
||||
public void setVersionListSource(DownloadSource versionListSource) {
|
||||
this.versionListSource = Objects.requireNonNull(versionListSource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HMCLCacheRepository getCacheRepository() {
|
||||
return HMCLCacheRepository.REPOSITORY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadCandidates getGameVersionListCandidates() {
|
||||
return getCandidates(
|
||||
versionListSource,
|
||||
GameRemoteVersion.VERSION_MANIFEST_URL,
|
||||
WebURL.parse(bmclRoot + "/mc/game/version_manifest.json")
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FabricLikeVersionListCandidates getFabricVersionListCandidates() {
|
||||
DownloadSource source = versionListSource;
|
||||
return new FabricLikeVersionListCandidates(
|
||||
getCandidates(source, FabricRemoteVersion.LOADER_META_URL, WebURL.parse(bmclRoot + "/fabric-meta/v2/versions/loader")),
|
||||
getCandidates(source, FabricRemoteVersion.GAME_META_URL, WebURL.parse(bmclRoot + "/fabric-meta/v2/versions/game"))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadCandidates getCleanroomVersionListCandidates() {
|
||||
return LocaleUtils.IS_CHINA_MAINLAND
|
||||
? DownloadCandidates.of(CleanroomRemoteVersion.LOADER_LIST_URL, WebURL.parse("https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/cleanroom/index.json"))
|
||||
: DownloadCandidates.of(CleanroomRemoteVersion.LOADER_LIST_URL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadCandidates getDownloadCandidates(List<String> urls) {
|
||||
return getCandidates(fileSource, rules, urls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadCandidates getVersionListCandidates(List<String> urls) {
|
||||
return getCandidates(versionListSource, rules, urls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadCandidates getAssetObjectCandidates(AssetObject assetObject) {
|
||||
return getCandidates(
|
||||
fileSource,
|
||||
WebURL.parse("https://resources.download.minecraft.net/" + assetObject.getLocation()),
|
||||
WebURL.parse(bmclRoot + "/assets/" + assetObject.getLocation())
|
||||
);
|
||||
}
|
||||
|
||||
@NotNullByDefault
|
||||
private static final class FallbackTask<T> extends Task<T> {
|
||||
|
||||
private final Task<? extends T> task;
|
||||
private final Task<? extends T> fallback;
|
||||
|
||||
private List<Task<?>> dependencies = List.of();
|
||||
|
||||
public FallbackTask(Task<? extends T> task, Task<? extends T> fallback) {
|
||||
this.task = task;
|
||||
this.fallback = fallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends Task<?>> getDependents() {
|
||||
return List.of(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRelyingOnDependents() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
if (isDependentsSucceeded()) {
|
||||
setResult(task.getResult());
|
||||
} else {
|
||||
dependencies = List.of(fallback);
|
||||
fallback.storeTo(this::setResult);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task<?>> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
}
|
||||
|
||||
private record MirrorRule(String source, String target, boolean fallback) {
|
||||
public MirrorRule(String source, String target) {
|
||||
this(source, target, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
public final class HMCLModpackProvider implements ModpackProvider {
|
||||
public static final HMCLModpackProvider INSTANCE = new HMCLModpackProvider();
|
||||
@@ -48,7 +49,12 @@ public final class HMCLModpackProvider implements ModpackProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, DefaultGameInstance instance, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
|
||||
public Task<?> createUpdateTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
DefaultGameInstance instance,
|
||||
Path zipFile,
|
||||
Modpack modpack,
|
||||
@Nullable Set<String> excludedFiles) throws MismatchedModpackTypeException {
|
||||
if (!(modpack.getManifest() instanceof HMCLModpackManifest))
|
||||
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
|
||||
|
||||
@@ -77,7 +83,12 @@ public final class HMCLModpackProvider implements ModpackProvider {
|
||||
|
||||
private final static class HMCLModpack extends Modpack {
|
||||
@Override
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, GameInstanceID instanceId, String iconUrl) {
|
||||
public Task<?> getInstallTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
Path zipFile,
|
||||
GameInstanceID instanceId,
|
||||
String iconUrl,
|
||||
@Nullable Set<String> excludedFiles) {
|
||||
return new HMCLModpackInstallTask((HMCLGameRepository) dependencyManager.getGameRepository(), zipFile, this, instanceId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,15 +32,14 @@ import org.jackhuang.hmcl.launch.*;
|
||||
import org.jackhuang.hmcl.modpack.ModpackCompletionException;
|
||||
import org.jackhuang.hmcl.modpack.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.modpack.ModpackProvider;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.GameSettings;
|
||||
import org.jackhuang.hmcl.setting.JavaVersionType;
|
||||
import org.jackhuang.hmcl.setting.LauncherVisibility;
|
||||
import org.jackhuang.hmcl.task.*;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
@@ -57,7 +56,6 @@ import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
@@ -356,26 +354,26 @@ public final class LauncherHelper {
|
||||
message = i18n("launch.failed.download_library", ((LibraryDownloadException) ex).getLibrary().name()) + "\n";
|
||||
if (ex.getCause() instanceof ResponseCodeException rce) {
|
||||
int responseCode = rce.getResponseCode();
|
||||
String uri = rce.getUri();
|
||||
String url = rce.getUrl();
|
||||
if (responseCode == 404)
|
||||
message += i18n("download.code.404", uri);
|
||||
message += i18n("download.code.404", url);
|
||||
else
|
||||
message += i18n("download.failed", uri, responseCode);
|
||||
message += i18n("download.failed", url, responseCode);
|
||||
} else {
|
||||
message += StringUtils.getStackTrace(ex.getCause());
|
||||
}
|
||||
} else if (ex instanceof DownloadException) {
|
||||
URI uri = ((DownloadException) ex).getUri();
|
||||
} else if (ex instanceof DownloadException de) {
|
||||
String url = de.getUrl();
|
||||
if (ex.getCause() instanceof SocketTimeoutException) {
|
||||
message = i18n("install.failed.downloading.timeout", uri);
|
||||
message = i18n("install.failed.downloading.timeout", url);
|
||||
} else if (ex.getCause() instanceof ResponseCodeException responseCodeException) {
|
||||
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
|
||||
message = i18n("download.code." + responseCodeException.getResponseCode(), uri);
|
||||
message = i18n("download.code." + responseCodeException.getResponseCode(), url);
|
||||
} else {
|
||||
message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
||||
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
||||
}
|
||||
} else {
|
||||
message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
||||
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
||||
}
|
||||
} else if (ex instanceof GameAssetIndexDownloadTask.GameAssetIndexMalformedException) {
|
||||
message = i18n("assets.index.malformed");
|
||||
@@ -385,11 +383,11 @@ public final class LauncherHelper {
|
||||
message = i18n("account.failed.character_deleted");
|
||||
} else if (ex instanceof ResponseCodeException rce) {
|
||||
int responseCode = rce.getResponseCode();
|
||||
String uri = rce.getUri();
|
||||
String url = rce.getUrl();
|
||||
if (responseCode == 404)
|
||||
message = i18n("download.code.404", uri);
|
||||
message = i18n("download.code.404", url);
|
||||
else
|
||||
message = i18n("download.failed", uri, responseCode);
|
||||
message = i18n("download.failed", url, responseCode);
|
||||
} else if (ex instanceof CommandTooLongException) {
|
||||
message = i18n("launch.failed.command_too_long");
|
||||
} else if (ex instanceof ExecutionPolicyLimitException) {
|
||||
@@ -744,7 +742,7 @@ public final class LauncherHelper {
|
||||
Throwable resolvedException = resolveException(exception);
|
||||
LOG.warning("Failed to download java", exception);
|
||||
if (!(resolvedException instanceof CancellationException)) {
|
||||
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
|
||||
Controllers.dialog(I18n.localizeErrorMessage(resolvedException), i18n("install.failed"));
|
||||
}
|
||||
future.completeExceptionally(new CancellationException());
|
||||
}
|
||||
|
||||
@@ -39,8 +39,6 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
|
||||
|
||||
protected abstract RemoteAddonRepository getBackedRemoteModRepository();
|
||||
|
||||
protected abstract SortType getBackedRemoteModRepositorySortOrder();
|
||||
|
||||
@Override
|
||||
public String getApiBaseUrl() {
|
||||
return getBackedRemoteModRepository().getApiBaseUrl();
|
||||
@@ -52,15 +50,15 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||
public SearchResult search(DownloadProvider downloadProvider, RemoteAddon.Type type, String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||
if (!StringUtils.containsChinese(searchFilter)) {
|
||||
return getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
|
||||
return getBackedRemoteModRepository().search(downloadProvider, type, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
|
||||
}
|
||||
|
||||
Set<String> englishSearchFiltersSet = new LinkedHashSet<>(INITIAL_CAPACITY);
|
||||
|
||||
int count = 0;
|
||||
for (ModTranslations.Mod mod : ModTranslations.getTranslationsByAddonType(getType()).searchMod(searchFilter)) {
|
||||
for (ModTranslations.Mod mod : ModTranslations.getTranslationsByAddonType(type).searchMod(searchFilter)) {
|
||||
String englishSearchFilter = String.join(" ", StringUtils.tokenize(StringUtils.isNotBlank(mod.getSubname()) ? mod.getSubname() : mod.getName()));
|
||||
if (StringUtils.isNotBlank(englishSearchFilter)) {
|
||||
englishSearchFiltersSet.add(englishSearchFilter);
|
||||
@@ -71,7 +69,7 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
|
||||
}
|
||||
|
||||
if (englishSearchFiltersSet.isEmpty()) {
|
||||
return getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
|
||||
return getBackedRemoteModRepository().search(downloadProvider, type, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
|
||||
}
|
||||
|
||||
RemoteAddon[] searchResultArray = new RemoteAddon[pageSize];
|
||||
@@ -80,8 +78,8 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
|
||||
SearchResult searchResult = null;
|
||||
List<RemoteAddon> remoteAddons = List.of();
|
||||
for (String englishSearchFilter : englishSearchFiltersSet) {
|
||||
searchResult = getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, englishSearchFilter, getBackedRemoteModRepositorySortOrder(), sortOrder);
|
||||
remoteAddons = searchResult.getUnsortedResults().toList();
|
||||
searchResult = getBackedRemoteModRepository().search(downloadProvider, type, gameVersion, category, pageOffset, pageSize, englishSearchFilter, SortType.RELEVANCY, sortOrder);
|
||||
remoteAddons = searchResult.results().toList();
|
||||
if (!remoteAddons.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
@@ -93,19 +91,19 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
|
||||
continue;
|
||||
}
|
||||
|
||||
ModTranslations.Mod chineseTranslation = ModTranslations.getTranslationsByAddonType(getType()).getModByCurseForgeId(remoteAddon.slug());
|
||||
ModTranslations.Mod chineseTranslation = ModTranslations.getTranslationsByAddonType(type).getModByCurseForgeId(remoteAddon.slug());
|
||||
if (chineseTranslation != null && !StringUtils.isBlank(chineseTranslation.getName()) && StringUtils.containsChinese(chineseTranslation.getName())) {
|
||||
searchResultArray[chineseIndex++] = remoteAddon;
|
||||
} else {
|
||||
searchResultArray[englishIndex--] = remoteAddon;
|
||||
}
|
||||
}
|
||||
totalPages = searchResult.getTotalPages();
|
||||
totalPages = searchResult.totalPages();
|
||||
}
|
||||
|
||||
StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator();
|
||||
return new SearchResult(Stream.concat(Arrays.stream(searchResultArray, 0, chineseIndex).map(remoteMod -> {
|
||||
ModTranslations.Mod chineseRemoteMod = ModTranslations.getTranslationsByAddonType(getType()).getModByCurseForgeId(remoteMod.slug());
|
||||
ModTranslations.Mod chineseRemoteMod = ModTranslations.getTranslationsByAddonType(type).getModByCurseForgeId(remoteMod.slug());
|
||||
if (chineseRemoteMod == null || StringUtils.isBlank(chineseRemoteMod.getName()) || !StringUtils.containsChinese(chineseRemoteMod.getName())) {
|
||||
return Pair.pair(remoteMod, Integer.MAX_VALUE);
|
||||
}
|
||||
@@ -124,8 +122,8 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<Category> getCategories() throws IOException {
|
||||
return getBackedRemoteModRepository().getCategories();
|
||||
public Stream<Category> getCategories(RemoteAddon.Type type) throws IOException {
|
||||
return getBackedRemoteModRepository().getCategories(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -56,6 +56,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
@@ -196,6 +197,26 @@ public final class ModpackHelper {
|
||||
}
|
||||
|
||||
public static Task<?> getInstallTask(HMCLGameRepository repository, Path zipFile, GameInstanceID instanceId, Modpack modpack, @Nullable String iconUrl) {
|
||||
return getInstallTask(repository, zipFile, instanceId, modpack, iconUrl, null);
|
||||
}
|
||||
|
||||
/// Creates an install task that respects optional-file selection.
|
||||
///
|
||||
/// @param repository the target repository
|
||||
/// @param zipFile the modpack archive
|
||||
/// @param instanceId the new instance id
|
||||
/// @param modpack the parsed modpack
|
||||
/// @param iconUrl the optional icon URL, or `null`
|
||||
/// @param excludedFiles keys of optional files the user chose not to install; `null` means install all.
|
||||
/// When non-null, must not contain `null` elements.
|
||||
/// @return the install task
|
||||
public static Task<?> getInstallTask(
|
||||
HMCLGameRepository repository,
|
||||
Path zipFile,
|
||||
GameInstanceID instanceId,
|
||||
Modpack modpack,
|
||||
@Nullable String iconUrl,
|
||||
@Nullable Set<String> excludedFiles) {
|
||||
ExceptionalRunnable<?> success = () -> {
|
||||
repository.refresh();
|
||||
repository.getInstance(instanceId).enableIsolation();
|
||||
@@ -209,17 +230,17 @@ public final class ModpackHelper {
|
||||
};
|
||||
|
||||
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl)
|
||||
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl, excludedFiles)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMultiMCPostInstallTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), instanceId))
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
else if (modpack.getManifest() instanceof McbbsModpackManifest)
|
||||
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl)
|
||||
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl, excludedFiles)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMcbbsPostInstallTask(repository, (McbbsModpackManifest) modpack.getManifest(), instanceId))
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
else
|
||||
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl)
|
||||
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl, excludedFiles)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
}
|
||||
@@ -240,17 +261,38 @@ public final class ModpackHelper {
|
||||
}
|
||||
|
||||
public static Task<?> getUpdateTask(HMCLGameRepository repository, Path zipFile, Charset charset, GameInstanceID instanceId, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
|
||||
return getUpdateTask(repository, zipFile, charset, instanceId, configuration, null);
|
||||
}
|
||||
|
||||
/// Creates an update task that respects optional-file selection.
|
||||
///
|
||||
/// @param repository the target repository
|
||||
/// @param zipFile the modpack archive
|
||||
/// @param charset the archive encoding
|
||||
/// @param instanceId the instance to update
|
||||
/// @param configuration the existing modpack configuration
|
||||
/// @param excludedFiles keys of optional files the user chose not to install; `null` means install all.
|
||||
/// When non-null, must not contain `null` elements.
|
||||
/// @return the update task
|
||||
public static Task<?> getUpdateTask(
|
||||
HMCLGameRepository repository,
|
||||
Path zipFile,
|
||||
Charset charset,
|
||||
GameInstanceID instanceId,
|
||||
ModpackConfiguration<?> configuration,
|
||||
@Nullable Set<String> excludedFiles)
|
||||
throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(zipFile, charset);
|
||||
ModpackProvider provider = getProviderByType(configuration.getType());
|
||||
if (provider == null) {
|
||||
throw new UnsupportedModpackException();
|
||||
}
|
||||
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return provider.createUpdateTask(repository.getDependency(), repository.getInstance(instanceId), zipFile, modpack)
|
||||
return provider.createUpdateTask(repository.getDependency(), repository.getInstance(instanceId), zipFile, modpack, excludedFiles)
|
||||
.thenComposeAsync(() -> createMultiMCPostUpdateTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), instanceId))
|
||||
.thenComposeAsync(repository.refreshAsync());
|
||||
else
|
||||
return provider.createUpdateTask(repository.getDependency(), repository.getInstance(instanceId), zipFile, modpack)
|
||||
return provider.createUpdateTask(repository.getDependency(), repository.getInstance(instanceId), zipFile, modpack, excludedFiles)
|
||||
.thenComposeAsync(repository.refreshAsync());
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.offline.Skin;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.*;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -99,7 +100,7 @@ public final class TexturesLoader {
|
||||
if (!Files.isRegularFile(file)) {
|
||||
// download it
|
||||
try {
|
||||
new FileDownloadTask(texture.url(), file).run();
|
||||
new FileDownloadTask(DownloadProviders.getDownloadProvider().getDownloadCandidates(texture.url()), file).run();
|
||||
LOG.info("Texture downloaded: " + texture.url());
|
||||
} catch (Exception e) {
|
||||
if (Files.isRegularFile(file)) {
|
||||
|
||||
@@ -26,15 +26,11 @@ import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.game.GameComponentAnalyzer;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
import org.jackhuang.hmcl.game.GameJavaVersion;
|
||||
import org.jackhuang.hmcl.game.JavaVersionConstraint;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.setting.SettingsManager;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.CacheRepository;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
@@ -444,7 +440,7 @@ public final class JavaManager {
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
searcher.searchAllOfficialJava(Path.of(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"), false);
|
||||
}
|
||||
searcher.searchAllOfficialJava(CacheRepository.getInstance().getCacheDirectory().resolve("java"), true);
|
||||
searcher.searchAllOfficialJava(HMCLCacheRepository.REPOSITORY.getCacheDirectory().resolve("java"), true);
|
||||
|
||||
// Search in PATH.
|
||||
if (System.getenv("PATH") != null) {
|
||||
|
||||
@@ -18,44 +18,47 @@
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.beans.InvalidationListener;
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.download.*;
|
||||
import org.jackhuang.hmcl.task.DownloadException;
|
||||
import org.jackhuang.hmcl.task.FetchTask;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
||||
import static org.jackhuang.hmcl.task.FetchTask.DEFAULT_CONCURRENCY;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class DownloadProviders {
|
||||
private DownloadProviders() {
|
||||
}
|
||||
|
||||
private static final DownloadProviderWrapper PROVIDER_WRAPPER;
|
||||
|
||||
private static final DownloadProvider MOJANG_PROVIDER;
|
||||
private static final BMCLAPIDownloadProvider BMCLAPI_PROVIDER;
|
||||
private static final DownloadProvider DEFAULT_PROVIDER;
|
||||
private static final String DEFAULT_BMCLAPI_ROOT = "https://bmclapi2.bangbang93.com";
|
||||
private static final String BMCLAPI_ROOT;
|
||||
|
||||
static {
|
||||
String bmclapiRoot = System.getProperty("hmcl.bmclapi.override", "https://bmclapi2.bangbang93.com");
|
||||
BMCLAPI_PROVIDER = new BMCLAPIDownloadProvider(bmclapiRoot);
|
||||
MOJANG_PROVIDER = new MojangDownloadProvider();
|
||||
DEFAULT_PROVIDER = createDownloadProvider(DownloadSource.DEFAULT, DownloadSource.DEFAULT);
|
||||
PROVIDER_WRAPPER = new DownloadProviderWrapper(DEFAULT_PROVIDER);
|
||||
@Nullable String bmclOverride = System.getProperty("hmcl.bmclapi.override");
|
||||
if (StringUtils.isBlank(bmclOverride)) {
|
||||
BMCLAPI_ROOT = DEFAULT_BMCLAPI_ROOT;
|
||||
} else {
|
||||
WebURL url;
|
||||
try {
|
||||
url = WebURL.parseBrowserInput(bmclOverride);
|
||||
} catch (Exception e) {
|
||||
url = null;
|
||||
LOG.warning("Invalid BMCLAPI override URL: " + bmclOverride, e);
|
||||
}
|
||||
|
||||
if (url == null) {
|
||||
BMCLAPI_ROOT = DEFAULT_BMCLAPI_ROOT;
|
||||
} else {
|
||||
BMCLAPI_ROOT = StringUtils.removeSuffix(url.toString(), "/");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final HMCLDownloadProvider PROVIDER = new HMCLDownloadProvider(BMCLAPI_ROOT);
|
||||
|
||||
/// Initializes download provider settings and synchronizes download thread settings.
|
||||
public static void init() {
|
||||
InvalidationListener onChangeDownloadThreads = observable -> {
|
||||
@@ -67,73 +70,15 @@ public final class DownloadProviders {
|
||||
settings().downloadThreadsProperty().addListener(onChangeDownloadThreads);
|
||||
onChangeDownloadThreads.invalidated(null);
|
||||
|
||||
InvalidationListener onChangeDownloadSource = observable -> {
|
||||
PROVIDER_WRAPPER.setProvider(createDownloadProvider(
|
||||
settings().versionListSourceProperty().get(),
|
||||
settings().fileDownloadSourceProperty().get()));
|
||||
};
|
||||
settings().versionListSourceProperty().addListener(onChangeDownloadSource);
|
||||
settings().fileDownloadSourceProperty().addListener(onChangeDownloadSource);
|
||||
onChangeDownloadSource.invalidated(null);
|
||||
}
|
||||
|
||||
/// Creates a download provider with independent version-list and file download preferences.
|
||||
private static DownloadProvider createDownloadProvider(DownloadSource versionListSource, DownloadSource fileDownloadSource) {
|
||||
return new AutoDownloadProvider(
|
||||
getCandidates(versionListSource),
|
||||
getCandidates(fileDownloadSource));
|
||||
}
|
||||
|
||||
/// Returns provider candidates ordered by the given source preference.
|
||||
private static List<DownloadProvider> getCandidates(DownloadSource source) {
|
||||
DownloadSource normalized = source != null ? source : DownloadSource.DEFAULT;
|
||||
return switch (normalized) {
|
||||
case DEFAULT -> LocaleUtils.IS_CHINA_MAINLAND
|
||||
? List.of(BMCLAPI_PROVIDER, MOJANG_PROVIDER)
|
||||
: List.of(MOJANG_PROVIDER, BMCLAPI_PROVIDER);
|
||||
case OFFICIAL -> List.of(MOJANG_PROVIDER);
|
||||
case MIRROR -> List.of(BMCLAPI_PROVIDER, MOJANG_PROVIDER);
|
||||
};
|
||||
FXUtils.onChangeAndOperate(settings().versionListSourceProperty(), PROVIDER::setVersionListSource);
|
||||
FXUtils.onChangeAndOperate(settings().fileDownloadSourceProperty(), PROVIDER::setFileSource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current primary preferred download provider
|
||||
*/
|
||||
public static DownloadProvider getDownloadProvider() {
|
||||
return PROVIDER_WRAPPER;
|
||||
return PROVIDER;
|
||||
}
|
||||
|
||||
public static String localizeErrorMessage(Throwable exception) {
|
||||
if (exception instanceof DownloadException) {
|
||||
URI uri = ((DownloadException) exception).getUri();
|
||||
if (exception.getCause() instanceof SocketTimeoutException) {
|
||||
return i18n("install.failed.downloading.timeout", uri);
|
||||
} else if (exception.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
|
||||
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
|
||||
return i18n("download.code." + responseCodeException.getResponseCode(), uri);
|
||||
} else {
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
}
|
||||
} else if (exception.getCause() instanceof FileNotFoundException) {
|
||||
return i18n("download.code.404", uri);
|
||||
} else if (exception.getCause() instanceof AccessDeniedException) {
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
|
||||
} else if (exception.getCause() instanceof ArtifactMalformedException) {
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.artifact_malformed");
|
||||
} else if (exception.getCause() instanceof SSLHandshakeException && !(exception.getCause().getMessage() != null && exception.getCause().getMessage().contains("Remote host terminated"))) {
|
||||
if (exception.getCause().getMessage() != null && (exception.getCause().getMessage().contains("No name matching") || exception.getCause().getMessage().contains("No subject alternative DNS name matching"))) {
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.dns.pollution");
|
||||
}
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.ssl_handshake");
|
||||
} else {
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
}
|
||||
} else if (exception instanceof ArtifactMalformedException) {
|
||||
return i18n("exception.artifact_malformed");
|
||||
} else if (exception instanceof CancellationException) {
|
||||
return i18n("message.cancelled");
|
||||
}
|
||||
return StringUtils.getStackTrace(exception);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,10 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
@@ -33,13 +36,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.UnknownNullability;
|
||||
import org.jetbrains.annotations.UnmodifiableView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.*;
|
||||
@@ -333,6 +330,7 @@ public final class GameDirectoryManager {
|
||||
if (source == target) {
|
||||
gameDirectory.setName(name);
|
||||
gameDirectory.setPath(path);
|
||||
rebuildGameDirectories();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.terracotta;
|
||||
|
||||
import kala.compress.archivers.tar.TarArchiveEntry;
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.download.ArtifactMalformedException;
|
||||
import org.jackhuang.hmcl.task.FetchTask;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
@@ -29,13 +30,12 @@ import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.UrlResponseInfo;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jackhuang.hmcl.util.tree.TarFileTree;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.DigestInputStream;
|
||||
@@ -50,13 +50,16 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
public final class TerracottaBundle {
|
||||
private final Path root;
|
||||
|
||||
private final List<URI> links;
|
||||
/// Candidate bundle URLs in attempt order.
|
||||
private final List<WebURL> links;
|
||||
|
||||
private final FileDownloadTask.IntegrityCheck hash;
|
||||
|
||||
private final Map<String, FileDownloadTask.IntegrityCheck> files;
|
||||
|
||||
public TerracottaBundle(Path root, List<URI> links, FileDownloadTask.IntegrityCheck hash, Map<String, FileDownloadTask.IntegrityCheck> files) {
|
||||
/// Creates a bundle using the supplied download candidates and archive/file integrity metadata.
|
||||
/// The candidate list and file map are retained without copying.
|
||||
public TerracottaBundle(Path root, List<WebURL> links, FileDownloadTask.IntegrityCheck hash, Map<String, FileDownloadTask.IntegrityCheck> files) {
|
||||
this.root = root;
|
||||
this.links = links;
|
||||
this.hash = hash;
|
||||
|
||||
@@ -22,6 +22,7 @@ import javafx.application.Platform;
|
||||
import javafx.beans.property.ReadOnlyDoubleWrapper;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.DownloadException;
|
||||
@@ -42,7 +43,6 @@ import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
@@ -288,7 +288,7 @@ public final class TerracottaManager {
|
||||
|
||||
public static Task<String> exportLogs() {
|
||||
if (STATE_V.get() instanceof TerracottaState.PortSpecific portSpecific) {
|
||||
return new GetTask(URI.create(String.format("http://127.0.0.1:%d/log?fetch=true", portSpecific.port)))
|
||||
return new GetTask(WebURL.parse(String.format("http://127.0.0.1:%d/log?fetch=true", portSpecific.port)))
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
}
|
||||
return Task.completed(null);
|
||||
@@ -297,7 +297,7 @@ public final class TerracottaManager {
|
||||
public static TerracottaState.Waiting setWaiting() {
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (state instanceof TerracottaState.PortSpecific portSpecific) {
|
||||
new GetTask(URI.create(String.format("http://127.0.0.1:%d/state/ide", portSpecific.port)))
|
||||
new GetTask(WebURL.parse(String.format("http://127.0.0.1:%d/state/ide", portSpecific.port)))
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.start();
|
||||
return new TerracottaState.Waiting(-1, -1, null);
|
||||
@@ -317,7 +317,7 @@ public final class TerracottaManager {
|
||||
.thenComposeAsync(nodes -> {
|
||||
List<Pair<String, String>> query = new ArrayList<>(nodes.size() + 1);
|
||||
query.add(pair("player", getPlayerName()));
|
||||
for (URI node : nodes) {
|
||||
for (WebURL node : nodes) {
|
||||
query.add(pair("public_nodes", node.toString()));
|
||||
}
|
||||
return new GetTask(NetworkUtils.withQuery(
|
||||
@@ -338,7 +338,7 @@ public final class TerracottaManager {
|
||||
ArrayList<Pair<String, String>> query = new ArrayList<>(nodes.size() + 2);
|
||||
query.add(pair("room", room));
|
||||
query.add(pair("player", getPlayerName()));
|
||||
for (URI node : nodes) {
|
||||
for (WebURL 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))
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.terracotta;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.terracotta.provider.AbstractTerracottaProvider;
|
||||
@@ -35,10 +36,10 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionRange;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -90,8 +91,8 @@ public final class TerracottaMetadata {
|
||||
}
|
||||
|
||||
Stream<String> stream = downloads.stream(), streamCN = downloadsCN.stream();
|
||||
List<URI> links = (LocaleUtils.IS_CHINA_MAINLAND ? Stream.concat(streamCN, stream) : Stream.concat(stream, streamCN))
|
||||
.map(link -> URI.create(options.replace(link)))
|
||||
@Unmodifiable List<WebURL> links = (LocaleUtils.IS_CHINA_MAINLAND ? Stream.concat(streamCN, stream) : Stream.concat(stream, streamCN))
|
||||
.map(link -> WebURL.parse(options.replace(link)))
|
||||
.toList();
|
||||
|
||||
Map<String, FileDownloadTask.IntegrityCheck> files = pkg.files.entrySet().stream().collect(Collectors.toUnmodifiableMap(
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.terracotta;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
@@ -26,9 +27,8 @@ 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 org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -43,17 +43,19 @@ public final class TerracottaNodeList {
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
Validation.requireNonNull(url, "TerracottaNode.url cannot be null");
|
||||
try {
|
||||
new URI(url);
|
||||
} catch (URISyntaxException e) {
|
||||
WebURL.parse(url);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new JsonParseException("Invalid URL: " + url, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static volatile List<URI> list;
|
||||
/// Cached node URLs, or `null` before the first fetch completes.
|
||||
private static volatile @Nullable @Unmodifiable List<WebURL> list;
|
||||
|
||||
public static List<URI> fetch() {
|
||||
List<URI> list = TerracottaNodeList.list;
|
||||
/// Returns the cached unmodifiable node URLs, fetching once and caching an empty list on failure.
|
||||
public static @Unmodifiable List<WebURL> fetch() {
|
||||
@Nullable @Unmodifiable List<WebURL> list = TerracottaNodeList.list;
|
||||
if (list != null)
|
||||
return list;
|
||||
|
||||
@@ -84,7 +86,7 @@ public final class TerracottaNodeList {
|
||||
|
||||
return StringUtils.isBlank(node.region) || LocaleUtils.IS_CHINA_MAINLAND == "CN".equalsIgnoreCase(node.region);
|
||||
})
|
||||
.map(it -> URI.create(it.url()))
|
||||
.map(it -> WebURL.parse(it.url()))
|
||||
.toList();
|
||||
LOG.info("Terracotta node list: " + list);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.glavo.monetfx.Brightness;
|
||||
import org.glavo.monetfx.ColorRole;
|
||||
import org.glavo.monetfx.ColorStyle;
|
||||
import org.glavo.monetfx.Contrast;
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.setting.BackgroundType;
|
||||
import org.jackhuang.hmcl.setting.LauncherSettings;
|
||||
@@ -36,8 +37,8 @@ import org.jackhuang.hmcl.util.MathUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.LocalizedText;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.ContentEncoding;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
@@ -48,7 +49,6 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
@@ -1497,28 +1497,28 @@ public final class ThemePackManager {
|
||||
List<Path> temporaryFiles,
|
||||
ResolvedBackground background) throws IOException {
|
||||
String url = requireNonBlank(background.networkImageUrl(), "background.url");
|
||||
URI uri = NetworkUtils.toURI(url);
|
||||
if (!NetworkUtils.isHttpUri(uri)) {
|
||||
WebURL webUrl = WebURL.parse(url);
|
||||
if (!NetworkUtils.isHttpUri(webUrl)) {
|
||||
throw new IOException("Theme background URL must be HTTP or HTTPS: " + url);
|
||||
}
|
||||
|
||||
String entryName = "assets/wallpapers/" + networkBackgroundAssetName(uri);
|
||||
String entryName = "assets/wallpapers/" + networkBackgroundAssetName(webUrl);
|
||||
if (background.networkImageCachePolicy() == NetworkBackgroundImageCachePolicy.ENABLED) {
|
||||
try {
|
||||
@Nullable Path cachedFile = new CacheFileTask(uri).run();
|
||||
@Nullable Path cachedFile = new CacheFileTask(webUrl).run();
|
||||
if (cachedFile != null && Files.isRegularFile(cachedFile)) {
|
||||
assets.add(new ThemePackAsset(cachedFile, entryName));
|
||||
return new ThemeBackground.Image(entryName);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to cache theme background, falling back to direct download: " + uri, e);
|
||||
LOG.warning("Failed to cache theme background, falling back to direct download: " + webUrl, e);
|
||||
}
|
||||
}
|
||||
|
||||
Path temporaryFile = Files.createTempFile("hmcl-theme-background-", ".tmp");
|
||||
boolean success = false;
|
||||
try {
|
||||
downloadNetworkBackground(uri, temporaryFile);
|
||||
downloadNetworkBackground(webUrl, temporaryFile);
|
||||
assets.add(new ThemePackAsset(temporaryFile, entryName));
|
||||
temporaryFiles.add(temporaryFile);
|
||||
success = true;
|
||||
@@ -1535,12 +1535,12 @@ public final class ThemePackManager {
|
||||
}
|
||||
|
||||
/// Downloads one network background into a temporary file without installing it into the persistent image cache.
|
||||
private static void downloadNetworkBackground(URI uri, Path outputFile) throws IOException {
|
||||
HttpURLConnection connection = NetworkUtils.resolveConnection(NetworkUtils.createHttpConnection(uri));
|
||||
private static void downloadNetworkBackground(WebURL url, Path outputFile) throws IOException {
|
||||
HttpURLConnection connection = NetworkUtils.resolveConnection(NetworkUtils.createHttpConnection(url));
|
||||
try {
|
||||
int responseCode = connection.getResponseCode();
|
||||
if (responseCode / 100 != 2) {
|
||||
throw new IOException("Failed to download theme background: HTTP " + responseCode + " " + uri);
|
||||
throw new IOException("Failed to download theme background: HTTP " + responseCode + " " + url);
|
||||
}
|
||||
|
||||
ContentEncoding contentEncoding = ContentEncoding.fromConnection(connection);
|
||||
@@ -1554,8 +1554,8 @@ public final class ThemePackManager {
|
||||
}
|
||||
|
||||
/// Returns a safe theme-pack asset file name for a downloaded network background.
|
||||
private static String networkBackgroundAssetName(URI uri) {
|
||||
String path = Objects.toString(uri.getPath(), "");
|
||||
private static String networkBackgroundAssetName(WebURL url) {
|
||||
String path = Objects.toString(url.getPath(), "");
|
||||
@Nullable Path fileNamePath = path.isBlank() ? null : Path.of(path).getFileName();
|
||||
String fileName = fileNamePath != null ? fileNamePath.toString() : "";
|
||||
String sanitized = sanitizePathSegment(fileName);
|
||||
|
||||
@@ -24,8 +24,7 @@ import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.binding.ObjectExpression;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.SetChangeListener;
|
||||
@@ -80,6 +79,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -91,6 +91,9 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
/// Provides the current launcher MonetFX theme and derived color bindings.
|
||||
@NotNullByDefault
|
||||
public final class Themes {
|
||||
/// The stage property retaining the listener for native appearance updates and marking completed registration.
|
||||
private static final String NATIVE_DARK_MODE_LISTENER = "Themes.applyNativeDarkMode.listener";
|
||||
|
||||
/// The seed color extracted from the last loaded wallpaper image.
|
||||
private static final ReadOnlyObjectWrapper<@Nullable ThemeColor> wallpaperThemeColor = new ReadOnlyObjectWrapper<>();
|
||||
|
||||
@@ -283,6 +286,9 @@ public final class Themes {
|
||||
colorScheme
|
||||
);
|
||||
|
||||
/// Whether the current color scheme brightness is inherited from system.
|
||||
private static final ReadOnlyBooleanWrapper autoBrightness = new ReadOnlyBooleanWrapper();
|
||||
|
||||
/// The current JavaFX launcher background and its node opacity.
|
||||
private static final ReadOnlyObjectWrapper<LauncherBackground> background = new ReadOnlyObjectWrapper<>(
|
||||
new LauncherBackground(
|
||||
@@ -380,6 +386,35 @@ public final class Themes {
|
||||
}
|
||||
|
||||
static {
|
||||
Supplier<@Nullable Brightness> effectiveBrightnessSupplier = () -> {
|
||||
try {
|
||||
return ThemePackManager.resolveCurrentThemeBrightness(ThemePackManager.currentResolveContext());
|
||||
} catch (IOException | RuntimeException e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
InvalidationListener autoBrightnessListener = o -> {
|
||||
boolean auto;
|
||||
if (settings().getThemeAppearanceOverrides().contains(LauncherSettings.THEME_APPEARANCE_BRIGHTNESS_MODE)) {
|
||||
String val = settings().themeBrightnessModeProperty().get();
|
||||
auto = val == null || switch (val.trim().toLowerCase(Locale.ROOT)) {
|
||||
case "light", "dark" -> false;
|
||||
default -> true;
|
||||
};
|
||||
} else {
|
||||
auto = effectiveBrightnessSupplier.get() == null;
|
||||
}
|
||||
autoBrightness.set(auto);
|
||||
};
|
||||
settings().selectedThemeProperty().addListener(autoBrightnessListener);
|
||||
settings().getThemeAppearanceOverrides().addListener(autoBrightnessListener);
|
||||
settings().themeBrightnessModeProperty().addListener(autoBrightnessListener);
|
||||
settings().backgroundTypeProperty().addListener(autoBrightnessListener);
|
||||
if (FXUtils.DARK_MODE != null) {
|
||||
FXUtils.DARK_MODE.addListener(autoBrightnessListener);
|
||||
}
|
||||
autoBrightnessListener.invalidated(null);
|
||||
|
||||
ChangeListener<ResolvedTheme> listener = (observable, oldValue, newValue) -> {
|
||||
if (!Objects.equals(oldValue, newValue)) {
|
||||
colorScheme.set(newValue.toColorScheme());
|
||||
@@ -433,7 +468,7 @@ public final class Themes {
|
||||
}
|
||||
|
||||
String themeBrightnessMode = settings().themeBrightnessModeProperty().get();
|
||||
return switch (Objects.toString(themeBrightnessMode, "").toLowerCase(Locale.ROOT).trim()) {
|
||||
return switch (Objects.toString(themeBrightnessMode, "").trim().toLowerCase(Locale.ROOT)) {
|
||||
case "light" -> Brightness.LIGHT;
|
||||
case "dark" -> Brightness.DARK;
|
||||
default -> getAutomaticBrightness();
|
||||
@@ -444,24 +479,16 @@ public final class Themes {
|
||||
///
|
||||
/// @return the effective launcher brightness
|
||||
public static Brightness getCurrentBrightness() {
|
||||
Brightness contextBrightness = getThemeConditionBrightness();
|
||||
if (!settings().getThemeAppearanceOverrides().contains(LauncherSettings.THEME_APPEARANCE_BRIGHTNESS_MODE)) {
|
||||
Brightness contextBrightness = getThemeConditionBrightness();
|
||||
try {
|
||||
return Objects.requireNonNullElse(
|
||||
ThemePackManager.resolveCurrentThemeBrightness(ThemeResolveContext.current(contextBrightness)),
|
||||
contextBrightness);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
return contextBrightness;
|
||||
} catch (IOException | RuntimeException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
String themeBrightnessMode = settings().themeBrightnessModeProperty().get();
|
||||
return switch (Objects.toString(themeBrightnessMode, "").toLowerCase(Locale.ROOT).trim()) {
|
||||
case "auto" -> getAutomaticBrightness();
|
||||
case "dark" -> Brightness.DARK;
|
||||
case "light" -> Brightness.LIGHT;
|
||||
default -> getAutomaticBrightness();
|
||||
};
|
||||
return contextBrightness;
|
||||
}
|
||||
|
||||
/// Returns the brightness requested by the current system or platform settings.
|
||||
@@ -1119,8 +1146,22 @@ public final class Themes {
|
||||
return darkMode;
|
||||
}
|
||||
|
||||
/// Applies native dark-mode integration to a JavaFX stage where the platform supports it.
|
||||
/// Returns whether the current color scheme brightness is inherited from system.
|
||||
public static ReadOnlyBooleanProperty autoBrightnessProperty() {
|
||||
return autoBrightness.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
/// Registers native dark-mode integration once per stage where the platform supports it.
|
||||
///
|
||||
/// Windows updates the stage's native frame; macOS updates the application appearance. The stage retains
|
||||
/// its listener, which observes theme changes weakly. Repeated calls for an already registered stage do
|
||||
/// nothing, including after hiding the stage or replacing its scene.
|
||||
///
|
||||
/// @param stage the stage retaining the registration, accessed on the JavaFX application thread
|
||||
public static void applyNativeDarkMode(Stage stage) {
|
||||
if (stage.getProperties().containsKey(NATIVE_DARK_MODE_LISTENER)) {
|
||||
return;
|
||||
}
|
||||
if (OperatingSystem.SYSTEM_VERSION.isAtLeast(OSVersion.WINDOWS_11) && NativeUtils.USE_JNA && Dwmapi.INSTANCE != null) {
|
||||
ChangeListener<Boolean> listener = FXUtils.onWeakChange(Themes.darkModeProperty(), darkMode -> {
|
||||
if (stage.isShowing()) {
|
||||
@@ -1137,7 +1178,7 @@ public final class Themes {
|
||||
});
|
||||
}
|
||||
});
|
||||
stage.getProperties().put("Themes.applyNativeDarkMode.listener", listener);
|
||||
stage.getProperties().put(NATIVE_DARK_MODE_LISTENER, listener);
|
||||
|
||||
if (stage.isShowing()) {
|
||||
listener.changed(null, false, Themes.darkModeProperty().get());
|
||||
@@ -1154,7 +1195,7 @@ public final class Themes {
|
||||
MacOSNativeUtils.setAppearance(darkModeProperty().get());
|
||||
|
||||
ChangeListener<Boolean> listener = FXUtils.onWeakChange(Themes.darkModeProperty(), MacOSNativeUtils::setAppearance);
|
||||
stage.getProperties().put("Themes.applyNativeDarkMode.listener", listener);
|
||||
stage.getProperties().put(NATIVE_DARK_MODE_LISTENER, listener);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ public final class Controllers {
|
||||
private static Lazy<RootPage> rootPage = new Lazy<>(RootPage::new);
|
||||
/// The coordinator for the main window's scene graph and navigation stack.
|
||||
private static @Nullable Decorator decorator;
|
||||
|
||||
private static DownloadPage downloadPage;
|
||||
private static Lazy<AccountListPage> accountListPage = new Lazy<>(() -> {
|
||||
AccountListPage accountListPage = new AccountListPage();
|
||||
@@ -210,6 +211,16 @@ public final class Controllers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Configures the main stage's application identity, shutdown action, and taskbar relaunch information.
|
||||
///
|
||||
/// @param stage the unshown main stage
|
||||
private static void configureMainStage(Stage stage) {
|
||||
stage.setOnCloseRequest(event -> Launcher.stopApplication());
|
||||
FXUtils.setIcon(stage);
|
||||
stage.setTitle(Metadata.FULL_TITLE);
|
||||
WindowsNativeUtils.installWindowsAppUserModelRelaunchProperties(stage);
|
||||
}
|
||||
|
||||
/// Initializes the main application stage, scene graph, and background services.
|
||||
///
|
||||
/// @param stage the primary application stage, which must not have been shown
|
||||
@@ -230,9 +241,9 @@ public final class Controllers {
|
||||
}
|
||||
}
|
||||
|
||||
stage.setOnCloseRequest(e -> Launcher.stopApplication());
|
||||
configureMainStage(stage);
|
||||
|
||||
decorator = new Decorator(getRootPage());
|
||||
decorator = new Decorator(getRootPage(), Controllers::configureMainStage);
|
||||
Scene mainScene = decorator.attachStage(stage);
|
||||
getRootPage().getMainPage().showUpdateProperty().bind(UpdateChecker.checkingUpdateProperty().not().and(UpdateChecker.outdatedProperty()));
|
||||
getRootPage().getMainPage().showUpdateDialogProperty().bind(
|
||||
@@ -251,9 +262,6 @@ public final class Controllers {
|
||||
|
||||
StyleSheets.init(mainScene);
|
||||
|
||||
FXUtils.setIcon(stage);
|
||||
stage.setTitle(Metadata.FULL_TITLE);
|
||||
|
||||
if (!Architecture.SYSTEM_ARCH.isX86() && SettingsManager.userState().platformPromptVersionProperty().get() < 1) {
|
||||
Runnable continueAction = () -> {
|
||||
UserState userState = userState();
|
||||
@@ -606,20 +614,20 @@ public final class Controllers {
|
||||
}
|
||||
}
|
||||
|
||||
public static void openUriOrCopy(@Nullable URI uri) {
|
||||
if (uri == null) return;
|
||||
openUriOrCopy(uri.toString());
|
||||
public static void openUriOrCopy(@Nullable URI url) {
|
||||
if (url == null) return;
|
||||
openUriOrCopy(url.toString());
|
||||
}
|
||||
|
||||
public static void openUriOrCopy(@Nullable String uri) {
|
||||
if (uri == null) return;
|
||||
public static void openUriOrCopy(@Nullable String url) {
|
||||
if (url == null) return;
|
||||
var dialog = new MessageDialogPane.Builder(
|
||||
i18n("web.open_in_browser", uri),
|
||||
i18n("web.open_in_browser", url),
|
||||
i18n("message.confirm"),
|
||||
MessageDialogPane.MessageType.QUESTION
|
||||
)
|
||||
.addAction(i18n("button.copy"), () -> FXUtils.copyText(uri))
|
||||
.yesOrNo(() -> FXUtils.openLink(uri), null)
|
||||
.addAction(i18n("button.copy"), () -> FXUtils.copyText(url))
|
||||
.yesOrNo(() -> FXUtils.openLink(url), null)
|
||||
.build();
|
||||
dialog(dialog);
|
||||
}
|
||||
|
||||
@@ -72,6 +72,8 @@ public class CrashWindow extends Stage {
|
||||
FXUtils.setIcon(this);
|
||||
setTitle(i18n("message.error"));
|
||||
|
||||
FXUtils.addMacOSCloseWindowHandler(this, null);
|
||||
|
||||
setOnCloseRequest(e -> javafx.application.Platform.exit());
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ public final class DialogUtils {
|
||||
private static void showInDecorator(Decorator decorator, StackPane dialogContainer, Node content) {
|
||||
show(dialogContainer, content, dialog -> {
|
||||
JFXDialogPane pane = (JFXDialogPane) dialog.getContent();
|
||||
decorator.capableDraggingWindow(dialog);
|
||||
decorator.capableDraggingOverlay(dialog);
|
||||
decorator.forbidDraggingWindow(pane);
|
||||
dialog.setDialogContainer(dialogContainer);
|
||||
});
|
||||
|
||||
@@ -58,12 +58,11 @@ import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.*;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.download.DownloadCandidates;
|
||||
import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.task.CacheFileTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -109,10 +108,7 @@ import java.nio.file.Path;
|
||||
import java.nio.file.PathMatcher;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.ToIntFunction;
|
||||
import java.util.function.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -618,21 +614,21 @@ public final class FXUtils {
|
||||
if (link == null)
|
||||
return;
|
||||
|
||||
String uri = NetworkUtils.encodeLocation(link);
|
||||
String url = NetworkUtils.encodeLocation(link);
|
||||
thread(() -> {
|
||||
try {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
Runtime.getRuntime().exec(new String[]{"rundll32.exe", "url.dll,FileProtocolHandler", uri});
|
||||
Runtime.getRuntime().exec(new String[]{"rundll32.exe", "url.dll,FileProtocolHandler", url});
|
||||
return;
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
Runtime.getRuntime().exec(new String[]{"open", uri});
|
||||
Runtime.getRuntime().exec(new String[]{"open", url});
|
||||
return;
|
||||
} else {
|
||||
for (String browser : linuxBrowsers) {
|
||||
Path path = SystemUtils.which(browser);
|
||||
if (path != null) {
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{path.toString(), uri});
|
||||
Runtime.getRuntime().exec(new String[]{path.toString(), url});
|
||||
return;
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
@@ -645,7 +641,7 @@ public final class FXUtils {
|
||||
}
|
||||
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().browse(new URI(uri));
|
||||
java.awt.Desktop.getDesktop().browse(new URI(url));
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to open link: " + link, e);
|
||||
}
|
||||
@@ -1006,8 +1002,9 @@ public final class FXUtils {
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
}
|
||||
|
||||
public static Task<Image> getRemoteImageTask(List<URI> uris, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
|
||||
return new CacheFileTask(uris)
|
||||
/// Creates a task that caches an image from HTTP(S) candidates and loads it with the requested sizing options.
|
||||
public static Task<Image> getRemoteImageTask(DownloadCandidates candidates, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
|
||||
return new CacheFileTask(candidates)
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenApplyAsync(file -> loadImage(file, requestedWidth, requestedHeight, preserveRatio, smooth))
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
@@ -1200,6 +1197,18 @@ public final class FXUtils {
|
||||
});
|
||||
}
|
||||
|
||||
public static void addMacOSCloseWindowHandler(Window window, @Nullable Supplier<Boolean> restriction) {
|
||||
window.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
|
||||
&& event.isMetaDown()
|
||||
&& event.getCode() == KeyCode.W
|
||||
&& (restriction == null || restriction.get())) {
|
||||
window.fireEvent(new WindowEvent(window, WindowEvent.WINDOW_CLOSE_REQUEST));
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static <N extends Parent> N prepareNode(N node) {
|
||||
Scene dummyScene = new Scene(node);
|
||||
StyleSheets.init(dummyScene);
|
||||
|
||||
@@ -114,6 +114,8 @@ public class GameCrashWindow extends Stage {
|
||||
setTitle(i18n("game.crash.title"));
|
||||
FXUtils.setIcon(this);
|
||||
|
||||
FXUtils.addMacOSCloseWindowHandler(this, null);
|
||||
|
||||
analyzeCrashReport();
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,8 @@ public final class LogWindow extends Stage {
|
||||
}
|
||||
|
||||
this.gameProcess = gameProcess;
|
||||
|
||||
FXUtils.addMacOSCloseWindowHandler(this, () -> !gameProcess.isRunning());
|
||||
}
|
||||
|
||||
public void logLine(Log log) {
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.sun.jna.Pointer;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.util.platform.macos.ObjectiveCRuntime;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -56,8 +58,30 @@ public final class MacOSNativeUtils {
|
||||
return pointer == null || Pointer.nativeValue(pointer) == 0;
|
||||
}
|
||||
|
||||
private static final ReadOnlyBooleanProperty autoBrightness = Themes.autoBrightnessProperty();
|
||||
|
||||
static {
|
||||
FXUtils.onChange(autoBrightness, b -> setAppearance(Themes.darkModeProperty().get()));
|
||||
}
|
||||
|
||||
public static void setAppearance(boolean dark) {
|
||||
setAppearance(dark, false);
|
||||
if (autoBrightness.get()) {
|
||||
clearAppearance();
|
||||
} else {
|
||||
setAppearance(dark, false);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearAppearance() {
|
||||
if (nsApp == null) return;
|
||||
|
||||
try {
|
||||
var objc = ObjectiveCRuntime.INSTANCE;
|
||||
Pointer setSel = objc.sel_registerName("setAppearance:");
|
||||
objc.objc_msgSend(nsApp, setSel, Pointer.NULL);
|
||||
} catch (Throwable t) {
|
||||
LOG.warning("Failed to set macOS appearance", t);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setAppearance(boolean dark, boolean highContrast) {
|
||||
|
||||
@@ -19,15 +19,11 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXProgressBar;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -38,18 +34,11 @@ public class DialogPane extends JFXDialogLayout {
|
||||
protected final SpinnerPane acceptPane = new SpinnerPane();
|
||||
protected final JFXButton cancelButton = new JFXButton();
|
||||
protected final Label warningLabel = new Label();
|
||||
private final JFXProgressBar progressBar = new JFXProgressBar();
|
||||
|
||||
public DialogPane() {
|
||||
Label titleLabel = new Label();
|
||||
titleLabel.textProperty().bind(title);
|
||||
setHeading(titleLabel);
|
||||
getChildren().add(progressBar);
|
||||
|
||||
progressBar.setVisible(false);
|
||||
StackPane.setMargin(progressBar, new Insets(-24.0D, -24.0D, -16.0D, -24.0D));
|
||||
StackPane.setAlignment(progressBar, Pos.TOP_CENTER);
|
||||
progressBar.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
JFXButton acceptButton = new JFXButton(i18n("button.ok"));
|
||||
acceptButton.setOnAction(e -> onAccept());
|
||||
@@ -66,10 +55,6 @@ public class DialogPane extends JFXDialogLayout {
|
||||
setActions(warningLabel, acceptPane, cancelButton);
|
||||
}
|
||||
|
||||
protected JFXProgressBar getProgressBar() {
|
||||
return progressBar;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
@@ -21,12 +21,14 @@ import com.jfoenix.controls.JFXListView;
|
||||
import javafx.beans.binding.DoubleBinding;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public abstract class MDListCell<T> extends ListCell<T> {
|
||||
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
private static final PseudoClass LAST = PseudoClass.getPseudoClass("last");
|
||||
|
||||
private final StackPane container = new StackPane();
|
||||
private final StackPane root = new StackPane();
|
||||
@@ -58,7 +60,10 @@ public abstract class MDListCell<T> extends ListCell<T> {
|
||||
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (oldItem == item && oldEmpty == empty) return;
|
||||
if (oldItem == item && oldEmpty == empty) {
|
||||
updateLastPseudoClass(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
ripplerContainer.releaseRippleImmediately();
|
||||
|
||||
@@ -68,6 +73,19 @@ public abstract class MDListCell<T> extends ListCell<T> {
|
||||
} else {
|
||||
setGraphic(root);
|
||||
}
|
||||
updateLastPseudoClass(empty || item == null);
|
||||
}
|
||||
|
||||
/// Updates the `:last` pseudo-class so the trailing divider can be suppressed in CSS.
|
||||
///
|
||||
/// @param empty whether this cell is empty
|
||||
private void updateLastPseudoClass(boolean empty) {
|
||||
ListView<T> listView = getListView();
|
||||
boolean last = !empty
|
||||
&& listView != null
|
||||
&& getIndex() >= 0
|
||||
&& getIndex() == listView.getItems().size() - 1;
|
||||
root.pseudoClassStateChanged(LAST, last);
|
||||
}
|
||||
|
||||
protected StackPane getContainer() {
|
||||
@@ -80,5 +98,9 @@ public abstract class MDListCell<T> extends ListCell<T> {
|
||||
});
|
||||
}
|
||||
|
||||
protected void onClicked(Runnable action) {
|
||||
FXUtils.onClicked(ripplerContainer, action);
|
||||
}
|
||||
|
||||
protected abstract void updateControl(T item, boolean empty);
|
||||
}
|
||||
@@ -33,45 +33,16 @@ import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.download.cleanroom.CleanroomInstallTask;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricAPIInstallTask;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricInstallTask;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeNewInstallTask;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeOldInstallTask;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameInstallTask;
|
||||
import org.jackhuang.hmcl.download.java.mojang.MojangJavaDownloadTask;
|
||||
import org.jackhuang.hmcl.download.legacyfabric.LegacyFabricInstallTask;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
|
||||
import org.jackhuang.hmcl.download.neoforge.NeoForgeInstallTask;
|
||||
import org.jackhuang.hmcl.download.neoforge.NeoForgeOldInstallTask;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltAPIInstallTask;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltInstallTask;
|
||||
import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
|
||||
import org.jackhuang.hmcl.java.JavaInstallTask;
|
||||
import org.jackhuang.hmcl.modpack.MinecraftInstanceTask;
|
||||
import org.jackhuang.hmcl.modpack.ModpackInstallTask;
|
||||
import org.jackhuang.hmcl.modpack.ModpackUpdateTask;
|
||||
import org.jackhuang.hmcl.modpack.curse.CurseCompletionTask;
|
||||
import org.jackhuang.hmcl.modpack.curse.CurseInstallTask;
|
||||
import org.jackhuang.hmcl.modpack.mcbbs.McbbsModpackCompletionTask;
|
||||
import org.jackhuang.hmcl.modpack.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.modrinth.ModrinthCompletionTask;
|
||||
import org.jackhuang.hmcl.modpack.modrinth.ModrinthInstallTask;
|
||||
import org.jackhuang.hmcl.modpack.modrinth.ModrinthModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCModpackInstallTask;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackCompletionTask;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackLocalInstallTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.task.TaskListener;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Collection;
|
||||
@@ -162,58 +133,16 @@ public final class TaskListPane extends StackPane {
|
||||
if (!task.getSignificance().shouldShow() || task.getName() == null)
|
||||
return;
|
||||
|
||||
if (task instanceof GameAssetDownloadTask) {
|
||||
task.setName(i18n("assets.download_all"));
|
||||
} else if (task instanceof GameInstallTask) {
|
||||
if (task.getInheritedStage() != null && task.getInheritedStage().startsWith("hmcl.install.game"))
|
||||
return;
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.game")));
|
||||
} else if (task instanceof CleanroomInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.cleanroom")));
|
||||
} else if (task instanceof LegacyFabricInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.legacyfabric")));
|
||||
} else if (task instanceof ForgeNewInstallTask || task instanceof ForgeOldInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.forge")));
|
||||
} else if (task instanceof NeoForgeInstallTask || task instanceof NeoForgeOldInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.neoforge")));
|
||||
} else if (task instanceof LiteLoaderInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.liteloader")));
|
||||
} else if (task instanceof OptiFineInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.optifine")));
|
||||
} else if (task instanceof FabricInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.fabric")));
|
||||
} else if (task instanceof FabricAPIInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.fabric-api")));
|
||||
} else if (task instanceof QuiltInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.quilt")));
|
||||
} else if (task instanceof QuiltAPIInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.quilt-api")));
|
||||
} else if (task instanceof CurseCompletionTask || task instanceof ModrinthCompletionTask || task instanceof ServerModpackCompletionTask || task instanceof McbbsModpackCompletionTask) {
|
||||
task.setName(i18n("modpack.completion"));
|
||||
} else if (task instanceof ModpackInstallTask) {
|
||||
task.setName(i18n("modpack.installing"));
|
||||
} else if (task instanceof ModpackUpdateTask) {
|
||||
task.setName(i18n("modpack.update"));
|
||||
} else if (task instanceof CurseInstallTask) {
|
||||
task.setName(i18n("modpack.installing.given", i18n("modpack.type.curse")));
|
||||
} else if (task instanceof MultiMCModpackInstallTask) {
|
||||
task.setName(i18n("modpack.installing.given", i18n("modpack.type.multimc")));
|
||||
} else if (task instanceof ModrinthInstallTask) {
|
||||
task.setName(i18n("modpack.installing.given", i18n("modpack.type.modrinth")));
|
||||
} else if (task instanceof ServerModpackLocalInstallTask) {
|
||||
task.setName(i18n("install.installing") + ": " + i18n("modpack.type.server"));
|
||||
} else if (task instanceof HMCLModpackInstallTask) {
|
||||
task.setName(i18n("modpack.installing.given", i18n("modpack.type.hmcl")));
|
||||
} else if (task instanceof McbbsModpackExportTask || task instanceof MultiMCModpackExportTask || task instanceof ServerModpackExportTask || task instanceof ModrinthModpackExportTask) {
|
||||
task.setName(i18n("modpack.export"));
|
||||
} else if (task instanceof MinecraftInstanceTask) {
|
||||
task.setName(i18n("modpack.scan"));
|
||||
} else if (task instanceof MojangJavaDownloadTask) {
|
||||
task.setName(i18n("download.java"));
|
||||
} else if (task instanceof JavaInstallTask) {
|
||||
task.setName(i18n("java.installing"));
|
||||
if (task instanceof GameInstallTask
|
||||
&& task.getInheritedStage() != null
|
||||
&& task.getInheritedStage().startsWith("hmcl.install.game")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@Nullable String localizedName = I18n.translateTaskName(task);
|
||||
if (localizedName != null)
|
||||
task.setName(localizedName);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
ProgressListNode node = new ProgressListNode(task);
|
||||
nodes.put(task, node);
|
||||
|
||||
@@ -24,12 +24,21 @@ import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.OverrunStyle;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/// Displays a title with optional tags and a subtitle.
|
||||
public class TwoLineListItem extends VBox {
|
||||
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
|
||||
|
||||
@@ -39,14 +48,28 @@ public class TwoLineListItem extends VBox {
|
||||
private final Label lblTitle;
|
||||
private Label lblSubtitle;
|
||||
|
||||
/// Creates an empty item whose children do not receive mouse events.
|
||||
public TwoLineListItem() {
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
setMouseTransparent(true);
|
||||
|
||||
lblTitle = new Label();
|
||||
lblTitle.getStyleClass().add("title");
|
||||
lblTitle.setTextOverrun(OverrunStyle.ELLIPSIS);
|
||||
|
||||
this.firstLine = new HBox(lblTitle);
|
||||
this.firstLine = new HBox(lblTitle) {
|
||||
/// Includes the tags' natural width when requesting space from the parent.
|
||||
@Override
|
||||
protected double computePrefWidth(double height) {
|
||||
double width = super.computePrefWidth(height);
|
||||
for (Node child : getManagedChildren()) {
|
||||
if (child instanceof TagsBox tagsBox) {
|
||||
width += snapSizeX(tagsBox.computeNaturalPrefWidth(height));
|
||||
}
|
||||
}
|
||||
return width;
|
||||
}
|
||||
};
|
||||
firstLine.getStyleClass().add("first-line");
|
||||
firstLine.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
@@ -158,17 +181,29 @@ public class TwoLineListItem extends VBox {
|
||||
return lblSubtitle;
|
||||
}
|
||||
|
||||
private ObservableList<Label> tags;
|
||||
private static Label createTag(String tag, PseudoClass pseudoClass) {
|
||||
var tagLabel = new Label(tag);
|
||||
tagLabel.getStyleClass().add("tag");
|
||||
tagLabel.setMinWidth(Label.USE_PREF_SIZE);
|
||||
if (pseudoClass != null)
|
||||
tagLabel.pseudoClassStateChanged(pseudoClass, true);
|
||||
return tagLabel;
|
||||
}
|
||||
|
||||
/// Stores the mutable tag list once tag support has been initialized.
|
||||
private @Nullable ObservableList<Label> tags;
|
||||
|
||||
/// Returns the mutable list of tags displayed after the title.
|
||||
public ObservableList<Label> getTags() {
|
||||
if (tags == null) {
|
||||
tags = FXCollections.observableArrayList();
|
||||
|
||||
var tagsBox = new HBox(8);
|
||||
var tagsBox = new TagsBox();
|
||||
tagsBox.getStyleClass().add("tags");
|
||||
tagsBox.setAlignment(Pos.CENTER_LEFT);
|
||||
tagsBox.setMinWidth(0);
|
||||
HBox.setHgrow(tagsBox, Priority.ALWAYS);
|
||||
|
||||
HBox.setHgrow(tagsBox, Priority.SOMETIMES);
|
||||
|
||||
Bindings.bindContent(tagsBox.getChildren(), tags);
|
||||
var isNotEmpty = Bindings.isNotEmpty(tags);
|
||||
tagsBox.managedProperty().bind(isNotEmpty);
|
||||
@@ -176,25 +211,53 @@ public class TwoLineListItem extends VBox {
|
||||
|
||||
FXUtils.setOverflowHidden(tagsBox);
|
||||
|
||||
lblTitle.setMinWidth(Label.USE_PREF_SIZE);
|
||||
HBox.setHgrow(lblTitle, Priority.ALWAYS);
|
||||
lblTitle.setMinWidth(0);
|
||||
firstLine.getChildren().setAll(lblTitle, tagsBox);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
/// Reports its natural width separately from the width used to allocate space within the title row.
|
||||
@NotNullByDefault
|
||||
private static final class TagsBox extends HBox {
|
||||
|
||||
/// Creates a tag container with zero minimum and preferred allocation widths.
|
||||
private TagsBox() {
|
||||
super(8);
|
||||
// Start at zero during HBox allocation so tags do not compete with the title for space.
|
||||
setPrefWidth(0);
|
||||
setMinWidth(0);
|
||||
}
|
||||
|
||||
/// Returns the computed preferred width without applying the zero preferred-width override.
|
||||
///
|
||||
/// @param height the available height, or -1 if unspecified
|
||||
/// @return the natural preferred width of the tags and their container
|
||||
private double computeNaturalPrefWidth(double height) {
|
||||
return super.computePrefWidth(height);
|
||||
}
|
||||
}
|
||||
|
||||
public void addTag(String tag, PseudoClass pseudoClass) {
|
||||
var tagLabel = new Label(tag);
|
||||
tagLabel.getStyleClass().add("tag");
|
||||
tagLabel.setMinWidth(Label.USE_PREF_SIZE);
|
||||
if (pseudoClass != null)
|
||||
tagLabel.pseudoClassStateChanged(pseudoClass, true);
|
||||
getTags().add(tagLabel);
|
||||
getTags().add(createTag(tag, pseudoClass));
|
||||
}
|
||||
|
||||
public void addTag(String tag) {
|
||||
addTag(tag, null);
|
||||
}
|
||||
|
||||
public void addTags(Collection<String> tags) {
|
||||
getTags().addAll(tags.stream().map(tag -> createTag(tag, null)).toList());
|
||||
}
|
||||
|
||||
public void addTagsIfNotExist(Collection<String> tags) {
|
||||
var current = getTags().stream().map(Label::getText).collect(Collectors.toSet());
|
||||
var target = new LinkedHashSet<>(tags);
|
||||
target.removeAll(current);
|
||||
addTags(target);
|
||||
}
|
||||
|
||||
private static final PseudoClass WARNING_PSEUDO_CLASS = PseudoClass.getPseudoClass("warning");
|
||||
|
||||
public void addTagWarning(String tag) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
@@ -61,6 +62,7 @@ import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
|
||||
import org.jackhuang.hmcl.setting.SettingsManager;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.DialogUtils;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -78,6 +80,9 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
|
||||
/// Coordinates the main window's visual tree, navigation, dialogs, and native window behavior.
|
||||
@@ -129,6 +134,29 @@ public final class Decorator {
|
||||
/// The navigation stack rendered in the main window.
|
||||
private final Navigator navigator = new Navigator();
|
||||
|
||||
/// Reflective access to system decoration, or `null` on unsupported runtimes.
|
||||
private final @Nullable NativeWindowDecoration nativeDecoration = NativeWindowDecoration.create();
|
||||
|
||||
/// Whether the attached stage delegates window decoration and gestures to the platform.
|
||||
private boolean nativeDecorationEnabled;
|
||||
|
||||
/// Installs application-specific identity and lifecycle integration on replacement stages before attachment.
|
||||
private final Consumer<Stage> stageInitializer;
|
||||
|
||||
/// Whether an appearance change has already queued a window-style check.
|
||||
private boolean windowStyleUpdateQueued;
|
||||
|
||||
/// Coalesces appearance changes so stage replacement runs after theme bindings have updated.
|
||||
private final ChangeListener<Boolean> windowStyleListener = (observable, oldValue, newValue) -> {
|
||||
if (!windowStyleUpdateQueued) {
|
||||
windowStyleUpdateQueued = true;
|
||||
Platform.runLater(() -> {
|
||||
windowStyleUpdateQueued = false;
|
||||
updateWindowStyle();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/// The clipped main-window content hosted inside [#shadowContainer].
|
||||
private final MainWindowPane mainWindowPane;
|
||||
|
||||
@@ -141,15 +169,18 @@ public final class Decorator {
|
||||
/// Whether the navigation close action should use the home icon.
|
||||
private final BooleanProperty showCloseAsHome = new SimpleBooleanProperty(this, "showCloseAsHome");
|
||||
|
||||
/// The width of the visible window content, excluding custom decoration insets.
|
||||
/// The width of the visible window content, excluding native borders and custom shadow padding.
|
||||
private final DoubleProperty contentWidth = new SimpleDoubleProperty(this, "contentWidth");
|
||||
|
||||
/// The height of the visible window content, excluding custom decoration insets.
|
||||
/// The height of the visible window content, excluding native borders and custom shadow padding.
|
||||
private final DoubleProperty contentHeight = new SimpleDoubleProperty(this, "contentHeight");
|
||||
|
||||
/// The stage currently controlled by this decorator, or `null` while detached.
|
||||
private @Nullable Stage stage;
|
||||
|
||||
/// Tracks the attached stage's content geometry, or `null` while detached.
|
||||
private @Nullable WindowBounds windowBounds;
|
||||
|
||||
/// The title-bar state supplied by the current page, or `null` before navigation is initialized.
|
||||
private final ObjectProperty<DecoratorPage.@Nullable State> state = new SimpleObjectProperty<>(this, "state");
|
||||
|
||||
@@ -198,49 +229,7 @@ public final class Decorator {
|
||||
private final InvalidationListener stageDecorationListener = observable -> {
|
||||
@Nullable Stage currentStage = stage;
|
||||
if (currentStage != null) {
|
||||
updateWindowDecoration(currentStage.isMaximized() || currentStage.isFullScreen());
|
||||
}
|
||||
};
|
||||
|
||||
/// Updates the changed visible-content bound and persists normal, non-iconified stage bounds.
|
||||
private final InvalidationListener stageBoundsListener = observable -> {
|
||||
@Nullable Stage currentStage = stage;
|
||||
if (currentStage == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Insets insets = getWindowInsets();
|
||||
boolean saveBounds = !currentStage.isIconified()
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4290
|
||||
&& (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
|
||||
|| !currentStage.isFullScreen() && !currentStage.isMaximized());
|
||||
|
||||
if (observable == currentStage.xProperty()) {
|
||||
if (saveBounds) {
|
||||
double currentContentX = currentStage.getX() + insets.getLeft();
|
||||
SettingsManager.state().setX(currentContentX / PRIMARY_SCREEN_BOUNDS.getWidth());
|
||||
}
|
||||
} else if (observable == currentStage.yProperty()) {
|
||||
if (saveBounds) {
|
||||
double currentContentY = currentStage.getY() + insets.getTop();
|
||||
SettingsManager.state().setY(currentContentY / PRIMARY_SCREEN_BOUNDS.getHeight());
|
||||
}
|
||||
} else if (observable == currentStage.widthProperty()) {
|
||||
double currentContentWidth = Math.max(
|
||||
MIN_CONTENT_WIDTH,
|
||||
currentStage.getWidth() - insets.getLeft() - insets.getRight());
|
||||
contentWidth.set(currentContentWidth);
|
||||
if (saveBounds) {
|
||||
SettingsManager.state().setWidth(currentContentWidth);
|
||||
}
|
||||
} else if (observable == currentStage.heightProperty()) {
|
||||
double currentContentHeight = Math.max(
|
||||
MIN_CONTENT_HEIGHT,
|
||||
currentStage.getHeight() - insets.getTop() - insets.getBottom());
|
||||
contentHeight.set(currentContentHeight);
|
||||
if (saveBounds) {
|
||||
SettingsManager.state().setHeight(currentContentHeight);
|
||||
}
|
||||
updateWindowDecoration(WindowState.isMaximized(currentStage) || currentStage.isFullScreen());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -251,9 +240,13 @@ public final class Decorator {
|
||||
///
|
||||
/// The decorator initially enables its custom window shadow. No scene is created until
|
||||
/// [#attachStage(Stage)] is called or [#getRoot()] is otherwise installed in a scene.
|
||||
/// Appearance changes replace the attached stage when required, preserving its retained scene and window state.
|
||||
///
|
||||
/// @param mainPage the permanent root page of the navigation stack
|
||||
public Decorator(Node mainPage) {
|
||||
/// @param stageInitializer configures each replacement stage on the JavaFX application thread before attachment;
|
||||
/// it must not show the stage and is not called for stages supplied to [#attachStage(Stage)]
|
||||
public Decorator(Node mainPage, Consumer<Stage> stageInitializer) {
|
||||
this.stageInitializer = Objects.requireNonNull(stageInitializer);
|
||||
navigator.setOnNavigated(this::onNavigated);
|
||||
|
||||
mainWindowPane = new MainWindowPane(this);
|
||||
@@ -285,21 +278,21 @@ public final class Decorator {
|
||||
return root;
|
||||
}
|
||||
|
||||
/// Returns the insets reserved outside the main-window content.
|
||||
/// Returns the space reserved inside the scene for custom window shadows.
|
||||
///
|
||||
/// @return the root's shadow insets for a normal window, or [Insets#EMPTY] while maximized or full-screen
|
||||
/// @return the custom shadow insets, or [Insets#EMPTY] with native decoration, supported maximization, or full-screen
|
||||
public Insets getWindowInsets() {
|
||||
return root.getPadding();
|
||||
}
|
||||
|
||||
/// Returns the visible window-content width property, excluding custom decoration insets.
|
||||
/// Returns the visible window-content width property, excluding native borders and custom shadow padding.
|
||||
///
|
||||
/// @return the current content-width property
|
||||
public ReadOnlyDoubleProperty contentWidthProperty() {
|
||||
return contentWidth;
|
||||
}
|
||||
|
||||
/// Returns the visible window-content height property, excluding custom decoration insets.
|
||||
/// Returns the visible window-content height property, excluding native borders and custom shadow padding.
|
||||
///
|
||||
/// @return the current content-height property
|
||||
public ReadOnlyDoubleProperty contentHeightProperty() {
|
||||
@@ -316,11 +309,12 @@ public final class Decorator {
|
||||
|
||||
/// Updates the shadow, outer insets, and content corner shape for an edge-to-edge window state.
|
||||
///
|
||||
/// @param edgeToEdge whether the attached stage is maximized or full-screen
|
||||
/// @param edgeToEdge whether the attached stage is maximized under [WindowState]'s policy or full-screen
|
||||
private void updateWindowDecoration(boolean edgeToEdge) {
|
||||
root.setPadding(edgeToEdge ? Insets.EMPTY : SHADOW_INSETS);
|
||||
shadowContainer.setEffect(edgeToEdge ? null : windowShadow);
|
||||
mainWindowPane.setWindowEdgeToEdge(edgeToEdge);
|
||||
boolean customDecoration = !nativeDecorationEnabled && !edgeToEdge;
|
||||
root.setPadding(customDecoration ? SHADOW_INSETS : Insets.EMPTY);
|
||||
shadowContainer.setEffect(customDecoration ? windowShadow : null);
|
||||
mainWindowPane.setWindowCornersRounded(customDecoration);
|
||||
}
|
||||
|
||||
/// Returns the pane on which application dialogs are stacked.
|
||||
@@ -405,7 +399,14 @@ public final class Decorator {
|
||||
///
|
||||
/// @param node the drag-enabled node
|
||||
public void capableDraggingWindow(Node node) {
|
||||
node.addEventHandler(MouseEvent.MOUSE_MOVED, event -> allowMove = true);
|
||||
if (nativeDecoration != null) {
|
||||
nativeDecoration.setDraggable(node, true);
|
||||
}
|
||||
node.addEventHandler(MouseEvent.MOUSE_MOVED, event -> {
|
||||
if (!nativeDecorationEnabled) {
|
||||
allowMove = true;
|
||||
}
|
||||
});
|
||||
node.addEventHandler(MouseEvent.MOUSE_EXITED, event -> {
|
||||
if (!dragging) {
|
||||
allowMove = false;
|
||||
@@ -413,6 +414,19 @@ public final class Decorator {
|
||||
});
|
||||
}
|
||||
|
||||
/// Configures window dragging through an overlay while retaining ordinary mouse picking.
|
||||
///
|
||||
/// Native decoration permits dragging only over underlying draggable header areas. Custom decoration
|
||||
/// permits dragging from the overlay itself. Interactive content must be excluded with [#forbidDraggingWindow(Node)].
|
||||
///
|
||||
/// @param overlay the overlay above the window content and title bar
|
||||
public void capableDraggingOverlay(Node overlay) {
|
||||
capableDraggingWindow(overlay);
|
||||
if (nativeDecoration != null) {
|
||||
nativeDecoration.setDragTransparent(overlay);
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers `node` as the title-bar area used to restore and maximize the attached stage.
|
||||
///
|
||||
/// @param node the title-bar node
|
||||
@@ -425,9 +439,14 @@ public final class Decorator {
|
||||
///
|
||||
/// @param node the node that must consume drag eligibility
|
||||
public void forbidDraggingWindow(Node node) {
|
||||
if (nativeDecoration != null) {
|
||||
nativeDecoration.setDraggable(node, false);
|
||||
}
|
||||
node.addEventHandler(MouseEvent.MOUSE_MOVED, event -> {
|
||||
allowMove = false;
|
||||
event.consume();
|
||||
if (!nativeDecorationEnabled) {
|
||||
allowMove = false;
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -436,14 +455,15 @@ public final class Decorator {
|
||||
/// @param event the title-bar click event
|
||||
private void onTitleBarDoubleClick(MouseEvent event) {
|
||||
@Nullable Stage currentStage = stage;
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
|
||||
if (nativeDecorationEnabled
|
||||
|| currentStage == null
|
||||
|| !WindowState.supportsMaximization(currentStage)
|
||||
|| event.getButton() != MouseButton.PRIMARY
|
||||
|| event.getClickCount() != 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentStage.setMaximized(!currentStage.isMaximized());
|
||||
currentStage.setMaximized(!WindowState.isMaximized(currentStage));
|
||||
event.consume();
|
||||
}
|
||||
|
||||
@@ -452,7 +472,7 @@ public final class Decorator {
|
||||
/// @param event the title-bar drag event
|
||||
private void onTitleBarDragged(MouseEvent event) {
|
||||
@Nullable Stage currentStage = stage;
|
||||
if (currentStage == null || dragging || !currentStage.isMaximized()) {
|
||||
if (nativeDecorationEnabled || currentStage == null || dragging || !WindowState.isMaximized(currentStage)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -531,10 +551,10 @@ public final class Decorator {
|
||||
/// @param event the pointer movement event
|
||||
private void onMouseMoved(MouseEvent event) {
|
||||
@Nullable Stage currentStage = stage;
|
||||
if (currentStage == null
|
||||
if (nativeDecorationEnabled || currentStage == null
|
||||
|| currentStage.isIconified()
|
||||
|| currentStage.isFullScreen()
|
||||
|| currentStage.isMaximized()
|
||||
|| WindowState.isMaximized(currentStage)
|
||||
|| !currentStage.isResizable()) {
|
||||
root.setCursor(Cursor.DEFAULT);
|
||||
return;
|
||||
@@ -597,10 +617,10 @@ public final class Decorator {
|
||||
/// @param event the primary-button drag event
|
||||
private void onMouseDragged(MouseEvent event) {
|
||||
@Nullable Stage currentStage = stage;
|
||||
if (currentStage == null
|
||||
if (nativeDecorationEnabled || currentStage == null
|
||||
|| currentStage.isIconified()
|
||||
|| currentStage.isFullScreen()
|
||||
|| currentStage.isMaximized()
|
||||
|| WindowState.isMaximized(currentStage)
|
||||
|| !event.isPrimaryButtonDown()
|
||||
|| event.isStillSincePress()) {
|
||||
return;
|
||||
@@ -673,13 +693,12 @@ public final class Decorator {
|
||||
return root.getScene();
|
||||
}
|
||||
|
||||
/// Applies the persisted content bounds and current decoration insets to `targetStage`.
|
||||
/// Returns the persisted normal content rectangle without native borders or custom shadow padding.
|
||||
///
|
||||
/// An off-screen persisted position is replaced with a position centered on the primary screen.
|
||||
///
|
||||
/// @param targetStage the stage receiving the initial outer bounds and minimum dimensions
|
||||
private void initializeStageBounds(Stage targetStage) {
|
||||
Insets insets = getWindowInsets();
|
||||
/// @return the initial content bounds in JavaFX screen coordinates
|
||||
private Rectangle2D initialContentBounds() {
|
||||
double initialContentWidth = Math.max(MIN_CONTENT_WIDTH, SettingsManager.state().getWidth());
|
||||
double initialContentHeight = Math.max(MIN_CONTENT_HEIGHT, SettingsManager.state().getHeight());
|
||||
double initialContentX = SettingsManager.state().getX() * PRIMARY_SCREEN_BOUNDS.getWidth();
|
||||
@@ -702,14 +721,68 @@ public final class Decorator {
|
||||
initialContentY = (PRIMARY_SCREEN_BOUNDS.getHeight() - initialContentHeight) / 2;
|
||||
}
|
||||
|
||||
targetStage.setX(initialContentX - insets.getLeft());
|
||||
targetStage.setY(initialContentY - insets.getTop());
|
||||
targetStage.setWidth(initialContentWidth + insets.getLeft() + insets.getRight());
|
||||
targetStage.setHeight(initialContentHeight + insets.getTop() + insets.getBottom());
|
||||
targetStage.setMinWidth(MIN_CONTENT_WIDTH + insets.getLeft() + insets.getRight());
|
||||
targetStage.setMinHeight(MIN_CONTENT_HEIGHT + insets.getTop() + insets.getBottom());
|
||||
contentWidth.set(initialContentWidth);
|
||||
contentHeight.set(initialContentHeight);
|
||||
return new Rectangle2D(initialContentX, initialContentY, initialContentWidth, initialContentHeight);
|
||||
}
|
||||
|
||||
/// Persists a normal content rectangle independently of the window's decoration style.
|
||||
///
|
||||
/// @param bounds the content bounds in JavaFX screen coordinates
|
||||
private void saveContentBounds(Rectangle2D bounds) {
|
||||
SettingsManager.state().setX(bounds.getMinX() / PRIMARY_SCREEN_BOUNDS.getWidth());
|
||||
SettingsManager.state().setY(bounds.getMinY() / PRIMARY_SCREEN_BOUNDS.getHeight());
|
||||
SettingsManager.state().setWidth(bounds.getWidth());
|
||||
SettingsManager.state().setHeight(bounds.getHeight());
|
||||
}
|
||||
|
||||
/// Returns the preferred style for the configured native-decoration policy and current theme.
|
||||
///
|
||||
/// @return the system style when available and permitted by the policy; otherwise the custom transparent style
|
||||
private StageStyle preferredStageStyle() {
|
||||
return nativeDecoration != null && nativeDecoration.isPreferred(
|
||||
Themes.windowTransparentProperty().get(), Themes.darkModeProperty().get())
|
||||
? nativeDecoration.style : StageStyle.TRANSPARENT;
|
||||
}
|
||||
|
||||
/// Replaces the attached stage when transparency or theme brightness requires a different decoration style.
|
||||
///
|
||||
/// Preserves the scene, navigation, dialogs, normal bounds, supported window states, and focus owner.
|
||||
/// Hidden windows remain hidden. Does nothing while detached or when the current style is still suitable.
|
||||
/// Must run on the JavaFX application thread after theme bindings have updated.
|
||||
private void updateWindowStyle() {
|
||||
FXUtils.checkFxUserThread();
|
||||
@Nullable Stage previousStage = stage;
|
||||
if (previousStage == null || previousStage.getStyle() == preferredStageStyle()) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean showing = previousStage.isShowing();
|
||||
boolean maximized = WindowState.isMaximized(previousStage);
|
||||
boolean fullScreen = previousStage.isFullScreen();
|
||||
boolean iconified = previousStage.isIconified();
|
||||
@Nullable Node focusOwner = previousStage.getScene().getFocusOwner();
|
||||
|
||||
Stage replacement = new Stage();
|
||||
stageInitializer.accept(replacement);
|
||||
replacement.setTitle(previousStage.getTitle());
|
||||
replacement.getIcons().setAll(previousStage.getIcons());
|
||||
replacement.setOnCloseRequest(previousStage.getOnCloseRequest());
|
||||
replacement.setResizable(previousStage.isResizable());
|
||||
replacement.setAlwaysOnTop(previousStage.isAlwaysOnTop());
|
||||
replacement.setFullScreenExitHint(previousStage.getFullScreenExitHint());
|
||||
replacement.setFullScreenExitKeyCombination(previousStage.getFullScreenExitKeyCombination());
|
||||
|
||||
detachStage();
|
||||
previousStage.hide();
|
||||
attachStage(replacement);
|
||||
replacement.setMaximized(maximized && WindowState.supportsMaximization(replacement));
|
||||
replacement.setFullScreen(fullScreen);
|
||||
replacement.setIconified(iconified);
|
||||
if (showing) {
|
||||
replacement.show();
|
||||
}
|
||||
if (focusOwner != null) {
|
||||
focusOwner.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
/// Attaches the retained scene and native window behavior to `newStage`.
|
||||
@@ -717,23 +790,40 @@ public final class Decorator {
|
||||
/// If the root has no scene, this method reuses `newStage`'s scene and replaces its root, or creates a
|
||||
/// transparent scene when the stage has none. If the retained scene belongs to another stage, it is detached
|
||||
/// from that stage before being installed on `newStage`. A newly attached stage receives the persisted normal
|
||||
/// content bounds and minimum size adjusted for the normal decoration insets. The stage and retained scene use
|
||||
/// transparent styles required by the custom decoration. Any active window animation is cancelled and reset.
|
||||
/// When window animations are enabled, each subsequent showing starts the opening animation. This method does
|
||||
/// not show the stage.
|
||||
/// content bounds. Native frame offsets and minimum size are resolved when the stage is shown.
|
||||
/// The native-decoration policy selects the system or custom transparent decoration. Automatic mode uses
|
||||
/// system decoration for supported opaque windows, except for dark windows on Windows; explicit enablement
|
||||
/// bypasses the brightness exclusion. Transparent windows always use custom decoration.
|
||||
/// Any active window animation is cancelled and reset. Custom window
|
||||
/// animations run only with custom decoration. Window-style listeners remain active until [#detachStage()].
|
||||
/// On macOS, native application appearance updates are installed once per stage and retained across detachment.
|
||||
/// This method does not show the stage or invoke the replacement-stage initializer.
|
||||
///
|
||||
/// @param newStage the stage to attach, which must be accessed on the JavaFX application thread
|
||||
/// @return the scene installed on `newStage`
|
||||
/// @throws IllegalStateException if `newStage` has already been shown with a non-transparent style, or if the
|
||||
/// @throws IllegalStateException if `newStage` has already been shown with a different style, or if the
|
||||
/// retained root or scene cannot be transferred from its current owner
|
||||
public Scene attachStage(Stage newStage) {
|
||||
FXUtils.checkFxUserThread();
|
||||
stopWindowAnimation();
|
||||
|
||||
if (newStage.getStyle() != StageStyle.TRANSPARENT) {
|
||||
newStage.initStyle(StageStyle.TRANSPARENT);
|
||||
if (stage != null && stage != newStage) {
|
||||
detachStage();
|
||||
}
|
||||
|
||||
StageStyle style = preferredStageStyle();
|
||||
if (newStage.getStyle() != style) {
|
||||
newStage.initStyle(style);
|
||||
}
|
||||
nativeDecorationEnabled = nativeDecoration != null && style == nativeDecoration.style;
|
||||
if (nativeDecoration != null) {
|
||||
nativeDecoration.setContent(null);
|
||||
if (nativeDecorationEnabled) {
|
||||
nativeDecoration.configureStage(newStage);
|
||||
}
|
||||
}
|
||||
mainWindowPane.setNativeDecoration(nativeDecorationEnabled ? nativeDecoration : null);
|
||||
|
||||
@Nullable Scene retainedScene = root.getScene();
|
||||
Scene scene;
|
||||
if (retainedScene == null) {
|
||||
@@ -759,37 +849,37 @@ public final class Decorator {
|
||||
}
|
||||
}
|
||||
|
||||
scene.setFill(Color.TRANSPARENT);
|
||||
if (nativeDecorationEnabled) {
|
||||
scene.fillProperty().bind(Themes.colorSchemeProperty().getSurfaceContainer());
|
||||
} else {
|
||||
scene.fillProperty().unbind();
|
||||
scene.setFill(Color.TRANSPARENT);
|
||||
}
|
||||
|
||||
if (newStage.getScene() != scene) {
|
||||
newStage.setScene(scene);
|
||||
}
|
||||
|
||||
if (stage != newStage) {
|
||||
playRestoreMinimizeAnimation = false;
|
||||
if (stage != null) {
|
||||
stage.removeEventHandler(WindowEvent.WINDOW_SHOWING, windowShowingHandler);
|
||||
stage.iconifiedProperty().removeListener(iconifiedListener);
|
||||
stage.maximizedProperty().removeListener(stageDecorationListener);
|
||||
stage.fullScreenProperty().removeListener(stageDecorationListener);
|
||||
stage.xProperty().removeListener(stageBoundsListener);
|
||||
stage.yProperty().removeListener(stageBoundsListener);
|
||||
stage.widthProperty().removeListener(stageBoundsListener);
|
||||
stage.heightProperty().removeListener(stageBoundsListener);
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
Themes.applyNativeDarkMode(newStage);
|
||||
}
|
||||
|
||||
playRestoreMinimizeAnimation = false;
|
||||
updateWindowDecoration(false);
|
||||
initializeStageBounds(newStage);
|
||||
stage = newStage;
|
||||
updateWindowDecoration(newStage.isMaximized() || newStage.isFullScreen());
|
||||
newStage.addEventHandler(WindowEvent.WINDOW_SHOWING, windowShowingHandler);
|
||||
newStage.iconifiedProperty().addListener(iconifiedListener);
|
||||
newStage.maximizedProperty().addListener(stageDecorationListener);
|
||||
newStage.fullScreenProperty().addListener(stageDecorationListener);
|
||||
newStage.xProperty().addListener(stageBoundsListener);
|
||||
newStage.yProperty().addListener(stageBoundsListener);
|
||||
newStage.widthProperty().addListener(stageBoundsListener);
|
||||
newStage.heightProperty().addListener(stageBoundsListener);
|
||||
windowBounds = new WindowBounds(newStage, root, initialContentBounds(),
|
||||
MIN_CONTENT_WIDTH, MIN_CONTENT_HEIGHT, this::saveContentBounds);
|
||||
contentWidth.bind(windowBounds.contentWidthProperty());
|
||||
contentHeight.bind(windowBounds.contentHeightProperty());
|
||||
updateWindowDecoration(WindowState.isMaximized(newStage) || newStage.isFullScreen());
|
||||
Themes.windowTransparentProperty().addListener(windowStyleListener);
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
Themes.darkModeProperty().addListener(windowStyleListener);
|
||||
}
|
||||
}
|
||||
root.setCursor(Cursor.DEFAULT);
|
||||
dragging = false;
|
||||
@@ -801,20 +891,27 @@ public final class Decorator {
|
||||
///
|
||||
/// The root, scene, navigation state, dialogs, and snackbar remain owned by this decorator and may subsequently
|
||||
/// be attached to another stage. Calling this method while detached has no effect beyond resetting root transforms.
|
||||
/// Removes window-style listeners; a queued style check does nothing unless another stage has been attached.
|
||||
/// Hiding a stage that will later be shown again does not require detachment.
|
||||
public void detachStage() {
|
||||
FXUtils.checkFxUserThread();
|
||||
stopWindowAnimation();
|
||||
|
||||
if (windowBounds != null) {
|
||||
windowBounds.close();
|
||||
contentWidth.unbind();
|
||||
contentHeight.unbind();
|
||||
windowBounds = null;
|
||||
}
|
||||
if (stage != null) {
|
||||
Themes.windowTransparentProperty().removeListener(windowStyleListener);
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
Themes.darkModeProperty().removeListener(windowStyleListener);
|
||||
}
|
||||
stage.removeEventHandler(WindowEvent.WINDOW_SHOWING, windowShowingHandler);
|
||||
stage.iconifiedProperty().removeListener(iconifiedListener);
|
||||
stage.maximizedProperty().removeListener(stageDecorationListener);
|
||||
stage.fullScreenProperty().removeListener(stageDecorationListener);
|
||||
stage.xProperty().removeListener(stageBoundsListener);
|
||||
stage.yProperty().removeListener(stageBoundsListener);
|
||||
stage.widthProperty().removeListener(stageBoundsListener);
|
||||
stage.heightProperty().removeListener(stageBoundsListener);
|
||||
if (stage.getScene() == root.getScene()) {
|
||||
stage.setScene(null);
|
||||
}
|
||||
@@ -880,7 +977,7 @@ public final class Decorator {
|
||||
/// Calling this method replaces any active minimize, restore, close, or earlier opening animation. If window
|
||||
/// animations are disabled, it restores the stable root transform without starting an animation.
|
||||
private void playOpenAnimation() {
|
||||
if (!AnimationUtils.playWindowAnimation()) {
|
||||
if (nativeDecorationEnabled || !AnimationUtils.playWindowAnimation()) {
|
||||
stopWindowAnimation();
|
||||
Controllers.trimHeap();
|
||||
return;
|
||||
@@ -915,7 +1012,7 @@ public final class Decorator {
|
||||
return;
|
||||
}
|
||||
|
||||
if (AnimationUtils.playWindowAnimation() && OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) {
|
||||
if (!nativeDecorationEnabled && AnimationUtils.playWindowAnimation() && OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) {
|
||||
Timeline timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(root.opacityProperty(), 1, Motion.EASE),
|
||||
@@ -946,7 +1043,7 @@ public final class Decorator {
|
||||
|
||||
/// Closes the application, using the configured window animation when enabled.
|
||||
void closeWindow() {
|
||||
if (AnimationUtils.playWindowAnimation()) {
|
||||
if (!nativeDecorationEnabled && AnimationUtils.playWindowAnimation()) {
|
||||
Timeline timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(root.opacityProperty(), 1, Motion.EASE),
|
||||
@@ -1074,6 +1171,11 @@ public final class Decorator {
|
||||
|
||||
/// Restores the root node's transform after an animated minimization.
|
||||
private void playRestoreAnimation() {
|
||||
if (nativeDecorationEnabled) {
|
||||
stopWindowAnimation();
|
||||
return;
|
||||
}
|
||||
|
||||
Timeline timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(root.opacityProperty(), 0, Motion.EASE),
|
||||
|
||||
@@ -63,7 +63,7 @@ final class MainWindowPane extends StackPane {
|
||||
/// The decorator whose state and actions are represented by this pane.
|
||||
private final Decorator decorator;
|
||||
|
||||
/// The clip that rounds normal window corners and becomes square while the window fills the screen.
|
||||
/// The content clip, rounded only for normal windows with custom decoration.
|
||||
private final Rectangle clip = new Rectangle();
|
||||
|
||||
/// The frame containing the title bar and current navigation page.
|
||||
@@ -72,6 +72,9 @@ final class MainWindowPane extends StackPane {
|
||||
/// The title bar containing page navigation state.
|
||||
private final BorderPane titleBar;
|
||||
|
||||
/// The custom help, minimize, and close buttons shared by both decoration styles.
|
||||
private final HBox windowButtons;
|
||||
|
||||
/// The transition container used when the title-bar state changes.
|
||||
private final TransitionPane navBarPane;
|
||||
|
||||
@@ -112,14 +115,15 @@ final class MainWindowPane extends StackPane {
|
||||
center.getChildren().setAll(decorator.getNavigator());
|
||||
frame.setCenter(center);
|
||||
|
||||
HBox rightButtonsContainer = createWindowButtons();
|
||||
windowButtons = createWindowButtons();
|
||||
titleBar = new BorderPane();
|
||||
titleBar.setPickOnBounds(false);
|
||||
titleBar.getStyleClass().add("jfx-tool-bar");
|
||||
titleBar.setRight(rightButtonsContainer);
|
||||
titleBar.setRight(windowButtons);
|
||||
|
||||
navBarPane = new TransitionPane();
|
||||
titleBar.setCenter(navBarPane);
|
||||
decorator.capableDraggingWindow(navBarPane);
|
||||
frame.setTop(titleBar);
|
||||
|
||||
updateTitleBarBackground();
|
||||
@@ -136,11 +140,27 @@ final class MainWindowPane extends StackPane {
|
||||
getChildren().setAll(backgroundNode, frame);
|
||||
}
|
||||
|
||||
/// Updates the content-corner shape for an edge-to-edge window state.
|
||||
/// Moves the custom title bar into or out of the native dragging header.
|
||||
///
|
||||
/// @param edgeToEdge whether the attached window is maximized or full-screen
|
||||
void setWindowEdgeToEdge(boolean edgeToEdge) {
|
||||
double arc = edgeToEdge ? 0.0 : ARC;
|
||||
/// @param decoration the native header support, or `null` for custom decoration
|
||||
void setNativeDecoration(@Nullable NativeWindowDecoration decoration) {
|
||||
frame.setTop(null);
|
||||
if (decoration == null) {
|
||||
frame.setTop(titleBar);
|
||||
} else {
|
||||
decoration.setContent(titleBar);
|
||||
decoration.headerBar.backgroundProperty().bind(titleBar.backgroundProperty());
|
||||
frame.setTop(decoration.headerBar);
|
||||
}
|
||||
}
|
||||
|
||||
/// Enables or disables custom rounding of the content corners.
|
||||
///
|
||||
/// Native decoration uses square content bounds so the platform can shape the window outline.
|
||||
///
|
||||
/// @param rounded whether to round the content corners
|
||||
void setWindowCornersRounded(boolean rounded) {
|
||||
double arc = rounded ? ARC : 0.0;
|
||||
clip.setArcWidth(arc);
|
||||
clip.setArcHeight(arc);
|
||||
}
|
||||
@@ -171,6 +191,7 @@ final class MainWindowPane extends StackPane {
|
||||
private HBox createWindowButtons() {
|
||||
HBox buttons = new HBox();
|
||||
buttons.setAlignment(Pos.TOP_RIGHT);
|
||||
decorator.capableDraggingWindow(buttons);
|
||||
buttons.setMaxSize(Region.USE_PREF_SIZE, 40);
|
||||
|
||||
JFXButton helpButton = new JFXButton();
|
||||
@@ -247,6 +268,7 @@ final class MainWindowPane extends StackPane {
|
||||
private Node createNavBar(DecoratorPage.State state) {
|
||||
HBox navBar = new HBox();
|
||||
navBar.setAlignment(Pos.CENTER_LEFT);
|
||||
decorator.capableDraggingWindow(navBar);
|
||||
|
||||
// Left navigation buttons
|
||||
if (state.backable()) {
|
||||
@@ -275,6 +297,7 @@ final class MainWindowPane extends StackPane {
|
||||
StackPane titleArea = new StackPane();
|
||||
titleArea.setAlignment(Pos.CENTER_LEFT);
|
||||
titleArea.setMinWidth(0);
|
||||
decorator.capableDraggingWindow(titleArea);
|
||||
HBox.setHgrow(titleArea, Priority.ALWAYS);
|
||||
if (state.titleNode() != null) {
|
||||
titleArea.getChildren().setAll(state.titleNode());
|
||||
@@ -284,6 +307,7 @@ final class MainWindowPane extends StackPane {
|
||||
titleLabel.textFillProperty().bind(Themes.titleFillProperty());
|
||||
titleLabel.getStyleClass().add("jfx-decorator-title");
|
||||
titleLabel.setMinWidth(0);
|
||||
titleLabel.setMouseTransparent(true);
|
||||
titleArea.getChildren().setAll(titleLabel);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
* 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.decorator;
|
||||
|
||||
import javafx.application.ConditionalFeature;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import org.jackhuang.hmcl.util.platform.OSVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// Accesses the JavaFX 27 extended-window APIs without linking them on older runtimes.
|
||||
@NotNullByDefault
|
||||
final class NativeWindowDecoration {
|
||||
/// Marks nodes whose title-content descendants are already observed for native hit testing.
|
||||
private static final Object DRAG_CONFIGURED = new Object();
|
||||
|
||||
/// Whether the user explicitly enabled native decoration regardless of automatic platform or brightness exclusions.
|
||||
private final boolean forced;
|
||||
|
||||
/// The platform-supported extended stage style.
|
||||
final StageStyle style;
|
||||
|
||||
/// The header bar that supplies native dragging around the custom title content.
|
||||
final Region headerBar;
|
||||
|
||||
/// Assigns the header bar's center content.
|
||||
private final MethodHandle setCenter;
|
||||
|
||||
/// Assigns a node's native header hit-test behavior.
|
||||
private final MethodHandle setDragType;
|
||||
|
||||
/// Reads explicit native drag exclusions on title nodes.
|
||||
private final MethodHandle getDragType;
|
||||
|
||||
/// Sets the system button height to zero to hide the platform-provided buttons.
|
||||
private final MethodHandle setSystemButtonHeight;
|
||||
|
||||
/// Marks only the specified node as draggable, preserving child control interaction.
|
||||
private final Object draggable;
|
||||
|
||||
/// Excludes a node and its descendants from native dragging.
|
||||
private final Object notDraggable;
|
||||
|
||||
/// Ignores an overlay and its descendants when testing underlying native header areas.
|
||||
private final Object transparentSubtree;
|
||||
|
||||
/// Resolves all required public APIs and creates a detached header bar.
|
||||
///
|
||||
/// @param forced whether to bypass the automatic theme brightness exclusion
|
||||
/// @throws Throwable if a required API cannot be resolved or invoked
|
||||
private NativeWindowDecoration(boolean forced) throws Throwable {
|
||||
this.forced = forced;
|
||||
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
|
||||
style = (StageStyle) lookup.findStaticGetter(StageStyle.class, "EXTENDED", StageStyle.class).invokeExact();
|
||||
Class<?> headerClass = Class.forName("javafx.scene.layout.HeaderBar");
|
||||
Class<?> dragClass = Class.forName("javafx.scene.layout.HeaderDragType");
|
||||
// Adapt runtime-only types once so cached handles can be invoked with exact, statically known signatures.
|
||||
setCenter = lookup.findVirtual(headerClass, "setCenter", MethodType.methodType(void.class, Node.class))
|
||||
.asType(MethodType.methodType(void.class, Region.class, Node.class));
|
||||
setDragType = lookup.findStatic(headerClass, "setDragType", MethodType.methodType(void.class, Node.class, dragClass))
|
||||
.asType(MethodType.methodType(void.class, Node.class, Object.class));
|
||||
getDragType = lookup.findStatic(headerClass, "getDragType", MethodType.methodType(dragClass, Node.class))
|
||||
.asType(MethodType.methodType(Object.class, Node.class));
|
||||
setSystemButtonHeight = lookup.findStatic(headerClass, "setSystemButtonHeight",
|
||||
MethodType.methodType(void.class, Stage.class, double.class));
|
||||
draggable = Objects.requireNonNull(lookup.findStaticGetter(dragClass, "DRAGGABLE", dragClass).invoke());
|
||||
notDraggable = Objects.requireNonNull(lookup.findStaticGetter(dragClass, "NONE", dragClass).invoke());
|
||||
transparentSubtree = Objects.requireNonNull(lookup.findStaticGetter(dragClass, "TRANSPARENT_SUBTREE", dragClass).invoke());
|
||||
headerBar = (Region) lookup.findConstructor(headerClass, MethodType.methodType(void.class)).invoke();
|
||||
}
|
||||
|
||||
/// Creates native decoration support according to the configured preference and runtime capabilities.
|
||||
///
|
||||
/// The `hmcl.nativeDecoration` system property takes precedence over `HMCL_NATIVE_DECORATION`.
|
||||
/// Values are case-insensitive and trimmed: `false` disables native decoration, `true` enables it on
|
||||
/// any supported platform, and `auto` enables it only on macOS or Windows 11 and later. Missing or
|
||||
/// unrecognized values use `auto`. All modes require JavaFX 27 or later and extended-window support.
|
||||
/// The preference is read when this method is called and retained by the returned instance.
|
||||
///
|
||||
/// @return the resolved support, or `null` when disabled by policy or unavailable on the runtime
|
||||
static @Nullable NativeWindowDecoration create() {
|
||||
@Nullable String preference = System.getProperty("hmcl.nativeDecoration", System.getenv("HMCL_NATIVE_DECORATION"));
|
||||
if (preference != null) {
|
||||
preference = preference.trim();
|
||||
}
|
||||
if ("false".equalsIgnoreCase(preference)) {
|
||||
return null;
|
||||
}
|
||||
boolean forced = "true".equalsIgnoreCase(preference);
|
||||
if (!forced && preference != null && !"auto".equalsIgnoreCase(preference)) {
|
||||
LOG.warning("Invalid native decoration preference: " + preference + "; using auto");
|
||||
}
|
||||
if (!forced && OperatingSystem.CURRENT_OS != OperatingSystem.MACOS
|
||||
&& !OperatingSystem.SYSTEM_VERSION.isAtLeast(OSVersion.WINDOWS_11)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
int version = Integer.parseInt(System.getProperty("javafx.version", "0").split("[.\\-+]")[0]);
|
||||
if (version < 27 || !Platform.isSupported(ConditionalFeature.valueOf("EXTENDED_WINDOW"))) {
|
||||
return null;
|
||||
}
|
||||
return new NativeWindowDecoration(forced);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Native window decoration is unavailable; using custom decoration", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether to use native decoration with the current transparency and theme brightness.
|
||||
///
|
||||
/// Transparent windows always use custom decoration. Automatic mode also excludes dark windows on Windows;
|
||||
/// explicitly enabling native decoration bypasses only that brightness exclusion.
|
||||
///
|
||||
/// @param transparent whether the theme requests window transparency
|
||||
/// @param dark whether the current theme is dark
|
||||
/// @return whether this instance permits native decoration for the supplied theme settings
|
||||
boolean isPreferred(boolean transparent, boolean dark) {
|
||||
return !transparent && (forced || !(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && dark));
|
||||
}
|
||||
|
||||
/// Sets or removes the header content; the caller must first detach it from any other parent.
|
||||
///
|
||||
/// Configures native dragging on non-interactive descendants and observes later child additions.
|
||||
/// Controls other than labels and nodes explicitly excluded from dragging retain ordinary input handling.
|
||||
///
|
||||
/// @param content the title-bar content, or `null` to detach it
|
||||
void setContent(@Nullable Node content) {
|
||||
try {
|
||||
if (content != null) {
|
||||
configureTitleDragging(content);
|
||||
}
|
||||
setCenter.invokeExact(headerBar, content);
|
||||
} catch (Throwable e) {
|
||||
throw new IllegalStateException("Cannot update native header content", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks a node as draggable or explicitly non-draggable during native header hit testing.
|
||||
///
|
||||
/// Draggability does not extend to descendants, so interactive title content retains its behavior.
|
||||
///
|
||||
/// @param node the node to configure
|
||||
/// @param enabled whether the node itself may move the window
|
||||
void setDraggable(Node node, boolean enabled) {
|
||||
try {
|
||||
setDragType.invokeExact(node, enabled ? draggable : notDraggable);
|
||||
} catch (Throwable e) {
|
||||
throw new IllegalStateException("Cannot configure native header dragging", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Lets native header hit testing pass through an overlay and its descendants.
|
||||
///
|
||||
/// Ordinary mouse picking is unchanged. This does not create draggable areas outside the underlying
|
||||
/// header. Descendants excluded with [#setDraggable(Node, boolean)] block native dragging instead.
|
||||
///
|
||||
/// @param overlay the overlay above the header
|
||||
void setDragTransparent(Node overlay) {
|
||||
try {
|
||||
setDragType.invokeExact(overlay, transparentSubtree);
|
||||
} catch (Throwable e) {
|
||||
throw new IllegalStateException("Cannot configure native header overlay", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Configures native hit testing for title content, including subsequently added descendants.
|
||||
///
|
||||
/// Containers and label graphics may move the window. Other controls and nodes explicitly excluded with
|
||||
/// [#setDraggable(Node, boolean)] retain ordinary input handling, including all of their descendants.
|
||||
///
|
||||
/// @param node the title content to configure
|
||||
private void configureTitleDragging(Node node) {
|
||||
if (node.getProperties().putIfAbsent(DRAG_CONFIGURED, Boolean.TRUE) != null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if ((Object) getDragType.invokeExact(node) == notDraggable) {
|
||||
return;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
throw new IllegalStateException("Cannot read native header dragging", e);
|
||||
}
|
||||
if (node instanceof Control && !(node instanceof Label)) {
|
||||
setDraggable(node, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Native hit testing starts at the deepest picked node; marking only its parent is insufficient.
|
||||
setDraggable(node, true);
|
||||
if (node instanceof Parent parent) {
|
||||
parent.getChildrenUnmodifiable().addListener((ListChangeListener<Node>) change -> {
|
||||
while (change.next()) {
|
||||
for (Node child : change.getAddedSubList()) {
|
||||
configureTitleDragging(child);
|
||||
}
|
||||
}
|
||||
});
|
||||
for (Node child : parent.getChildrenUnmodifiable()) {
|
||||
configureTitleDragging(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hides platform-provided window buttons.
|
||||
///
|
||||
/// @param stage the extended stage to configure
|
||||
void configureStage(Stage stage) {
|
||||
try {
|
||||
setSystemButtonHeight.invokeExact(stage, 0.0);
|
||||
} catch (Throwable e) {
|
||||
throw new IllegalStateException("Cannot hide native window buttons", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
* 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.decorator;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleWrapper;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import javafx.stage.WindowEvent;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/// Tracks content bounds independently of the stage's native frame and the root's shadow padding.
|
||||
///
|
||||
/// All access must occur on the JavaFX application thread. The scene and root must remain attached to the stage
|
||||
/// until [#close()] is called. Normal bounds are sampled after layout, and once more before hiding or detaching.
|
||||
/// Maximized bounds under [WindowState]'s policy, full-screen bounds, and iconified bounds are not reported
|
||||
/// as normal bounds. Transparent macOS windows retain normal bounds despite native zoom notifications.
|
||||
/// Minimum sizes are configured separately after showing or a display-scale or resizability change;
|
||||
/// ordinary geometry tracking does not write window constraints.
|
||||
@NotNullByDefault
|
||||
final class WindowBounds implements AutoCloseable {
|
||||
/// The stage whose outer geometry is managed.
|
||||
private final Stage stage;
|
||||
|
||||
/// The scene providing client dimensions and offsets within the native window.
|
||||
private final Scene scene;
|
||||
|
||||
/// The root whose padding reserves space for custom shadows.
|
||||
private final Region root;
|
||||
|
||||
/// The padding used in the normal window state, even when maximized padding is empty.
|
||||
private final Insets normalInsets;
|
||||
|
||||
/// The minimum content width in JavaFX screen coordinates.
|
||||
private final double minWidth;
|
||||
|
||||
/// The minimum content height in JavaFX screen coordinates.
|
||||
private final double minHeight;
|
||||
|
||||
/// Receives normal content bounds in JavaFX screen coordinates.
|
||||
private final Consumer<Rectangle2D> saveBounds;
|
||||
|
||||
/// The latest normal content rectangle, retained through hiding and non-normal window states.
|
||||
private Rectangle2D normalBounds;
|
||||
|
||||
/// The content width excluding both native borders and custom shadow padding.
|
||||
private final ReadOnlyDoubleWrapper contentWidth = new ReadOnlyDoubleWrapper();
|
||||
|
||||
/// The content height excluding both native borders and custom shadow padding.
|
||||
private final ReadOnlyDoubleWrapper contentHeight = new ReadOnlyDoubleWrapper();
|
||||
|
||||
/// Whether the current show operation is still resolving native client offsets.
|
||||
private boolean restoringPosition;
|
||||
|
||||
/// Whether native geometry can be sampled, including during WINDOW_HIDING.
|
||||
private boolean shown;
|
||||
|
||||
/// Whether geometry must be sampled after the next layout pulse.
|
||||
private boolean dirty;
|
||||
|
||||
/// Whether the native minimum needs configuration once normal geometry has stopped changing between pulses.
|
||||
private boolean minimumSizePending;
|
||||
|
||||
/// Whether this tracker has released its listeners.
|
||||
private boolean closed;
|
||||
|
||||
/// Marks geometry for sampling after layout without assuming an order for stage and scene notifications.
|
||||
private final ChangeListener<Object> boundsListener = (observable, oldValue, newValue) -> invalidate();
|
||||
|
||||
/// Compensates for native offsets discovered while creating the window peer.
|
||||
private final InvalidationListener offsetListener = observable -> restorePosition();
|
||||
|
||||
/// Requests new native minimum dimensions when the display scale or resizability changes.
|
||||
private final ChangeListener<Object> minimumSizeListener = (observable, oldValue, newValue) -> invalidateMinimumSize();
|
||||
|
||||
/// Establishes the normal restore rectangle before the native peer becomes visible.
|
||||
private final EventHandler<WindowEvent> showingHandler = event -> restoreBounds();
|
||||
|
||||
/// Completes initial positioning and enables geometry sampling.
|
||||
private final EventHandler<WindowEvent> shownHandler = event -> {
|
||||
restorePosition();
|
||||
restoringPosition = false;
|
||||
shown = true;
|
||||
invalidateMinimumSize();
|
||||
};
|
||||
|
||||
/// Samples geometry before JavaFX clears the hidden scene's native offsets.
|
||||
private final EventHandler<WindowEvent> hidingHandler = event -> {
|
||||
updateBounds();
|
||||
shown = false;
|
||||
restoringPosition = false;
|
||||
};
|
||||
|
||||
/// Samples geometry and separately processes pending minimum-size configuration after layout.
|
||||
private final Runnable pulseListener = this::onPulse;
|
||||
|
||||
/// The properties observed for geometry, state, and padding changes.
|
||||
private final ObservableValue<?> @Unmodifiable [] observed;
|
||||
|
||||
/// The properties that can change the normal native frame dimensions.
|
||||
private final ObservableValue<?> @Unmodifiable [] minimumSizeObserved;
|
||||
|
||||
/// Initializes client sizing and installs listeners on an attached stage and scene.
|
||||
///
|
||||
/// The root's current padding must be its normal-state shadow padding. Initial bounds exclude that padding
|
||||
/// and native decoration. [Stage#sizeToScene()] lets JavaFX determine the required native outer size.
|
||||
///
|
||||
/// @param stage the stage with `root` installed as its scene root
|
||||
/// @param root the scene root, whose minimum and preferred sizes are managed by this tracker
|
||||
/// @param initialBounds the normal content rectangle to restore
|
||||
/// @param minWidth the minimum content width
|
||||
/// @param minHeight the minimum content height
|
||||
/// @param saveBounds the callback receiving sampled normal content bounds
|
||||
WindowBounds(Stage stage, Region root, Rectangle2D initialBounds,
|
||||
double minWidth, double minHeight, Consumer<Rectangle2D> saveBounds) {
|
||||
this.stage = stage;
|
||||
this.scene = Objects.requireNonNull(stage.getScene());
|
||||
this.root = root;
|
||||
this.normalInsets = root.getPadding();
|
||||
this.normalBounds = initialBounds;
|
||||
this.minWidth = minWidth;
|
||||
this.minHeight = minHeight;
|
||||
this.saveBounds = saveBounds;
|
||||
// Enforce minimum content size on the stage, independently of the current page's layout minimum.
|
||||
root.setMinSize(0, 0);
|
||||
contentWidth.set(initialBounds.getWidth());
|
||||
contentHeight.set(initialBounds.getHeight());
|
||||
observed = new ObservableValue<?>[] {
|
||||
stage.xProperty(), stage.yProperty(), stage.widthProperty(), stage.heightProperty(),
|
||||
stage.maximizedProperty(), stage.fullScreenProperty(), stage.iconifiedProperty(),
|
||||
scene.xProperty(), scene.yProperty(), scene.widthProperty(), scene.heightProperty(),
|
||||
root.paddingProperty()
|
||||
};
|
||||
for (ObservableValue<?> observable : observed) {
|
||||
observable.addListener(boundsListener);
|
||||
}
|
||||
scene.xProperty().addListener(offsetListener);
|
||||
scene.yProperty().addListener(offsetListener);
|
||||
minimumSizeObserved = new ObservableValue<?>[] {
|
||||
stage.outputScaleXProperty(), stage.outputScaleYProperty(), stage.resizableProperty()
|
||||
};
|
||||
for (ObservableValue<?> observable : minimumSizeObserved) {
|
||||
observable.addListener(minimumSizeListener);
|
||||
}
|
||||
scene.addPostLayoutPulseListener(pulseListener);
|
||||
stage.addEventHandler(WindowEvent.WINDOW_SHOWING, showingHandler);
|
||||
stage.addEventHandler(WindowEvent.WINDOW_SHOWN, shownHandler);
|
||||
stage.addEventHandler(WindowEvent.WINDOW_HIDING, hidingHandler);
|
||||
stage.setMinWidth(minWidth + normalInsets.getLeft() + normalInsets.getRight());
|
||||
stage.setMinHeight(minHeight + normalInsets.getTop() + normalInsets.getBottom());
|
||||
restoreBounds();
|
||||
if (stage.isShowing()) {
|
||||
restoringPosition = false;
|
||||
shown = true;
|
||||
invalidateMinimumSize();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the content width, excluding native borders and the root's current padding.
|
||||
ReadOnlyDoubleProperty contentWidthProperty() {
|
||||
return contentWidth.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
/// Returns the content height, excluding native borders and the root's current padding.
|
||||
ReadOnlyDoubleProperty contentHeightProperty() {
|
||||
return contentHeight.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
/// Requests one layout pulse to coalesce geometry notifications.
|
||||
private void invalidate() {
|
||||
dirty = true;
|
||||
Platform.requestNextPulse();
|
||||
}
|
||||
|
||||
/// Schedules native minimum-size configuration independently of ordinary geometry sampling.
|
||||
private void invalidateMinimumSize() {
|
||||
minimumSizePending = true;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/// Samples dirty geometry, then configures pending minimums on a subsequent quiet layout pulse.
|
||||
private void onPulse() {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
if (dirty) {
|
||||
updateBounds();
|
||||
if (minimumSizePending && isNormal()) {
|
||||
Platform.requestNextPulse();
|
||||
}
|
||||
} else if (minimumSizePending && isNormal()) {
|
||||
updateMinimumSize();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the visible stage can supply normal window geometry.
|
||||
private boolean isNormal() {
|
||||
return shown && !restoringPosition && !stage.isIconified()
|
||||
&& !WindowState.isMaximized(stage) && !stage.isFullScreen();
|
||||
}
|
||||
|
||||
/// Requests the normal client size and starts compensating for native client offsets.
|
||||
private void restoreBounds() {
|
||||
restoringPosition = true;
|
||||
root.setPrefSize(normalBounds.getWidth() + normalInsets.getLeft() + normalInsets.getRight(),
|
||||
normalBounds.getHeight() + normalInsets.getTop() + normalInsets.getBottom());
|
||||
restorePosition();
|
||||
stage.sizeToScene();
|
||||
}
|
||||
|
||||
/// Aligns the content origin while the native peer is being initialized.
|
||||
private void restorePosition() {
|
||||
if (restoringPosition) {
|
||||
stage.setX(normalBounds.getMinX() - scene.getX() - normalInsets.getLeft());
|
||||
stage.setY(normalBounds.getMinY() - scene.getY() - normalInsets.getTop());
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates content dimensions and saves normal bounds when native geometry is available.
|
||||
private void updateBounds() {
|
||||
dirty = false;
|
||||
if (!shown || restoringPosition || stage.isIconified()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Insets insets = root.getPadding();
|
||||
double width = scene.getWidth() - insets.getLeft() - insets.getRight();
|
||||
double height = scene.getHeight() - insets.getTop() - insets.getBottom();
|
||||
if (width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
contentWidth.set(width);
|
||||
contentHeight.set(height);
|
||||
if (WindowState.isMaximized(stage) || stage.isFullScreen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
normalBounds = new Rectangle2D(
|
||||
stage.getX() + scene.getX() + insets.getLeft(),
|
||||
stage.getY() + scene.getY() + insets.getTop(),
|
||||
Math.max(minWidth, width), Math.max(minHeight, height));
|
||||
saveBounds.accept(normalBounds);
|
||||
}
|
||||
|
||||
/// Measures the normal native frame and applies content minimums including frame and shadow padding.
|
||||
private void updateMinimumSize() {
|
||||
// Stage and Scene sizes arrive separately. Only measure after pending geometry notifications
|
||||
// have been processed, never from an individual size listener or an ordinary resize pulse.
|
||||
boolean hasFrame = stage.getStyle() != StageStyle.TRANSPARENT && stage.getStyle() != StageStyle.UNDECORATED;
|
||||
double frameWidth = hasFrame ? Math.max(0, stage.getWidth() - scene.getWidth()) : 0;
|
||||
double frameHeight = hasFrame ? Math.max(0, stage.getHeight() - scene.getHeight()) : 0;
|
||||
minimumSizePending = false;
|
||||
stage.setMinWidth(minWidth + normalInsets.getLeft() + normalInsets.getRight() + frameWidth);
|
||||
stage.setMinHeight(minHeight + normalInsets.getTop() + normalInsets.getBottom() + frameHeight);
|
||||
}
|
||||
|
||||
/// Saves the last available bounds and removes all listeners without hiding or detaching the stage.
|
||||
/// Repeated calls have no effect.
|
||||
@Override
|
||||
public void close() {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
updateBounds();
|
||||
closed = true;
|
||||
for (ObservableValue<?> observable : observed) {
|
||||
observable.removeListener(boundsListener);
|
||||
}
|
||||
scene.xProperty().removeListener(offsetListener);
|
||||
scene.yProperty().removeListener(offsetListener);
|
||||
for (ObservableValue<?> observable : minimumSizeObserved) {
|
||||
observable.removeListener(minimumSizeListener);
|
||||
}
|
||||
scene.removePostLayoutPulseListener(pulseListener);
|
||||
stage.removeEventHandler(WindowEvent.WINDOW_SHOWING, showingHandler);
|
||||
stage.removeEventHandler(WindowEvent.WINDOW_SHOWN, shownHandler);
|
||||
stage.removeEventHandler(WindowEvent.WINDOW_HIDING, hidingHandler);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.decorator;
|
||||
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
/// Applies the main window's platform-specific maximization policy.
|
||||
/// All methods must be called on the JavaFX application thread.
|
||||
@NotNullByDefault
|
||||
public final class WindowState {
|
||||
/// Prevents instantiation of this utility class.
|
||||
private WindowState() {
|
||||
}
|
||||
|
||||
/// Returns whether the launcher honors maximization for the stage's platform and style.
|
||||
///
|
||||
/// Transparent windows on macOS do not support maximization in the launcher: native zoom notifications
|
||||
/// can mark an ordinary borderless window as maximized, including during initialization.
|
||||
/// This policy does not suppress full-screen or iconified states.
|
||||
///
|
||||
/// @param stage the main stage to inspect
|
||||
/// @return `false` for transparent stages on macOS; `true` otherwise
|
||||
public static boolean supportsMaximization(Stage stage) {
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4290
|
||||
return OperatingSystem.CURRENT_OS != OperatingSystem.MACOS || stage.getStyle() != StageStyle.TRANSPARENT;
|
||||
}
|
||||
|
||||
/// Returns whether the stage is maximized under the launcher's platform policy.
|
||||
///
|
||||
/// @param stage the main stage to inspect
|
||||
/// @return `true` if maximization is supported and the stage reports that it is maximized
|
||||
public static boolean isMaximized(Stage stage) {
|
||||
return supportsMaximization(stage) && stage.isMaximized();
|
||||
}
|
||||
}
|
||||
@@ -41,11 +41,7 @@ import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
import org.jackhuang.hmcl.setting.SettingsManager;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.LineFileChooserButton;
|
||||
import org.jackhuang.hmcl.ui.construct.LineToggleButton;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
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.FXThread;
|
||||
import org.jackhuang.hmcl.util.PortablePath;
|
||||
@@ -125,10 +121,12 @@ public final class GameDirectoryPage extends BorderPane implements DecoratorPage
|
||||
setMessage(i18n("game_directory.already_exists"));
|
||||
}
|
||||
|
||||
/// Rejects names used by other game directories, excluding the entry being edited.
|
||||
@Override
|
||||
protected void eval() {
|
||||
JFXTextField control = (JFXTextField) this.getSrcControl();
|
||||
hasErrors.set(GameDirectoryManager.getGameDirectories().stream()
|
||||
.filter(existingGameDirectory -> existingGameDirectory != gameDirectory)
|
||||
.anyMatch(existingGameDirectory -> Objects.equals(
|
||||
GameDirectoryManager.getGameDirectoryCustomName(existingGameDirectory), control.getText())));
|
||||
}
|
||||
@@ -170,43 +168,52 @@ public final class GameDirectoryPage extends BorderPane implements DecoratorPage
|
||||
StackPane.setAlignment(saveButton, Pos.BOTTOM_RIGHT);
|
||||
saveButton.setPrefSize(100, 40);
|
||||
saveButton.setOnAction(e -> onSave());
|
||||
String initialLocation = getLocation();
|
||||
boolean initialUseRelativePath = toggleUseRelativePath.isSelected();
|
||||
saveButton.disableProperty().bind(Bindings.createBooleanBinding(
|
||||
() -> !txtGameDirectoryName.validate() || StringUtils.isBlank(getLocation()),
|
||||
txtGameDirectoryName.textProperty(), location));
|
||||
() -> !txtGameDirectoryName.validate() || StringUtils.isBlank(getLocation())
|
||||
|| (gameDirectory != null
|
||||
&& Objects.equals(txtGameDirectoryName.getText(), gameDirectoryDisplayName)
|
||||
&& Objects.equals(getLocation(), initialLocation)
|
||||
&& toggleUseRelativePath.isSelected() == initialUseRelativePath),
|
||||
txtGameDirectoryName.textProperty(), location, toggleUseRelativePath.selectedProperty()));
|
||||
}
|
||||
|
||||
ChangeListener<String> locationChangeListener = (observable, oldValue, newValue) -> {
|
||||
Path newPath;
|
||||
try {
|
||||
newPath = FileUtils.toAbsolute(Path.of(newValue));
|
||||
} catch (InvalidPathException ignored) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!".minecraft".equals(FileUtils.getName(newPath)))
|
||||
return;
|
||||
|
||||
Path parent = newPath.getParent();
|
||||
if (parent == null)
|
||||
return;
|
||||
|
||||
String suggestedName = FileUtils.getName(parent);
|
||||
if (!suggestedName.isBlank()) {
|
||||
txtGameDirectoryName.setText(suggestedName);
|
||||
}
|
||||
};
|
||||
locationChangeListener.changed(null, this.location.get(), this.location.get());
|
||||
locationProperty().addListener(locationChangeListener);
|
||||
|
||||
txtGameDirectoryName.textProperty().addListener(new ChangeListener<>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
|
||||
if (txtGameDirectoryName.isFocused()) {
|
||||
txtGameDirectoryName.textProperty().removeListener(this);
|
||||
locationProperty().removeListener(locationChangeListener);
|
||||
if (gameDirectory == null) {
|
||||
ChangeListener<String> locationChangeListener = (observable, oldValue, newValue) -> {
|
||||
Path newPath;
|
||||
try {
|
||||
newPath = FileUtils.toAbsolute(Path.of(newValue));
|
||||
} catch (InvalidPathException ignored) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!".minecraft".equals(FileUtils.getName(newPath)))
|
||||
return;
|
||||
|
||||
@Nullable Path parent = newPath.getParent();
|
||||
if (parent == null)
|
||||
return;
|
||||
|
||||
String suggestedName = FileUtils.getName(parent);
|
||||
if (!suggestedName.isBlank()) {
|
||||
txtGameDirectoryName.setText(suggestedName);
|
||||
}
|
||||
};
|
||||
locationChangeListener.changed(locationProperty(), this.location.get(), this.location.get());
|
||||
locationProperty().addListener(locationChangeListener);
|
||||
|
||||
txtGameDirectoryName.textProperty().addListener(new ChangeListener<>() {
|
||||
/// Stops automatic name suggestions after the user edits the name.
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
|
||||
if (txtGameDirectoryName.isFocused()) {
|
||||
txtGameDirectoryName.textProperty().removeListener(this);
|
||||
locationProperty().removeListener(locationChangeListener);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@FXThread
|
||||
@@ -237,21 +244,25 @@ public final class GameDirectoryPage extends BorderPane implements DecoratorPage
|
||||
Controllers.confirmBackupAndOverwrite(i18n("settings.game_directories.read_only"), () -> {
|
||||
SettingsManager.forceOverwriteUserGameDirectories();
|
||||
GameDirectoryManager.addUserGameDirectory(newGameDirectory);
|
||||
GameDirectoryManager.setSelectedGameDirectory(newGameDirectory);
|
||||
fireEvent(new PageCloseEvent());
|
||||
});
|
||||
return;
|
||||
}
|
||||
GameDirectoryManager.addUserGameDirectory(newGameDirectory);
|
||||
GameDirectoryManager.setSelectedGameDirectory(newGameDirectory);
|
||||
} else {
|
||||
if (SettingsManager.isLocalGameDirectoriesReadOnly()) {
|
||||
Controllers.confirmBackupAndOverwrite(i18n("settings.game_directories.read_only"), () -> {
|
||||
SettingsManager.forceOverwriteLocalGameDirectories();
|
||||
GameDirectoryManager.addLocalGameDirectory(newGameDirectory);
|
||||
GameDirectoryManager.setSelectedGameDirectory(newGameDirectory);
|
||||
fireEvent(new PageCloseEvent());
|
||||
});
|
||||
return;
|
||||
}
|
||||
GameDirectoryManager.addLocalGameDirectory(newGameDirectory);
|
||||
GameDirectoryManager.setSelectedGameDirectory(newGameDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,9 +56,9 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
|
||||
|
||||
protected BooleanProperty installable = new SimpleBooleanProperty();
|
||||
|
||||
public AbstractInstallersPage(WizardController controller, String gameVersion, DownloadProvider downloadProvider) {
|
||||
public AbstractInstallersPage(WizardController controller, GameVersionNumber gameVersion, DownloadProvider downloadProvider) {
|
||||
this.controller = controller;
|
||||
this.group = new InstallerItem.InstallerItemGroup(GameVersionNumber.asGameVersion(gameVersion), getInstallerItemStyle());
|
||||
this.group = new InstallerItem.InstallerItemGroup(gameVersion, getInstallerItemStyle());
|
||||
|
||||
for (InstallerItem component : group.getComponents()) {
|
||||
GameComponentType type = component.getComponentType();
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.jackhuang.hmcl.ui.InstallerItem;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -36,11 +37,11 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
class AdditionalInstallersPage extends AbstractInstallersPage {
|
||||
protected final BooleanProperty compatible = new SimpleBooleanProperty();
|
||||
protected final String gameVersion;
|
||||
protected final GameVersionNumber gameVersion;
|
||||
protected final GameInstanceManifest manifest;
|
||||
protected final HMCLGameInstance instance;
|
||||
|
||||
public AdditionalInstallersPage(HMCLGameInstance instance, String gameVersion, WizardController controller, DownloadProvider downloadProvider) {
|
||||
public AdditionalInstallersPage(HMCLGameInstance instance, GameVersionNumber gameVersion, WizardController controller, DownloadProvider downloadProvider) {
|
||||
super(controller, gameVersion, downloadProvider);
|
||||
this.instance = instance;
|
||||
this.gameVersion = gameVersion;
|
||||
@@ -59,7 +60,7 @@ class AdditionalInstallersPage extends AbstractInstallersPage {
|
||||
});
|
||||
}
|
||||
|
||||
installable.bind(Bindings.createBooleanBinding(() -> compatible.get() && txtName.validate(), txtName.textProperty(), compatible));
|
||||
installable.bind(Bindings.createBooleanBinding(() -> txtName.validate(), txtName.textProperty()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -80,27 +81,14 @@ class AdditionalInstallersPage extends AbstractInstallersPage {
|
||||
|
||||
@Override
|
||||
protected void reload() {
|
||||
boolean gameVersionChanged = !instance.getVersion().toString().equals(getVersion(GameComponentType.GAME));
|
||||
boolean compatible = true;
|
||||
|
||||
for (InstallerItem component : group.getComponents()) {
|
||||
GameComponentType componentType = component.getComponentType();
|
||||
String version = instance.getComponentVersion(component.getComponentType());
|
||||
String libraryVersion = Lang.requireNonNullElse(getVersion(componentType), version);
|
||||
boolean alreadyInstalled = version != null && !(controller.getSettings().get(componentType.getPatchId()) instanceof UpdateInstallerWizardProvider.RemoveComponentAction);
|
||||
if (component.getComponentType() != GameComponentType.GAME && gameVersionChanged && getVersion(componentType) == null && alreadyInstalled) {
|
||||
// For third-party libraries, if game version is being changed, and the library is not being reinstalled,
|
||||
// warns the user that we should update the library.
|
||||
component.versionProperty().set(new InstallerItem.InstalledState(libraryVersion, false, true));
|
||||
compatible = false;
|
||||
} else if (alreadyInstalled || getVersion(componentType) != null) {
|
||||
component.versionProperty().set(new InstallerItem.InstalledState(libraryVersion, false, false));
|
||||
if (controller.getSettings().containsKey(componentType.getPatchId())) {
|
||||
component.versionProperty().set(new InstallerItem.InstalledState(getVersion(componentType), false, false));
|
||||
} else {
|
||||
component.versionProperty().set(null);
|
||||
}
|
||||
}
|
||||
|
||||
this.compatible.set(compatible);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -56,7 +56,9 @@ import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.FileNameSet;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
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.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.file.Path;
|
||||
@@ -94,7 +96,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
|
||||
public DownloadPage(GameInstanceID uploadInstance) {
|
||||
newGameTab.setNodeSupplier(loadVersionFor(() -> new VersionsPage(versionPageNavigator, i18n("install.installer.choose", i18n("install.installer.game")), "", DownloadProviders.getDownloadProvider(),
|
||||
newGameTab.setNodeSupplier(loadVersionFor(() -> new VersionsPage(versionPageNavigator, i18n("install.installer.choose", i18n("install.installer.game")), GameVersionNumber.unknown(), DownloadProviders.getDownloadProvider(),
|
||||
GameComponentType.GAME, versionPageNavigator::onGameSelected)));
|
||||
modpackTab.setNodeSupplier(loadVersionFor(() -> {
|
||||
DownloadListPage page = HMCLLocalizedDownloadListPage.ofModPack((downloadProvider, repository, __, modpack, file) -> {
|
||||
@@ -110,7 +112,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod(FOR_MOD, true)));
|
||||
resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack(FOR_RESOURCE_PACK, true)));
|
||||
shaderTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofShaderPack(FOR_SHADER, true)));
|
||||
worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteAddonRepository.WORLDS)));
|
||||
worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(RemoteAddon.Type.WORLD, CurseForgeRemoteAddonRepository.getInstance())));
|
||||
tab = new TabHeader(transitionPane, newGameTab, modpackTab, modTab, resourcePackTab, shaderTab, worldTab);
|
||||
|
||||
GameDirectoryManager.registerVersionsListener(this::loadVersions);
|
||||
@@ -162,13 +164,13 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
Path dest = targetPath.resolve(result);
|
||||
|
||||
Controllers.taskDialog(Task.composeAsync(() -> {
|
||||
var task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(file.file().url()), dest);
|
||||
var task = new FileDownloadTask(downloadProvider.getDownloadCandidates(file.file().url()), dest);
|
||||
task.setName(file.name());
|
||||
return task;
|
||||
}).whenComplete(Schedulers.javafx(), exception -> {
|
||||
if (exception != null) {
|
||||
if (!(exception instanceof CancellationException)) {
|
||||
Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR);
|
||||
Controllers.dialog(I18n.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
} else {
|
||||
Controllers.showToast(i18n("install.success"));
|
||||
@@ -307,13 +309,13 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
/// @param settings the installer selections and target instance id
|
||||
/// @return the builder task with its stage hints preserved as the outermost task wrapper
|
||||
private Task<?> finishVersionDownloadingAsync(SettingsMap settings) {
|
||||
GameInstanceID instanceId = settings.get(AbstractInstallersPage.INSTANCE_ID);
|
||||
@Nullable GameInstanceID instanceId = settings.get(AbstractInstallersPage.INSTANCE_ID);
|
||||
if (instanceId == null) {
|
||||
throw new IllegalStateException("Instance ID is not set");
|
||||
}
|
||||
|
||||
try (HMCLGameBuilder builder = dependencyManager.newGameBuilder(instanceId)) {
|
||||
builder.component(GameComponentType.GAME, ((ComponentRemoteVersion) settings.get(GameComponentType.GAME.getPatchId())).getGameVersion());
|
||||
builder.component(GameComponentType.GAME, ((ComponentRemoteVersion) settings.get(GameComponentType.GAME.getPatchId())).getGameVersion().toString());
|
||||
|
||||
settings.asStringMap().forEach((key, value) -> {
|
||||
if (!GameComponentType.GAME.getPatchId().equals(key)
|
||||
@@ -329,6 +331,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
|
||||
Task<?> buildTask = builder.buildAsync();
|
||||
settings.put(TaskCleanup.KEY, builder::abort);
|
||||
buildTask.onDone().register(event -> {
|
||||
if (!event.isFailed()) {
|
||||
runInFX(() -> repository.setSelectedInstance(repository.getInstance(instanceId)));
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import static javafx.beans.binding.Bindings.createBooleanBinding;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -38,7 +39,7 @@ public class InstallersPage extends AbstractInstallersPage {
|
||||
|
||||
private boolean isNameModifiedByUser = false;
|
||||
|
||||
public InstallersPage(WizardController controller, HMCLGameRepository repository, String gameVersion, DownloadProvider downloadProvider) {
|
||||
public InstallersPage(WizardController controller, HMCLGameRepository repository, GameVersionNumber gameVersion, DownloadProvider downloadProvider) {
|
||||
super(controller, gameVersion, downloadProvider);
|
||||
|
||||
txtName.getValidators().addAll(
|
||||
@@ -52,7 +53,7 @@ public class InstallersPage extends AbstractInstallersPage {
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return ((ComponentRemoteVersion) controller.getSettings().get("game")).getGameVersion();
|
||||
return ((ComponentRemoteVersion) controller.getSettings().get("game")).getGameVersion().toString();
|
||||
}
|
||||
|
||||
private String getVersion(String id) {
|
||||
|
||||
@@ -20,11 +20,18 @@ package org.jackhuang.hmcl.ui.download;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ObservableSet;
|
||||
import javafx.collections.transformation.FilteredList;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.ManuallyCreatedModpackException;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.modpack.Modpack;
|
||||
import org.jackhuang.hmcl.modpack.ModpackFile;
|
||||
import org.jackhuang.hmcl.modpack.ModpackManifest;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -39,9 +46,15 @@ import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -51,10 +64,22 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
private final BooleanProperty installAsVersion = new SimpleBooleanProperty(true);
|
||||
private Modpack manifest = null;
|
||||
private Charset charset;
|
||||
private final ObservableList<ModpackFile> allFiles = FXCollections.observableList(new ArrayList<>());
|
||||
private final ObservableSet<String> excludedFiles = FXCollections.observableSet(new HashSet<>());
|
||||
private final BooleanProperty loadingOptionalFiles = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty loadedOptionalFiles = new SimpleBooleanProperty(true);
|
||||
|
||||
public LocalModpackPage(WizardController controller) {
|
||||
super(controller);
|
||||
|
||||
btnOptionalFiles.setOnAction(ev -> controller.onNext(new OptionalFilesPage(
|
||||
this::onInstall,
|
||||
this::loadOptionalFiles,
|
||||
loadingOptionalFiles,
|
||||
loadedOptionalFiles,
|
||||
new FilteredList<>(allFiles, ModpackFile::optional),
|
||||
excludedFiles)));
|
||||
|
||||
HMCLGameRepository repository = controller.getSettings().get(ModpackPage.REPOSITORY);
|
||||
|
||||
String name = controller.getSettings().get(MODPACK_NAME);
|
||||
@@ -103,8 +128,7 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(selectedFile))
|
||||
.thenApplyAsync(encoding -> {
|
||||
charset = encoding;
|
||||
manifest = ModpackHelper.readModpackManifest(selectedFile, encoding);
|
||||
return manifest;
|
||||
return ModpackHelper.readModpackManifest(selectedFile, encoding);
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (manifest, exception) -> {
|
||||
if (exception instanceof ManuallyCreatedModpackException) {
|
||||
@@ -126,7 +150,11 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
LOG.warning("Failed to read modpack manifest", exception);
|
||||
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
Platform.runLater(controller::onEnd);
|
||||
} else if (manifest == null) {
|
||||
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
Platform.runLater(controller::onEnd);
|
||||
} else {
|
||||
this.manifest = manifest;
|
||||
hideSpinner();
|
||||
controller.getSettings().put(MODPACK_MANIFEST, manifest);
|
||||
nameProperty.set(manifest.getName());
|
||||
@@ -139,6 +167,33 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
}
|
||||
|
||||
btnDescription.setVisible(StringUtils.isNotBlank(manifest.getDescription()));
|
||||
|
||||
if (manifest.getManifest() instanceof ModpackManifest.SupportOptional supportOptional) {
|
||||
allFiles.setAll(supportOptional.getFiles());
|
||||
if (allFiles.stream().anyMatch(ModpackFile::optional)) {
|
||||
loadOptionalFiles();
|
||||
btnOptionalFiles.setVisible(true);
|
||||
btnOptionalFiles.setManaged(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void loadOptionalFiles() {
|
||||
Objects.requireNonNull(manifest);
|
||||
loadingOptionalFiles.set(true);
|
||||
loadedOptionalFiles.set(false);
|
||||
Task.supplyAsync(() -> manifest.getManifest().getProvider().loadFiles(DownloadProviders.getDownloadProvider(), manifest.getManifest()))
|
||||
.whenComplete(Schedulers.javafx(), (manifest1, exception) -> {
|
||||
loadingOptionalFiles.set(false);
|
||||
List<? extends ModpackFile> files = ((ModpackManifest.SupportOptional) manifest1).getFiles();
|
||||
manifest.setManifest(manifest1);
|
||||
allFiles.setAll(files);
|
||||
if (files.stream().anyMatch(s -> s.optional() && (!s.addonQueried() || s.fileName() == null))) {
|
||||
LOG.warning("Failed to load optional files");
|
||||
} else {
|
||||
loadedOptionalFiles.set(true);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
@@ -157,21 +212,28 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
i18n("install.name.invalid"),
|
||||
i18n("message.warning"),
|
||||
MessageDialogPane.MessageType.QUESTION)
|
||||
.yesOrNo(() -> {
|
||||
controller.getSettings().put(MODPACK_NAME, name);
|
||||
controller.getSettings().put(MODPACK_CHARSET, charset);
|
||||
controller.onFinish();
|
||||
}, () -> {
|
||||
.yesOrNo(() -> finishInstall(name), () -> {
|
||||
// The user selects Cancel and does nothing.
|
||||
})
|
||||
.build());
|
||||
} else {
|
||||
controller.getSettings().put(MODPACK_NAME, name);
|
||||
controller.getSettings().put(MODPACK_CHARSET, charset);
|
||||
controller.onFinish();
|
||||
finishInstall(name);
|
||||
}
|
||||
}
|
||||
|
||||
private void finishInstall(String name) {
|
||||
controller.getSettings().put(MODPACK_NAME, name);
|
||||
controller.getSettings().put(MODPACK_CHARSET, charset);
|
||||
controller.getSettings().put(MODPACK_EXCLUDED_FILES, getExcludedFiles());
|
||||
controller.onFinish();
|
||||
}
|
||||
|
||||
private @Nullable Set<String> getExcludedFiles() {
|
||||
if (allFiles.isEmpty())
|
||||
return null;
|
||||
return Set.copyOf(excludedFiles);
|
||||
}
|
||||
|
||||
protected void onDescribe() {
|
||||
if (manifest != null)
|
||||
Controllers.navigate(new WebPage(i18n("modpack.description"), manifest.getDescription()));
|
||||
@@ -183,4 +245,5 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
public static final SettingsMap.Key<Charset> MODPACK_CHARSET = new SettingsMap.Key<>("MODPACK_CHARSET");
|
||||
public static final SettingsMap.Key<Boolean> MODPACK_MANUALLY_CREATED = new SettingsMap.Key<>("MODPACK_MANUALLY_CREATED");
|
||||
public static final SettingsMap.Key<String> MODPACK_ICON_URL = new SettingsMap.Key<>("MODPACK_ICON_URL");
|
||||
public static final SettingsMap.Key<Set<String>> MODPACK_EXCLUDED_FILES = new SettingsMap.Key<>("MODPACK_EXCLUDED_FILES");
|
||||
}
|
||||
@@ -90,6 +90,7 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
String iconUrl = settings.get(LocalModpackPage.MODPACK_ICON_URL);
|
||||
Charset charset = settings.get(LocalModpackPage.MODPACK_CHARSET);
|
||||
boolean isManuallyCreated = settings.getOrDefault(LocalModpackPage.MODPACK_MANUALLY_CREATED, false);
|
||||
var excludedFiles = settings.get(LocalModpackPage.MODPACK_EXCLUDED_FILES);
|
||||
|
||||
if (isManuallyCreated) {
|
||||
return ModpackHelper.getInstallManuallyCreatedModpackTask(selected, name, charset);
|
||||
@@ -108,7 +109,7 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
if (serverModpackManifest != null) {
|
||||
return ModpackHelper.getUpdateTask(repository, serverModpackManifest, modpack.getEncoding(), instanceId, ModpackHelper.readModpackConfiguration(repository.getLayout().getModpackConfigurationFile(instanceId)));
|
||||
} else {
|
||||
return ModpackHelper.getUpdateTask(repository, selected, modpack.getEncoding(), instanceId, ModpackHelper.readModpackConfiguration(repository.getLayout().getModpackConfigurationFile(instanceId)));
|
||||
return ModpackHelper.getUpdateTask(repository, selected, modpack.getEncoding(), instanceId, ModpackHelper.readModpackConfiguration(repository.getLayout().getModpackConfigurationFile(instanceId)), excludedFiles);
|
||||
}
|
||||
} catch (UnsupportedModpackException | ManuallyCreatedModpackException e) {
|
||||
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageType.ERROR);
|
||||
@@ -123,7 +124,7 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
return ModpackHelper.getInstallTask(repository, serverModpackManifest, instanceId, modpack)
|
||||
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(repository.getInstance(instanceId)));
|
||||
} else {
|
||||
return ModpackHelper.getInstallTask(repository, selected, instanceId, modpack, iconUrl)
|
||||
return ModpackHelper.getInstallTask(repository, selected, instanceId, modpack, iconUrl, excludedFiles)
|
||||
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(repository.getInstance(instanceId)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.setting.GameDirectory;
|
||||
@@ -49,6 +50,7 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
|
||||
protected final JFXTextField txtModpackName;
|
||||
protected final JFXButton btnInstall;
|
||||
protected final JFXButton btnDescription;
|
||||
protected final JFXButton btnOptionalFiles;
|
||||
|
||||
protected ModpackPage(WizardController controller) {
|
||||
this.controller = controller;
|
||||
@@ -66,7 +68,6 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
|
||||
|
||||
txtModpackName = new JFXTextField();
|
||||
txtModpackName.setPrefWidth(300);
|
||||
// BorderPane.setMargin(txtModpackName, new Insets(0, 0, 8, 32));
|
||||
BorderPane.setAlignment(txtModpackName, Pos.CENTER_RIGHT);
|
||||
archiveNamePane.setRight(txtModpackName);
|
||||
}
|
||||
@@ -95,10 +96,19 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
|
||||
btnDescription.setOnAction(e -> onDescribe());
|
||||
descriptionPane.setLeft(btnDescription);
|
||||
|
||||
var installHBox = new HBox(8);
|
||||
btnOptionalFiles = FXUtils.newRaisedButton(i18n("modpack.optional_files"));
|
||||
btnOptionalFiles.setVisible(false);
|
||||
btnOptionalFiles.setManaged(false);
|
||||
installHBox.getChildren().add(btnOptionalFiles);
|
||||
|
||||
btnInstall = FXUtils.newRaisedButton(i18n("button.install"));
|
||||
btnInstall.setOnAction(e -> onInstall());
|
||||
btnInstall.disableProperty().bind(createBooleanBinding(() -> !txtModpackName.validate(), txtModpackName.textProperty()));
|
||||
descriptionPane.setRight(btnInstall);
|
||||
var nameInvalid = createBooleanBinding(() -> !txtModpackName.validate(), txtModpackName.textProperty());
|
||||
btnInstall.disableProperty().bind(nameInvalid);
|
||||
btnOptionalFiles.disableProperty().bind(nameInvalid);
|
||||
installHBox.getChildren().add(btnInstall);
|
||||
descriptionPane.setRight(installHBox);
|
||||
}
|
||||
|
||||
componentList.getContent().setAll(
|
||||
|
||||
@@ -27,6 +27,7 @@ import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackManifest;
|
||||
@@ -125,7 +126,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
try {
|
||||
if (url.endsWith("server-manifest.json")) {
|
||||
// if urlString ends with .json, we assume that the url is server-manifest.json
|
||||
Controllers.taskDialog(new GetTask(url).whenComplete(Schedulers.javafx(), (result, e) -> {
|
||||
Controllers.taskDialog(new GetTask(WebURL.parse(url)).whenComplete(Schedulers.javafx(), (result, e) -> {
|
||||
ServerModpackManifest manifest = JsonUtils.fromMaybeMalformedJson(result, ServerModpackManifest.class);
|
||||
if (manifest == null) {
|
||||
handler.reject(i18n("modpack.type.server.malformed"));
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* 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.download;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ObservableSet;
|
||||
import javafx.collections.SetChangeListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.modpack.ModpackFile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.ui.construct.MDListCell;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/// Wizard page that lets the user choose which optional modpack files to install.
|
||||
@NotNullByDefault
|
||||
public class OptionalFilesPage extends SpinnerPane implements WizardPage {
|
||||
|
||||
/// Fixed row height used to size [#body] to its items.
|
||||
private static final double CELL_HEIGHT = 48;
|
||||
|
||||
/// Maximum list viewport height before scrolling.
|
||||
private static final double MAX_LIST_HEIGHT = 320;
|
||||
|
||||
/// Keys of files the user unchecked; empty means all optional files will be installed.
|
||||
private final ObservableSet<String> excludedFiles;
|
||||
|
||||
/// Per-file selected state mirrored from [#excludedFiles] (`true` means install / not excluded).
|
||||
private final Map<String, BooleanProperty> selectedByKey = new HashMap<>();
|
||||
|
||||
/// Whether [#selectedByKey] is being updated from an [#excludedFiles] set change.
|
||||
private boolean updatingFromSet;
|
||||
|
||||
/// List view showing optional files.
|
||||
private final JFXListView<ModpackFile> body = new JFXListView<>();
|
||||
|
||||
/// Creates the optional-files selection page.
|
||||
///
|
||||
/// @param install callback that finishes the install wizard
|
||||
/// @param retry callback that reloads optional-file metadata
|
||||
/// @param loading whether remote metadata is still loading
|
||||
/// @param successful whether remote metadata loaded successfully
|
||||
/// @param optionalFiles optional files to display
|
||||
/// @param excludedFiles mutable set of unchecked optional file keys
|
||||
public OptionalFilesPage(
|
||||
Runnable install,
|
||||
Runnable retry,
|
||||
ObservableBooleanValue loading,
|
||||
ObservableBooleanValue successful,
|
||||
ObservableList<ModpackFile> optionalFiles,
|
||||
ObservableSet<String> excludedFiles) {
|
||||
this.excludedFiles = excludedFiles;
|
||||
excludedFiles.addListener((SetChangeListener<String>) change -> {
|
||||
updatingFromSet = true;
|
||||
try {
|
||||
if (change.wasRemoved()) {
|
||||
BooleanProperty selected = selectedByKey.get(change.getElementRemoved());
|
||||
if (selected != null)
|
||||
selected.set(true);
|
||||
}
|
||||
if (change.wasAdded()) {
|
||||
BooleanProperty selected = selectedByKey.get(change.getElementAdded());
|
||||
if (selected != null)
|
||||
selected.set(false);
|
||||
}
|
||||
} finally {
|
||||
updatingFromSet = false;
|
||||
}
|
||||
});
|
||||
|
||||
VBox borderPane = new VBox();
|
||||
borderPane.setAlignment(Pos.CENTER);
|
||||
borderPane.setMaxHeight(Region.USE_PREF_SIZE);
|
||||
FXUtils.setLimitWidth(borderPane, 500);
|
||||
|
||||
ComponentList componentList = new ComponentList();
|
||||
|
||||
Label lblRetry = new Label(i18n("modpack.retry_optional_files"));
|
||||
lblRetry.setOnMouseClicked(e -> retry.run());
|
||||
|
||||
VBox tail = new VBox();
|
||||
{
|
||||
var descPane = new BorderPane();
|
||||
HBox selectionButtons = new HBox(8);
|
||||
JFXButton btnSelectAll = FXUtils.newBorderButton(i18n("button.select_all"));
|
||||
btnSelectAll.setOnAction(e -> excludedFiles.clear());
|
||||
JFXButton btnClear = FXUtils.newBorderButton(i18n("button.clear"));
|
||||
btnClear.setOnAction(e -> {
|
||||
excludedFiles.clear();
|
||||
for (ModpackFile file : optionalFiles) {
|
||||
excludedFiles.add(file.key());
|
||||
}
|
||||
});
|
||||
selectionButtons.getChildren().setAll(btnSelectAll, btnClear);
|
||||
descPane.setLeft(selectionButtons);
|
||||
|
||||
var btnInstall = FXUtils.newRaisedButton(i18n("button.install"));
|
||||
descPane.setRight(btnInstall);
|
||||
btnInstall.setOnAction(e -> install.run());
|
||||
tail.getChildren().add(descPane);
|
||||
}
|
||||
|
||||
// ListView defaults to a large preferred height (~400) regardless of item count, which
|
||||
// leaves empty viewport space when few optional files exist. Size exactly to content —
|
||||
// do not add extra pixels, or a blank strip appears under the last row.
|
||||
body.setFixedCellSize(CELL_HEIGHT);
|
||||
body.prefHeightProperty().bind(Bindings.createDoubleBinding(
|
||||
() -> Math.min(MAX_LIST_HEIGHT, optionalFiles.size() * CELL_HEIGHT + 2),
|
||||
optionalFiles));
|
||||
body.setMaxHeight(Region.USE_PREF_SIZE);
|
||||
body.setCellFactory(it -> new OptionalFileEntry(body));
|
||||
body.setItems(optionalFiles);
|
||||
|
||||
Runnable refreshContent = () -> {
|
||||
if (successful.get()) {
|
||||
componentList.getContent().setAll(body, tail);
|
||||
} else {
|
||||
componentList.getContent().setAll(lblRetry, body, tail);
|
||||
}
|
||||
};
|
||||
refreshContent.run();
|
||||
successful.addListener((obs, oldVal, newVal) -> refreshContent.run());
|
||||
|
||||
borderPane.getChildren().setAll(componentList);
|
||||
setContent(borderPane);
|
||||
loadingProperty().bind(loading);
|
||||
}
|
||||
|
||||
/// Returns the selected property for `key`, creating and wiring it on first use.
|
||||
///
|
||||
/// @param key the file exclusion key
|
||||
/// @return a property that is `true` when the file is selected for install
|
||||
private BooleanProperty selectedProperty(String key) {
|
||||
return selectedByKey.computeIfAbsent(key, k -> {
|
||||
BooleanProperty selected = new SimpleBooleanProperty(!excludedFiles.contains(k));
|
||||
selected.addListener((obs, wasSelected, isSelected) -> {
|
||||
if (updatingFromSet)
|
||||
return;
|
||||
if (isSelected) {
|
||||
excludedFiles.remove(k);
|
||||
} else {
|
||||
excludedFiles.add(k);
|
||||
}
|
||||
});
|
||||
return selected;
|
||||
});
|
||||
}
|
||||
|
||||
/// List cell for one optional file with checkbox and detail button.
|
||||
private final class OptionalFileEntry extends MDListCell<ModpackFile> {
|
||||
private final JFXCheckBox checkBox = new JFXCheckBox();
|
||||
private final TwoLineListItem content = new TwoLineListItem();
|
||||
private final JFXButton infoButton = new JFXButton();
|
||||
private final HBox container = new HBox(8);
|
||||
|
||||
/// Currently bidirectionally bound selected property, or `null` when unbound.
|
||||
private @Nullable BooleanProperty boundSelected = null;
|
||||
|
||||
/// Creates a cell bound to `listView`.
|
||||
///
|
||||
/// @param listView the owning list view
|
||||
public OptionalFileEntry(JFXListView<ModpackFile> listView) {
|
||||
super(listView);
|
||||
container.setPickOnBounds(false);
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
content.setMouseTransparent(true);
|
||||
setSelectable();
|
||||
container.getChildren().setAll(checkBox, content);
|
||||
|
||||
infoButton.getStyleClass().add("toggle-icon4");
|
||||
infoButton.setGraphic(SVG.INFO.createIcon());
|
||||
container.getChildren().add(infoButton);
|
||||
getContainer().getChildren().setAll(container);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateControl(ModpackFile item, boolean empty) {
|
||||
if (empty) {
|
||||
if (boundSelected != null) {
|
||||
checkBox.selectedProperty().unbindBidirectional(boundSelected);
|
||||
boundSelected = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
String name = item.fileName();
|
||||
if (name != null) {
|
||||
content.setTitle(name);
|
||||
} else {
|
||||
content.setTitle(i18n("modpack.unknown_optional_file"));
|
||||
}
|
||||
RemoteAddon addon = item.remoteAddon();
|
||||
if (addon != null) {
|
||||
content.setSubtitle(addon.title());
|
||||
infoButton.setOnMouseClicked(e -> Controllers.dialog(new ModInfo(addon)));
|
||||
infoButton.setManaged(true);
|
||||
infoButton.setVisible(true);
|
||||
} else {
|
||||
content.setSubtitle("");
|
||||
infoButton.setOnMouseClicked(null);
|
||||
infoButton.setManaged(false);
|
||||
infoButton.setVisible(false);
|
||||
}
|
||||
BooleanProperty selected = OptionalFilesPage.this.selectedProperty(item.key());
|
||||
if (boundSelected != selected) {
|
||||
if (boundSelected != null)
|
||||
checkBox.selectedProperty().unbindBidirectional(boundSelected);
|
||||
checkBox.selectedProperty().bindBidirectional(selected);
|
||||
boundSelected = selected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Dialog showing remote addon details for an optional file.
|
||||
private static final class ModInfo extends JFXDialogLayout {
|
||||
/// Creates a detail dialog for `addon`.
|
||||
///
|
||||
/// @param addon the remote addon
|
||||
public ModInfo(RemoteAddon addon) {
|
||||
HBox container = new HBox(8);
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
ImageView imageView = new ImageView();
|
||||
imageView.setFitHeight(32);
|
||||
imageView.setFitWidth(32);
|
||||
spinnerPane.setContent(imageView);
|
||||
spinnerPane.setPrefSize(32, 32);
|
||||
spinnerPane.setLoading(true);
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return FXUtils.getRemoteImageTask(addon.iconUrl(), 32, 32, true, true).run();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}, Schedulers.io()).thenAcceptAsync((image) -> {
|
||||
imageView.setImage(image);
|
||||
spinnerPane.setLoading(false);
|
||||
}, Schedulers.javafx());
|
||||
container.getChildren().add(spinnerPane);
|
||||
|
||||
TwoLineListItem title = new TwoLineListItem();
|
||||
title.setTitle(addon.title());
|
||||
title.setSubtitle(addon.author());
|
||||
container.getChildren().add(title);
|
||||
setHeading(container);
|
||||
|
||||
Label description = new Label(addon.description());
|
||||
description.setWrapText(true);
|
||||
description.setPadding(new Insets(8, 0, 0, 0));
|
||||
setBody(description);
|
||||
|
||||
JFXHyperlink pageButton = new JFXHyperlink(i18n("mods.url"));
|
||||
pageButton.setOnAction(e -> FXUtils.openLink(addon.pageUrl()));
|
||||
getActions().add(pageButton);
|
||||
|
||||
JFXButton okButton = new JFXButton();
|
||||
okButton.getStyleClass().add("dialog-accept");
|
||||
okButton.setText(i18n("button.ok"));
|
||||
okButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
||||
getActions().add(okButton);
|
||||
|
||||
onEscPressed(this, okButton::fire);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return i18n("modpack.optional_files");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(SettingsMap settings) {
|
||||
}
|
||||
}
|
||||
+15
-13
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.download;
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.download.*;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
|
||||
import org.jackhuang.hmcl.game.GameComponentType;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
@@ -35,11 +36,11 @@ import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.zip.ZipException;
|
||||
@@ -65,6 +66,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
public void start(SettingsMap settings) {
|
||||
}
|
||||
|
||||
/// Creates a task that updates components and synchronizes the client JAR with the resulting manifest.
|
||||
@Override
|
||||
public Object finish(SettingsMap settings) {
|
||||
settings.put("title", i18n("install.change_version.process"));
|
||||
@@ -83,7 +85,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
}
|
||||
}
|
||||
|
||||
return gameInstance.getRepository().updateInstanceAsync(gameInstance.getId(), publishedInstance -> {
|
||||
return dependencyManager.updateInstanceAsync(gameInstance.getId(), publishedInstance -> {
|
||||
Task<GameInstanceManifest> update = Task.supplyAsync(publishedInstance::getManifest);
|
||||
for (Object value : settings.asStringMap().values()) {
|
||||
if (value instanceof ComponentRemoteVersion remoteVersion) {
|
||||
@@ -103,11 +105,11 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
public Node createPage(WizardController controller, int step, SettingsMap settings) {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + componentType.getPatchId())), gameInstance.getVersion().toString(), downloadProvider, componentType, () -> {
|
||||
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + componentType.getPatchId())), gameInstance.getVersion(), downloadProvider, componentType, () -> {
|
||||
if (oldLibraryVersion == null) {
|
||||
controller.onFinish();
|
||||
} else if (componentType == GameComponentType.GAME) {
|
||||
String newGameVersion = ((ComponentRemoteVersion) settings.get(componentType.getPatchId())).getSelfVersion();
|
||||
GameVersionNumber newGameVersion = ((GameRemoteVersion) settings.get(componentType.getPatchId())).getGameVersion();
|
||||
controller.onNext(new AdditionalInstallersPage(gameInstance, newGameVersion, controller, downloadProvider));
|
||||
} else {
|
||||
Controllers.confirm(i18n("install.change_version.confirm", i18n("install.installer." + componentType.getPatchId()), oldLibraryVersion, ((ComponentRemoteVersion) settings.get(componentType.getPatchId())).getSelfVersion()),
|
||||
@@ -136,27 +138,27 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
String message = i18n("launch.failed.download_library", ((LibraryDownloadException) exception).getLibrary().name()) + "\n";
|
||||
if (exception.getCause() instanceof ResponseCodeException rce) {
|
||||
int responseCode = rce.getResponseCode();
|
||||
String uri = rce.getUri();
|
||||
String url = rce.getUrl();
|
||||
if (responseCode == 404)
|
||||
message += i18n("download.code.404", uri);
|
||||
message += i18n("download.code.404", url);
|
||||
else
|
||||
message += i18n("download.failed", uri, responseCode);
|
||||
message += i18n("download.failed", url, responseCode);
|
||||
} else {
|
||||
message += StringUtils.getStackTrace(exception.getCause());
|
||||
}
|
||||
Controllers.dialog(message, i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
} else if (exception instanceof DownloadException) {
|
||||
URI uri = ((DownloadException) exception).getUri();
|
||||
} else if (exception instanceof DownloadException de) {
|
||||
String url = de.getUrl();
|
||||
if (exception.getCause() instanceof SocketTimeoutException) {
|
||||
Controllers.dialog(i18n("install.failed.downloading.timeout", uri), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
Controllers.dialog(i18n("install.failed.downloading.timeout", url), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
} else if (exception.getCause() instanceof ResponseCodeException responseCodeException) {
|
||||
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
|
||||
Controllers.dialog(i18n("download.code." + responseCodeException.getResponseCode(), uri), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
Controllers.dialog(i18n("download.code." + responseCodeException.getResponseCode(), url), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
} else {
|
||||
Controllers.dialog(i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
Controllers.dialog(i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
}
|
||||
} else {
|
||||
Controllers.dialog(i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
Controllers.dialog(i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
}
|
||||
} else if (exception instanceof UnsupportedInstallationException unsupportedInstallationException) {
|
||||
switch (unsupportedInstallationException.getReason()) {
|
||||
|
||||
@@ -32,7 +32,6 @@ import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.ComponentRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.ComponentVersionList;
|
||||
import org.jackhuang.hmcl.download.cleanroom.CleanroomRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricAPIRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricRemoteVersion;
|
||||
@@ -66,6 +65,7 @@ import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -77,31 +77,29 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class VersionsPage extends Control implements WizardPage, Refreshable {
|
||||
private final String gameVersion;
|
||||
private final @Nullable GameVersionNumber gameVersion;
|
||||
private final GameComponentType componentType;
|
||||
private final String title;
|
||||
private final Navigation navigation;
|
||||
private final DownloadProvider downloadProvider;
|
||||
private final ComponentVersionList<?> versionList;
|
||||
private final Runnable callback;
|
||||
|
||||
private final ObservableList<ComponentRemoteVersion> versions = FXCollections.observableArrayList();
|
||||
private final ObjectProperty<Status> status = new SimpleObjectProperty<>(Status.LOADING);
|
||||
|
||||
public VersionsPage(Navigation navigation,
|
||||
String title, String gameVersion,
|
||||
String title, GameVersionNumber gameVersion,
|
||||
DownloadProvider downloadProvider,
|
||||
GameComponentType componentType,
|
||||
Runnable callback) {
|
||||
this.title = title;
|
||||
this.gameVersion = gameVersion;
|
||||
this.gameVersion = componentType == GameComponentType.GAME ? null : gameVersion;
|
||||
this.componentType = componentType;
|
||||
this.navigation = navigation;
|
||||
this.downloadProvider = downloadProvider;
|
||||
this.versionList = downloadProvider.getVersionList(componentType);
|
||||
this.callback = callback;
|
||||
|
||||
refresh();
|
||||
load(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -111,9 +109,12 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
|
||||
@Override
|
||||
public void refresh() {
|
||||
load(true);
|
||||
}
|
||||
|
||||
private void load(boolean refresh) {
|
||||
status.set(Status.LOADING);
|
||||
Task<?> task = versionList.refreshAsync(gameVersion)
|
||||
.thenSupplyAsync(() -> versionList.getVersions(gameVersion).stream().sorted().collect(Collectors.toList()))
|
||||
Task<?> task = downloadProvider.getVersionsAsync(componentType, gameVersion, refresh)
|
||||
.whenComplete(Schedulers.javafx(), (items, exception) -> {
|
||||
if (exception == null) {
|
||||
versions.setAll(items);
|
||||
@@ -240,7 +241,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
|
||||
if (remoteVersion instanceof GameRemoteVersion) {
|
||||
ComponentRemoteVersion.Type versionType = remoteVersion.getVersionType();
|
||||
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(remoteVersion.getGameVersion());
|
||||
GameVersionNumber gameVersion = remoteVersion.getGameVersion();
|
||||
|
||||
switch (versionType) {
|
||||
case RELEASE -> {
|
||||
@@ -248,8 +249,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
imageView.setImage(GameInstanceIconType.GRASS.getIcon());
|
||||
}
|
||||
case SNAPSHOT, PENDING, UNOBFUSCATED -> {
|
||||
if (versionType == ComponentRemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) {
|
||||
if (versionType == ComponentRemoteVersion.Type.SNAPSHOT && gameVersion.isAprilFools()) {
|
||||
twoLineListItem.addTag(i18n("instance.game.april_fools"));
|
||||
imageView.setImage(GameInstanceIconType.APRIL_FOOLS.getIcon());
|
||||
} else {
|
||||
@@ -289,7 +289,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
iconType = GameInstanceIconType.COMMAND;
|
||||
|
||||
imageView.setImage(iconType.getIcon());
|
||||
String displayGameVersion = I18n.getDisplayVersion(GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()));
|
||||
String displayGameVersion = I18n.getDisplayVersion(remoteVersion.getGameVersion());
|
||||
|
||||
if (twoLineListItem.getSubtitle() == null)
|
||||
twoLineListItem.setSubtitle(displayGameVersion);
|
||||
@@ -326,7 +326,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
column2.setMaxWidth(150);
|
||||
ColumnConstraints column3 = new ColumnConstraints();
|
||||
|
||||
if (control.versionList.hasType())
|
||||
if (control.downloadProvider.hasType(control.componentType))
|
||||
searchPane.getColumnConstraints().setAll(nameColumn, column1, nameColumn, column2, column3);
|
||||
else
|
||||
searchPane.getColumnConstraints().setAll(nameColumn, column1, column3);
|
||||
@@ -365,7 +365,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
JFXButton refreshButton = FXUtils.newRaisedButton(i18n("button.refresh"));
|
||||
refreshButton.setOnAction(event -> control.onRefresh());
|
||||
|
||||
if (control.versionList.hasType()) {
|
||||
if (control.downloadProvider.hasType(control.componentType)) {
|
||||
searchPane.addRow(rowIndex++,
|
||||
new Label(i18n("instance.search")), nameField,
|
||||
new Label(i18n("instance.game.type")), categoryField,
|
||||
@@ -470,7 +470,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
|| versionType == ComponentRemoteVersion.Type.PENDING
|
||||
|| versionType == ComponentRemoteVersion.Type.UNOBFUSCATED;
|
||||
case APRIL_FOOLS -> versionType == ComponentRemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(it.getGameVersion()).isAprilFools();
|
||||
&& it.getGameVersion().isAprilFools();
|
||||
case OLD -> versionType == ComponentRemoteVersion.Type.OLD;
|
||||
// case ALL,
|
||||
default -> true;
|
||||
|
||||
@@ -24,14 +24,11 @@ import org.jackhuang.hmcl.modpack.ModAdviser;
|
||||
import org.jackhuang.hmcl.modpack.ModpackExportInfo;
|
||||
import org.jackhuang.hmcl.modpack.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.modrinth.ModrinthModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCInstanceConfiguration;
|
||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackExportTask;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
@@ -97,9 +94,6 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
case ModpackTypeSelectionPage.MODPACK_TYPE_MCBBS:
|
||||
exportTask = exportAsMcbbs(exportInfo, dest);
|
||||
break;
|
||||
case ModpackTypeSelectionPage.MODPACK_TYPE_MULTIMC:
|
||||
exportTask = exportAsMultiMC(exportInfo, dest);
|
||||
break;
|
||||
case ModpackTypeSelectionPage.MODPACK_TYPE_SERVER:
|
||||
exportTask = exportAsServer(exportInfo, dest);
|
||||
break;
|
||||
@@ -183,55 +177,6 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
};
|
||||
}
|
||||
|
||||
private Task<?> exportAsMultiMC(ModpackExportInfo exportInfo, Path modpackFile) {
|
||||
return new Task<Void>() {
|
||||
@Nullable Task<?> dependency;
|
||||
|
||||
{
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
HMCLGameInstance instance = resolveCurrentGameInstance();
|
||||
GameSettings.Effective setting = instance.getEffectiveSettings();
|
||||
dependency = new MultiMCModpackExportTask(instance, exportInfo.getWhitelist(),
|
||||
new MultiMCInstanceConfiguration(
|
||||
"OneSix",
|
||||
exportInfo.getName() + "-" + exportInfo.getVersion(),
|
||||
null,
|
||||
Lang.toIntOrNull(setting.getInheritable(GameSettings::permSizeProperty)),
|
||||
setting.getInheritable(GameSettings::commandWrapperProperty),
|
||||
setting.getInheritable(GameSettings::preLaunchCommandProperty),
|
||||
null,
|
||||
exportInfo.getDescription(),
|
||||
null,
|
||||
exportInfo.getJavaArguments(),
|
||||
setting.getInheritable(GameSettings::windowTypeProperty) == GameWindowType.FULLSCREEN,
|
||||
setting.getWidth(),
|
||||
setting.getHeight(),
|
||||
null,
|
||||
exportInfo.getMinMemory(),
|
||||
setting.getInheritable(GameSettings::showLogsProperty),
|
||||
/* showConsoleOnError */ true,
|
||||
/* autoCloseConsole */ false,
|
||||
/* overrideMemory */ true,
|
||||
/* overrideJavaLocation */ false,
|
||||
/* overrideJavaArgs */ true,
|
||||
/* overrideConsole */ true,
|
||||
/* overrideCommands */ true,
|
||||
/* overrideWindow */ true,
|
||||
/* iconKey */ null // TODO
|
||||
), modpackFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task<?>> getDependencies() {
|
||||
return Collections.singleton(dependency);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Task<?> exportAsServer(ModpackExportInfo exportInfo, Path modpackFile) {
|
||||
return new Task<Void>() {
|
||||
@Nullable Task<?> dependency;
|
||||
|
||||
@@ -18,17 +18,17 @@
|
||||
package org.jackhuang.hmcl.ui.export;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXCheckTreeCell;
|
||||
import com.jfoenix.controls.JFXTreeView;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.CheckBoxTreeItem;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.modpack.ModAdviser;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -41,6 +41,7 @@ import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -66,13 +67,14 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
private final ModAdviser adviser;
|
||||
private @Nullable ModpackFileTreeItem rootNode;
|
||||
|
||||
/// Creates a file-selection page and starts loading the instance's exportable files.
|
||||
public ModpackFileSelectionPage(WizardController controller, HMCLGameInstance gameInstance, ModAdviser adviser) {
|
||||
this.controller = controller;
|
||||
this.gameInstance = gameInstance;
|
||||
this.adviser = adviser;
|
||||
GameInstanceID instanceId = gameInstance.getId();
|
||||
|
||||
JFXTreeView<String> treeView = new JFXTreeView<>();
|
||||
treeView.setCellFactory(__ -> new ModpackFileTreeCell());
|
||||
treeView.setSelectionModel(new NoneMultipleSelectionModel<>());
|
||||
onEscPressed(treeView, () -> controller.onPrev(true));
|
||||
|
||||
@@ -196,14 +198,16 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
return node;
|
||||
}
|
||||
|
||||
private void getFilesNeeded(ModpackFileTreeItem node, String basePath, List<String> list) {
|
||||
/// Appends selected paths relative to `minecraft/`, traversing partially selected directories.
|
||||
/// A `null` node contributes no paths.
|
||||
private void getFilesNeeded(@Nullable ModpackFileTreeItem node, String basePath, List<String> list) {
|
||||
if (node == null) return;
|
||||
if (node.isSelected() || node.isIndeterminate()) {
|
||||
if (basePath.length() > "minecraft/".length())
|
||||
list.add(StringUtils.substringAfter(basePath, "minecraft/"));
|
||||
for (TreeItem<String> child : node.getChildren()) {
|
||||
if (child instanceof ModpackFileTreeItem mChild) {
|
||||
getFilesNeeded(mChild, basePath + "/" + mChild.getFileName(), list);
|
||||
getFilesNeeded(mChild, basePath + "/" + mChild.getValue(), list);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,39 +248,69 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
pair("minecraft/scripts", i18n("modpack.files.scripts"))
|
||||
);
|
||||
|
||||
/// Stores a file name, optional explanation, and hierarchical selection state.
|
||||
@NotNullByDefault
|
||||
private static final class ModpackFileTreeItem extends CheckBoxTreeItem<String> {
|
||||
|
||||
private final String fileName;
|
||||
/// The localized explanation for a recognized path, or `null` if none exists.
|
||||
private final @Nullable String comment;
|
||||
|
||||
/// Creates an item whose value is the file name; only the `minecraft` root starts expanded.
|
||||
public ModpackFileTreeItem(String fileName, String basePath) {
|
||||
this.fileName = fileName;
|
||||
|
||||
HBox graphic = new HBox();
|
||||
JFXCheckBox checkBox = new JFXCheckBox();
|
||||
checkBox.selectedProperty().bindBidirectional(this.selectedProperty());
|
||||
checkBox.indeterminateProperty().bindBidirectional(this.indeterminateProperty());
|
||||
graphic.getChildren().add(checkBox);
|
||||
|
||||
{
|
||||
Label text = new Label(fileName);
|
||||
text.setMouseTransparent(true);
|
||||
graphic.getChildren().add(text);
|
||||
}
|
||||
if (TRANSLATION.containsKey(basePath)) {
|
||||
Label comment = new Label(TRANSLATION.get(basePath));
|
||||
comment.setStyle("-fx-text-fill: -monet-on-surface-variant;");
|
||||
comment.setMouseTransparent(true);
|
||||
graphic.getChildren().add(comment);
|
||||
}
|
||||
graphic.setPickOnBounds(false);
|
||||
super(fileName);
|
||||
this.comment = TRANSLATION.get(basePath);
|
||||
this.setExpanded("minecraft".equals(basePath));
|
||||
this.setValue(""); // To disable the default display of text
|
||||
this.setGraphic(graphic);
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders file names and localized explanations alongside the bound checkbox.
|
||||
@NotNullByDefault
|
||||
private static final class ModpackFileTreeCell extends JFXCheckTreeCell<String> {
|
||||
|
||||
/// Contains the inherited checkbox graphic followed by the file labels.
|
||||
private final HBox content = new HBox();
|
||||
|
||||
/// Displays the current file name.
|
||||
private final Label fileName = new Label();
|
||||
|
||||
/// Displays the current path's optional explanation.
|
||||
private final Label comment = new Label();
|
||||
|
||||
/// Creates reusable labels whose mouse events pass through to the cell.
|
||||
private ModpackFileTreeCell() {
|
||||
fileName.setMouseTransparent(true);
|
||||
comment.setStyle("-fx-text-fill: -monet-on-surface-variant;");
|
||||
comment.setMouseTransparent(true);
|
||||
content.setAlignment(Pos.CENTER_LEFT);
|
||||
content.setPickOnBounds(false);
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
/// Refreshes the file labels and removes content when the cell is cleared.
|
||||
@Override
|
||||
protected void updateDisplay(@Nullable String item, boolean empty) {
|
||||
content.getChildren().clear();
|
||||
super.updateDisplay(item, empty);
|
||||
|
||||
fileName.setText(null);
|
||||
comment.setText(null);
|
||||
if (empty || item == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Updating the text makes the skin reattach the current graphic to the cell.
|
||||
// Clear it before moving the checkbox graphic into the content container.
|
||||
setText(null);
|
||||
@Nullable Node graphic = getGraphic();
|
||||
if (graphic != null) {
|
||||
content.getChildren().add(graphic);
|
||||
}
|
||||
fileName.setText(item);
|
||||
content.getChildren().add(fileName);
|
||||
if (getTreeItem() instanceof ModpackFileTreeItem treeItem && treeItem.comment != null) {
|
||||
comment.setText(treeItem.comment);
|
||||
content.getChildren().add(comment);
|
||||
}
|
||||
setGraphic(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.modpack.ModpackExportInfo;
|
||||
import org.jackhuang.hmcl.modpack.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.modrinth.ModrinthModpackExportTask;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
@@ -55,7 +54,6 @@ public final class ModpackTypeSelectionPage extends VBox implements WizardPage {
|
||||
this.getChildren().setAll(
|
||||
title,
|
||||
createButton(MODPACK_TYPE_MCBBS, McbbsModpackExportTask.OPTION),
|
||||
createButton(MODPACK_TYPE_MULTIMC, MultiMCModpackExportTask.OPTION),
|
||||
createButton(MODPACK_TYPE_SERVER, ServerModpackExportTask.OPTION),
|
||||
createButton(MODPACK_TYPE_MODRINTH, ModrinthModpackExportTask.OPTION)
|
||||
);
|
||||
@@ -98,7 +96,6 @@ public final class ModpackTypeSelectionPage extends VBox implements WizardPage {
|
||||
public static final SettingsMap.Key<String> MODPACK_TYPE = new SettingsMap.Key<>("modpack.type");
|
||||
|
||||
public static final String MODPACK_TYPE_MCBBS = "mcbbs";
|
||||
public static final String MODPACK_TYPE_MULTIMC = "multimc";
|
||||
public static final String MODPACK_TYPE_SERVER = "server";
|
||||
public static final String MODPACK_TYPE_MODRINTH = "modrinth";
|
||||
}
|
||||
@@ -829,10 +829,14 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
nativeLibrarySettings.getContent().add(nativesDirPane);
|
||||
nativesDirPane.setTitle(i18n("settings.advanced.natives_directory"));
|
||||
{
|
||||
var txtNativesDir = new JFXTextField();
|
||||
txtNativesDir.setPrefWidth(400);
|
||||
nativesDirPane.setRight(txtNativesDir);
|
||||
bindIndependentTextField(nativesDirPane, txtNativesDir, GameSettings::nativesDirectoryProperty);
|
||||
var nativesDirSelector = new FileSelector()
|
||||
.setSelectionMode(FileSelector.SelectionMode.DIRECTORY);
|
||||
nativesDirSelector.setPrefWidth(400);
|
||||
nativesDirPane.setRight(nativesDirSelector);
|
||||
bindInheritableStringProperty(
|
||||
nativesDirPane,
|
||||
nativesDirSelector.valueProperty(),
|
||||
GameSettings::nativesDirectoryProperty);
|
||||
}
|
||||
|
||||
var noNativesPatchPane = createIndependentBooleanButton(GameSettings::notPatchNativesProperty);
|
||||
|
||||
@@ -342,7 +342,7 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
|
||||
private void loadChangelog(AddonUpdateObject object, SpinnerPane spinnerPane, ScrollPane scrollPane) {
|
||||
spinnerPane.setLoading(true);
|
||||
RemoteAddonRepository repo = object.data.source().getRepoForType(object.data.repoType());
|
||||
RemoteAddonRepository repo = object.data.source().getRepository();
|
||||
Task.supplyAsync(() -> {
|
||||
if (object.changelog != null) {
|
||||
return object.changelog;
|
||||
@@ -368,7 +368,7 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
|
||||
private void loadVersionPageUrl(AddonUpdateObject object, JFXHyperlink button) {
|
||||
Task.supplyAsync(() -> {
|
||||
RemoteAddonRepository repo = object.data.source().getRepoForType(object.data.repoType());
|
||||
RemoteAddonRepository repo = object.data.source().getRepository();
|
||||
return repo == null ? null : repo.getVersionPageUrl(object.data.targetVersion());
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null && StringUtils.isNotBlank(result)) {
|
||||
|
||||
@@ -27,7 +27,6 @@ import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.transformation.FilteredList;
|
||||
@@ -46,7 +45,6 @@ import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.addon.datapack.DataPack;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
@@ -55,15 +53,13 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.ItemPropertyAsyncCache;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.IntStream;
|
||||
@@ -263,11 +259,13 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
private final BooleanProperty activeProperty;
|
||||
private final DataPack.Pack packInfo;
|
||||
|
||||
private SoftReference<CompletableFuture<Image>> iconCache;
|
||||
private final ItemPropertyAsyncCache<Image, DataPackInfoObject> iconCache;
|
||||
|
||||
DataPackInfoObject(DataPack.Pack packInfo) {
|
||||
this.packInfo = packInfo;
|
||||
this.activeProperty = packInfo.activeProperty();
|
||||
|
||||
this.iconCache = new ItemPropertyAsyncCache.Soft<>(this, this::loadIcon, this::getDefaultIcon);
|
||||
}
|
||||
|
||||
String getTitle() {
|
||||
@@ -282,7 +280,11 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
return packInfo;
|
||||
}
|
||||
|
||||
Image loadIcon() {
|
||||
private Image getDefaultIcon() {
|
||||
return FXUtils.newBuiltinImage("/assets/img/unknown_pack.png");
|
||||
}
|
||||
|
||||
private Image loadIcon() {
|
||||
Image image = null;
|
||||
Path imagePath;
|
||||
if (this.getPackInfo().isDirectory()) {
|
||||
@@ -291,7 +293,6 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
image = FXUtils.loadImage(imagePath, 64, 64, true, true);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("fail to load image, datapack path: " + getPackInfo().getPath(), e);
|
||||
return FXUtils.newBuiltinImage("/assets/img/unknown_pack.png");
|
||||
}
|
||||
} else {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(getPackInfo().getPath())) {
|
||||
@@ -301,7 +302,6 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("fail to load image, datapack path: " + getPackInfo().getPath(), e);
|
||||
return FXUtils.newBuiltinImage("/assets/img/unknown_pack.png");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,36 +309,9 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
Math.abs(image.getWidth() - image.getHeight()) < 1) {
|
||||
return image;
|
||||
} else {
|
||||
return FXUtils.newBuiltinImage("/assets/img/unknown_pack.png");
|
||||
return getDefaultIcon();
|
||||
}
|
||||
}
|
||||
|
||||
public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference<ObjectProperty<DataPackInfoObject>> current) {
|
||||
SoftReference<CompletableFuture<Image>> iconCache = this.iconCache;
|
||||
CompletableFuture<Image> imageFuture;
|
||||
if (iconCache != null && (imageFuture = iconCache.get()) != null) {
|
||||
Image image = imageFuture.getNow(null);
|
||||
if (image != null) {
|
||||
imageContainer.setImage(image);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
imageFuture = CompletableFuture.supplyAsync(this::loadIcon, Schedulers.io());
|
||||
this.iconCache = new SoftReference<>(imageFuture);
|
||||
}
|
||||
imageContainer.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"));
|
||||
imageFuture.thenAcceptAsync(image -> {
|
||||
if (current != null) {
|
||||
ObjectProperty<DataPackInfoObject> infoObjectProperty = current.get();
|
||||
if (infoObjectProperty == null || infoObjectProperty.get() != this) {
|
||||
// The current ListCell has already switched to another object
|
||||
return;
|
||||
}
|
||||
}
|
||||
imageContainer.setImage(image);
|
||||
}, Schedulers.javafx());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private final class DataPackInfoListCell extends MDListCell<DataPackInfoObject> {
|
||||
@@ -377,7 +350,7 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
|
||||
}
|
||||
checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.activeProperty);
|
||||
dataItem.loadIcon(imageContainer, new WeakReference<>(this.itemProperty()));
|
||||
dataItem.iconCache.attachValue(imageContainer.imageProperty(), new WeakReference<>(this.itemProperty()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,11 +37,12 @@ import javafx.scene.image.Image;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.download.DownloadCandidates;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -57,7 +58,6 @@ import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -85,16 +85,18 @@ public class DownloadListPage extends Control implements DecoratorPage {
|
||||
protected final StringProperty downloadSource = new SimpleStringProperty();
|
||||
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
|
||||
private int searchID = 0;
|
||||
protected RemoteAddonRepository repository;
|
||||
protected final RemoteAddonRepository repository;
|
||||
protected final @NotNull RemoteAddon.Type type;
|
||||
private final DownloadProvider downloadProvider;
|
||||
|
||||
private Runnable retrySearch;
|
||||
|
||||
public DownloadListPage(RemoteAddonRepository repository) {
|
||||
this(repository, null, false);
|
||||
public DownloadListPage(@NotNull RemoteAddon.Type type, RemoteAddonRepository repository) {
|
||||
this(type, repository, null, false);
|
||||
}
|
||||
|
||||
public DownloadListPage(RemoteAddonRepository repository, DownloadPage.DownloadCallback callback, boolean instanceSelection) {
|
||||
public DownloadListPage(@NotNull RemoteAddon.Type type, RemoteAddonRepository repository, DownloadPage.DownloadCallback callback, boolean instanceSelection) {
|
||||
this.type = Objects.requireNonNull(type);
|
||||
this.repository = repository;
|
||||
this.callback = callback;
|
||||
this.instanceSelection = instanceSelection;
|
||||
@@ -117,7 +119,7 @@ public class DownloadListPage extends Control implements DecoratorPage {
|
||||
|
||||
if (!searchInitialized) {
|
||||
searchInitialized = true;
|
||||
search("", null, 0, "", RemoteAddonRepository.SortType.POPULARITY);
|
||||
search("", null, 0, "", RemoteAddonRepository.SortType.RELEVANCY);
|
||||
}
|
||||
|
||||
if (instanceSelection) {
|
||||
@@ -158,6 +160,10 @@ public class DownloadListPage extends Control implements DecoratorPage {
|
||||
FXUtils.runInFX(() -> selectedInstance.set(instanceId));
|
||||
}
|
||||
|
||||
public DownloadPage.DownloadCallback getCallback() {
|
||||
return callback;
|
||||
}
|
||||
|
||||
private void search(String userGameVersion, RemoteAddonRepository.Category category, int pageOffset, String searchFilter, RemoteAddonRepository.SortType sort) {
|
||||
retrySearch = null;
|
||||
setLoading(true);
|
||||
@@ -174,7 +180,7 @@ public class DownloadListPage extends Control implements DecoratorPage {
|
||||
return version != GameVersionNumber.unknown() ? version.toString() : "";
|
||||
}
|
||||
}).thenApplyAsync(
|
||||
gameVersion -> repository.search(downloadProvider, gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteAddonRepository.SortOrder.DESC)
|
||||
gameVersion -> repository.search(downloadProvider, type, gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteAddonRepository.SortOrder.DESC)
|
||||
).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (searchID != currentSearchID) {
|
||||
return;
|
||||
@@ -182,8 +188,8 @@ public class DownloadListPage extends Control implements DecoratorPage {
|
||||
|
||||
setLoading(false);
|
||||
if (exception == null) {
|
||||
items.setAll(result.getResults().collect(Collectors.toList()));
|
||||
pageCount.set(result.getTotalPages());
|
||||
items.setAll(result.results().collect(Collectors.toList()));
|
||||
pageCount.set(result.totalPages());
|
||||
failed.set(false);
|
||||
} else {
|
||||
failed.set(true);
|
||||
@@ -248,8 +254,8 @@ public class DownloadListPage extends Control implements DecoratorPage {
|
||||
|
||||
iconLoader = new RemoteImageLoader(control.downloadProvider) {
|
||||
@Override
|
||||
protected @NotNull Task<Image> createLoadTask(@NotNull List<URI> uris) {
|
||||
return FXUtils.getRemoteImageTask(uris, 80, 80, true, true);
|
||||
protected @NotNull Task<Image> createLoadTask(@NotNull DownloadCandidates urls) {
|
||||
return FXUtils.getRemoteImageTask(urls, 80, 80, true, true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -350,7 +356,7 @@ public class DownloadListPage extends Control implements DecoratorPage {
|
||||
categoryComboBox.getItems().setAll(CategoryIndented.ALL);
|
||||
categoryComboBox.getSelectionModel().select(0);
|
||||
|
||||
Task.supplyAsync(() -> getSkinnable().repository.getCategories())
|
||||
Task.supplyAsync(() -> getSkinnable().repository.getCategories(getSkinnable().type))
|
||||
.thenAcceptAsync(Schedulers.javafx(), categories -> {
|
||||
if (!Objects.equals(getSkinnable().downloadSource.get(), downloadSource)) {
|
||||
return;
|
||||
@@ -371,7 +377,7 @@ public class DownloadListPage extends Control implements DecoratorPage {
|
||||
sortStackPane.getChildren().setAll(sortComboBox);
|
||||
sortComboBox.prefWidthProperty().bind(sortStackPane.widthProperty());
|
||||
sortComboBox.getStyleClass().add("fit-width");
|
||||
sortComboBox.setConverter(stringConverter(sortType -> i18n("curse.sort." + sortType.name().toLowerCase(Locale.ROOT))));
|
||||
sortComboBox.setConverter(stringConverter(sortType -> i18n("addon.sort." + sortType.name().toLowerCase(Locale.ROOT))));
|
||||
sortComboBox.getItems().setAll(RemoteAddonRepository.SortType.values());
|
||||
sortComboBox.getSelectionModel().select(0);
|
||||
searchPane.addRow(rowIndex++, new Label(i18n("addon.category")), categoryStackPane, new Label(i18n("search.sort")), sortStackPane);
|
||||
@@ -600,7 +606,7 @@ public class DownloadListPage extends Control implements DecoratorPage {
|
||||
: PADDING
|
||||
);
|
||||
|
||||
ModTranslations.Mod mod = ModTranslations.getTranslationsByAddonType(getSkinnable().repository.getType()).getModByCurseForgeId(item.slug());
|
||||
ModTranslations.Mod mod = ModTranslations.getTranslationsByAddonType(getSkinnable().type).getModByCurseForgeId(item.slug());
|
||||
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : item.title());
|
||||
String description = item.description();
|
||||
if (description != null) {
|
||||
|
||||
@@ -29,6 +29,7 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.addon.AddonLoader;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
@@ -78,7 +79,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
this.page = page;
|
||||
this.repository = page.repository;
|
||||
this.addon = addon;
|
||||
this.type = Objects.requireNonNullElse(addon.type(), repository.getType());
|
||||
this.type = Objects.requireNonNullElse(addon.type(), page.type);
|
||||
this.translations = ModTranslations.getTranslationsByAddonType(this.type);
|
||||
this.mod = translations.getModByCurseForgeId(addon.slug());
|
||||
this.instanceReference = instanceReference;
|
||||
@@ -281,8 +282,8 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
resolve:
|
||||
for (RemoteAddon.Version addonVersion : addonVersions) {
|
||||
if (getSkinnable().type == RemoteAddon.Type.MOD) {
|
||||
for (ModLoaderType loader : addonVersion.loaders()) {
|
||||
if (targetLoaders.contains(loader)) {
|
||||
for (AddonLoader loader : addonVersion.loaders()) {
|
||||
if (loader.type() instanceof ModLoaderType modLoaderType && targetLoaders.contains(modLoaderType)) {
|
||||
list.getContent().addAll(
|
||||
ComponentList.createComponentListTitle(i18n("addon.download.recommend", gameVersion)),
|
||||
new AddonItem(control.addon, addonVersion, control)
|
||||
@@ -460,31 +461,11 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
graphicPane.getChildren().setAll(icon);
|
||||
}
|
||||
|
||||
for (ModLoaderType modLoaderType : dataItem.loaders()) {
|
||||
switch (modLoaderType) {
|
||||
case FORGE:
|
||||
content.addTag(i18n("install.installer.forge"));
|
||||
break;
|
||||
case CLEANROOM:
|
||||
content.addTag(i18n("install.installer.cleanroom"));
|
||||
break;
|
||||
case NEO_FORGE:
|
||||
content.addTag(i18n("install.installer.neoforge"));
|
||||
break;
|
||||
case FABRIC:
|
||||
content.addTag(i18n("install.installer.fabric"));
|
||||
break;
|
||||
case LITE_LOADER:
|
||||
content.addTag(i18n("install.installer.liteloader"));
|
||||
break;
|
||||
case QUILT:
|
||||
content.addTag(i18n("install.installer.quilt"));
|
||||
break;
|
||||
case LEGACY_FABRIC:
|
||||
content.addTag(i18n("install.installer.legacyfabric"));
|
||||
break;
|
||||
}
|
||||
Set<String> tags = new LinkedHashSet<>();
|
||||
for (AddonLoader loader : dataItem.loaders()) {
|
||||
tags.add(I18n.translateLoaderName(loader));
|
||||
}
|
||||
content.addTags(tags);
|
||||
|
||||
descPane.getChildren().setAll(graphicPane, content);
|
||||
}
|
||||
@@ -624,7 +605,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
|
||||
return Task.allOf(queue).thenSupplyAsync(() -> {
|
||||
var dependenciesStream = dependencies.values().stream().flatMap(types -> {
|
||||
if (types.value().isEmpty()) return Stream.of();
|
||||
if (types.value().isEmpty()) return Stream.empty();
|
||||
return Stream.concat(
|
||||
Stream.of(types.key()),
|
||||
types.value().stream().sorted(Comparator.comparing(item -> item.addon.slug(), String.CASE_INSENSITIVE_ORDER)));
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.stage.FileChooser;
|
||||
@@ -27,17 +30,18 @@ import org.jackhuang.hmcl.setting.GameInstanceIconType;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class GameInstanceIconDialog extends DialogPane {
|
||||
public class GameInstanceIconDialog extends JFXDialogLayout {
|
||||
private final HMCLGameInstance gameInstance;
|
||||
private final Runnable onFinish;
|
||||
private final GameSettings.@Nullable Instance setting;
|
||||
@@ -47,10 +51,9 @@ public class GameInstanceIconDialog extends DialogPane {
|
||||
this.onFinish = onFinish;
|
||||
this.setting = gameInstance.getSettingsOrCreate();
|
||||
|
||||
setTitle(i18n("settings.icon"));
|
||||
FlowPane pane = new FlowPane();
|
||||
setBody(pane);
|
||||
setHeading(new Label(i18n("settings.icon")));
|
||||
|
||||
FlowPane pane = new FlowPane();
|
||||
pane.getChildren().setAll(
|
||||
createCustomIcon(),
|
||||
createIcon(GameInstanceIconType.GRASS),
|
||||
@@ -68,6 +71,14 @@ public class GameInstanceIconDialog extends DialogPane {
|
||||
createIcon(GameInstanceIconType.FURNACE),
|
||||
createIcon(GameInstanceIconType.QUILT)
|
||||
);
|
||||
setBody(pane);
|
||||
|
||||
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
||||
cancelButton.getStyleClass().add("dialog-cancel");
|
||||
cancelButton.setOnAction(event -> fireEvent(new DialogCloseEvent()));
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
|
||||
setActions(cancelButton);
|
||||
}
|
||||
|
||||
private void exploreIcon() {
|
||||
@@ -114,10 +125,9 @@ public class GameInstanceIconDialog extends DialogPane {
|
||||
return container;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAccept() {
|
||||
// Icon file / settings.iconProperty updates already invalidate iconImageProperty.
|
||||
onFinish.run();
|
||||
super.onAccept();
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}
|
||||
}
|
||||
@@ -207,6 +207,14 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator
|
||||
FXUtils.openFolder(gameInstance.getRunDirectory().resolve(sub));
|
||||
}
|
||||
|
||||
private void onBrowseSchematics() {
|
||||
HMCLGameInstance gameInstance = requireGameInstance();
|
||||
if (gameInstance == null) {
|
||||
return;
|
||||
}
|
||||
FXUtils.openFolder(gameInstance.getSchematicsDirectory());
|
||||
}
|
||||
|
||||
private void redownloadAssetIndex() {
|
||||
HMCLGameInstance gameInstance = requireGameInstance();
|
||||
if (gameInstance != null) {
|
||||
@@ -358,7 +366,7 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator
|
||||
new IconedMenuItem(SVG.EXTENSION, i18n("folder.mod"), () -> control.onBrowse("mods"), browsePopup),
|
||||
new IconedMenuItem(SVG.TEXTURE, i18n("folder.resourcepacks"), () -> control.onBrowse("resourcepacks"), browsePopup),
|
||||
new IconedMenuItem(SVG.PUBLIC, i18n("folder.saves"), () -> control.onBrowse("saves"), browsePopup),
|
||||
new IconedMenuItem(SVG.SCHEMA, i18n("folder.schematics"), () -> control.onBrowse("schematics"), browsePopup),
|
||||
new IconedMenuItem(SVG.SCHEMA, i18n("folder.schematics"), control::onBrowseSchematics, browsePopup),
|
||||
new IconedMenuItem(SVG.WB_SUNNY, i18n("folder.shaderpacks"), () -> control.onBrowse("shaderpacks"), browsePopup),
|
||||
new IconedMenuItem(SVG.SCREENSHOT_MONITOR, i18n("folder.screenshots"), () -> control.onBrowse("screenshots"), browsePopup),
|
||||
new IconedMenuItem(SVG.SETTINGS, i18n("folder.config"), () -> control.onBrowse("config"), browsePopup),
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.beans.binding.Bindings;
|
||||
@@ -38,10 +39,7 @@ import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
@@ -49,10 +47,7 @@ import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.directory.GameDirectoryListItem;
|
||||
@@ -84,6 +79,16 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
gameDirectoryListItems = MappedObservableList.create(GameDirectoryManager.getGameDirectories(), gameDirectory -> {
|
||||
GameDirectoryListItem item = new GameDirectoryListItem(gameDirectory);
|
||||
FXUtils.setLimitWidth(item, 200);
|
||||
FXUtils.onSecondaryButtonClicked(item, () -> {
|
||||
PopupMenu menu = new PopupMenu();
|
||||
JFXPopup popup = new JFXPopup(menu);
|
||||
menu.getContent().add(new IconedMenuItem(
|
||||
SVG.EDIT,
|
||||
i18n("button.edit"),
|
||||
() -> Controllers.navigate(new GameDirectoryPage(gameDirectory)),
|
||||
popup));
|
||||
popup.show(item, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, item.getWidth(), 0);
|
||||
});
|
||||
return item;
|
||||
});
|
||||
|
||||
|
||||
@@ -73,7 +73,9 @@ public final class GameListPopupMenu extends StackPane {
|
||||
public static void show(Node owner, JFXPopup.PopupVPosition vAlign, JFXPopup.PopupHPosition hAlign,
|
||||
double initOffsetX, double initOffsetY,
|
||||
List<HMCLGameInstance> instances) {
|
||||
showAndGetPopup(owner, vAlign, hAlign, initOffsetX, initOffsetY, instances);
|
||||
if (!hideShowing(owner)) {
|
||||
showAndGetPopup(owner, vAlign, hAlign, initOffsetX, initOffsetY, instances);
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows and returns an instance selection popup relative to its owner.
|
||||
@@ -117,9 +119,7 @@ public final class GameListPopupMenu extends StackPane {
|
||||
Label placeholder = new Label(i18n("instance.empty"));
|
||||
placeholder.setStyle("-fx-padding: 10px; -fx-text-fill: -monet-on-surface-variant; -fx-font-style: italic;");
|
||||
|
||||
FXUtils.onChangeAndOperate(isEmpty, empty -> {
|
||||
getChildren().setAll(empty ? placeholder : listView);
|
||||
});
|
||||
FXUtils.onChangeAndOperate(isEmpty, empty -> getChildren().setAll(empty ? placeholder : listView));
|
||||
}
|
||||
|
||||
public ObservableList<GameItem> getItems() {
|
||||
|
||||
+22
-34
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.game.LocalizedRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
@@ -32,31 +34,34 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
|
||||
public static DownloadListPage ofMod(DownloadPage.DownloadCallback callback, boolean instanceSelection) {
|
||||
return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.MOD, CurseForgeRemoteAddonRepository.MODS, ModrinthRemoteAddonRepository.MODS);
|
||||
return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.MOD, true, true);
|
||||
}
|
||||
|
||||
public static DownloadListPage ofModrinthMod(DownloadPage.DownloadCallback callback, boolean instanceSelection) {
|
||||
return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.MOD, false, true);
|
||||
}
|
||||
|
||||
public static DownloadListPage ofModPack(DownloadPage.DownloadCallback callback, boolean instanceSelection) {
|
||||
return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.MODPACK, CurseForgeRemoteAddonRepository.MODPACKS, ModrinthRemoteAddonRepository.MODPACKS);
|
||||
return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.MODPACK, true, true);
|
||||
}
|
||||
|
||||
public static DownloadListPage ofResourcePack(DownloadPage.DownloadCallback callback, boolean instanceSelection) {
|
||||
return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.RESOURCE_PACK, CurseForgeRemoteAddonRepository.RESOURCE_PACKS, ModrinthRemoteAddonRepository.RESOURCE_PACKS);
|
||||
return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.RESOURCE_PACK, true, true);
|
||||
}
|
||||
|
||||
public static DownloadListPage ofShaderPack(DownloadPage.DownloadCallback callback, boolean instanceSelection) {
|
||||
var page = new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.SHADER_PACK, CurseForgeRemoteAddonRepository.SHADERS, ModrinthRemoteAddonRepository.SHADER_PACKS);
|
||||
var page = new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.SHADER_PACK, true, true);
|
||||
page.supportChinese.set(false);
|
||||
return page;
|
||||
}
|
||||
|
||||
private HMCLLocalizedDownloadListPage(DownloadPage.DownloadCallback callback, boolean instanceSelection, RemoteAddon.Type type, CurseForgeRemoteAddonRepository curseForge, ModrinthRemoteAddonRepository modrinth) {
|
||||
super(null, callback, instanceSelection);
|
||||
|
||||
repository = new Repository(type, curseForge, modrinth);
|
||||
private HMCLLocalizedDownloadListPage(DownloadPage.DownloadCallback callback, boolean instanceSelection, RemoteAddon.Type type, boolean curseForge, boolean modrinth) {
|
||||
super(type, new Repository(), callback, instanceSelection);
|
||||
((Repository) repository).downloadSource.bind(downloadSource);
|
||||
|
||||
supportChinese.set(true);
|
||||
|
||||
boolean supportedCurseForge = CurseForgeRemoteAddonRepository.isAvailable() && curseForge != null;
|
||||
boolean supportedCurseForge = curseForge && CurseForgeRemoteAddonRepository.isAvailable();
|
||||
|
||||
downloadSources.setAll("addon.modrinth");
|
||||
if (supportedCurseForge) {
|
||||
@@ -66,13 +71,13 @@ public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
|
||||
if ("curseforge".equalsIgnoreCase(settings().defaultAddonSourceProperty().get())) {
|
||||
if (supportedCurseForge) {
|
||||
downloadSource.set("addon.curseforge");
|
||||
} else if (modrinth != null) {
|
||||
} else if (modrinth) {
|
||||
downloadSource.set("addon.modrinth");
|
||||
} else {
|
||||
throw new AssertionError("Should not be here.");
|
||||
}
|
||||
} else {
|
||||
if (modrinth != null) {
|
||||
if (modrinth) {
|
||||
downloadSource.set("addon.modrinth");
|
||||
} else if (supportedCurseForge) {
|
||||
downloadSource.set("addon.curseforge");
|
||||
@@ -82,39 +87,22 @@ public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
|
||||
}
|
||||
}
|
||||
|
||||
private class Repository extends LocalizedRemoteAddonRepository {
|
||||
private final RemoteAddon.Type type;
|
||||
private final CurseForgeRemoteAddonRepository curseForge;
|
||||
private final ModrinthRemoteAddonRepository modrinth;
|
||||
private static class Repository extends LocalizedRemoteAddonRepository {
|
||||
|
||||
public Repository(RemoteAddon.Type type, CurseForgeRemoteAddonRepository curseForge, ModrinthRemoteAddonRepository modrinth) {
|
||||
this.type = type;
|
||||
this.curseForge = curseForge;
|
||||
this.modrinth = modrinth;
|
||||
public Repository() {
|
||||
}
|
||||
|
||||
public final StringProperty downloadSource = new SimpleStringProperty();
|
||||
|
||||
@Override
|
||||
protected RemoteAddonRepository getBackedRemoteModRepository() {
|
||||
if ("addon.modrinth".equals(downloadSource.get())) {
|
||||
return modrinth;
|
||||
return ModrinthRemoteAddonRepository.getInstance();
|
||||
} else {
|
||||
return curseForge;
|
||||
return CurseForgeRemoteAddonRepository.getInstance();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SortType getBackedRemoteModRepositorySortOrder() {
|
||||
if ("addon.modrinth".equals(downloadSource.get())) {
|
||||
return SortType.NAME;
|
||||
} else {
|
||||
return SortType.POPULARITY;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteAddon.Type getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -66,8 +66,9 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
|
||||
return new InstallerListPageSkin();
|
||||
}
|
||||
|
||||
/// Rebuilds the component controls for the selected instance, or clears them if none is selected.
|
||||
public void loadInstance(HMCLGameInstance.Optional instance) {
|
||||
HMCLGameInstance gameInstance = instance.instance();
|
||||
@Nullable HMCLGameInstance gameInstance = instance.instance();
|
||||
this.gameInstance = gameInstance;
|
||||
if (gameInstance == null) {
|
||||
itemsProperty().clear();
|
||||
@@ -103,7 +104,7 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
|
||||
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(gameInstance, component.getComponentType(), libraryVersion));
|
||||
});
|
||||
|
||||
component.setOnRemove(() -> repository.updateInstanceAsync(
|
||||
component.setOnRemove(() -> repository.getDependency().updateInstanceAsync(
|
||||
gameInstance.getId(),
|
||||
publishedInstance -> repository.getDependency().removeComponentAsync(
|
||||
publishedInstance,
|
||||
@@ -123,7 +124,7 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
|
||||
|
||||
InstallerItem installerItem = new InstallerItem(mark.componentType(), InstallerItem.Style.LIST_ITEM);
|
||||
installerItem.versionProperty().set(new InstallerItem.InstalledState(mark.version(), false, false));
|
||||
installerItem.setOnRemove(() -> repository.updateInstanceAsync(
|
||||
installerItem.setOnRemove(() -> repository.getDependency().updateInstanceAsync(
|
||||
gameInstance.getId(),
|
||||
publishedInstance -> repository.getDependency().removeComponentAsync(
|
||||
publishedInstance,
|
||||
@@ -148,13 +149,14 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
|
||||
if (file != null) doInstallOffline(file);
|
||||
}
|
||||
|
||||
/// Runs a local component installer for the current modifiable instance and displays its result.
|
||||
private void doInstallOffline(Path file) {
|
||||
if (gameInstance == null || !gameInstance.getManifest().isModifiable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
HMCLGameRepository repository = gameInstance.getRepository();
|
||||
Task<?> task = repository.updateInstanceAsync(
|
||||
Task<?> task = repository.getDependency().updateInstanceAsync(
|
||||
gameInstance.getId(),
|
||||
publishedInstance -> repository.getDependency().installComponentLocalAsync(publishedInstance, file));
|
||||
task.setName(i18n("install.installer.install_offline"));
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DownloadCandidates;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameDownloadTask;
|
||||
@@ -46,15 +47,14 @@ import org.jackhuang.hmcl.ui.export.ExportWizardProvider;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -81,9 +81,9 @@ public final class Instances {
|
||||
|
||||
public static void downloadModpackImpl(DownloadProvider downloadProvider, HMCLGameRepository repository, GameInstanceID instanceId, RemoteAddon mod, RemoteAddon.Version file) {
|
||||
Path modpack;
|
||||
List<URI> downloadURLs;
|
||||
DownloadCandidates downloadURLs;
|
||||
try {
|
||||
downloadURLs = downloadProvider.injectURLWithCandidates(file.file().url());
|
||||
downloadURLs = downloadProvider.getDownloadCandidates(file.file().url());
|
||||
modpack = Files.createTempFile("modpack", ".zip");
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
Controllers.dialog(
|
||||
@@ -235,7 +235,7 @@ public final class Instances {
|
||||
repository.setSelectedInstance(repository.getInstance(instanceId));
|
||||
} else {
|
||||
Controllers.dialog(
|
||||
DownloadProviders.localizeErrorMessage(exception), i18n("install.failed"), MessageDialogPane.MessageType.ERROR);
|
||||
I18n.localizeErrorMessage(exception), i18n("install.failed"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
}), i18n("install.new_game"), TaskCancellationAction.NORMAL);
|
||||
}, FileUtils.getNameWithoutExtension(file), new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidInstanceId), new Validator(i18n("install.new_game.already_exists"), newVersionName -> !repository.instanceIdConflicts(newVersionName)));
|
||||
|
||||
@@ -23,7 +23,6 @@ import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.DoubleBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ListChangeListener;
|
||||
@@ -41,6 +40,7 @@ import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.addon.AddonLoader;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
|
||||
@@ -63,13 +63,13 @@ import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.ItemPropertyAsyncCache;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
@@ -95,7 +95,6 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
|
||||
private ModManager modManager;
|
||||
private @Nullable HMCLGameInstance gameInstance;
|
||||
private String gameVersion;
|
||||
|
||||
final EnumSet<ModLoaderType> supportedLoaders = EnumSet.noneOf(ModLoaderType.class);
|
||||
|
||||
@@ -137,8 +136,6 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
return;
|
||||
}
|
||||
|
||||
this.gameVersion = gameInstance.getVersion().toString();
|
||||
|
||||
loadMods(gameInstance.getModManager());
|
||||
}
|
||||
|
||||
@@ -164,7 +161,8 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
return;
|
||||
}
|
||||
|
||||
updateSupportedLoaders(modManager);
|
||||
supportedLoaders.clear();
|
||||
supportedLoaders.addAll(modManager.getSupportedLoaders());
|
||||
|
||||
if (exception == null) {
|
||||
getItems().setAll(list);
|
||||
@@ -176,51 +174,6 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
}, Schedulers.javafx());
|
||||
}
|
||||
|
||||
private void updateSupportedLoaders(ModManager modManager) {
|
||||
supportedLoaders.clear();
|
||||
|
||||
GameComponentAnalyzer analyzer = modManager.getComponentAnalyzer();
|
||||
if (analyzer == null) {
|
||||
Collections.addAll(supportedLoaders, ModLoaderType.values());
|
||||
return;
|
||||
}
|
||||
|
||||
for (GameComponentType type : GameComponentType.MOD_LOADERS) {
|
||||
if (type.isModLoader() && analyzer.has(type)) {
|
||||
ModLoaderType modLoaderType = type.getModLoaderType();
|
||||
if (modLoaderType != null) {
|
||||
supportedLoaders.add(modLoaderType);
|
||||
|
||||
if (modLoaderType == ModLoaderType.CLEANROOM)
|
||||
supportedLoaders.add(ModLoaderType.FORGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (analyzer.has(GameComponentType.NEO_FORGE) && "1.20.1".equals(gameVersion)) {
|
||||
supportedLoaders.add(ModLoaderType.FORGE);
|
||||
}
|
||||
|
||||
if (analyzer.has(GameComponentType.QUILT)) {
|
||||
supportedLoaders.add(ModLoaderType.FABRIC);
|
||||
}
|
||||
|
||||
if (analyzer.has(GameComponentType.LEGACY_FABRIC)) {
|
||||
supportedLoaders.add(ModLoaderType.FABRIC);
|
||||
}
|
||||
|
||||
if (analyzer.has(GameComponentType.FABRIC) && modManager.hasMod("kilt", ModLoaderType.FABRIC)) {
|
||||
supportedLoaders.add(ModLoaderType.FORGE);
|
||||
supportedLoaders.add(ModLoaderType.NEO_FORGE);
|
||||
}
|
||||
|
||||
// Sinytra Connector
|
||||
if (analyzer.has(GameComponentType.NEO_FORGE) && (modManager.hasMod("connector", ModLoaderType.NEO_FORGE) || modManager.hasMod("connectormod", ModLoaderType.NEO_FORGE))
|
||||
|| "1.20.1".equals(gameVersion) && analyzer.has(GameComponentType.FORGE) && modManager.hasMod("connectormod", ModLoaderType.FORGE)) {
|
||||
supportedLoaders.add(ModLoaderType.FABRIC);
|
||||
}
|
||||
}
|
||||
|
||||
public void add() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("mods.add.title"));
|
||||
@@ -598,13 +551,15 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
private final LocalModFile localModFile;
|
||||
private final @Nullable ModTranslations.Mod modTranslations;
|
||||
|
||||
private SoftReference<CompletableFuture<Image>> iconCache;
|
||||
private final ItemPropertyAsyncCache<Image, ModInfoObject> iconCache;
|
||||
|
||||
ModInfoObject(LocalModFile localModFile) {
|
||||
this.localModFile = localModFile;
|
||||
this.active = localModFile.activeProperty();
|
||||
|
||||
this.modTranslations = ModTranslations.MOD.getMod(localModFile.getId(), localModFile.getName());
|
||||
|
||||
this.iconCache = new ItemPropertyAsyncCache.Soft<>(this, this::loadIcon, this::getDefaultIcon);
|
||||
}
|
||||
|
||||
public LocalModFile getModInfo() {
|
||||
@@ -615,7 +570,10 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
return modTranslations;
|
||||
}
|
||||
|
||||
@FXThread
|
||||
private Image getDefaultIcon() {
|
||||
return GameInstanceIconType.getIconType(this.localModFile.getModLoaderType()).getIcon();
|
||||
}
|
||||
|
||||
private Image loadIcon() {
|
||||
List<String> iconPaths = new ArrayList<>();
|
||||
|
||||
@@ -638,34 +596,7 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
LOG.warning("Failed to load mod icons", e);
|
||||
}
|
||||
|
||||
return GameInstanceIconType.getIconType(this.localModFile.getModLoaderType()).getIcon();
|
||||
}
|
||||
|
||||
public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference<ObjectProperty<ModInfoObject>> current) {
|
||||
SoftReference<CompletableFuture<Image>> iconCache = this.iconCache;
|
||||
CompletableFuture<Image> imageFuture;
|
||||
if (iconCache != null && (imageFuture = iconCache.get()) != null) {
|
||||
Image image = imageFuture.getNow(null);
|
||||
if (image != null) {
|
||||
imageContainer.setImage(image);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
imageFuture = CompletableFuture.supplyAsync(this::loadIcon, Schedulers.io());
|
||||
this.iconCache = new SoftReference<>(imageFuture);
|
||||
}
|
||||
imageContainer.setImage(GameInstanceIconType.getIconType(localModFile.getModLoaderType()).getIcon());
|
||||
imageFuture.thenAcceptAsync(image -> {
|
||||
if (current != null) {
|
||||
ObjectProperty<ModInfoObject> infoObjectProperty = current.get();
|
||||
if (infoObjectProperty == null || infoObjectProperty.get() != this) {
|
||||
// The current ListCell has already switched to another object
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
imageContainer.setImage(image);
|
||||
}, Schedulers.javafx());
|
||||
return getDefaultIcon();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -682,9 +613,10 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
|
||||
var imageContainer = new ImageContainer(40);
|
||||
titleContainer.setAlignment(Pos.CENTER_LEFT);
|
||||
modInfo.loadIcon(imageContainer, null);
|
||||
modInfo.iconCache.attachValue(imageContainer.imageProperty(), null);
|
||||
|
||||
TwoLineListItem title = new TwoLineListItem();
|
||||
title.getTitleLabel().setWrapText(true);
|
||||
if (modInfo.getModTranslations() != null && I18n.isUseChinese())
|
||||
title.setTitle(modInfo.getModTranslations().getDisplayName());
|
||||
else
|
||||
@@ -725,8 +657,8 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getId())) {
|
||||
for (Pair<String, ? extends RemoteAddonRepository> item : Arrays.asList(
|
||||
pair("addon.curseforge", CurseForgeRemoteAddonRepository.MODS),
|
||||
pair("addon.modrinth", ModrinthRemoteAddonRepository.MODS)
|
||||
pair("addon.curseforge", CurseForgeRemoteAddonRepository.getInstance()),
|
||||
pair("addon.modrinth", ModrinthRemoteAddonRepository.getInstance())
|
||||
)) {
|
||||
RemoteAddonRepository repository = item.getValue();
|
||||
JFXHyperlink button = new JFXHyperlink(i18n(item.getKey()));
|
||||
@@ -735,25 +667,12 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
if (versionOptional.isPresent()) {
|
||||
RemoteAddon remoteAddon = repository.getAddonById(DownloadProviders.getDownloadProvider(), versionOptional.get().projectId());
|
||||
FXUtils.runInFX(() -> {
|
||||
for (ModLoaderType modLoaderType : versionOptional.get().loaders()) {
|
||||
String loaderName = switch (modLoaderType) {
|
||||
case FORGE -> i18n("install.installer.forge");
|
||||
case CLEANROOM -> i18n("install.installer.cleanroom");
|
||||
case LEGACY_FABRIC -> i18n("install.installer.legacyfabric");
|
||||
case NEO_FORGE -> i18n("install.installer.neoforge");
|
||||
case FABRIC -> i18n("install.installer.fabric");
|
||||
case LITE_LOADER -> i18n("install.installer.liteloader");
|
||||
case QUILT -> i18n("install.installer.quilt");
|
||||
default -> null;
|
||||
};
|
||||
if (loaderName == null)
|
||||
continue;
|
||||
if (title.getTags()
|
||||
.stream()
|
||||
.noneMatch(it -> it.getText().equals(loaderName))) {
|
||||
title.addTag(loaderName);
|
||||
}
|
||||
Set<String> tags = new LinkedHashSet<>();
|
||||
for (AddonLoader loader : versionOptional.get().loaders()) {
|
||||
String tag = I18n.translateLoaderName(loader);
|
||||
if (tag != null) tags.add(tag);
|
||||
}
|
||||
title.addTagsIfNotExist(tags);
|
||||
|
||||
button.setExternalLink(remoteAddon.pageUrl());
|
||||
button.setDisable(false);
|
||||
@@ -856,7 +775,7 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
|
||||
ModLoaderType modLoaderType = modInfo.getModLoaderType();
|
||||
|
||||
dataItem.loadIcon(imageContainer, new WeakReference<>(this.itemProperty()));
|
||||
dataItem.iconCache.attachValue(imageContainer.imageProperty(), new WeakReference<>(this.itemProperty()));
|
||||
|
||||
String displayName = modInfo.getName();
|
||||
if (modTranslations != null && I18n.isUseChinese()) {
|
||||
@@ -892,15 +811,7 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
content.addTagWarning(i18n("mods.unknown"));
|
||||
} else if (!page.supportedLoaders.contains(modLoaderType)) {
|
||||
warning.add(i18n("mods.warning.loader_mismatch"));
|
||||
switch (dataItem.getModInfo().getModLoaderType()) {
|
||||
case FORGE -> content.addTagWarning(i18n("install.installer.forge"));
|
||||
case LEGACY_FABRIC -> content.addTagWarning(i18n("install.installer.legacyfabric"));
|
||||
case CLEANROOM -> content.addTagWarning(i18n("install.installer.cleanroom"));
|
||||
case NEO_FORGE -> content.addTagWarning(i18n("install.installer.neoforge"));
|
||||
case FABRIC -> content.addTagWarning(i18n("install.installer.fabric"));
|
||||
case LITE_LOADER -> content.addTagWarning(i18n("install.installer.liteloader"));
|
||||
case QUILT -> content.addTagWarning(i18n("install.installer.quilt"));
|
||||
}
|
||||
content.addTagWarning(I18n.translateLoaderType(dataItem.getModInfo().getModLoaderType()));
|
||||
}
|
||||
|
||||
String modVersion = modInfo.getVersion();
|
||||
|
||||
@@ -57,14 +57,14 @@ 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.*;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.javafx.ItemPropertyAsyncCache;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.Objects;
|
||||
@@ -494,13 +494,18 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
}
|
||||
}
|
||||
|
||||
public static class ResourcePackInfoObject {
|
||||
public static final class ResourcePackInfoObject {
|
||||
|
||||
private final ResourcePackFile file;
|
||||
private final BooleanProperty enabled;
|
||||
|
||||
private final ItemPropertyAsyncCache<Image, ResourcePackInfoObject> iconCache;
|
||||
|
||||
public ResourcePackInfoObject(Pair<ResourcePackFile, Boolean> pair) {
|
||||
this.file = pair.key();
|
||||
this.enabled = new SimpleBooleanProperty(this, "enabled", pair.value());
|
||||
|
||||
this.iconCache = new ItemPropertyAsyncCache.Soft<>(this, this::loadIcon, this::getDefaultIcon);
|
||||
}
|
||||
|
||||
public ResourcePackFile getFile() {
|
||||
@@ -511,13 +516,16 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
return enabled;
|
||||
}
|
||||
|
||||
Image getIcon() {
|
||||
Image image = file.getIcon();
|
||||
if (image == null || image.isError() || image.getWidth() <= 0 || image.getHeight() <= 0 ||
|
||||
(Math.abs(image.getWidth() - image.getHeight()) >= 1)) {
|
||||
image = FXUtils.newBuiltinImage("/assets/img/unknown_pack.png");
|
||||
private Image getDefaultIcon() {
|
||||
return FXUtils.newBuiltinImage("/assets/img/unknown_pack.png");
|
||||
}
|
||||
|
||||
private Image loadIcon() {
|
||||
Image icon = file.loadIcon();
|
||||
if (icon != null && !icon.isError() && icon.getWidth() > 0 && icon.getHeight() > 0 && Math.abs(icon.getWidth() - icon.getHeight()) < 1) {
|
||||
return icon;
|
||||
}
|
||||
return image;
|
||||
return getDefaultIcon();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,7 +591,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
if (empty || item == null) return;
|
||||
|
||||
ResourcePackFile file = item.getFile();
|
||||
imageContainer.setImage(item.getIcon());
|
||||
item.iconCache.attachValue(imageContainer.imageProperty(), new WeakReference<>(itemProperty()));
|
||||
|
||||
content.getTags().clear();
|
||||
content.setTitle(file.getFileName());
|
||||
@@ -623,7 +631,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
maxWidthProperty().bind(Controllers.getDecorator().contentWidthProperty().multiply(0.7));
|
||||
|
||||
ImageContainer imageContainer = new ImageContainer(40);
|
||||
imageContainer.setImage(packInfoObject.getIcon());
|
||||
packInfoObject.iconCache.attachValue(imageContainer.imageProperty(), null);
|
||||
|
||||
TwoLineListItem title = new TwoLineListItem();
|
||||
title.setTitle(pack.getFileName());
|
||||
@@ -654,8 +662,8 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
setBody(descriptionPane);
|
||||
|
||||
for (Pair<String, ? extends RemoteAddonRepository> item : Arrays.asList(
|
||||
pair("addon.curseforge", CurseForgeRemoteAddonRepository.RESOURCE_PACKS),
|
||||
pair("addon.modrinth", ModrinthRemoteAddonRepository.RESOURCE_PACKS)
|
||||
pair("addon.curseforge", CurseForgeRemoteAddonRepository.getInstance()),
|
||||
pair("addon.modrinth", ModrinthRemoteAddonRepository.getInstance())
|
||||
)) {
|
||||
RemoteAddonRepository repository = item.getValue();
|
||||
JFXHyperlink button = new JFXHyperlink(i18n(item.getKey()));
|
||||
|
||||
@@ -20,32 +20,48 @@ package org.jackhuang.hmcl.ui.instances;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import javafx.scene.image.WritableImage;
|
||||
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 javafx.scene.text.Text;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.mod.LocalMod;
|
||||
import org.jackhuang.hmcl.addon.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.game.GameComponentType;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.schematic.LitematicFile;
|
||||
import org.jackhuang.hmcl.schematic.Schematic;
|
||||
import org.jackhuang.hmcl.schematic.SchematicType;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.FileNameSet;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.ui.nbt.NBTEditorPage;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.SynchronizedExceptionalLazy;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -55,10 +71,12 @@ import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.*;
|
||||
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;
|
||||
|
||||
@@ -67,6 +85,23 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
*/
|
||||
public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
|
||||
private static final SynchronizedExceptionalLazy<RemoteAddon> litematicaLazy = new SynchronizedExceptionalLazy<>(() -> {
|
||||
try {
|
||||
return ModrinthRemoteAddonRepository.getInstance().getAddonById(DownloadProviders.getDownloadProvider(), "litematica");
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to fetch litematica", e);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
private static final SynchronizedExceptionalLazy<RemoteAddon> forgematicaLazy = new SynchronizedExceptionalLazy<>(() -> {
|
||||
try {
|
||||
return ModrinthRemoteAddonRepository.getInstance().getAddonById(DownloadProviders.getDownloadProvider(), "forgematica");
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to fetch forgematica", e);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
private static String translateAuthorName(String author) {
|
||||
if (I18n.isUseChinese() && "hsds".equals(author)) {
|
||||
return "黑山大叔";
|
||||
@@ -74,9 +109,22 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
return author;
|
||||
}
|
||||
|
||||
private static String translateType(SchematicType type) {
|
||||
return switch (type) {
|
||||
case SCHEM -> i18n("schematics.info.type.schem");
|
||||
case NBT_STRUCTURE -> i18n("schematics.info.type.nbt_structure");
|
||||
case LITEMATIC -> i18n("schematics.info.type.litematic");
|
||||
};
|
||||
}
|
||||
|
||||
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
|
||||
|
||||
private @Nullable HMCLGameInstance gameInstance;
|
||||
private Path schematicsDirectory;
|
||||
private DirItem currentDirectory;
|
||||
|
||||
private final ObjectProperty<DirItem> currentDirectory = new SimpleObjectProperty<>(this, "currentDirectory", null);
|
||||
private final BooleanProperty isRootProperty = new SimpleBooleanProperty(this, "isRoot", true);
|
||||
private final ObjectProperty<RemoteAddon> downloadTarget = new SimpleObjectProperty<>(this, "downloadTarget");
|
||||
|
||||
/// Creates a schematics list that reloads when `instanceContext` changes.
|
||||
///
|
||||
@@ -84,7 +132,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
public SchematicsPage(ObservableValue<? extends HMCLGameInstance.Optional> instanceContext) {
|
||||
Objects.requireNonNull(instanceContext, "instanceContext");
|
||||
FXUtils.applyDragListener(this,
|
||||
file -> currentDirectory != null && Files.isRegularFile(file) && FileUtils.getName(file).endsWith(".litematic"),
|
||||
file -> currentDirectoryProperty().get() != null && Schematic.isFileSchematic(file),
|
||||
this::addFiles
|
||||
);
|
||||
|
||||
@@ -97,53 +145,120 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new SchematicsPageSkin();
|
||||
return new SchematicsPageSkin(this);
|
||||
}
|
||||
|
||||
public void loadInstance(HMCLGameInstance.Optional instance) {
|
||||
HMCLGameInstance gameInstance = instance.instance();
|
||||
this.schematicsDirectory = gameInstance != null ? gameInstance.getSchematicsDirectory() : null;
|
||||
this.gameInstance = instance.instance();
|
||||
|
||||
if (gameInstance == null) {
|
||||
this.schematicsDirectory = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.schematicsDirectory = gameInstance.getSchematicsDirectory();
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
private ObjectProperty<DirItem> currentDirectoryProperty() {
|
||||
return currentDirectory;
|
||||
}
|
||||
|
||||
public BooleanProperty isRootProperty() {
|
||||
return isRootProperty;
|
||||
}
|
||||
|
||||
public void navigateBack() {
|
||||
var d = currentDirectoryProperty().get();
|
||||
if (d == null) return;
|
||||
if (d.parent != null) navigateTo(d.parent);
|
||||
}
|
||||
|
||||
public void downloadLitematica() {
|
||||
if (downloadTarget.get() != null && gameInstance != null) {
|
||||
var modDownloads = HMCLLocalizedDownloadListPage.ofModrinthMod(org.jackhuang.hmcl.ui.download.DownloadPage.FOR_MOD, false);
|
||||
modDownloads.loadInstance(HMCLGameInstance.Optional.of(gameInstance));
|
||||
Controllers.navigate(new DownloadPage(
|
||||
modDownloads,
|
||||
downloadTarget.get(),
|
||||
modDownloads.getInstanceOptional(),
|
||||
modDownloads.getCallback())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
Path schematicsDirectory = this.schematicsDirectory;
|
||||
if (schematicsDirectory == null) return;
|
||||
HMCLGameInstance currentGameInstance = this.gameInstance;
|
||||
if (currentGameInstance == null) return;
|
||||
|
||||
setLoading(true);
|
||||
Task.supplyAsync(() -> loadAll(schematicsDirectory, null))
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
setLoading(false);
|
||||
if (exception == null) {
|
||||
DirItem target = result;
|
||||
if (currentDirectory != null) {
|
||||
loop:
|
||||
for (int i = 0; i < currentDirectory.relativePath.size(); i++) {
|
||||
String dirName = currentDirectory.relativePath.get(i);
|
||||
|
||||
for (Item child : target.children) {
|
||||
if (child instanceof DirItem && child.getName().equals(dirName)) {
|
||||
target = (DirItem) child;
|
||||
continue loop;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
navigateTo(target);
|
||||
} else {
|
||||
LOG.warning("Failed to load schematics", exception);
|
||||
DirItem currentDir = currentDirectoryProperty().get();
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
DirItem target = loadRoot(schematicsDirectory);
|
||||
if (currentDir != null) {
|
||||
loop:
|
||||
for (String dirName : currentDir.relativePath) {
|
||||
try {
|
||||
target.preLoad();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to preload sub-directories in " + target, e);
|
||||
break;
|
||||
}
|
||||
}).start();
|
||||
for (var dirChild : target.dirChildren) {
|
||||
if (dirChild.getName().equals(dirName)) {
|
||||
target = dirChild;
|
||||
continue loop;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (!Objects.equals(currentGameInstance, this.gameInstance)) return;
|
||||
if (exception == null) {
|
||||
navigateTo(result);
|
||||
} else {
|
||||
LOG.warning("Failed to load schematics", exception);
|
||||
}
|
||||
setLoading(false);
|
||||
}).start();
|
||||
|
||||
downloadTarget.set(null);
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
var modManager = currentGameInstance.getModManager();
|
||||
modManager.analyze();
|
||||
var analyzer = modManager.getComponentAnalyzer();
|
||||
if (analyzer == null) return null;
|
||||
var modIds = modManager.getLocalFiles().stream()
|
||||
.map(LocalModFile::getMod)
|
||||
.map(LocalMod::getId)
|
||||
.collect(Collectors.toSet());
|
||||
if (modIds.contains("litematica") || modIds.contains("forgematica")) return null;
|
||||
var gameVersionNumber = GameVersionNumber.asGameVersion(modManager.getGameVersion());
|
||||
if (!gameVersionNumber.isAtLeast("1.12", "17w31a"))
|
||||
return null;
|
||||
if ((analyzer.has(GameComponentType.FORGE) || analyzer.has(GameComponentType.NEO_FORGE))
|
||||
&& gameVersionNumber.isAtLeast("1.16.4", "20w45a"))
|
||||
return forgematicaLazy.get();
|
||||
return litematicaLazy.get();
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (!Objects.equals(currentGameInstance, gameInstance)) return;
|
||||
if (exception == null) {
|
||||
downloadTarget.set(result);
|
||||
} else {
|
||||
LOG.warning("Failed to fetch litematica", exception);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
public void addFiles(List<Path> files) {
|
||||
if (currentDirectory == null)
|
||||
if (currentDirectoryProperty().get() == null)
|
||||
return;
|
||||
|
||||
Path dir = currentDirectory.path;
|
||||
Path dir = currentDirectoryProperty().get().getPath();
|
||||
try {
|
||||
// Can be executed in the background, but be careful that users can call loadVersion during this time
|
||||
Files.createDirectories(dir);
|
||||
@@ -162,7 +277,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(i18n("schematics.add.title"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(
|
||||
i18n("extension.schematic"), "*.litematic"));
|
||||
i18n("extension.schematic"), "*.litematic", "*.nbt", "*.schematic", "*.schem"));
|
||||
List<Path> files = Controllers.showOpenMultipleDialog(fileChooser);
|
||||
if (files != null && !files.isEmpty()) {
|
||||
addFiles(files);
|
||||
@@ -170,10 +285,9 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
}
|
||||
|
||||
public void onCreateDirectory() {
|
||||
if (currentDirectory == null)
|
||||
return;
|
||||
if (currentDirectoryProperty().get() == null) return;
|
||||
|
||||
Path parent = currentDirectory.path;
|
||||
Path parent = currentDirectoryProperty().get().getPath();
|
||||
|
||||
FileNameSet existingPaths;
|
||||
try {
|
||||
@@ -208,37 +322,29 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
new Validator(i18n("schematics.create_directory.failed.already_exists"), existingPaths::notContains));
|
||||
}
|
||||
|
||||
private DirItem loadAll(Path dir, @Nullable DirItem parent) {
|
||||
DirItem item = new DirItem(dir, parent);
|
||||
public void onRevealSchematicsFolder() {
|
||||
var d = currentDirectoryProperty().get();
|
||||
var p = d != null ? d.getPath() : schematicsDirectory;
|
||||
if (p != null) FXUtils.openFolder(p);
|
||||
}
|
||||
|
||||
try (Stream<Path> stream = Files.list(dir)) {
|
||||
for (Path path : Lang.toIterable(stream)) {
|
||||
if (Files.isDirectory(path)) {
|
||||
item.children.add(loadAll(path, item));
|
||||
} else if (path.getFileName().toString().endsWith(".litematic") && Files.isRegularFile(path)) {
|
||||
try {
|
||||
item.children.add(new LitematicFileItem(LitematicFile.load(path)));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to load litematic file: " + path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (NoSuchFileException ignored) {
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to load schematics in " + dir, e);
|
||||
}
|
||||
|
||||
item.children.sort(Comparator.naturalOrder());
|
||||
private DirItem loadRoot(Path dir) {
|
||||
var item = new DirItem(dir, null);
|
||||
item.load();
|
||||
return item;
|
||||
}
|
||||
|
||||
private void navigateTo(DirItem item) {
|
||||
currentDirectory = item;
|
||||
getItems().clear();
|
||||
if (item.parent != null) {
|
||||
getItems().add(new BackItem(item.parent));
|
||||
}
|
||||
getItems().addAll(item.children);
|
||||
if (currentDirectoryProperty().get() == item) return;
|
||||
currentDirectoryProperty().set(item);
|
||||
isRootProperty().set(item.parent == null);
|
||||
setLoading(true);
|
||||
Task.runAsync(item::load).whenComplete(Schedulers.javafx(), exception -> {
|
||||
if (currentDirectoryProperty().get() == item) {
|
||||
getItems().setAll(item.children);
|
||||
setLoading(false);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
abstract sealed class Item implements Comparable<Item> {
|
||||
@@ -280,12 +386,28 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
}
|
||||
}
|
||||
|
||||
private final class BackItem extends Item {
|
||||
private final class DirItem extends Item {
|
||||
final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
private final DirItem parent;
|
||||
final Path path;
|
||||
final @Nullable DirItem parent;
|
||||
final List<Item> children = new ArrayList<>();
|
||||
final List<DirItem> dirChildren = new ArrayList<>();
|
||||
final List<String> relativePath;
|
||||
int size = 0;
|
||||
boolean preLoaded = false;
|
||||
boolean loaded = false;
|
||||
|
||||
BackItem(DirItem parent) {
|
||||
DirItem(Path path, @Nullable DirItem parent) {
|
||||
this.path = path;
|
||||
this.parent = parent;
|
||||
|
||||
if (parent != null) {
|
||||
this.relativePath = new ArrayList<>(parent.relativePath);
|
||||
relativePath.add(FileUtils.getName(path));
|
||||
} else {
|
||||
this.relativePath = Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -293,65 +415,6 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
Path getPath() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
String getName() {
|
||||
return "..";
|
||||
}
|
||||
|
||||
@Override
|
||||
String getDescription() {
|
||||
return i18n("schematics.back_to", parent.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
SVG getIcon() {
|
||||
return SVG.FOLDER;
|
||||
}
|
||||
|
||||
@Override
|
||||
void onClick() {
|
||||
navigateTo(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
void onReveal() {
|
||||
throw new UnsupportedOperationException("Unreachable");
|
||||
}
|
||||
|
||||
@Override
|
||||
void onDelete() {
|
||||
throw new UnsupportedOperationException("Unreachable");
|
||||
}
|
||||
}
|
||||
|
||||
private final class DirItem extends Item {
|
||||
final Path path;
|
||||
final @Nullable DirItem parent;
|
||||
final List<Item> children = new ArrayList<>();
|
||||
final List<String> relativePath;
|
||||
|
||||
DirItem(Path path, @Nullable DirItem parent) {
|
||||
this.path = path;
|
||||
this.parent = parent;
|
||||
|
||||
if (parent != null) {
|
||||
this.relativePath = new ArrayList<>(parent.relativePath);
|
||||
relativePath.add(path.getFileName().toString());
|
||||
} else {
|
||||
this.relativePath = Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
int order() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
Path getPath() {
|
||||
return path;
|
||||
@@ -359,12 +422,12 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return path.getFileName().toString();
|
||||
return FileUtils.getName(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getDescription() {
|
||||
return i18n("schematics.sub_items", children.size());
|
||||
return i18n("schematics.sub_items", size);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -372,6 +435,66 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
return SVG.FOLDER;
|
||||
}
|
||||
|
||||
void preLoad() throws IOException {
|
||||
lock.lock();
|
||||
try {
|
||||
if (this.preLoaded) return;
|
||||
this.size = 0;
|
||||
this.dirChildren.clear();
|
||||
try (Stream<Path> stream = Files.list(path)) {
|
||||
stream.forEach(p -> {
|
||||
boolean b1 = Files.isDirectory(p);
|
||||
boolean b2 = Schematic.isFileSchematic(p);
|
||||
if (b1 || b2) this.size++;
|
||||
if (b1) {
|
||||
var child = new DirItem(p, this);
|
||||
this.dirChildren.add(child);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.preLoaded = true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
void load() {
|
||||
lock.lock();
|
||||
try {
|
||||
if (this.loaded) return;
|
||||
|
||||
this.children.clear();
|
||||
|
||||
preLoad();
|
||||
for (var dir : dirChildren) {
|
||||
try {
|
||||
dir.preLoad();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to pre-load schematics in " + dir.path, e);
|
||||
}
|
||||
this.children.add(dir);
|
||||
}
|
||||
try (Stream<Path> stream = Files.list(path)) {
|
||||
stream.filter(Schematic::isFileSchematic)
|
||||
.forEach(p -> {
|
||||
try {
|
||||
this.children.add(new SchematicItem(p));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to load schematic file: " + p, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.children.sort(Comparator.naturalOrder());
|
||||
this.loaded = true;
|
||||
} catch (NoSuchFileException ignored) {
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to load schematics in " + path, e);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void onClick() {
|
||||
navigateTo(this);
|
||||
@@ -385,8 +508,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
@Override
|
||||
void onDelete() {
|
||||
try {
|
||||
FileUtils.cleanDirectory(path);
|
||||
Files.deleteIfExists(path);
|
||||
FileUtils.deleteDirectory(path);
|
||||
refresh();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to delete directory: " + path, e);
|
||||
@@ -394,19 +516,27 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
}
|
||||
}
|
||||
|
||||
private final class LitematicFileItem extends Item {
|
||||
final LitematicFile file;
|
||||
private final class SchematicItem extends Item {
|
||||
final Path path;
|
||||
final Schematic file;
|
||||
final String name;
|
||||
final Image image;
|
||||
|
||||
private LitematicFileItem(LitematicFile file) {
|
||||
this.file = file;
|
||||
private SchematicItem(Path path) throws IOException {
|
||||
this.path = path;
|
||||
this.file = Schematic.load(path);
|
||||
|
||||
String name = file.getName();
|
||||
if (name != null && !"Unnamed".equals(name)) {
|
||||
this.name = name;
|
||||
if (file == null) throw new IOException("Unsupported or deleted file: " + path);
|
||||
|
||||
if (this.file instanceof LitematicFile lFile) {
|
||||
String name = lFile.getName();
|
||||
if (StringUtils.isNotBlank(name) && !"Unnamed".equals(name)) {
|
||||
this.name = name;
|
||||
} else {
|
||||
this.name = FileUtils.getNameWithoutExtension(path);
|
||||
}
|
||||
} else {
|
||||
this.name = StringUtils.removeSuffix(file.getFile().getFileName().toString(), ".litematic");
|
||||
this.name = FileUtils.getNameWithoutExtension(path);
|
||||
}
|
||||
|
||||
WritableImage image = null;
|
||||
@@ -430,12 +560,12 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
|
||||
@Override
|
||||
int order() {
|
||||
return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
Path getPath() {
|
||||
return file.getFile();
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -445,7 +575,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
|
||||
@Override
|
||||
String getDescription() {
|
||||
return file.getFile().getFileName().toString();
|
||||
return path.getFileName().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -469,25 +599,25 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
|
||||
@Override
|
||||
void onClick() {
|
||||
Controllers.dialog(new LitematicInfoDialog());
|
||||
Controllers.dialog(new SchematicInfoDialog());
|
||||
}
|
||||
|
||||
@Override
|
||||
void onReveal() {
|
||||
FXUtils.showFileInExplorer(file.getFile());
|
||||
FXUtils.showFileInExplorer(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
void onDelete() {
|
||||
try {
|
||||
Files.deleteIfExists(file.getFile());
|
||||
Files.deleteIfExists(path);
|
||||
refresh();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to delete litematic file: " + file.getFile(), e);
|
||||
LOG.warning("Failed to delete schematic file: " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
private final class LitematicInfoDialog extends JFXDialogLayout {
|
||||
private final class SchematicInfoDialog extends JFXDialogLayout {
|
||||
private final ComponentList details;
|
||||
|
||||
private void addDetailItem(String key, Object detail) {
|
||||
@@ -497,38 +627,45 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
details.getContent().add(borderPane);
|
||||
}
|
||||
|
||||
private void updateContent(LitematicFile file) {
|
||||
private void updateContent(Schematic file) {
|
||||
details.getContent().clear();
|
||||
addDetailItem(i18n("schematics.info.name"), file.getName());
|
||||
addDetailItem(i18n("schematics.info.type"), translateType(file.getType()));
|
||||
if (StringUtils.isNotBlank(file.getAuthor()))
|
||||
addDetailItem(i18n("schematics.info.schematic_author"), translateAuthorName(file.getAuthor()));
|
||||
if (file.getTimeCreated() != null)
|
||||
addDetailItem(i18n("schematics.info.time_created"), I18n.formatDateTime(file.getTimeCreated()));
|
||||
if (file.getTimeModified() != null && !file.getTimeModified().equals(file.getTimeCreated()))
|
||||
addDetailItem(i18n("schematics.info.time_modified"), I18n.formatDateTime(file.getTimeModified()));
|
||||
if (file.getRegionCount() > 0)
|
||||
addDetailItem(i18n("schematics.info.region_count"), String.valueOf(file.getRegionCount()));
|
||||
if (file.getTotalVolume() > 0)
|
||||
addDetailItem(i18n("schematics.info.total_volume"), file.getTotalVolume());
|
||||
if (file.getTotalBlocks() > 0)
|
||||
addDetailItem(i18n("schematics.info.total_blocks"), file.getTotalBlocks());
|
||||
if (file.getRegionCount().isPresent())
|
||||
addDetailItem(i18n("schematics.info.region_count"), String.valueOf(file.getRegionCount().getAsInt()));
|
||||
if (file.getTotalVolume().isPresent())
|
||||
addDetailItem(i18n("schematics.info.total_volume"), file.getTotalVolume().getAsLong());
|
||||
if (file.getTotalBlocks().isPresent())
|
||||
addDetailItem(i18n("schematics.info.total_blocks"), file.getTotalBlocks().getAsInt());
|
||||
if (file.getEnclosingSize() != null)
|
||||
addDetailItem(i18n("schematics.info.enclosing_size"),
|
||||
String.format("%d x %d x %d", (int) file.getEnclosingSize().getX(),
|
||||
(int) file.getEnclosingSize().getY(),
|
||||
(int) file.getEnclosingSize().getZ()));
|
||||
|
||||
addDetailItem(i18n("schematics.info.version"), file.getVersion());
|
||||
String.format("%d x %d x %d", file.getEnclosingSize().x(),
|
||||
file.getEnclosingSize().y(),
|
||||
file.getEnclosingSize().z()));
|
||||
if (StringUtils.isNotBlank(file.getMinecraftVersion()))
|
||||
addDetailItem(i18n("schematics.info.mc_data_version"), file.getMinecraftVersion());
|
||||
if (file.getVersion().isPresent())
|
||||
addDetailItem(i18n("schematics.info.version"),
|
||||
file.getSubVersion().isPresent()
|
||||
? "%d.%d".formatted(file.getVersion().getAsInt(), file.getSubVersion().getAsInt())
|
||||
: file.getVersion().getAsInt()
|
||||
);
|
||||
}
|
||||
|
||||
LitematicInfoDialog() {
|
||||
SchematicInfoDialog() {
|
||||
HBox titleBox = new HBox(8);
|
||||
{
|
||||
Node icon = getIcon(40);
|
||||
|
||||
TwoLineListItem title = new TwoLineListItem();
|
||||
title.setTitle(getName());
|
||||
title.setSubtitle(file.getFile().getFileName().toString());
|
||||
title.setSubtitle(path.getFileName().toString());
|
||||
|
||||
titleBox.getChildren().setAll(icon, title);
|
||||
setHeading(titleBox);
|
||||
@@ -536,10 +673,15 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
|
||||
{
|
||||
this.details = new ComponentList();
|
||||
details.setStyle("-fx-effect: null;");
|
||||
StackPane detailsContainer = new StackPane();
|
||||
detailsContainer.setPadding(new Insets(10, 0, 0, 0));
|
||||
detailsContainer.getChildren().add(details);
|
||||
setBody(detailsContainer);
|
||||
ScrollPane scrollPane = new ScrollPane(detailsContainer);
|
||||
scrollPane.setFitToWidth(true);
|
||||
FXUtils.setOverflowHidden(scrollPane, 8);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
StackPane.setMargin(scrollPane, new Insets(10, 0, 0, 0));
|
||||
setBody(scrollPane);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -552,15 +694,16 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
onEscPressed(this, okButton::fire);
|
||||
}
|
||||
|
||||
this.prefWidthProperty().bind(Controllers.getDecorator().contentWidthProperty().multiply(0.6));
|
||||
this.maxHeightProperty().bind(Controllers.getDecorator().contentHeightProperty().multiply(0.8));
|
||||
|
||||
updateContent(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Cell extends ListCell<Item> {
|
||||
private static final class Cell extends MDListCell<Item> {
|
||||
|
||||
private final RipplerContainer graphics;
|
||||
private final BorderPane root;
|
||||
private final StackPane left;
|
||||
private final TwoLineListItem center;
|
||||
private final HBox right;
|
||||
@@ -568,12 +711,17 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
private final ImageContainer iconImageView;
|
||||
private final SVGContainer iconSVGView;
|
||||
|
||||
private final BooleanProperty isFileProperty = new SimpleBooleanProperty(this, "isFile", false);
|
||||
private final BooleanProperty isDirectoryProperty = new SimpleBooleanProperty(this, "isDirectory", false);
|
||||
|
||||
private final Tooltip tooltip = new Tooltip();
|
||||
|
||||
public Cell() {
|
||||
this.root = new BorderPane();
|
||||
root.getStyleClass().add("md-list-cell");
|
||||
root.setPadding(new Insets(8));
|
||||
public Cell(JFXListView<Item> listView) {
|
||||
super(listView);
|
||||
|
||||
var box = new HBox(8);
|
||||
box.setAlignment(Pos.CENTER_LEFT);
|
||||
box.setPickOnBounds(false);
|
||||
|
||||
{
|
||||
this.left = new StackPane();
|
||||
@@ -581,115 +729,186 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
|
||||
this.iconImageView = new ImageContainer(32);
|
||||
this.iconSVGView = new SVGContainer(32);
|
||||
|
||||
BorderPane.setAlignment(left, Pos.CENTER);
|
||||
root.setLeft(left);
|
||||
}
|
||||
|
||||
{
|
||||
this.center = new TwoLineListItem();
|
||||
root.setCenter(center);
|
||||
HBox.setHgrow(center, Priority.ALWAYS);
|
||||
}
|
||||
|
||||
{
|
||||
this.right = new HBox(8);
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
JFXButton btnReveal = FXUtils.newToggleButton4(SVG.FOLDER_OPEN);
|
||||
FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager"));
|
||||
{
|
||||
var fo = SVG.FOLDER_OPEN.createIcon();
|
||||
var f = SVG.FOLDER.createIcon();
|
||||
btnReveal.graphicProperty().bind(Bindings.when(isDirectoryProperty).then(fo).otherwise(f));
|
||||
|
||||
var tooltip = new Tooltip();
|
||||
tooltip.textProperty().bind(Bindings.when(isDirectoryProperty).then(i18n("button.reveal_dir")).otherwise(i18n("reveal.in_file_manager")));
|
||||
FXUtils.installFastTooltip(btnReveal, tooltip);
|
||||
}
|
||||
btnReveal.setOnAction(event -> {
|
||||
Item item = getItem();
|
||||
if (item != null && !(item instanceof BackItem))
|
||||
item.onReveal();
|
||||
if (item != null) item.onReveal();
|
||||
});
|
||||
|
||||
JFXButton btnExplore = FXUtils.newToggleButton4(SVG.EXPLORE); // Change the icon if allows editing
|
||||
btnExplore.setOnAction(event -> {
|
||||
Item item = getItem();
|
||||
if (item instanceof SchematicItem) {
|
||||
try {
|
||||
Controllers.navigate(new NBTEditorPage(item.getPath()));
|
||||
} catch (IOException ignored) { // Should be impossible
|
||||
}
|
||||
}
|
||||
});
|
||||
btnExplore.visibleProperty().bind(isFileProperty);
|
||||
|
||||
JFXButton btnDelete = FXUtils.newToggleButton4(SVG.DELETE_FOREVER);
|
||||
btnDelete.setOnAction(event -> {
|
||||
Item item = getItem();
|
||||
if (item != null && !(item instanceof BackItem)) {
|
||||
if (item != null) {
|
||||
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"),
|
||||
item::onDelete, null);
|
||||
}
|
||||
});
|
||||
|
||||
right.getChildren().setAll(btnReveal, btnDelete);
|
||||
right.getChildren().setAll(btnExplore, btnReveal, btnDelete);
|
||||
}
|
||||
|
||||
this.graphics = new RipplerContainer(root);
|
||||
FXUtils.onClicked(graphics, () -> {
|
||||
Item item = getItem();
|
||||
if (item != null)
|
||||
item.onClick();
|
||||
box.getChildren().setAll(left, center, right);
|
||||
StackPane.setMargin(box, new Insets(8));
|
||||
getContainer().getChildren().setAll(box);
|
||||
onClicked(() -> {
|
||||
var item = getItem();
|
||||
if (item != null) item.onClick();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Item item, boolean empty) {
|
||||
Item oldItem = getItem();
|
||||
boolean oldEmpty = isEmpty();
|
||||
protected void updateControl(Item item, boolean empty) {
|
||||
if (empty || item == null) return;
|
||||
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (oldItem == item && oldEmpty == empty) return;
|
||||
|
||||
graphics.releaseRippleImmediately();
|
||||
iconImageView.setImage(null);
|
||||
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
center.setTitle("");
|
||||
center.setSubtitle("");
|
||||
isFileProperty.set(item instanceof SchematicItem);
|
||||
isDirectoryProperty.set(item.isDirectory());
|
||||
|
||||
if (item instanceof SchematicItem fileItem && fileItem.getImage() != null) {
|
||||
iconImageView.setImage(fileItem.getImage());
|
||||
left.getChildren().setAll(iconImageView);
|
||||
} else {
|
||||
if (item instanceof LitematicFileItem fileItem && fileItem.getImage() != null) {
|
||||
iconImageView.setImage(fileItem.getImage());
|
||||
left.getChildren().setAll(iconImageView);
|
||||
} else {
|
||||
iconSVGView.setIcon(item.getIcon());
|
||||
left.getChildren().setAll(iconSVGView);
|
||||
}
|
||||
iconSVGView.setIcon(item.getIcon());
|
||||
left.getChildren().setAll(iconSVGView);
|
||||
}
|
||||
|
||||
center.setTitle(item.getName());
|
||||
center.setSubtitle(item.getDescription());
|
||||
center.setTitle(item.getName());
|
||||
center.setSubtitle(item.getDescription());
|
||||
|
||||
Path path = item.getPath();
|
||||
if (path != null) {
|
||||
tooltip.setText(FileUtils.getAbsolutePath(path));
|
||||
FXUtils.installSlowTooltip(left, tooltip);
|
||||
} else {
|
||||
tooltip.setText("");
|
||||
Tooltip.uninstall(left, tooltip);
|
||||
}
|
||||
|
||||
root.setRight(item instanceof BackItem ? null : right);
|
||||
|
||||
setGraphic(graphics);
|
||||
Path path = item.getPath();
|
||||
if (path != null) {
|
||||
tooltip.setText(FileUtils.getAbsolutePath(path));
|
||||
FXUtils.installSlowTooltip(left, tooltip);
|
||||
} else {
|
||||
Tooltip.uninstall(left, tooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class SchematicsPageSkin extends ToolbarListPageSkin<Item, SchematicsPage> {
|
||||
SchematicsPageSkin() {
|
||||
super(SchematicsPage.this);
|
||||
private static final class SchematicsPageSkin extends SkinBase<SchematicsPage> {
|
||||
|
||||
StackPane placeholderContainer = new StackPane();
|
||||
placeholderContainer.getStyleClass().add("notice-pane");
|
||||
Label placeholderLabel = new Label(i18n("schematics.empty"));
|
||||
placeholderContainer.getChildren().add(placeholderLabel);
|
||||
listView.setPlaceholder(placeholderContainer);
|
||||
}
|
||||
private final JFXListView<Item> listView;
|
||||
|
||||
@Override
|
||||
protected List<Node> initializeToolbar(SchematicsPage skinnable) {
|
||||
return Arrays.asList(
|
||||
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
|
||||
createToolbarButton2(i18n("schematics.add"), SVG.ADD, skinnable::onAddFiles),
|
||||
createToolbarButton2(i18n("schematics.create_directory"), SVG.CREATE_NEW_FOLDER, skinnable::onCreateDirectory)
|
||||
);
|
||||
}
|
||||
SchematicsPageSkin(SchematicsPage skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
@Override
|
||||
protected ListCell<Item> createListCell(JFXListView<Item> listView) {
|
||||
return new Cell();
|
||||
StackPane pane = new StackPane();
|
||||
pane.setPadding(new Insets(10));
|
||||
pane.getStyleClass().addAll("notice-pane");
|
||||
|
||||
ComponentList root = new ComponentList();
|
||||
root.getStyleClass().add("no-padding");
|
||||
listView = new JFXListView<>();
|
||||
listView.setSelectionModel(new NoneMultipleSelectionModel<>());
|
||||
|
||||
{
|
||||
StackPane placeholderContainer = new StackPane();
|
||||
placeholderContainer.getStyleClass().add("notice-pane");
|
||||
Label placeholderLabel = new Label(i18n("schematics.empty"));
|
||||
placeholderContainer.getChildren().add(placeholderLabel);
|
||||
listView.setPlaceholder(placeholderContainer);
|
||||
}
|
||||
|
||||
{
|
||||
var toolbar = new HBox();
|
||||
JFXButton btnGoBack = createToolbarButton2("", SVG.ARROW_BACK, skinnable::navigateBack);
|
||||
btnGoBack.disableProperty().bind(skinnable.isRootProperty());
|
||||
JFXButton btnDownload = createToolbarButton2(i18n("schematics.install_mod"), SVG.DOWNLOAD, skinnable::downloadLitematica);
|
||||
FXUtils.onChangeAndOperate(skinnable.downloadTarget, (t) -> btnDownload.setDisable(t == null));
|
||||
toolbar.getChildren().setAll(
|
||||
btnGoBack,
|
||||
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
|
||||
createToolbarButton2(i18n("schematics.add"), SVG.ADD, skinnable::onAddFiles),
|
||||
createToolbarButton2(i18n("schematics.create_directory"), SVG.CREATE_NEW_FOLDER, skinnable::onCreateDirectory),
|
||||
createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::onRevealSchematicsFolder),
|
||||
btnDownload
|
||||
);
|
||||
FXUtils.setOverflowHidden(toolbar, 8);
|
||||
root.getContent().add(toolbar);
|
||||
}
|
||||
|
||||
{
|
||||
SpinnerPane center = new SpinnerPane();
|
||||
ComponentList.setVgrow(center, Priority.ALWAYS);
|
||||
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||
|
||||
listView.setCellFactory(x -> new Cell(listView));
|
||||
listView.setSelectionModel(new NoneMultipleSelectionModel<>());
|
||||
Bindings.bindContent(listView.getItems(), skinnable.getItems());
|
||||
|
||||
// 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);
|
||||
listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
|
||||
|
||||
center.setContent(listView);
|
||||
root.getContent().add(center);
|
||||
}
|
||||
|
||||
{
|
||||
var relPath = new HBox();
|
||||
relPath.getStyleClass().add("jfx-tool-bar-tip");
|
||||
relPath.setAlignment(Pos.CENTER_LEFT);
|
||||
FXUtils.onChangeAndOperate(skinnable.currentDirectoryProperty(), currentDir -> {
|
||||
relPath.getChildren().clear();
|
||||
var d = currentDir;
|
||||
while (d != null) {
|
||||
if (d != currentDir) {
|
||||
var box = new HBox(new Text(">"));
|
||||
box.setPadding(new Insets(3));
|
||||
relPath.getChildren().add(0, box);
|
||||
}
|
||||
var txt = new Text(d.getName());
|
||||
var box = new HBox(txt);
|
||||
box.setPadding(new Insets(3));
|
||||
box.setMouseTransparent(true);
|
||||
var rippler = new RipplerContainer(box);
|
||||
var finalD = d;
|
||||
FXUtils.onClicked(rippler, () -> skinnable.navigateTo(finalD));
|
||||
relPath.getChildren().add(0, rippler);
|
||||
d = d.parent;
|
||||
}
|
||||
});
|
||||
var relPathPane = new HBox(relPath);
|
||||
relPathPane.setPadding(new Insets(2));
|
||||
root.getContent().add(relPathPane);
|
||||
}
|
||||
|
||||
pane.getChildren().setAll(root);
|
||||
getChildren().setAll(pane);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -57,6 +57,7 @@ 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;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
@@ -200,7 +201,7 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
|
||||
if (exceptionToDisplay != null) {
|
||||
LOG.warning("Failed to download java", exceptionToDisplay);
|
||||
Controllers.dialog(DownloadProviders.localizeErrorMessage(exceptionToDisplay), i18n("install.failed"));
|
||||
Controllers.dialog(I18n.localizeErrorMessage(exceptionToDisplay), i18n("install.failed"));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -372,7 +373,7 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
if (version == null)
|
||||
return;
|
||||
|
||||
Controllers.taskDialog(new GetTask(downloadProvider.injectURLWithCandidates(version.getLinks().pkgInfoUri()))
|
||||
Controllers.taskDialog(new GetTask(downloadProvider.getDownloadCandidates(version.getLinks().pkgInfoUri()))
|
||||
.setExecutor(Schedulers.io())
|
||||
.thenComposeAsync(json -> {
|
||||
DiscoResult<DiscoRemoteFileInfo> result = JsonUtils.fromNonNullJson(json, DiscoResult.typeOf(DiscoRemoteFileInfo.class));
|
||||
@@ -392,8 +393,8 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
Task<FileDownloadTask.IntegrityCheck> getIntegrityCheck;
|
||||
if (StringUtils.isNotBlank(fileInfo.checksum()))
|
||||
getIntegrityCheck = Task.completed(new FileDownloadTask.IntegrityCheck(fileInfo.checksumType(), fileInfo.checksum()));
|
||||
else if (StringUtils.isNotBlank(fileInfo.checksumUri()))
|
||||
getIntegrityCheck = new GetTask(downloadProvider.injectURLWithCandidates(fileInfo.checksumUri()))
|
||||
else if (StringUtils.isNotBlank(fileInfo.checksumUri())) {
|
||||
getIntegrityCheck = new GetTask(downloadProvider.getDownloadCandidates(fileInfo.checksumUri()))
|
||||
.thenApplyAsync(checksum -> {
|
||||
checksum = checksum.trim();
|
||||
|
||||
@@ -403,13 +404,16 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
|
||||
return new FileDownloadTask.IntegrityCheck(fileInfo.checksumType(), checksum);
|
||||
});
|
||||
}
|
||||
else
|
||||
getIntegrityCheck = Task.completed(null);
|
||||
|
||||
return getIntegrityCheck
|
||||
.thenComposeAsync(integrityCheck ->
|
||||
new FileDownloadTask(downloadProvider.injectURLWithCandidates(fileInfo.directDownloadUri()),
|
||||
targetFile, integrityCheck).setName(fileInfo.fileName()))
|
||||
{
|
||||
return new FileDownloadTask(downloadProvider.getDownloadCandidates(fileInfo.directDownloadUri()),
|
||||
targetFile, integrityCheck).setName(fileInfo.fileName());
|
||||
})
|
||||
.thenSupplyAsync(() -> targetFile);
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), ((result, exception) -> {
|
||||
@@ -432,7 +436,7 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
LOG.warning("Failed to download java", exception);
|
||||
Throwable resolvedException = resolveException(exception);
|
||||
if (!(resolvedException instanceof CancellationException)) {
|
||||
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
|
||||
Controllers.dialog(I18n.localizeErrorMessage(resolvedException), i18n("install.failed"));
|
||||
}
|
||||
}
|
||||
})), i18n("java.download"), TaskCancellationAction.NORMAL);
|
||||
|
||||
@@ -45,7 +45,6 @@ import javafx.scene.text.TextFlow;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.ComponentVersionList;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
@@ -71,7 +70,6 @@ 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 org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.UnmodifiableView;
|
||||
|
||||
@@ -360,13 +358,12 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
|
||||
private void launchNoGame() {
|
||||
DownloadProvider downloadProvider = DownloadProviders.getDownloadProvider();
|
||||
ComponentVersionList<?> versionList = downloadProvider.getVersionList(GameComponentType.GAME);
|
||||
|
||||
Holder<GameInstanceID> instanceHolder = new Holder<>();
|
||||
Task<?> task = versionList.refreshAsync("")
|
||||
.thenSupplyAsync(() -> versionList.getVersions("").stream()
|
||||
Task<?> task = downloadProvider.getVersionsAsync(GameComponentType.GAME, null, false)
|
||||
.thenApplyAsync(versions -> versions.stream()
|
||||
.filter(it -> it.getVersionType() == RELEASE)
|
||||
.filter(it -> NativePatcher.checkSupportedStatus(GameVersionNumber.asGameVersion(it.getGameVersion()), Platform.SYSTEM_PLATFORM, OperatingSystem.SYSTEM_VERSION) != NativePatcher.SupportStatus.UNSUPPORTED)
|
||||
.filter(it -> NativePatcher.checkSupportedStatus(it.getGameVersion(), Platform.SYSTEM_PLATFORM, OperatingSystem.SYSTEM_VERSION) != NativePatcher.SupportStatus.UNSUPPORTED)
|
||||
.sorted()
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IOException("No versions found")))
|
||||
@@ -374,7 +371,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository();
|
||||
HMCLDependencyManager dependency = repository.getDependency();
|
||||
|
||||
String gameVersion = version.getGameVersion();
|
||||
String gameVersion = version.getGameVersion().toString();
|
||||
GameInstanceID instanceId = new GameInstanceID(gameVersion);
|
||||
|
||||
instanceHolder.value = instanceId;
|
||||
|
||||
@@ -435,7 +435,7 @@ public class PersonalizationPage extends StackPane {
|
||||
LineSelectButton<String> brightnessPane = new LineSelectButton<>();
|
||||
brightnessPane.setTitle(i18n("settings.launcher.brightness"));
|
||||
brightnessPane.setConverter(name -> i18n("settings.launcher.brightness."
|
||||
+ Objects.requireNonNullElse(name, "auto")));
|
||||
+ Objects.requireNonNullElse(name, "auto").trim().toLowerCase(Locale.ROOT)));
|
||||
brightnessPane.setItems(Arrays.asList("auto", "light", "dark"));
|
||||
bindThemeAppearanceLineSelectButton(
|
||||
brightnessPane,
|
||||
|
||||
@@ -37,10 +37,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChannel;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.upgrade.*;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
@@ -121,10 +118,18 @@ public final class SettingsPage extends ScrollPane {
|
||||
updateButton.setManaged(outdated);
|
||||
updatePane.pseudoClassStateChanged(PseudoClass.getPseudoClass("active"), outdated);
|
||||
|
||||
if (UpdateChecker.isOutdated()) {
|
||||
if (outdated) {
|
||||
lblUpdateSubProperty.set(i18n("update.newest_version", UpdateChecker.getLatestVersion().version()));
|
||||
} else if (UpdateChecker.isCheckingUpdate()) {
|
||||
lblUpdateSubProperty.set(i18n("update.checking"));
|
||||
} else if (UpdateChecker.errorProperty().get() != null) {
|
||||
Throwable t = UpdateChecker.errorProperty().get();
|
||||
if (t instanceof SelfVerificationException) {
|
||||
lblUpdateSubProperty.set(i18n("update.unverified"));
|
||||
} else {
|
||||
String msg = t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
|
||||
lblUpdateSubProperty.set(i18n("update.check_failed", msg));
|
||||
}
|
||||
} else {
|
||||
lblUpdateSubProperty.set(i18n("update.latest"));
|
||||
}
|
||||
@@ -132,6 +137,7 @@ public final class SettingsPage extends ScrollPane {
|
||||
UpdateChecker.latestVersionProperty().addListener(new WeakInvalidationListener(updateListener));
|
||||
UpdateChecker.outdatedProperty().addListener(new WeakInvalidationListener(updateListener));
|
||||
UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener));
|
||||
UpdateChecker.errorProperty().addListener(new WeakInvalidationListener(updateListener));
|
||||
updateListener.invalidated(null);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import java.nio.file.Path;
|
||||
|
||||
/// @author Glavo
|
||||
public enum NBTFileType {
|
||||
COMPRESSED("nbt", "dat", "dat_old") {
|
||||
COMPRESSED("dat", "dat_old", "litematic", "nbt", "schematic", "schem") {
|
||||
@Override
|
||||
public Tag read(Path file) throws IOException {
|
||||
return NBTCodec.of().readTag(file);
|
||||
|
||||
+27
-6
@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.CancellationException;
|
||||
@@ -41,9 +42,16 @@ public abstract class TaskExecutorDialogWizardDisplayer extends AbstractWizardDi
|
||||
super(cancelQueue);
|
||||
}
|
||||
|
||||
/// Displays a task and releases its optional resources after execution stops.
|
||||
@Override
|
||||
public void handleTask(SettingsMap settings, Task<?> task) {
|
||||
@Nullable WizardProvider.TaskCleanup cleanup = settings.remove(WizardProvider.TaskCleanup.KEY);
|
||||
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(new TaskCancellationAction(it -> {
|
||||
if (cleanup != null) {
|
||||
// Keep the installation modal until onStop has released its repository draft.
|
||||
it.setCancel(null);
|
||||
return;
|
||||
}
|
||||
it.fireEvent(new DialogCloseEvent());
|
||||
onEnd();
|
||||
}));
|
||||
@@ -61,24 +69,37 @@ public abstract class TaskExecutorDialogWizardDisplayer extends AbstractWizardDi
|
||||
TaskExecutor executor = task.executor(new TaskListener() {
|
||||
@Override
|
||||
public void onStop(boolean success, TaskExecutor executor) {
|
||||
@Nullable Exception failure = success ? null : executor.getException();
|
||||
if (cleanup != null) {
|
||||
try {
|
||||
cleanup.cleanup();
|
||||
} catch (Exception cleanupFailure) {
|
||||
if (failure == null || failure instanceof CancellationException) {
|
||||
failure = cleanupFailure;
|
||||
} else if (failure != cleanupFailure) {
|
||||
failure.addSuppressed(cleanupFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
@Nullable Exception completedFailure = failure;
|
||||
runInFX(() -> {
|
||||
if (success) {
|
||||
if (success && completedFailure == null) {
|
||||
if (settings.get("success_message") instanceof String successMessage)
|
||||
Controllers.dialog(successMessage, null, MessageType.SUCCESS, () -> onEnd());
|
||||
else if (!settings.containsKey("forbid_success_message"))
|
||||
Controllers.dialog(i18n("message.success"), null, MessageType.SUCCESS, () -> onEnd());
|
||||
} else {
|
||||
if (executor.getException() == null) {
|
||||
if (completedFailure == null) {
|
||||
onEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
if (executor.getException() instanceof CancellationException) {
|
||||
if (completedFailure instanceof CancellationException) {
|
||||
onEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
if (executor.getException().getCause() instanceof OutOfMemoryError outOfMemoryError) {
|
||||
if (completedFailure.getCause() instanceof OutOfMemoryError outOfMemoryError) {
|
||||
try {
|
||||
Controllers.dialog(StringUtils.getStackTrace(outOfMemoryError), null, MessageType.ERROR, () -> onEnd());
|
||||
} catch (OutOfMemoryError ignored) {
|
||||
@@ -87,9 +108,9 @@ public abstract class TaskExecutorDialogWizardDisplayer extends AbstractWizardDi
|
||||
return;
|
||||
}
|
||||
|
||||
String appendix = StringUtils.getStackTrace(executor.getException());
|
||||
String appendix = StringUtils.getStackTrace(completedFailure);
|
||||
if (settings.get(WizardProvider.FailureCallback.KEY) != null)
|
||||
settings.get(WizardProvider.FailureCallback.KEY).onFail(settings, executor.getException(), () -> onEnd());
|
||||
settings.get(WizardProvider.FailureCallback.KEY).onFail(settings, completedFailure, () -> onEnd());
|
||||
else if (settings.get("failure_message") instanceof String failureMessage)
|
||||
Controllers.dialog(appendix, failureMessage, MessageType.ERROR, () -> onEnd());
|
||||
else if (!settings.containsKey("forbid_failure_message"))
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.wizard;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
public interface WizardProvider {
|
||||
|
||||
@@ -39,4 +40,20 @@ public interface WizardProvider {
|
||||
|
||||
void onFail(SettingsMap settings, Exception exception, Runnable next);
|
||||
}
|
||||
|
||||
/// Releases resources owned by a task displayed by [TaskExecutorDialogWizardDisplayer].
|
||||
///
|
||||
/// Cleanup runs on the executor's completion thread after task execution stops, including
|
||||
/// cancellation before the first task starts, and before the dialog reports its outcome.
|
||||
@NotNullByDefault
|
||||
@FunctionalInterface
|
||||
interface TaskCleanup {
|
||||
/// Optional cleanup consumed by the task dialog when it accepts the task.
|
||||
SettingsMap.Key<TaskCleanup> KEY = new SettingsMap.Key<>("task_cleanup");
|
||||
|
||||
/// Releases the completed task's resources.
|
||||
///
|
||||
/// @throws Exception if cleanup fails; the task dialog reports the failure
|
||||
void cleanup() throws Exception;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.upgrade;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class SelfVerificationException extends IOException {
|
||||
|
||||
public SelfVerificationException() {
|
||||
super("Self verification failed");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import javafx.beans.value.ObservableBooleanValue;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -54,6 +55,7 @@ public final class UpdateChecker {
|
||||
},
|
||||
latestVersion);
|
||||
private static final ReadOnlyBooleanWrapper checkingUpdate = new ReadOnlyBooleanWrapper(false);
|
||||
private static final ReadOnlyObjectWrapper<@Nullable Throwable> error = new ReadOnlyObjectWrapper<>();
|
||||
|
||||
public static void init() {
|
||||
requestCheckUpdate(UpdateChannel.getChannel(), settings().acceptPreviewUpdateProperty().get());
|
||||
@@ -83,9 +85,13 @@ public final class UpdateChecker {
|
||||
return checkingUpdate.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public static ReadOnlyObjectProperty<@Nullable Throwable> errorProperty() {
|
||||
return error.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
private static RemoteVersion checkUpdate(UpdateChannel channel, boolean preview) throws IOException {
|
||||
if (!IntegrityChecker.DISABLE_SELF_INTEGRITY_CHECK && !IntegrityChecker.isSelfVerified()) {
|
||||
throw new IOException("Self verification failed");
|
||||
throw new SelfVerificationException();
|
||||
}
|
||||
|
||||
var query = new LinkedHashMap<String, String>();
|
||||
@@ -109,16 +115,23 @@ public final class UpdateChecker {
|
||||
|
||||
thread(() -> {
|
||||
RemoteVersion result = null;
|
||||
Throwable t = null;
|
||||
try {
|
||||
result = checkUpdate(channel, preview);
|
||||
LOG.info("Latest version (" + channel + ", preview=" + preview + ") is " + result);
|
||||
} catch (Throwable e) {
|
||||
t = e;
|
||||
LOG.warning("Failed to check for update", e);
|
||||
}
|
||||
Throwable throwable = t;
|
||||
|
||||
RemoteVersion finalResult = result;
|
||||
Platform.runLater(() -> {
|
||||
if (finalResult != null) {
|
||||
if (throwable != null) {
|
||||
latestVersion.set(null);
|
||||
error.set(throwable);
|
||||
} else {
|
||||
error.set(null);
|
||||
latestVersion.set(finalResult);
|
||||
}
|
||||
checkingUpdate.set(false);
|
||||
|
||||
@@ -172,7 +172,7 @@ public final class UpdateHandler {
|
||||
|
||||
Path self = getCurrentLocation();
|
||||
if (!IntegrityChecker.DISABLE_SELF_INTEGRITY_CHECK && !IntegrityChecker.isSelfVerified()) {
|
||||
throw new IOException("Self verification failed");
|
||||
throw new SelfVerificationException();
|
||||
}
|
||||
ExecutableHeaderHelper.copyWithHeader(self, target);
|
||||
|
||||
|
||||
@@ -37,9 +37,9 @@ public final class ChunkBaseApp {
|
||||
};
|
||||
|
||||
public static final String[] STRONGHOLD_FINDER_GAME_VERSIONS = {
|
||||
"26.2", "26.1", "1.21.9", "1.21.6", "1.21.5", "1.21.4", "1.21.2",
|
||||
"1.21", "1.20", "1.19.3", "1.19", "1.18", "1.17", "1.16", "1.15",
|
||||
"1.14", "1.13", "1.12", "1.11", "1.10", "1.9", "1.8", "1.7"
|
||||
"26.3", "26.2", "26.1", "1.21.9", "1.21.6", "1.21.5", "1.21.4",
|
||||
"1.21.2", "1.21", "1.20", "1.19.3", "1.19", "1.18", "1.17", "1.16",
|
||||
"1.15", "1.14", "1.13", "1.12", "1.11", "1.10", "1.9", "1.8", "1.7"
|
||||
};
|
||||
|
||||
public static final String[] NETHER_FORTRESS_GAME_VERSIONS = {
|
||||
@@ -49,9 +49,9 @@ public final class ChunkBaseApp {
|
||||
};
|
||||
|
||||
public static final String[] END_CITY_GAME_VERSIONS = {
|
||||
"26.2", "26.1", "1.21.9", "1.21.6", "1.21.5", "1.21.4", "1.21.2",
|
||||
"1.21", "1.20", "1.19.3", "1.19", "1.18", "1.17", "1.16", "1.15",
|
||||
"1.14", "1.13"
|
||||
"26.3", "26.2", "26.1", "1.21.9", "1.21.6", "1.21.5", "1.21.4",
|
||||
"1.21.2", "1.21", "1.20", "1.19.3", "1.19", "1.18", "1.17", "1.16",
|
||||
"1.15", "1.14", "1.13"
|
||||
};
|
||||
|
||||
public static boolean isSupported(@NotNull World world) {
|
||||
|
||||
@@ -107,7 +107,7 @@ public final class NativePatcher {
|
||||
.filter(library -> {
|
||||
if (library.classifier() != null && library.classifier().startsWith("natives")
|
||||
&& "org.lwjgl".equals(library.groupId())) {
|
||||
if ((useNativeGLFWorSDL && ("lwjgl-glfw".equals(library.artifactId()) || library.artifactId().contains("sdl")))
|
||||
if ((useNativeGLFWorSDL && ("lwjgl-glfw".equals(library.artifactId()) || "lwjgl-sdl".equals(library.artifactId())))
|
||||
|| (useNativeOpenAL && "lwjgl-openal".equals(library.artifactId()))) {
|
||||
LOG.info("Filter out " + library.name());
|
||||
return false;
|
||||
|
||||
@@ -19,6 +19,8 @@ package org.jackhuang.hmcl.util;
|
||||
|
||||
import javafx.beans.value.WritableValue;
|
||||
import javafx.scene.image.Image;
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.download.DownloadCandidates;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -27,7 +29,6 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.URI;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -35,9 +36,12 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
/// @author Glavo
|
||||
public abstract class RemoteImageLoader {
|
||||
private final DownloadProvider downloadProvider;
|
||||
private final Map<URI, WeakReference<Image>> cache = new HashMap<>();
|
||||
private final Map<URI, List<WeakReference<WritableValue<Image>>>> pendingRequests = new HashMap<>();
|
||||
private final WeakHashMap<WritableValue<Image>, URI> reverseLookup = new WeakHashMap<>();
|
||||
/// Loaded images indexed by normalized URL; image references do not prevent garbage collection.
|
||||
private final Map<WebURL, WeakReference<@Nullable Image>> cache = new HashMap<>();
|
||||
/// Image targets awaiting each URL's active load task.
|
||||
private final Map<WebURL, List<WeakReference<WritableValue<Image>>>> pendingRequests = new HashMap<>();
|
||||
/// Current requested URL for each target, used to discard obsolete load completions.
|
||||
private final WeakHashMap<WritableValue<Image>, WebURL> reverseLookup = new WeakHashMap<>();
|
||||
|
||||
public RemoteImageLoader(DownloadProvider downloadProvider) {
|
||||
this.downloadProvider = downloadProvider;
|
||||
@@ -47,59 +51,61 @@ public abstract class RemoteImageLoader {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected abstract @NotNull Task<Image> createLoadTask(@NotNull List<URI> uris);
|
||||
/// Creates a task that loads an image from candidate URLs in attempt order.
|
||||
protected abstract @NotNull Task<Image> createLoadTask(DownloadCandidates candidates);
|
||||
|
||||
/// Loads an image or assigns the placeholder when the URL is absent or invalid.
|
||||
@FXThread
|
||||
public void load(@NotNull WritableValue<Image> writableValue, String url) {
|
||||
URI uri = NetworkUtils.toURIOrNull(url);
|
||||
if (uri == null) {
|
||||
public void load(@NotNull WritableValue<Image> writableValue, @Nullable String url) {
|
||||
@Nullable WebURL webUrl = NetworkUtils.toWebURLOrNull(url);
|
||||
if (webUrl == null) {
|
||||
reverseLookup.remove(writableValue);
|
||||
writableValue.setValue(getPlaceholder());
|
||||
return;
|
||||
}
|
||||
|
||||
WeakReference<Image> reference = cache.get(uri);
|
||||
@Nullable WeakReference<@Nullable Image> reference = cache.get(webUrl);
|
||||
if (reference != null) {
|
||||
Image image = reference.get();
|
||||
@Nullable Image image = reference.get();
|
||||
if (image != null) {
|
||||
reverseLookup.remove(writableValue);
|
||||
writableValue.setValue(image);
|
||||
return;
|
||||
}
|
||||
cache.remove(uri);
|
||||
cache.remove(webUrl);
|
||||
}
|
||||
|
||||
writableValue.setValue(getPlaceholder());
|
||||
|
||||
{
|
||||
List<WeakReference<WritableValue<Image>>> list = pendingRequests.get(uri);
|
||||
@Nullable List<WeakReference<WritableValue<Image>>> list = pendingRequests.get(webUrl);
|
||||
if (list != null) {
|
||||
list.add(new WeakReference<>(writableValue));
|
||||
reverseLookup.put(writableValue, uri);
|
||||
reverseLookup.put(writableValue, webUrl);
|
||||
return;
|
||||
} else {
|
||||
list = new ArrayList<>(1);
|
||||
list.add(new WeakReference<>(writableValue));
|
||||
pendingRequests.put(uri, list);
|
||||
reverseLookup.put(writableValue, uri);
|
||||
pendingRequests.put(webUrl, list);
|
||||
reverseLookup.put(writableValue, webUrl);
|
||||
}
|
||||
}
|
||||
|
||||
createLoadTask(downloadProvider.injectURLWithCandidates(url)).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
Image image;
|
||||
createLoadTask(downloadProvider.getDownloadCandidates(url)).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
@Nullable Image image;
|
||||
if (exception == null) {
|
||||
image = result;
|
||||
} else {
|
||||
LOG.warning("Failed to load image from " + uri, exception);
|
||||
LOG.warning("Failed to load image from " + webUrl, exception);
|
||||
image = getPlaceholder();
|
||||
}
|
||||
|
||||
cache.put(uri, new WeakReference<>(image));
|
||||
List<WeakReference<WritableValue<Image>>> list = pendingRequests.remove(uri);
|
||||
cache.put(webUrl, new WeakReference<>(image));
|
||||
@Nullable List<WeakReference<WritableValue<Image>>> list = pendingRequests.remove(webUrl);
|
||||
if (list != null) {
|
||||
for (WeakReference<WritableValue<Image>> ref : list) {
|
||||
WritableValue<Image> target = ref.get();
|
||||
if (target != null && uri.equals(reverseLookup.get(target))) {
|
||||
@Nullable WritableValue<Image> target = ref.get();
|
||||
if (target != null && webUrl.equals(reverseLookup.get(target))) {
|
||||
reverseLookup.remove(target);
|
||||
target.setValue(image);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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 org.jackhuang.hmcl.util.function.ExceptionalSupplier;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* Thread-safe exceptional lazy initialization wrapper.
|
||||
* <p>
|
||||
* On failure, the result is set to null in order to allow retrying.
|
||||
*
|
||||
* @param <T> value type
|
||||
*/
|
||||
public final class SynchronizedExceptionalLazy<T> {
|
||||
private final ExceptionalSupplier<@Nullable T, Exception> supplier;
|
||||
private volatile Result<T> result = null;
|
||||
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
public SynchronizedExceptionalLazy(ExceptionalSupplier<@Nullable T, Exception> supplier) {
|
||||
this.supplier = Objects.requireNonNull(supplier);
|
||||
}
|
||||
|
||||
/// Gets and returns the result.
|
||||
///
|
||||
/// If the previous attempt succeeded, the cached value is returned directly.
|
||||
///
|
||||
/// Otherwise, the supplier is invoked to compute a new value.If this attempt fails,
|
||||
/// {@code null} is returned; if succeeded, the computed value is cached and returned.
|
||||
///
|
||||
/// @return the result, or null on failure
|
||||
public T get() {
|
||||
if (result != null) return result.value();
|
||||
lock.lock();
|
||||
try {
|
||||
if (result == null) {
|
||||
try {
|
||||
result = new Result<>(supplier.get());
|
||||
} catch (Exception e) {
|
||||
result = null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return result.value();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private record Result<T>(T value) {
|
||||
}
|
||||
}
|
||||
@@ -17,18 +17,62 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.i18n;
|
||||
|
||||
import org.jackhuang.hmcl.addon.AddonLoader;
|
||||
import org.jackhuang.hmcl.addon.AddonLoaderType;
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.download.ArtifactMalformedException;
|
||||
import org.jackhuang.hmcl.download.ComponentRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.cleanroom.CleanroomInstallTask;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricAPIInstallTask;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricInstallTask;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeNewInstallTask;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeOldInstallTask;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameInstallTask;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.java.mojang.MojangJavaDownloadTask;
|
||||
import org.jackhuang.hmcl.download.legacyfabric.LegacyFabricInstallTask;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
|
||||
import org.jackhuang.hmcl.download.neoforge.NeoForgeInstallTask;
|
||||
import org.jackhuang.hmcl.download.neoforge.NeoForgeOldInstallTask;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltAPIInstallTask;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltInstallTask;
|
||||
import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
|
||||
import org.jackhuang.hmcl.java.JavaInstallTask;
|
||||
import org.jackhuang.hmcl.modpack.MinecraftInstanceTask;
|
||||
import org.jackhuang.hmcl.modpack.ModpackInstallTask;
|
||||
import org.jackhuang.hmcl.modpack.ModpackUpdateTask;
|
||||
import org.jackhuang.hmcl.modpack.curse.CurseCompletionTask;
|
||||
import org.jackhuang.hmcl.modpack.curse.CurseInstallTask;
|
||||
import org.jackhuang.hmcl.modpack.mcbbs.McbbsModpackCompletionTask;
|
||||
import org.jackhuang.hmcl.modpack.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.modrinth.ModrinthCompletionTask;
|
||||
import org.jackhuang.hmcl.modpack.modrinth.ModrinthInstallTask;
|
||||
import org.jackhuang.hmcl.modpack.modrinth.ModrinthModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCModpackInstallTask;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackCompletionTask;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackLocalInstallTask;
|
||||
import org.jackhuang.hmcl.task.DownloadException;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.translator.Translator;
|
||||
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.PropertyKey;
|
||||
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
public final class I18n {
|
||||
|
||||
@@ -124,6 +168,117 @@ public final class I18n {
|
||||
return MinecraftWiki.getWikiLink(locale, remoteVersion);
|
||||
}
|
||||
|
||||
public static String translateLoaderType(AddonLoaderType loaderType) {
|
||||
if (loaderType instanceof ModLoaderType modLoaderType) {
|
||||
return switch (modLoaderType) {
|
||||
case FORGE -> i18n("install.installer.forge");
|
||||
case CLEANROOM -> i18n("install.installer.cleanroom");
|
||||
case NEO_FORGE -> i18n("install.installer.neoforge");
|
||||
case FABRIC -> i18n("install.installer.fabric");
|
||||
case LITE_LOADER -> i18n("install.installer.liteloader");
|
||||
case QUILT -> i18n("install.installer.quilt");
|
||||
case LEGACY_FABRIC -> i18n("install.installer.legacyfabric");
|
||||
default -> modLoaderType.displayName();
|
||||
};
|
||||
}
|
||||
return loaderType.displayName();
|
||||
}
|
||||
|
||||
public static String translateLoaderName(AddonLoader loader) {
|
||||
if (loader.type() != null)
|
||||
return translateLoaderType(loader.type());
|
||||
else return "bungeecord".equalsIgnoreCase(loader.name())
|
||||
? "BungeeCord"
|
||||
: StringUtils.capitalizeWords(loader.name());
|
||||
}
|
||||
|
||||
public static @Nullable String translateTaskName(Task<?> task) {
|
||||
if (task instanceof GameAssetDownloadTask) {
|
||||
return i18n("assets.download_all");
|
||||
} else if (task instanceof GameInstallTask) {
|
||||
return i18n("install.installer.install", i18n("install.installer.game"));
|
||||
} else if (task instanceof CleanroomInstallTask) {
|
||||
return i18n("install.installer.install", i18n("install.installer.cleanroom"));
|
||||
} else if (task instanceof LegacyFabricInstallTask) {
|
||||
return i18n("install.installer.install", i18n("install.installer.legacyfabric"));
|
||||
} else if (task instanceof ForgeNewInstallTask || task instanceof ForgeOldInstallTask) {
|
||||
return i18n("install.installer.install", i18n("install.installer.forge"));
|
||||
} else if (task instanceof NeoForgeInstallTask || task instanceof NeoForgeOldInstallTask) {
|
||||
return i18n("install.installer.install", i18n("install.installer.neoforge"));
|
||||
} else if (task instanceof LiteLoaderInstallTask) {
|
||||
return i18n("install.installer.install", i18n("install.installer.liteloader"));
|
||||
} else if (task instanceof OptiFineInstallTask) {
|
||||
return i18n("install.installer.install", i18n("install.installer.optifine"));
|
||||
} else if (task instanceof FabricInstallTask) {
|
||||
return i18n("install.installer.install", i18n("install.installer.fabric"));
|
||||
} else if (task instanceof FabricAPIInstallTask) {
|
||||
return i18n("install.installer.install", i18n("install.installer.fabric-api"));
|
||||
} else if (task instanceof QuiltInstallTask) {
|
||||
return i18n("install.installer.install", i18n("install.installer.quilt"));
|
||||
} else if (task instanceof QuiltAPIInstallTask) {
|
||||
return i18n("install.installer.install", i18n("install.installer.quilt-api"));
|
||||
} else if (task instanceof CurseCompletionTask || task instanceof ModrinthCompletionTask || task instanceof ServerModpackCompletionTask || task instanceof McbbsModpackCompletionTask) {
|
||||
return i18n("modpack.completion");
|
||||
} else if (task instanceof ModpackInstallTask) {
|
||||
return i18n("modpack.installing");
|
||||
} else if (task instanceof ModpackUpdateTask) {
|
||||
return i18n("modpack.update");
|
||||
} else if (task instanceof CurseInstallTask) {
|
||||
return i18n("modpack.installing.given", i18n("modpack.type.curse"));
|
||||
} else if (task instanceof MultiMCModpackInstallTask) {
|
||||
return i18n("modpack.installing.given", i18n("modpack.type.multimc"));
|
||||
} else if (task instanceof ModrinthInstallTask) {
|
||||
return i18n("modpack.installing.given", i18n("modpack.type.modrinth"));
|
||||
} else if (task instanceof ServerModpackLocalInstallTask) {
|
||||
return i18n("install.installing") + ": " + i18n("modpack.type.server");
|
||||
} else if (task instanceof HMCLModpackInstallTask) {
|
||||
return i18n("modpack.installing.given", "HMCL");
|
||||
} else if (task instanceof McbbsModpackExportTask || task instanceof ServerModpackExportTask || task instanceof ModrinthModpackExportTask) {
|
||||
return i18n("modpack.export");
|
||||
} else if (task instanceof MinecraftInstanceTask) {
|
||||
return i18n("modpack.scan");
|
||||
} else if (task instanceof MojangJavaDownloadTask) {
|
||||
return i18n("download.java");
|
||||
} else if (task instanceof JavaInstallTask) {
|
||||
return i18n("java.installing");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String localizeErrorMessage(Throwable exception) {
|
||||
if (exception instanceof DownloadException de) {
|
||||
String url = de.getUrl();
|
||||
if (exception.getCause() instanceof SocketTimeoutException) {
|
||||
return i18n("install.failed.downloading.timeout", url);
|
||||
} else if (exception.getCause() instanceof ResponseCodeException responseCodeException) {
|
||||
if (hasKey("download.code." + responseCodeException.getResponseCode())) {
|
||||
return i18n("download.code." + responseCodeException.getResponseCode(), url);
|
||||
} else {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
}
|
||||
} else if (exception.getCause() instanceof FileNotFoundException) {
|
||||
return i18n("download.code.404", url);
|
||||
} else if (exception.getCause() instanceof AccessDeniedException) {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
|
||||
} else if (exception.getCause() instanceof ArtifactMalformedException) {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.artifact_malformed");
|
||||
} else if (exception.getCause() instanceof SSLHandshakeException && !(exception.getCause().getMessage() != null && exception.getCause().getMessage().contains("Remote host terminated"))) {
|
||||
if (exception.getCause().getMessage() != null && (exception.getCause().getMessage().contains("No name matching") || exception.getCause().getMessage().contains("No subject alternative DNS name matching"))) {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.dns.pollution");
|
||||
}
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.ssl_handshake");
|
||||
} else {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
}
|
||||
} else if (exception instanceof ArtifactMalformedException) {
|
||||
return i18n("exception.artifact_malformed");
|
||||
} else if (exception instanceof CancellationException) {
|
||||
return i18n("message.cancelled");
|
||||
}
|
||||
return StringUtils.getStackTrace(exception);
|
||||
}
|
||||
|
||||
public static boolean hasKey(String key) {
|
||||
return getResourceBundle().containsKey(key);
|
||||
}
|
||||
|
||||
+46
-16
@@ -17,45 +17,65 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.url.data;
|
||||
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class DataUri {
|
||||
/// Parses a data URL's media type, charset, and percent-decoded payload.
|
||||
/// Query content is included in the payload; the fragment is excluded.
|
||||
/// Percent escapes are decoded as UTF-8 before the declared charset is applied by the read methods.
|
||||
///
|
||||
/// @author Glavo
|
||||
public final class DataURL {
|
||||
/// Scheme identifying inline data URLs.
|
||||
public static final String SCHEME = "data";
|
||||
|
||||
private static IllegalArgumentException invalidUri(URI uri) {
|
||||
return new IllegalArgumentException("Invalid data URI: " + uri);
|
||||
/// Creates a parsing failure for a data URL without a metadata/payload separator.
|
||||
private static IllegalArgumentException invalidUri(WebURL url) {
|
||||
return new IllegalArgumentException("Invalid data URL: " + url);
|
||||
}
|
||||
|
||||
public static boolean isDataUri(URI uri) {
|
||||
return uri != null && SCHEME.equals(uri.getScheme());
|
||||
/// Returns whether the URL uses the data scheme, or `false` for `null`.
|
||||
public static boolean isDataUri(@Nullable WebURL url) {
|
||||
return url != null && SCHEME.equals(url.getScheme());
|
||||
}
|
||||
|
||||
/// Trimmed media type and parameters, excluding the Base64 marker.
|
||||
private final @NotNull String mediaType;
|
||||
/// Declared charset, or UTF-8 when absent or unrecognized.
|
||||
private final @NotNull Charset charset;
|
||||
/// Whether the metadata ends with the case-sensitive Base64 marker.
|
||||
private final boolean base64;
|
||||
/// Percent-decoded payload before optional Base64 decoding.
|
||||
private final @NotNull String rawData;
|
||||
|
||||
public DataUri(URI uri) {
|
||||
if (!uri.getScheme().equals(SCHEME)) {
|
||||
throw new IllegalArgumentException("URI scheme must be " + SCHEME);
|
||||
/// Parses metadata and payload without decoding Base64 content.
|
||||
///
|
||||
/// @param url the data URL
|
||||
/// @throws IllegalArgumentException if the scheme is not data or the decoded content has no comma
|
||||
public DataURL(WebURL url) {
|
||||
if (!SCHEME.equals(url.getScheme())) {
|
||||
throw new IllegalArgumentException("URL scheme must be " + SCHEME);
|
||||
}
|
||||
|
||||
String schemeSpecificPart = uri.getSchemeSpecificPart();
|
||||
if (schemeSpecificPart == null)
|
||||
throw invalidUri(uri);
|
||||
String schemeSpecificPart = url.getPath();
|
||||
@Nullable String authority = url.getAuthority();
|
||||
if (authority != null)
|
||||
schemeSpecificPart = "//" + authority + schemeSpecificPart;
|
||||
|
||||
// The query delimiter and content belong to the data, while the fragment does not.
|
||||
@Nullable String query = url.getQuery();
|
||||
if (query != null)
|
||||
schemeSpecificPart += "?" + query;
|
||||
|
||||
int comma = schemeSpecificPart.indexOf(',');
|
||||
if (comma < 0)
|
||||
throw invalidUri(uri);
|
||||
throw invalidUri(url);
|
||||
|
||||
String mediaType = schemeSpecificPart.substring(0, comma);
|
||||
boolean base64 = mediaType.endsWith(";base64");
|
||||
@@ -68,22 +88,29 @@ public final class DataUri {
|
||||
this.rawData = schemeSpecificPart.substring(comma + 1);
|
||||
}
|
||||
|
||||
/// Returns the trimmed media type and parameters without the Base64 marker; may be empty.
|
||||
public @NotNull String getMediaType() {
|
||||
return mediaType;
|
||||
}
|
||||
|
||||
/// Returns the declared charset, or UTF-8 when absent or unrecognized.
|
||||
public @NotNull Charset getCharset() {
|
||||
return charset;
|
||||
}
|
||||
|
||||
/// Returns whether the payload is marked for Base64 decoding.
|
||||
public boolean isBase64() {
|
||||
return base64;
|
||||
}
|
||||
|
||||
/// Returns the percent-decoded payload before optional Base64 decoding, preserving literal plus signs.
|
||||
public @NotNull String getRawData() {
|
||||
return rawData;
|
||||
}
|
||||
|
||||
/// Returns a new byte array by decoding Base64 or encoding the text payload with [#getCharset()].
|
||||
///
|
||||
/// @throws IOException if a Base64 payload cannot be decoded
|
||||
public byte[] readBytes() throws IOException {
|
||||
if (base64) {
|
||||
try {
|
||||
@@ -96,6 +123,9 @@ public final class DataUri {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the text payload, decoding Base64 bytes with [#getCharset()] when marked as Base64.
|
||||
///
|
||||
/// @throws IOException if a Base64 payload cannot be decoded
|
||||
public String readString() throws IOException {
|
||||
if (base64) {
|
||||
try {
|
||||
@@ -27,20 +27,20 @@ import java.net.URLConnection;
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class DataURLConnection extends URLConnection {
|
||||
private final DataUri dataUri;
|
||||
private final DataURL dataUrl;
|
||||
private byte[] data;
|
||||
private InputStream inputStream;
|
||||
|
||||
DataURLConnection(URL url, DataUri dataUri) {
|
||||
DataURLConnection(URL url, DataURL dataUrl) {
|
||||
super(url);
|
||||
this.dataUri = dataUri;
|
||||
this.dataUrl = dataUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() throws IOException {
|
||||
if (!connected) {
|
||||
connected = true;
|
||||
data = dataUri.readBytes();
|
||||
data = dataUrl.readBytes();
|
||||
inputStream = new ByteArrayInputStream(this.data);
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ public final class DataURLConnection extends URLConnection {
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
return dataUri.getMediaType();
|
||||
return dataUrl.getMediaType();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -17,23 +17,26 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.url.data;
|
||||
|
||||
import org.glavo.url.WebURL;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
/// Opens data URL connections using [WebURL] to parse the address.
|
||||
///
|
||||
/// @author Glavo
|
||||
public final class DataURLHandle extends URLStreamHandler {
|
||||
/// Creates a connection without decoding its Base64 payload.
|
||||
///
|
||||
/// @throws MalformedURLException if the address cannot be parsed as a data URL
|
||||
@Override
|
||||
protected URLConnection openConnection(URL u) throws IOException {
|
||||
try {
|
||||
URI uri = u.toURI();
|
||||
if (!DataUri.isDataUri(uri))
|
||||
throw new MalformedURLException("URI is not a data URI: " + u);
|
||||
|
||||
return new DataURLConnection(u, new DataUri(uri));
|
||||
} catch (URISyntaxException e) {
|
||||
throw new MalformedURLException(e.getMessage());
|
||||
return new DataURLConnection(u, new DataURL(WebURL.of(u)));
|
||||
} catch (IllegalArgumentException e) {
|
||||
MalformedURLException exception = new MalformedURLException(e.getMessage());
|
||||
exception.initCause(e);
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -713,6 +713,10 @@
|
||||
-fx-text-fill: BLACK;
|
||||
}
|
||||
|
||||
.jfx-tool-bar Text {
|
||||
|
||||
}
|
||||
|
||||
.jfx-tool-bar .jfx-options-burger {
|
||||
-fx-padding: 22px;
|
||||
}
|
||||
@@ -745,6 +749,7 @@
|
||||
|
||||
.jfx-tool-bar-button {
|
||||
-fx-text-fill: -monet-on-surface;
|
||||
-fx-font-size: 14px;
|
||||
-fx-toggle-icon4-size: 37px;
|
||||
-fx-pref-height: -fx-toggle-icon4-size;
|
||||
-fx-max-height: -fx-toggle-icon4-size;
|
||||
@@ -760,6 +765,11 @@
|
||||
-fx-padding: 10.0;
|
||||
}
|
||||
|
||||
.jfx-tool-bar-tip Text {
|
||||
-fx-font-size: 13;
|
||||
-fx-fill: -monet-on-surface;
|
||||
}
|
||||
|
||||
.jfx-decorator-button {
|
||||
-fx-max-width: 40px;
|
||||
-fx-background-radius: 5px;
|
||||
@@ -1092,6 +1102,10 @@
|
||||
-fx-border-width: 0 0 1 0;
|
||||
}
|
||||
|
||||
.md-list-cell:last {
|
||||
-fx-border-width: 0;
|
||||
}
|
||||
|
||||
.md-list-cell:selected {
|
||||
-fx-background-color: -monet-secondary-container;
|
||||
}
|
||||
|
||||
@@ -190,6 +190,11 @@ addon.download.recommend=Recommended Version for Minecraft %1s
|
||||
addon.download.title.release=Minecraft %s
|
||||
addon.download.title.snapshot=Minecraft %s (Snapshots)
|
||||
addon.modrinth=Modrinth
|
||||
addon.sort.date_created=Date Created
|
||||
addon.sort.last_updated=Last Updated
|
||||
addon.sort.popularity=Popularity
|
||||
addon.sort.relevancy=Relevancy
|
||||
addon.sort.total_downloads=Total Downloads
|
||||
|
||||
archive.author=Author(s)
|
||||
archive.date=Publish Date
|
||||
@@ -371,13 +376,6 @@ curse.category.6553=Realistic
|
||||
curse.category.6554=Fantasy
|
||||
curse.category.6555=Vanilla
|
||||
|
||||
curse.sort.author=Author
|
||||
curse.sort.date_created=Date Created
|
||||
curse.sort.last_updated=Last Updated
|
||||
curse.sort.name=Name
|
||||
curse.sort.popularity=Popularity
|
||||
curse.sort.total_downloads=Total Downloads
|
||||
|
||||
datetime.format=MMM d, yyyy, h\:mm\:ss a
|
||||
|
||||
download=Download
|
||||
@@ -433,32 +431,32 @@ 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.create_hmcl_current_directory_failure=HMCL cannot create the HMCL directory (%s). Please move HMCL to another location and reopen it.
|
||||
fatal.javafx.incomplete=The JavaFX environment is incomplete.\n\
|
||||
Please try replacing your Java or reinstalling OpenJFX.
|
||||
fatal.javafx.missing=Missing JavaFX environment. Please open Hello Minecraft! Launcher with Java, which includes OpenJFX.
|
||||
fatal.config_change_owner_root=You are using the root account to open Hello Minecraft! Launcher. This may prevent you from opening HMCL with another account in the future.\n\
|
||||
fatal.javafx.missing=Missing JavaFX environment. Please open HMCL with Java, which includes OpenJFX.
|
||||
fatal.config_change_owner_root=You are using the root account to open HMCL. This may prevent you from opening HMCL with another account in the future.\n\
|
||||
Do you still want to continue?
|
||||
fatal.config_in_temp_dir=You are opening Hello Minecraft! Launcher in a temporary directory. Your settings and game data may be lost.\n\
|
||||
fatal.config_in_temp_dir=You are opening HMCL in a temporary directory. Your settings and game data may be lost.\n\
|
||||
It is recommended to move HMCL to another location and reopen it.\n\
|
||||
Do you still want to continue?
|
||||
fatal.config_loading_failure=Cannot load configuration files.\n\
|
||||
Please make sure that Hello Minecraft! Launcher has read and write access to "%s" and the files in it.\n\
|
||||
Please make sure that HMCL has read and write access to "%s" and the files in it.\n\
|
||||
For macOS, try putting HMCL somewhere with permissions other than "Desktop", "Downloads", and "Documents" and try again.
|
||||
fatal.config_loading_failure.unix=Hello Minecraft! Launcher could not load the configuration file because it was created by user "%1$s".\n\
|
||||
fatal.config_loading_failure.unix=HMCL could not load the configuration file because it was created by user "%1$s".\n\
|
||||
Please open HMCL as root user (not recommended), or execute the following command in the terminal to change the ownership of the configuration file to the current user:\n%2$s
|
||||
fatal.config_unsupported_version=The current configuration file was created by a newer version of Hello Minecraft! Launcher, and this version of HMCL cannot load it properly.\n\
|
||||
fatal.config_unsupported_version=The current configuration file was created by a newer version of HMCL, and this version of HMCL cannot load it properly.\n\
|
||||
Please update and restart HMCL.\n\
|
||||
Before updating the launcher, any settings you modify will not be saved.
|
||||
fatal.mac_app_translocation=Hello Minecraft! Launcher is isolated to a temporary directory by the OS due to macOS security mechanisms.\n\
|
||||
fatal.mac_app_translocation=HMCL is isolated to a temporary directory by the OS due to macOS security mechanisms.\n\
|
||||
Please move HMCL to a different directory before attempting to open. Otherwise, your settings and game data may be lost after restarting.\n\
|
||||
Do you still want to continue?
|
||||
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher has been upgraded. Please restart the launcher.
|
||||
fatal.apply_update_failure=We are sorry, but Hello Minecraft! Launcher is unable to update automatically.\n\
|
||||
fatal.migration_requires_manual_reboot=HMCL has been upgraded. Please restart the launcher.
|
||||
fatal.apply_update_failure=We are sorry, but HMCL is unable to update automatically.\n\
|
||||
\n\
|
||||
You can update manually by downloading a newer launcher version from %s.\n\
|
||||
If the problem persists, please consider reporting this to us.
|
||||
fatal.apply_update_need_win7=Hello Minecraft! Launcher cannot automatically update on Windows XP/Vista.\n\
|
||||
fatal.apply_update_need_win7=HMCL cannot automatically update on Windows XP/Vista.\n\
|
||||
\n\
|
||||
You can update manually by downloading a newer launcher version from %s.
|
||||
fatal.deprecated_java_version=HMCL will require Java 17 or later to run in the future, but will still support launching games with Java 6~16.\n\
|
||||
@@ -467,14 +465,14 @@ It is recommended to install the latest version of Java to ensure that HMCL work
|
||||
\n\
|
||||
You can continue to keep the old version of Java. HMCL can recognize and manage multiple Java versions and will automatically select the appropriate Java for you based on the game version.
|
||||
fatal.deprecated_java_version.download_link=Download Java %d
|
||||
fatal.samba=If you opened Hello Minecraft! Launcher from a Samba network drive, some features might not be working. Please try updating your Java or moving the launcher to another directory.
|
||||
fatal.samba=If you opened HMCL from a Samba network drive, some features might not be working. Please try updating your Java or moving the launcher to another directory.
|
||||
fatal.illegal_char=Your working directory contains an illegal character "=". You will not be able to use authlib-injector or change the skin of your offline account.
|
||||
fatal.unsupported_platform=Minecraft is not fully supported on your platform yet, so you may experience missing features or even be unable to launch the game.\n\
|
||||
\n\
|
||||
If you cannot launch Minecraft 1.17 and later, you can try switching the "Renderer" to "Mesa LLVMpipe" or "Mesa Lavapipe" in "Global/Instance-specific Settings → Graphics Settings" to use CPU rendering for better compatibility.
|
||||
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher has provided support for the Loongson platform.\n\
|
||||
fatal.unsupported_platform.loongarch=HMCL has provided support for the Loongson platform.\n\
|
||||
If you encounter problems when playing a game, you can visit https://docs.hmcl.net/groups.html for help.
|
||||
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher has provided native support for the Windows on Arm platform. If you encounter problems when playing a game, please try launching the game with Java based on x86 architecture.\n\
|
||||
fatal.unsupported_platform.windows_arm64=HMCL has provided native support for the Windows on Arm platform. If you encounter problems when playing a game, please try launching the game with Java based on x86 architecture.\n\
|
||||
\n\
|
||||
If you are using the <b>Qualcomm</b> platform, you may need to install the <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL Compatibility Pack</a> before playing games.\n\
|
||||
Click the link to navigate to the Microsoft Store and install the compatibility pack.
|
||||
@@ -735,7 +733,7 @@ game_directory.use_relative_path=Use a relative path for the game directory if p
|
||||
game_directory.root=Using the disk root directory as the game folder may cause the game to fail to launch and will create multiple files and folders in the disk root directory.\n\nAre you sure you want to continue?
|
||||
|
||||
help=Help
|
||||
help.doc=Hello Minecraft! Launcher Documentation
|
||||
help.doc=HMCL Documentation
|
||||
help.detail=For data pack and modpack makers.
|
||||
|
||||
input.email=The username must be an email address.
|
||||
@@ -954,9 +952,9 @@ launcher.cache_directory.default=Default ("%APPDATA%/.minecraft" or "~/.minecraf
|
||||
launcher.cache_directory.disabled=Disabled
|
||||
launcher.cache_directory.invalid=Failed to create a cache directory, falling back to default.
|
||||
launcher.contact=Contact Us
|
||||
launcher.crash=Hello Minecraft! Launcher has encountered a fatal error! Please copy the following log and ask for help on our Discord, QQ group, GitHub, or other Minecraft forum.
|
||||
launcher.crash.java_internal_error=Hello Minecraft! Launcher has encountered a fatal error because your Java is corrupted. Please uninstall your Java and download a suitable Java <a href="https://bell-sw.com/pages/downloads/#downloads">here</a>.
|
||||
launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher has encountered a fatal error! Your launcher is outdated. Please update your launcher!
|
||||
launcher.crash=HMCL has encountered a fatal error! Please copy the following log and ask for help on our Discord, QQ group, GitHub, or other Minecraft forum.
|
||||
launcher.crash.java_internal_error=HMCL has encountered a fatal error because your Java is corrupted. Please uninstall your Java and download a suitable Java <a href="https://bell-sw.com/pages/downloads/#downloads">here</a>.
|
||||
launcher.crash.hmcl_out_dated=HMCL has encountered a fatal error! Your launcher is outdated. Please update your launcher!
|
||||
launcher.update_java=Please update your Java version.
|
||||
|
||||
libraries.download=Downloading Libraries
|
||||
@@ -1025,6 +1023,9 @@ modpack.installing=Installing modpack
|
||||
modpack.installing.given=Installing %s modpack
|
||||
modpack.invalid=Invalid modpack, you can try downloading it again.
|
||||
modpack.mismatched_type=Modpack type mismatched, the current instance is a(n) %1$s type, but the provided one is %2$s type.
|
||||
modpack.optional_files=Optional Files
|
||||
modpack.retry_optional_files=Failed to load optional files. Click here to retry.
|
||||
modpack.unknown_optional_file=Unknown file
|
||||
modpack.name=Modpack Name
|
||||
modpack.origin=Source
|
||||
modpack.origin.url=Official Website
|
||||
@@ -1038,11 +1039,11 @@ modpack.type.curse.error=Failed to download dependencies. Please try again or us
|
||||
modpack.type.curse.not_found=Some dependencies are no longer available. Please try installing a newer modpack version.
|
||||
modpack.type.manual.warning=The modpack is manually packaged by the publisher, which may already contain a launcher. It is recommended to try extracting the modpack and running the game with its own launcher. HMCL can still import it, with no guarantee of its usability. Still continue?
|
||||
modpack.type.mcbbs=MCBBS
|
||||
modpack.type.mcbbs.export=Can be imported by Hello Minecraft! Launcher
|
||||
modpack.type.mcbbs.export=Can be imported by HMCL
|
||||
modpack.type.modrinth=Modrinth
|
||||
modpack.type.modrinth.export=Can be imported by popular third-party launchers
|
||||
modpack.type.multimc=MultiMC
|
||||
modpack.type.multimc.export=Can be imported by Hello Minecraft! Launcher and MultiMC
|
||||
modpack.type.multimc.export=Can be imported by HMCL and MultiMC
|
||||
modpack.type.server=Auto-Update Modpack from Server
|
||||
modpack.type.server.export=Allows server owner to remotely update the game instance
|
||||
modpack.type.server.malformed=Invalid modpack manifest. Please contact the modpack maker to resolve this problem.
|
||||
@@ -1332,7 +1333,6 @@ schematics=Schematics
|
||||
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=New Directory
|
||||
schematics.create_directory.prompt=Please enter the new directory name
|
||||
schematics.create_directory.failed=Failed to create directory
|
||||
@@ -1348,7 +1348,13 @@ schematics.info.time_created=Created Time
|
||||
schematics.info.time_modified=Modified Time
|
||||
schematics.info.total_blocks=Total Blocks
|
||||
schematics.info.total_volume=Total Volume
|
||||
schematics.info.type=Schematic Type
|
||||
schematics.info.type.litematic=Litematic (Litematica)
|
||||
schematics.info.type.nbt_structure=NBT Structure (Vanilla, Create)
|
||||
schematics.info.type.schem=Schem (Schematica, WE, MCEdit)
|
||||
schematics.info.version=Schematic Version
|
||||
schematics.info.mc_data_version=Minecraft Data Version
|
||||
schematics.install_mod=Install Litematica Mod
|
||||
schematics.manage=Schematics
|
||||
schematics.sub_items=%d sub-item(s)
|
||||
|
||||
@@ -1785,6 +1791,7 @@ update.channel.nightly.hint=You are currently using a Nightly channel build of t
|
||||
update.channel.nightly.title=Nightly Channel Notice
|
||||
update.channel.stable=Stable
|
||||
update.checking=Checking for updates
|
||||
update.check_failed=Failed to check for update. %s
|
||||
update.disable_auto_show_update_dialog=Do not show update dialog automatically
|
||||
update.disable_auto_show_update_dialog.subtitle=Enable this option to prevent HMCL from automatically showing the update dialog.
|
||||
update.failed=Failed to update
|
||||
@@ -1798,6 +1805,7 @@ update.latest=This is the latest version
|
||||
update.tooltip=Update
|
||||
update.preview=Preview HMCL releases early
|
||||
update.preview.subtitle=Enable this option to receive new versions of HMCL early for testing before their official release.
|
||||
update.unverified=Update unavailable due to this launcher instance not verified
|
||||
|
||||
warning.java_interpreted_mode=HMCL is running in an interpreted Java environment, which will greatly affect performance.\n\
|
||||
\n\
|
||||
|
||||
@@ -47,6 +47,8 @@ about.thanks_to.shulkersakura.statement=لتوفير الشعار الخاص ب
|
||||
about.thanks_to.users=أعضاء مجموعة مستخدمي HMCL
|
||||
about.thanks_to.users.statement=شكراً على التبرعات وتقارير الأخطاء وغير ذلك.
|
||||
about.thanks_to.yushijinhun.statement=لتوفير الدعم المتعلق بـ authlib-injector.
|
||||
# about.thanks_to.8mi-tech=
|
||||
# about.thanks_to.8mi-tech.statement=
|
||||
about.open_source=المصدر المفتوح
|
||||
about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL)
|
||||
|
||||
@@ -76,12 +78,14 @@ account.failed.server_response_malformed=استجابة الخادم غير صا
|
||||
account.failed.ssl=حدث خطأ SSL أثناء الاتصال بالخادم. يُرجى محاولة تحديث Java.
|
||||
account.failed.dns=حدث خطأ SSL أثناء الاتصال بالخادم. قد يكون تحليل DNS غير صحيح. يُرجى تغيير خادم DNS الخاص بك أو استخدام خدمة وكيل.
|
||||
account.failed.wrong_account=لقد سجّلت الدخول إلى حساب خاطئ.
|
||||
# account.storage.read_only=
|
||||
account.injector.add=خادم مصادقة جديد
|
||||
account.injector.empty=لا يوجد (يمكنك النقر على "+" لإضافة واحد)
|
||||
account.injector.http=تحذير: يستخدم هذا الخادم بروتوكول HTTP غير الآمن. أي شخص يعترض اتصالك سيتمكن من رؤية بياناتك بنص واضح.
|
||||
account.injector.link.homepage=الصفحة الرئيسية
|
||||
account.injector.link.register=التسجيل
|
||||
account.injector.server=خادم المصادقة
|
||||
# account.injector.server.storage.read_only=
|
||||
account.injector.server_url=رابط الخادم
|
||||
account.injector.server_name=اسم الخادم
|
||||
account.login=تسجيل الدخول
|
||||
@@ -103,6 +107,7 @@ account.methods.microsoft.close_page=اكتمل تفويض حساب Microsoft.\n
|
||||
account.methods.microsoft.error.add_family=يُرجى النقر <a href="https://support.microsoft.com/account-billing/837badbc-999e-54d2-2617-d19206b9540a">هنا</a> لتغيير تاريخ ميلاد حسابك ليكون فوق 18 عاماً، أو إضافة حسابك إلى عائلة.
|
||||
account.methods.microsoft.error.country_unavailable=XBOX Live غير متاح في بلدك/منطقتك الحالية.
|
||||
account.methods.microsoft.error.missing_xbox_account=لا يوجد حساب XBOX مرتبط بحساب Microsoft الخاص بك بعد. يُرجى النقر <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">هنا</a> لربط واحد.
|
||||
# account.methods.microsoft.error.no_license=
|
||||
account.methods.microsoft.error.no_character=يُرجى التأكد من أنك اشتريت Minecraft: Java Edition.\n\
|
||||
إذا اشتريته بالفعل، فقد لا يكون ملف اللعبة قد تم إنشاؤه. يُرجى النقر <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">هنا</a> لإنشاء ملف اللعبة.
|
||||
account.methods.microsoft.error.banned=قد يكون حسابك محظوراً من XBOX Live.\n\
|
||||
@@ -112,6 +117,7 @@ account.methods.microsoft.error.wrong_verify_method=فشل تسجيل الدخو
|
||||
account.methods.microsoft.hint=انقر على زر "تسجيل الدخول" لبدء إضافة حساب Microsoft الخاص بك.
|
||||
account.methods.microsoft.methods.device=تسجيل الدخول برمز QR
|
||||
account.methods.microsoft.methods.device.hint=امسح رمز QR أو زر <a href="%s">%s</a> لإتمام تسجيل الدخول، وأدخل <b>%s</b> في الصفحة المفتوحة.
|
||||
# account.methods.microsoft.methods.device.hint.completed=
|
||||
account.methods.microsoft.methods.browser=تسجيل الدخول عبر المتصفح
|
||||
account.methods.microsoft.methods.browser.hint=انقر على زر "تسجيل الدخول" أو <a href="%s">انسخ الرابط</a> والصقه في المتصفح لتسجيل الدخول.
|
||||
account.methods.microsoft.manual=<b>إذا كان اتصالك بالإنترنت ضعيفاً، قد يتسبب ذلك في بطء تحميل صفحات الويب أو فشلها كلياً.\nيمكنك المحاولة لاحقاً أو التبديل إلى اتصال إنترنت مختلف.</b>
|
||||
@@ -157,9 +163,12 @@ account.skin.invalid_skin=ملف المظهر غير صالح.
|
||||
account.username=اسم المستخدم
|
||||
|
||||
addon.category=الفئة
|
||||
# addon.changelog=
|
||||
# addon.changelog.empty=
|
||||
addon.channel.alpha=ألفا
|
||||
addon.channel.beta=بيتا
|
||||
addon.channel.release=إصدار رسمي
|
||||
# addon.check_update=
|
||||
addon.check_update.button=تحديث
|
||||
addon.check_update.confirm=تحديث
|
||||
addon.check_update.current_version=الإصدار الحالي
|
||||
@@ -170,7 +179,23 @@ addon.check_update.file=الملف
|
||||
addon.check_update.source=المصدر
|
||||
addon.check_update.target_version=الإصدار المستهدف
|
||||
addon.curseforge=CurseForge
|
||||
# addon.dependency.embedded=
|
||||
# addon.dependency.optional=
|
||||
# addon.dependency.required=
|
||||
# addon.dependency.tool=
|
||||
# addon.dependency.include=
|
||||
# addon.dependency.incompatible=
|
||||
# addon.dependency.broken=
|
||||
# addon.dependencies.has_broken=
|
||||
# addon.download.recommend=
|
||||
# addon.download.title.release=
|
||||
# addon.download.title.snapshot=
|
||||
addon.modrinth=Modrinth
|
||||
addon.sort.date_created=تاريخ الإنشاء
|
||||
addon.sort.last_updated=آخر تحديث
|
||||
addon.sort.popularity=الشعبية
|
||||
# addon.sort.relevancy=
|
||||
addon.sort.total_downloads=إجمالي التنزيلات
|
||||
|
||||
archive.author=المؤلف/المؤلفون
|
||||
archive.date=تاريخ النشر
|
||||
@@ -184,6 +209,7 @@ assets.index.malformed=ملفات فهرس الأصول المنزّلة تال
|
||||
button.cancel=إلغاء
|
||||
button.change_source=تغيير مصدر التنزيل
|
||||
button.clear=مسح
|
||||
# button.copy=
|
||||
button.copy_and_exit=نسخ والخروج
|
||||
button.delete=حذف
|
||||
button.disable=تعطيل
|
||||
@@ -242,6 +268,8 @@ curse.category.4472=تقنية
|
||||
curse.category.4473=سحر
|
||||
curse.category.5128=Vanilla+
|
||||
curse.category.7418=رعب
|
||||
# curse.category.9243=
|
||||
# curse.category.10683=
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
|
||||
curse.category.5299=تعليم
|
||||
@@ -267,6 +295,9 @@ curse.category.419=سحر
|
||||
curse.category.412=تكنولوجيا
|
||||
curse.category.4557=ريدستون
|
||||
curse.category.428=Tinker's Construct
|
||||
# curse.category.8937=
|
||||
# curse.category.9026=
|
||||
# curse.category.10775=
|
||||
# '
|
||||
curse.category.414=نقل اللاعب
|
||||
curse.category.4486=Lucky Blocks
|
||||
@@ -299,6 +330,8 @@ curse.category.4558=ريدستون
|
||||
curse.category.4843=أتمتة
|
||||
curse.category.4906=MCreator
|
||||
curse.category.7669=Twilight Forest
|
||||
# curse.category.9049=
|
||||
# curse.category.10754=
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/12
|
||||
curse.category.5244=حزم خطوط
|
||||
@@ -317,6 +350,7 @@ curse.category.404=متحرك
|
||||
curse.category.4465=دعم المودات
|
||||
curse.category.402=قرون وسطى
|
||||
curse.category.401=عصري
|
||||
# curse.category.8939=
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/17
|
||||
curse.category.4464=عالم المودات
|
||||
@@ -340,12 +374,9 @@ curse.category.4550=مهام
|
||||
curse.category.4555=توليد العالم
|
||||
curse.category.4552=سكريبتات
|
||||
|
||||
curse.sort.author=المؤلف
|
||||
curse.sort.date_created=تاريخ الإنشاء
|
||||
curse.sort.last_updated=آخر تحديث
|
||||
curse.sort.name=الاسم
|
||||
curse.sort.popularity=الشعبية
|
||||
curse.sort.total_downloads=إجمالي التنزيلات
|
||||
# curse.category.6553=
|
||||
# curse.category.6554=
|
||||
# curse.category.6555=
|
||||
|
||||
datetime.format=MMM d, yyyy, h\:mm\:ss a
|
||||
|
||||
@@ -358,9 +389,16 @@ download.failed.empty=لا توجد إصدارات متاحة. انقر هنا
|
||||
download.failed.no_code=فشل التنزيل
|
||||
download.failed.refresh=فشل في جلب قائمة الإصدارات. يرجى النقر هنا لإعادة المحاولة.
|
||||
download.game=New Game
|
||||
# download.provider.bmclapi=
|
||||
# download.provider.bmclapi.desc=
|
||||
# download.provider.mojang=
|
||||
# download.provider.mojang.desc=
|
||||
download.provider.official=من المصادر الرسمية
|
||||
# download.provider.official.desc=
|
||||
download.provider.balanced=من أسرع المصادر المتاحة
|
||||
# download.provider.balanced.desc=
|
||||
download.provider.mirror=من مرآة
|
||||
# download.provider.mirror.desc=
|
||||
download.java=جارٍ تنزيل Java
|
||||
download.java.process=عملية تنزيل Java
|
||||
download.javafx=جارٍ تنزيل مكتبات المشغّل...
|
||||
@@ -395,32 +433,32 @@ extension.resourcepack=أرشيف حزمة الموارد
|
||||
extension.schematic=ملف مخطط
|
||||
extension.world=أرشيف العالم
|
||||
|
||||
fatal.create_hmcl_current_directory_failure=لا يستطيع Hello Minecraft! Launcher إنشاء مجلد HMCL (%s). يُرجى نقل HMCL إلى موقع آخر وإعادة تشغيله.
|
||||
fatal.create_hmcl_current_directory_failure=لا يستطيع HMCL إنشاء مجلد HMCL (%s). يُرجى نقل HMCL إلى موقع آخر وإعادة تشغيله.
|
||||
fatal.javafx.incomplete=بيئة JavaFX غير مكتملة.\n\
|
||||
يُرجى محاولة استبدال Java أو إعادة تثبيت OpenJFX.
|
||||
fatal.javafx.missing=بيئة JavaFX مفقودة. يُرجى فتح Hello Minecraft! Launcher باستخدام Java الذي يتضمن OpenJFX.
|
||||
fatal.config_change_owner_root=أنت تستخدم حساب الجذر لفتح Hello Minecraft! Launcher. قد يمنعك ذلك من فتح HMCL بحساب آخر في المستقبل.\n\
|
||||
fatal.javafx.missing=بيئة JavaFX مفقودة. يُرجى فتح HMCL باستخدام Java الذي يتضمن OpenJFX.
|
||||
fatal.config_change_owner_root=أنت تستخدم حساب الجذر لفتح HMCL. قد يمنعك ذلك من فتح HMCL بحساب آخر في المستقبل.\n\
|
||||
هل تريد المتابعة؟
|
||||
fatal.config_in_temp_dir=أنت تفتح Hello Minecraft! Launcher في مجلد مؤقت. قد تُفقد إعداداتك وبيانات اللعبة.\n\
|
||||
fatal.config_in_temp_dir=أنت تفتح HMCL في مجلد مؤقت. قد تُفقد إعداداتك وبيانات اللعبة.\n\
|
||||
يُنصح بنقل HMCL إلى موقع آخر وإعادة تشغيله.\n\
|
||||
هل تريد المتابعة؟
|
||||
fatal.config_loading_failure=لا يمكن تحميل ملفات الإعدادات.\n\
|
||||
يُرجى التأكد من أن Hello Minecraft! Launcher يملك صلاحيات القراءة والكتابة على "%s" والملفات الموجودة فيه.\n\
|
||||
يُرجى التأكد من أن HMCL يملك صلاحيات القراءة والكتابة على "%s" والملفات الموجودة فيه.\n\
|
||||
لمستخدمي macOS، جرّب وضع HMCL في موقع بصلاحيات غير "سطح المكتب" و"التنزيلات" و"المستندات" وأعد المحاولة.
|
||||
fatal.config_loading_failure.unix=لا يستطيع Hello Minecraft! Launcher تحميل ملف الإعدادات لأنه أُنشئ بواسطة المستخدم "%1$s".\n\
|
||||
fatal.config_loading_failure.unix=لا يستطيع HMCL تحميل ملف الإعدادات لأنه أُنشئ بواسطة المستخدم "%1$s".\n\
|
||||
يُرجى فتح HMCL كمستخدم جذر (غير موصى به)، أو تنفيذ الأمر التالي في الطرفية لتغيير ملكية ملف الإعدادات إلى المستخدم الحالي:\n%2$s
|
||||
fatal.config_unsupported_version=ملف الإعدادات الحالي أُنشئ بواسطة إصدار أحدث من Hello Minecraft! Launcher، ولا يستطيع هذا الإصدار من HMCL تحميله بشكل صحيح.\n\
|
||||
fatal.config_unsupported_version=ملف الإعدادات الحالي أُنشئ بواسطة إصدار أحدث من HMCL، ولا يستطيع هذا الإصدار من HMCL تحميله بشكل صحيح.\n\
|
||||
يُرجى تحديث HMCL وإعادة تشغيله.\n\
|
||||
قبل تحديث المشغّل، لن يتم حفظ أي إعدادات تقوم بتعديلها.
|
||||
fatal.mac_app_translocation=تم عزل Hello Minecraft! Launcher في مجلد مؤقت بواسطة نظام التشغيل بسبب آليات أمان macOS.\n\
|
||||
fatal.mac_app_translocation=تم عزل HMCL في مجلد مؤقت بواسطة نظام التشغيل بسبب آليات أمان macOS.\n\
|
||||
يُرجى نقل HMCL إلى مجلد آخر قبل محاولة الفتح. وإلا، قد تُفقد إعداداتك وبيانات اللعبة بعد إعادة التشغيل.\n\
|
||||
هل تريد المتابعة؟
|
||||
fatal.migration_requires_manual_reboot=تم ترقية Hello Minecraft! Launcher. يُرجى إعادة تشغيل المشغّل.
|
||||
fatal.apply_update_failure=نأسف، لكن Hello Minecraft! Launcher غير قادر على التحديث التلقائي.\n\
|
||||
fatal.migration_requires_manual_reboot=تم ترقية HMCL. يُرجى إعادة تشغيل المشغّل.
|
||||
fatal.apply_update_failure=نأسف، لكن HMCL غير قادر على التحديث التلقائي.\n\
|
||||
\n\
|
||||
يمكنك التحديث يدوياً بتنزيل إصدار أحدث من المشغّل من %s.\n\
|
||||
إذا استمرت المشكلة، يُرجى إرسال تقرير عنها إلينا.
|
||||
fatal.apply_update_need_win7=لا يستطيع Hello Minecraft! Launcher التحديث التلقائي على Windows XP/Vista.\n\
|
||||
fatal.apply_update_need_win7=لا يستطيع HMCL التحديث التلقائي على Windows XP/Vista.\n\
|
||||
\n\
|
||||
يمكنك التحديث يدوياً بتنزيل إصدار أحدث من المشغّل من %s.
|
||||
fatal.deprecated_java_version=سيتطلب HMCL Java 17 أو أحدث للتشغيل في المستقبل، لكنه سيستمر في دعم تشغيل الألعاب باستخدام Java 6~16.\n\
|
||||
@@ -429,23 +467,25 @@ fatal.deprecated_java_version=سيتطلب HMCL Java 17 أو أحدث للتشغ
|
||||
\n\
|
||||
يمكنك الاحتفاظ بإصدار Java القديم. يستطيع HMCL التعرف على إصدارات Java المتعددة وإدارتها وسيختار تلقائياً Java المناسب بناءً على إصدار اللعبة.
|
||||
fatal.deprecated_java_version.download_link=تنزيل Java %d
|
||||
fatal.samba=إذا فتحت Hello Minecraft! Launcher من محرك أقراص شبكة Samba، قد لا تعمل بعض الميزات. يُرجى محاولة تحديث Java أو نقل المشغّل إلى مجلد آخر.
|
||||
fatal.samba=إذا فتحت HMCL من محرك أقراص شبكة Samba، قد لا تعمل بعض الميزات. يُرجى محاولة تحديث Java أو نقل المشغّل إلى مجلد آخر.
|
||||
fatal.illegal_char=مسار المستخدم الخاص بك يحتوي على حرف غير مسموح به "=". لن تتمكن من استخدام authlib-injector أو تغيير مظهر حسابك غير المتصل.
|
||||
fatal.unsupported_platform=لا يدعم Minecraft منصتك الحالية بشكل كامل بعد، لذا قد تواجه ميزات مفقودة أو عدم القدرة على تشغيل اللعبة.\n\
|
||||
\n\
|
||||
إذا لم تتمكن من تشغيل Minecraft 1.17 أو أحدث، يمكنك محاولة تغيير "المُصيِّر" إلى "Mesa LLVMpipe" في "الإعدادات العامة/الخاصة بالنسخة ← الإعدادات المتقدمة" لاستخدام معالجة المعالج المركزي لتوافق أفضل.
|
||||
fatal.unsupported_platform.loongarch=وفّر Hello Minecraft! Launcher دعماً لمنصة Loongson.\n\
|
||||
fatal.unsupported_platform.loongarch=وفّر HMCL دعماً لمنصة Loongson.\n\
|
||||
إذا واجهت مشاكل أثناء اللعب، يمكنك زيارة https://docs.hmcl.net/groups.html للحصول على مساعدة.
|
||||
fatal.unsupported_platform.windows_arm64=وفّر Hello Minecraft! Launcher دعماً أصلياً لمنصة Windows on Arm. إذا واجهت مشاكل أثناء اللعب، يُرجى محاولة تشغيل اللعبة بـ Java المبني على معمارية x86.\n\
|
||||
fatal.unsupported_platform.windows_arm64=وفّر HMCL دعماً أصلياً لمنصة Windows on Arm. إذا واجهت مشاكل أثناء اللعب، يُرجى محاولة تشغيل اللعبة بـ Java المبني على معمارية x86.\n\
|
||||
\n\
|
||||
إذا كنت تستخدم منصة <b>Qualcomm</b>، قد تحتاج إلى تثبيت <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">حزمة توافق OpenGL</a> قبل اللعب.\n\
|
||||
انقر على الرابط للانتقال إلى Microsoft Store وتثبيت حزمة التوافق.
|
||||
# fatal.wine_warning=
|
||||
|
||||
file=ملف
|
||||
|
||||
folder.config=الإعدادات
|
||||
folder.game=مجلد العمل
|
||||
folder.logs=السجلات
|
||||
# folder.crash-reports=
|
||||
folder.mod=المودات
|
||||
folder.resourcepacks=حزم الموارد
|
||||
folder.shaderpacks=حزم الظلال
|
||||
@@ -691,9 +731,10 @@ game_directory.instance_directory.choose=اختر مجلد اللعبة
|
||||
game_directory.name=الاسم
|
||||
game_directory.new=مجلد جديد
|
||||
game_directory.use_relative_path=استخدام مسار نسبي لمجلد اللعبة إن أمكن
|
||||
# game_directory.root=
|
||||
|
||||
help=مساعدة
|
||||
help.doc=توثيق Hello Minecraft! Launcher
|
||||
help.doc=توثيق HMCL
|
||||
help.detail=لصانعي حزم البيانات وحزم المودات.
|
||||
|
||||
input.email=يجب أن يكون اسم المستخدم بريداً إلكترونياً.
|
||||
@@ -703,6 +744,7 @@ input.url=يجب أن يكون الإدخال رابطاً صالحاً.
|
||||
|
||||
install=نسخة جديدة
|
||||
install.change_version=تغيير الإصدار
|
||||
# install.change_version.title=
|
||||
install.change_version.confirm=هل أنت متأكد من تغيير %1$s من الإصدار %2$s إلى %3$s؟
|
||||
install.change_version.process=عملية تغيير الإصدار
|
||||
install.failed=فشل التثبيت
|
||||
@@ -711,6 +753,7 @@ install.failed.downloading.detail=فشل تنزيل الملف: %s
|
||||
install.failed.downloading.timeout=انتهت مهلة التنزيل عند جلب: %s
|
||||
install.failed.install_online=فشل التعرف على الملف المقدم. إذا كنت تثبّت مود، انتقل إلى صفحة "المودات".
|
||||
install.failed.malformed=الملفات المنزّلة تالفة. يمكنك محاولة حل هذه المشكلة بالتبديل إلى مصدر تنزيل آخر في "الإعدادات ← التنزيل ← مصدر التنزيل".
|
||||
# install.failed.cleanroom_not_compatible_with_forge=
|
||||
install.failed.optifine_conflict=لا يمكن تثبيت كل من OptiFine وFabric معاً على Minecraft 1.13 أو أحدث.
|
||||
install.failed.optifine_forge_1.17=في Minecraft 1.17.1، يتوافق Forge فقط مع OptiFine H1 pre2 أو أحدث. يمكنك تثبيتها بتفعيل "الإصدارات التجريبية" عند اختيار إصدار OptiFine في HMCL.
|
||||
install.failed.version_mismatch=يتطلب هذا المحمّل إصدار اللعبة %1$s، لكن الإصدار المثبت هو %2$s.
|
||||
@@ -744,6 +787,7 @@ install.new_game=تثبيت نسخة
|
||||
install.new_game.already_exists=اسم هذه النسخة موجود مسبقاً. يُرجى اختيار اسم آخر.
|
||||
install.new_game.installation=تثبيت النسخة
|
||||
install.new_game.malformed=اسم غير صالح.
|
||||
# install.new_game.malformed_json=
|
||||
install.success=تم التثبيت بنجاح.
|
||||
|
||||
instance=الألعاب
|
||||
@@ -844,6 +888,8 @@ launch.advice.forge28_2_2_optifine=إصدار Forge 28.2.2 أو أحدث غير
|
||||
launch.advice.forge37_0_60=إصدارات Forge قبل 37.0.60 غير متوافقة مع Java 17. يُرجى تحديث Forge إلى 37.0.60 أو أحدث، أو تشغيل اللعبة بـ Java 16.
|
||||
launch.advice.java8_51_1_13=قد يتعطل Minecraft 1.13 على إصدارات Java 8 قبل 1.8.0_51. يُرجى تثبيت أحدث إصدار من Java 8.
|
||||
launch.advice.java9=لا يمكن تشغيل Minecraft 1.12 أو أقدم على Java 9 أو أحدث. يُرجى استخدام Java 8.
|
||||
# launch.advice.lwjgl_3_4_1=
|
||||
# launch.advice.lwjgl_3_4_1.title=
|
||||
launch.advice.modded_java=قد لا تكون بعض المودات متوافقة مع إصدارات Java الأحدث. يُنصح باستخدام Java %1$s لتشغيل Minecraft %2$s.
|
||||
launch.advice.modlauncher8=إصدار Forge الذي تستخدمه غير متوافق مع إصدار Java الحالي. يُرجى محاولة تحديث Forge.
|
||||
launch.advice.newer_java=أنت تستخدم إصداراً قديماً من Java لتشغيل اللعبة. يُنصح بالتحديث إلى Java 8، وإلا قد تتسبب بعض المودات في تعطل اللعبة.
|
||||
@@ -880,13 +926,26 @@ launcher.agreement=الشروط واتفاقية الترخيص
|
||||
launcher.agreement.accept=قبول
|
||||
launcher.agreement.decline=رفض
|
||||
launcher.agreement.hint=يجب الموافقة على اتفاقية الترخيص لاستخدام هذا البرنامج.
|
||||
# launcher.april_fools.switch_lzh=
|
||||
# launcher.april_fools.switch_lzh.confirm=
|
||||
launcher.background=صورة الخلفية
|
||||
launcher.background.choose=اختر صورة الخلفية
|
||||
# launcher.background.builtin=
|
||||
launcher.background.default=افتراضي
|
||||
launcher.background.default.tooltip=أو "background.png/.jpg/.gif/.webp" والصور في مجلد "bg"
|
||||
# launcher.background.fallback=
|
||||
# launcher.background.fallback.builtin=
|
||||
# launcher.background.fallback.paint=
|
||||
# launcher.background.fallback.theme_color=
|
||||
# launcher.background.load_policy=
|
||||
# launcher.background.load_policy.show_fallback_while_loading=
|
||||
# launcher.background.load_policy.wait_for_background=
|
||||
# launcher.background.loading=
|
||||
launcher.background.network=من رابط
|
||||
# launcher.background.network.cache=
|
||||
launcher.background.paint=لون خالص
|
||||
launcher.background.theme=اتبع السمة
|
||||
# launcher.background.theme_color=
|
||||
launcher.cache_directory=مجلد الذاكرة المؤقتة
|
||||
launcher.cache_directory.clean=مسح الذاكرة المؤقتة
|
||||
launcher.cache_directory.choose=اختر مجلد الذاكرة المؤقتة
|
||||
@@ -894,20 +953,22 @@ launcher.cache_directory.default=افتراضي ("%APPDATA%/.minecraft" أو "~/
|
||||
launcher.cache_directory.disabled=معطّل
|
||||
launcher.cache_directory.invalid=فشل إنشاء مجلد الذاكرة المؤقتة، جارٍ الرجوع إلى الافتراضي.
|
||||
launcher.contact=تواصل معنا
|
||||
launcher.crash=واجه Hello Minecraft! Launcher خطأً فادحاً! يُرجى نسخ السجل التالي وطلب المساعدة على Discord أو مجموعة QQ أو GitHub أو أي منتدى Minecraft آخر.
|
||||
launcher.crash.java_internal_error=واجه Hello Minecraft! Launcher خطأً فادحاً بسبب تلف Java. يُرجى إلغاء تثبيت Java وتنزيل إصدار مناسب من <a href="https://bell-sw.com/pages/downloads/#downloads">هنا</a>.
|
||||
launcher.crash.hmcl_out_dated=واجه Hello Minecraft! Launcher خطأً فادحاً! المشغّل قديم. يُرجى تحديثه!
|
||||
launcher.crash=واجه HMCL خطأً فادحاً! يُرجى نسخ السجل التالي وطلب المساعدة على Discord أو مجموعة QQ أو GitHub أو أي منتدى Minecraft آخر.
|
||||
launcher.crash.java_internal_error=واجه HMCL خطأً فادحاً بسبب تلف Java. يُرجى إلغاء تثبيت Java وتنزيل إصدار مناسب من <a href="https://bell-sw.com/pages/downloads/#downloads">هنا</a>.
|
||||
launcher.crash.hmcl_out_dated=واجه HMCL خطأً فادحاً! المشغّل قديم. يُرجى تحديثه!
|
||||
launcher.update_java=يُرجى تحديث إصدار Java الخاص بك.
|
||||
|
||||
libraries.download=جارٍ تنزيل المكتبات
|
||||
|
||||
login.enter_password=يُرجى إدخال كلمة المرور.
|
||||
|
||||
# logwindow.always_on_top=
|
||||
logwindow.show_lines=إظهار أرقام الأسطر
|
||||
logwindow.terminate_game=إنهاء عملية اللعبة
|
||||
logwindow.title=السجل
|
||||
logwindow.help=يمكنك الانتقال إلى مجتمع HMCL وإيجاد مساعدة من الآخرين.
|
||||
logwindow.autoscroll=التمرير التلقائي
|
||||
# logwindow.wrap_text=
|
||||
logwindow.export_game_crash_logs=تصدير سجلات التعطل
|
||||
logwindow.export_dump=تصدير تفريغ مكدس اللعبة
|
||||
logwindow.export_dump.no_dependency=لا يحتوي Java الخاص بك على المكتبات اللازمة لإنشاء تفريغ المكدس. يُرجى الانضمام إلى Discord أو مجموعة QQ للحصول على مساعدة.
|
||||
@@ -947,8 +1008,10 @@ modpack.file_api=بادئة رابط حزمة المودات
|
||||
modpack.files.blueprints=مخططات BuildCraft
|
||||
modpack.files.config=ملفات إعداد المودات
|
||||
modpack.files.dumps=ملفات تصحيح NEI
|
||||
# modpack.files.empty=
|
||||
modpack.files.hmclversion_cfg=ملف إعداد المشغّل
|
||||
modpack.files.liteconfig=ملفات LiteLoader
|
||||
# modpack.files.load_failed=
|
||||
modpack.files.mods=المودات
|
||||
modpack.files.mods.voxelmods=خيارات VoxelMods
|
||||
modpack.files.options_txt=ملف خيارات Minecraft
|
||||
@@ -961,6 +1024,9 @@ modpack.installing=جارٍ تثبيت حزمة المودات
|
||||
modpack.installing.given=جارٍ تثبيت حزمة المودات %s
|
||||
modpack.invalid=حزمة مودات غير صالحة، يمكنك محاولة تنزيلها مجدداً.
|
||||
modpack.mismatched_type=نوع حزمة المودات غير متطابق، النسخة الحالية من نوع %1$s، لكن الحزمة المقدمة من نوع %2$s.
|
||||
modpack.optional_files=ملفات اختيارية
|
||||
modpack.retry_optional_files=فشل تحميل الملفات الاختيارية. انقر لإعادة المحاولة.
|
||||
modpack.unknown_optional_file=ملف غير معروف
|
||||
modpack.name=اسم حزمة المودات
|
||||
modpack.origin=المصدر
|
||||
modpack.origin.url=الموقع الرسمي
|
||||
@@ -974,11 +1040,11 @@ modpack.type.curse.error=فشل تنزيل التبعيات. يُرجى المح
|
||||
modpack.type.curse.not_found=بعض التبعيات لم تعد متاحة. يُرجى محاولة تثبيت إصدار أحدث من الحزمة.
|
||||
modpack.type.manual.warning=هذه الحزمة معبّأة يدوياً من قِبل الناشر، وقد تحتوي بالفعل على مشغّل. يُنصح بمحاولة فك ضغط الحزمة وتشغيل اللعبة بمشغّلها الخاص. يستطيع HMCL استيرادها مع عدم ضمان قابليتها للاستخدام. هل تريد المتابعة؟
|
||||
modpack.type.mcbbs=MCBBS
|
||||
modpack.type.mcbbs.export=يمكن استيرادها بواسطة Hello Minecraft! Launcher
|
||||
modpack.type.mcbbs.export=يمكن استيرادها بواسطة HMCL
|
||||
modpack.type.modrinth=Modrinth
|
||||
modpack.type.modrinth.export=يمكن استيرادها بواسطة مشغّلات شائعة من أطراف ثالثة
|
||||
modpack.type.multimc=MultiMC
|
||||
modpack.type.multimc.export=يمكن استيرادها بواسطة Hello Minecraft! Launcher وMultiMC
|
||||
modpack.type.multimc.export=يمكن استيرادها بواسطة HMCL وMultiMC
|
||||
modpack.type.server=تحديث تلقائي لحزمة المودات من الخادم
|
||||
modpack.type.server.export=يتيح لمالك الخادم تحديث نسخة اللعبة عن بُعد
|
||||
modpack.type.server.malformed=ملف بيانات الحزمة غير صالح. يُرجى التواصل مع صانع الحزمة لحل هذه المشكلة.
|
||||
@@ -1105,6 +1171,7 @@ mods.add.title=اختر ملف المود الذي تريد إضافته
|
||||
mods.disable=تعطيل
|
||||
mods.download=تنزيل مود
|
||||
mods.download.title=تنزيل مود - %1s
|
||||
# mods.empty=
|
||||
mods.enable=تفعيل
|
||||
mods.game.version=إصدار اللعبة
|
||||
mods.manage=المودات
|
||||
@@ -1137,6 +1204,7 @@ nbt.title=عرض الملف - %s
|
||||
datapack=حزم البيانات
|
||||
datapack.add=إضافة
|
||||
datapack.add.title=اختر أرشيف حزمة البيانات التي تريد إضافتها
|
||||
# datapack.empty=
|
||||
datapack.reload.toast=اللعبة تعمل حالياً، يُرجى استخدام الأمر /reload لإعادة تحميل حزمة البيانات
|
||||
datapack.title=العالم [%s] - حزم البيانات
|
||||
|
||||
@@ -1155,6 +1223,7 @@ world.backup.create.new_one=نسخة احتياطية جديدة
|
||||
world.backup.create.failed=فشل إنشاء النسخة الاحتياطية.\n%s
|
||||
world.backup.create.success=تم إنشاء نسخة احتياطية جديدة بنجاح: %s
|
||||
world.backup.delete=حذف هذه النسخة الاحتياطية
|
||||
# world.backup.empty=
|
||||
world.backup.processing=جارٍ النسخ الاحتياطي...
|
||||
world.chunkbase=Chunk Base
|
||||
world.chunkbase.end_city=مدينة النهاية
|
||||
@@ -1173,6 +1242,7 @@ world.delete=حذف العالم
|
||||
world.delete.failed=فشل حذف العالم.\n%s
|
||||
world.download=تنزيل
|
||||
world.download.title=تنزيل العالم - %1s
|
||||
# world.empty=
|
||||
world.export=تصدير العالم
|
||||
world.export.title=اختر المجلد لهذا العالم المُصدَّر
|
||||
world.export.location=حفظ باسم
|
||||
@@ -1224,6 +1294,7 @@ world.load.fail=فشل تحميل العالم
|
||||
world.locked=قيد الاستخدام
|
||||
world.locked.failed=العالم قيد الاستخدام حالياً. يُرجى إغلاق اللعبة والمحاولة مجدداً.
|
||||
world.manage=العوالم
|
||||
# world.manage.button=
|
||||
world.manage.title=العالم - %s
|
||||
world.name=اسم العالم
|
||||
world.name.enter=أدخل اسم العالم
|
||||
@@ -1241,12 +1312,20 @@ repositories.chooser.title=اختر مصدر تنزيل JavaFX
|
||||
|
||||
resourcepack=حزم الموارد
|
||||
resourcepack.add=إضافة
|
||||
resourcepack.add.title=اختر أرشيف حزمة الموارد التي تريد إضافتها
|
||||
resourcepack.manage=حزم الموارد
|
||||
resourcepack.download=تنزيل
|
||||
resourcepack.add.failed=فشل إضافة حزمة الموارد
|
||||
resourcepack.add.title=اختر أرشيف حزمة الموارد التي تريد إضافتها
|
||||
resourcepack.delete.failed=فشل حذف حزمة الموارد
|
||||
resourcepack.download=تنزيل
|
||||
resourcepack.download.title=تنزيل حزمة موارد - %1s
|
||||
# resourcepack.empty=
|
||||
resourcepack.manage=حزم الموارد
|
||||
# resourcepack.update_in_modpack.warning=
|
||||
# resourcepack.warning.invalid=
|
||||
# resourcepack.warning.manipulate=
|
||||
# resourcepack.warning.missing_game_meta=
|
||||
# resourcepack.warning.missing_pack_meta=
|
||||
# resourcepack.warning.too_new=
|
||||
# resourcepack.warning.too_old=
|
||||
|
||||
reveal.in_file_manager=فتح في مدير الملفات
|
||||
|
||||
@@ -1254,12 +1333,12 @@ schematics=المخططات
|
||||
schematics.add=إضافة
|
||||
schematics.add.failed=فشل إضافة ملفات المخططات
|
||||
schematics.add.title=اختر ملف المخطط الذي تريد استيراده
|
||||
schematics.back_to=العودة إلى "%s"
|
||||
schematics.create_directory=مجلد جديد
|
||||
schematics.create_directory.prompt=يُرجى إدخال اسم المجلد الجديد
|
||||
schematics.create_directory.failed=فشل إنشاء المجلد
|
||||
schematics.create_directory.failed.already_exists=المجلد موجود مسبقاً
|
||||
schematics.create_directory.failed.invalid_name=الاسم يحتوي على أحرف غير صالحة
|
||||
# schematics.empty=
|
||||
schematics.info.description=الوصف
|
||||
schematics.info.enclosing_size=الحجم المحيط
|
||||
schematics.info.name=الاسم
|
||||
@@ -1269,7 +1348,13 @@ schematics.info.time_created=وقت الإنشاء
|
||||
schematics.info.time_modified=وقت التعديل
|
||||
schematics.info.total_blocks=إجمالي البلوكات
|
||||
schematics.info.total_volume=الحجم الكلي
|
||||
# schematics.info.type=
|
||||
# schematics.info.type.litematic=
|
||||
# schematics.info.type.nbt_structure=
|
||||
# schematics.info.type.schem=
|
||||
schematics.info.version=إصدار المخطط
|
||||
# schematics.info.mc_data_version=
|
||||
# schematics.install_mod=
|
||||
schematics.manage=المخططات
|
||||
schematics.sub_items=%d عنصر فرعي
|
||||
|
||||
@@ -1281,11 +1366,13 @@ search.sort=ترتيب حسب
|
||||
search.first_page=الأولى
|
||||
search.previous_page=السابقة
|
||||
search.next_page=التالية
|
||||
# search.no_results_found=
|
||||
search.last_page=الأخيرة
|
||||
search.page_n=%1$d / %2$s
|
||||
|
||||
selector.choose=اختر
|
||||
selector.choose_file=اختر ملفاً
|
||||
# selector.choose_directory=
|
||||
selector.custom=مخصص
|
||||
|
||||
settings=الإعدادات
|
||||
@@ -1298,14 +1385,30 @@ settings.advanced.dont_check_game_completeness=عدم التحقق من سلام
|
||||
settings.advanced.dont_check_jvm_validity=عدم التحقق من توافق JVM
|
||||
settings.advanced.dont_patch_natives=عدم محاولة استبدال المكتبات الأصلية تلقائياً
|
||||
settings.advanced.environment_variables=متغيرات البيئة
|
||||
# settings.advanced.environment_variables.subtitle=
|
||||
settings.advanced.game_dir.default=افتراضي (".minecraft/")
|
||||
settings.advanced.game_dir.independent=معزول (".minecraft/versions/<اسم النسخة>/", باستثناء الأصول والمكتبات)
|
||||
# settings.advanced.graphics=
|
||||
# settings.advanced.graphics_backend=
|
||||
# settings.advanced.graphics_backend.desc=
|
||||
# settings.advanced.graphics_backend.default=
|
||||
# settings.advanced.graphics_backend.default.desc=
|
||||
# settings.advanced.graphics_backend.opengl=
|
||||
# settings.advanced.graphics_backend.opengl.desc=
|
||||
# settings.advanced.graphics_backend.vulkan=
|
||||
# settings.advanced.graphics_backend.vulkan.desc=
|
||||
# settings.advanced.graphics_backend.vulkan.desc.global=
|
||||
# settings.advanced.graphics_backend.vulkan.desc.unsupported=
|
||||
settings.advanced.java_permanent_generation_space=مساحة PermGen
|
||||
settings.advanced.jvm=خيارات JVM
|
||||
settings.advanced.jvm_args=وسيطات JVM
|
||||
settings.advanced.jvm_args.prompt=\ · إذا كانت الوسيطات المُدخلة في "وسيطات JVM" مطابقة للوسيطات الافتراضية، فلن تُضاف.\n\
|
||||
\ · أدخل أي وسيطات GC في "وسيطات JVM"، وسيتم تعطيل وسيطة G1 الافتراضية.\n\
|
||||
\ · فعّل "عدم إضافة وسيطات JVM الافتراضية" لتشغيل اللعبة بدون إضافة الوسيطات الافتراضية.
|
||||
# settings.advanced.jvm_memory.deprecated=
|
||||
# settings.advanced.jvm_memory.deprecated.subtitle=
|
||||
# settings.advanced.launch_options=
|
||||
# settings.advanced.launch_options.subtitle=
|
||||
settings.advanced.launcher_visibility.close=إغلاق المشغّل بعد تشغيل اللعبة
|
||||
settings.advanced.launcher_visibility.hide=إخفاء المشغّل بعد تشغيل اللعبة
|
||||
settings.advanced.launcher_visibility.hide_and_reopen=إخفاء المشغّل وإظهاره عند إغلاق اللعبة
|
||||
@@ -1313,7 +1416,9 @@ settings.advanced.launcher_visibility.keep=إبقاء المشغّل ظاهرا
|
||||
settings.advanced.launcher_visible=ظهور المشغّل
|
||||
settings.advanced.minecraft_arguments=وسيطات التشغيل
|
||||
settings.advanced.minecraft_arguments.prompt=افتراضي
|
||||
# settings.advanced.natives_settings=
|
||||
settings.advanced.natives_directory=مسار المكتبات الأصلية
|
||||
# settings.advanced.natives_directory.custom.enabled=
|
||||
settings.advanced.no_jvm_args=عدم إضافة وسيطات JVM الافتراضية
|
||||
settings.advanced.no_optimizing_jvm_args=عدم إضافة وسيطات تحسين JVM الافتراضية
|
||||
settings.advanced.precall_command=أمر ما قبل التشغيل
|
||||
@@ -1333,15 +1438,56 @@ settings.advanced.post_exit_command=أمر ما بعد الإغلاق
|
||||
settings.advanced.post_exit_command.prompt=أوامر تُنفَّذ بعد إغلاق اللعبة
|
||||
settings.advanced.renderer=المُصيِّر
|
||||
settings.advanced.renderer.default=افتراضي
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
# settings.advanced.renderer.default.desc=
|
||||
# settings.advanced.renderer.opengl=
|
||||
# settings.advanced.renderer.vulkan=
|
||||
# settings.advanced.renderer.gpu_preferences=
|
||||
# settings.advanced.renderer.lavapipe=
|
||||
# settings.advanced.renderer.lavapipe.desc=
|
||||
# settings.advanced.renderer.dozen=
|
||||
# settings.advanced.renderer.dozen.desc=
|
||||
# settings.advanced.renderer.nvidia_vulkan=
|
||||
# settings.advanced.renderer.nvidia_vulkan.desc=
|
||||
# settings.advanced.renderer.nvidia_nvk=
|
||||
# settings.advanced.renderer.nvidia_nvk.desc=
|
||||
# settings.advanced.renderer.amdvlk=
|
||||
# settings.advanced.renderer.amdvlk.desc=
|
||||
# settings.advanced.renderer.amd_radv=
|
||||
# settings.advanced.renderer.amd_radv.desc=
|
||||
# settings.advanced.renderer.intel_vulkan=
|
||||
# settings.advanced.renderer.intel_vulkan.desc=
|
||||
# settings.advanced.renderer.intel_anv=
|
||||
# settings.advanced.renderer.intel_anv.desc=
|
||||
# settings.advanced.renderer.intel_hasvk=
|
||||
# settings.advanced.renderer.intel_hasvk.desc=
|
||||
# settings.advanced.renderer.qualcomm=
|
||||
# settings.advanced.renderer.qualcomm.desc=
|
||||
# settings.advanced.renderer.turnip=
|
||||
# settings.advanced.renderer.turnip.desc=
|
||||
# settings.advanced.renderer.moltenvk=
|
||||
# settings.advanced.renderer.moltenvk.desc=
|
||||
# settings.advanced.renderer.kosmickrisp=
|
||||
# settings.advanced.renderer.kosmickrisp.desc=
|
||||
# settings.advanced.renderer.powervr=
|
||||
# settings.advanced.renderer.powervr.desc=
|
||||
# settings.advanced.renderer.panvk=
|
||||
# settings.advanced.renderer.panvk.desc=
|
||||
# settings.advanced.renderer.v3dv=
|
||||
# settings.advanced.renderer.v3dv.desc=
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
# settings.advanced.renderer.llvmpipe.desc=
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
# settings.advanced.renderer.d3d12.desc=
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
# settings.advanced.renderer.zink.desc=
|
||||
settings.advanced.server_ip=عنوان الخادم
|
||||
settings.advanced.server_ip.prompt=الانضمام تلقائياً بعد تشغيل اللعبة
|
||||
settings.advanced.unsupported_system_options=إعدادات غير مطبّقة على النظام الحالي
|
||||
settings.advanced.use_native_glfw_or_sdl=استخدام GLFW / SDL النظام
|
||||
settings.advanced.use_native_openal=استخدام OpenAL النظام
|
||||
# settings.advanced.use_native_sdl=
|
||||
settings.advanced.linux_freebsd_only=Linux/FreeBSD فقط
|
||||
# settings.advanced.windows_only=
|
||||
settings.advanced.wrapper_launcher=أمر التغليف
|
||||
settings.advanced.wrapper_launcher.prompt=يتيح التشغيل باستخدام برنامج تغليف إضافي مثل "optirun" على Linux
|
||||
|
||||
@@ -1349,9 +1495,22 @@ settings.custom=مخصص
|
||||
|
||||
settings.game=الإعدادات
|
||||
settings.game.current=اللعبة
|
||||
# settings.game.default_isolation=
|
||||
# settings.game.default_isolation.subtitle=
|
||||
# settings.game.default_isolation.always=
|
||||
# settings.game.default_isolation.always.desc=
|
||||
# settings.game.default_isolation.modded=
|
||||
# settings.game.default_isolation.modded.desc=
|
||||
# settings.game.default_isolation.never=
|
||||
# settings.game.default_isolation.never.desc=
|
||||
settings.game.dimension=الدقة
|
||||
settings.game.exploration=استكشاف
|
||||
settings.game.fullscreen=ملء الشاشة
|
||||
# settings.game.inherit_global=
|
||||
# settings.game.instance_settings.unsupported=
|
||||
# settings.game.presets.unsupported=
|
||||
# settings.game.isolation=
|
||||
# settings.game.isolation.subtitle=
|
||||
settings.game.java_directory=Java
|
||||
settings.game.java_directory.auto=اختيار تلقائي
|
||||
settings.game.java_directory.auto.not_found=لم يتم تثبيت أي إصدار Java مناسب.
|
||||
@@ -1361,28 +1520,51 @@ settings.game.java_directory.invalid=مسار Java غير صحيح
|
||||
settings.game.java_directory.version=تحديد إصدار Java
|
||||
settings.game.java_directory.template=%1$s (%2$s)
|
||||
settings.game.management=إدارة
|
||||
# settings.game.override_global=
|
||||
# settings.game.quick_play=
|
||||
# settings.game.quick_play.multiplayer=
|
||||
# settings.game.quick_play.none=
|
||||
# settings.game.quick_play.realms=
|
||||
# settings.game.quick_play.singleplayer=
|
||||
# settings.game.quick_play.subtitle=
|
||||
# settings.game.running_directory=
|
||||
# settings.game.section.basic=
|
||||
# settings.game.section.game=
|
||||
# settings.game.window_type=
|
||||
# settings.game.window_type.fullscreen=
|
||||
# settings.game.window_type.maximized=
|
||||
# settings.game.window_type.windowed=
|
||||
settings.game.working_directory.choose=اختر مجلد العمل
|
||||
|
||||
settings.icon=الأيقونة
|
||||
# settings.game_directories.read_only=
|
||||
|
||||
settings.launcher=إعدادات المشغّل
|
||||
# settings.launcher.allow_auto_agent=
|
||||
# settings.launcher.allow_auto_agent.subtitle=
|
||||
# settings.launcher.animation=
|
||||
settings.launcher.appearance=المظهر
|
||||
settings.launcher.brightness=وضع السمة
|
||||
settings.launcher.brightness.auto=متابعة إعدادات النظام
|
||||
settings.launcher.brightness.dark=الوضع الداكن
|
||||
settings.launcher.brightness.light=الوضع الفاتح
|
||||
settings.launcher.debug=تصحيح
|
||||
# settings.launcher.disable_april_fools=
|
||||
settings.launcher.disable_auto_game_options=عدم تغيير لغة اللعبة تلقائياً
|
||||
settings.launcher.download=التنزيل
|
||||
settings.launcher.download.threads=الخيوط
|
||||
settings.launcher.download.threads.auto=تحديد تلقائي
|
||||
# settings.launcher.download.threads.custom=
|
||||
settings.launcher.download_source=مصدر التنزيل
|
||||
settings.launcher.download_source.auto=اختيار مصادر التنزيل تلقائياً
|
||||
# settings.launcher.default_addon_source=
|
||||
# settings.launcher.misc=
|
||||
settings.launcher.font=الخط
|
||||
settings.launcher.font.anti_aliasing=مضاد التعرج
|
||||
settings.launcher.font.anti_aliasing.auto=تلقائي
|
||||
settings.launcher.font.anti_aliasing.gray=تدرج رمادي
|
||||
settings.launcher.font.anti_aliasing.lcd=بكسل فرعي
|
||||
# settings.launcher.fonts=
|
||||
settings.launcher.general=عام
|
||||
settings.launcher.language=اللغة
|
||||
settings.launcher.launcher_log.export=تصدير سجلات المشغّل
|
||||
@@ -1402,9 +1584,35 @@ settings.launcher.proxy.port=المنفذ
|
||||
settings.launcher.proxy.socks=SOCKS
|
||||
settings.launcher.proxy.username=اسم المستخدم
|
||||
settings.launcher.theme=لون السمة
|
||||
# settings.launcher.theme_color=
|
||||
# settings.launcher.theme_color_style=
|
||||
# settings.launcher.theme_color_style.content=
|
||||
# settings.launcher.theme_color_style.content.desc=
|
||||
# settings.launcher.theme_color_style.expressive=
|
||||
# settings.launcher.theme_color_style.expressive.desc=
|
||||
# settings.launcher.theme_color_style.fidelity=
|
||||
# settings.launcher.theme_color_style.fidelity.desc=
|
||||
# settings.launcher.theme_color_style.fruit_salad=
|
||||
# settings.launcher.theme_color_style.fruit_salad.desc=
|
||||
# settings.launcher.theme_color_style.monochrome=
|
||||
# settings.launcher.theme_color_style.monochrome.desc=
|
||||
# settings.launcher.theme_color_style.neutral=
|
||||
# settings.launcher.theme_color_style.neutral.desc=
|
||||
# settings.launcher.theme_color_style.rainbow=
|
||||
# settings.launcher.theme_color_style.rainbow.desc=
|
||||
# settings.launcher.theme_color_style.tonal_spot=
|
||||
# settings.launcher.theme_color_style.tonal_spot.desc=
|
||||
# settings.launcher.theme_color_style.vibrant=
|
||||
# settings.launcher.theme_color_style.vibrant.desc=
|
||||
# settings.launcher.theme_color_type.background=
|
||||
# settings.launcher.theme_color_type.custom=
|
||||
# settings.launcher.theme_color_type.custom.description=
|
||||
# settings.launcher.theme_color_type.default=
|
||||
# settings.launcher.theme_color_type.system=
|
||||
settings.launcher.title_transparent=شريط عنوان شفاف
|
||||
settings.launcher.turn_off_animations=تعطيل الحركات
|
||||
settings.launcher.version_list_source=قائمة الإصدارات
|
||||
# settings.launcher.window_transparent=
|
||||
settings.launcher.background.settings.opacity=الشفافية
|
||||
|
||||
settings.memory=الذاكرة
|
||||
@@ -1412,15 +1620,30 @@ settings.memory.allocate=%1$.1f جيجابايت مخصص
|
||||
settings.memory.allocate.exceeded=%1$.1f جيجابايت مخصص (%2$.1f جيجابايت متاح)
|
||||
settings.memory.auto_allocate=تخصيص تلقائي
|
||||
settings.memory.lower_bound=الحد الأدنى للذاكرة
|
||||
# settings.memory.manual_allocate=
|
||||
settings.memory.unit.mib=ميجابايت
|
||||
settings.memory.used_per_total=%1$.1f جيجابايت مستخدم / %2$.1f جيجابايت إجمالي
|
||||
settings.physical_memory=حجم الذاكرة الفعلية
|
||||
settings.show_log=عرض السجلات
|
||||
settings.enable_debug_log_output=إخراج سجل التصحيح
|
||||
# settings.file.force_write=
|
||||
# settings.file.force_write.confirm=
|
||||
settings.tabs.installers=محمّلات المودات
|
||||
settings.take_effect_after_restart=يُطبَّق بعد إعادة التشغيل
|
||||
settings.type=نوع إعدادات النسخة
|
||||
settings.type.global.manage=الإعدادات العامة
|
||||
# settings.type.global.preset=
|
||||
# settings.type.global.preset.create=
|
||||
# settings.type.global.preset.default=
|
||||
# settings.type.global.preset.duplicate_name=
|
||||
# settings.type.global.preset.help=
|
||||
# settings.type.global.preset.manage_all=
|
||||
# settings.type.global.preset.name=
|
||||
# settings.type.global.preset.rename=
|
||||
# settings.type.global.preset.remove=
|
||||
# settings.type.global.preset.remove.confirm=
|
||||
# settings.type.global.preset.subtitle=
|
||||
# settings.type.global.preset.auto_name=
|
||||
|
||||
shaderpack.download.title=تنزيل ظلال - %1s
|
||||
|
||||
@@ -1433,6 +1656,7 @@ terracotta.terracotta=Terracotta | متعدد اللاعبين
|
||||
terracotta.status=الردهة
|
||||
terracotta.back=خروج
|
||||
terracotta.feedback.title=ملء استمارة التقييم
|
||||
# terracotta.feedback.desc=
|
||||
terracotta.feedback.desc.update=مع تحديث HMCL لنواة متعدد اللاعبين، نأمل أن تخصص 10 ثوانٍ لملء استمارة التقييم.
|
||||
terracotta.sudo_installing=يجب على HMCL التحقق من كلمة مرورك قبل تثبيت نواة متعدد اللاعبين
|
||||
terracotta.difficulty.easiest=شبكة ممتازة: الاتصال شبه مضمون!
|
||||
@@ -1509,6 +1733,40 @@ terracotta.unsupported.arch.32bit=متعدد اللاعبين غير مدعوم
|
||||
terracotta.unsupported.arch.loongarch64_ow=متعدد اللاعبين غير مدعوم على توزيعات Linux LoongArch64 القديمة. يُرجى التحديث إلى توزيعة العالم الجديد (مثل AOSC OS).
|
||||
terracotta.unsupported.region=ميزة متعدد اللاعبين متاحة حالياً فقط لمستخدمي البر الرئيسي الصيني وقد لا تكون متاحة في منطقتك.
|
||||
|
||||
# theme_pack.appearance.follow_theme.tooltip=
|
||||
# theme_pack.appearance.custom.tooltip=
|
||||
# theme_pack.apply=
|
||||
# theme_pack.apply.confirm=
|
||||
# theme_pack.apply.failed=
|
||||
# theme_pack.builtin=
|
||||
# theme_pack.current.missing=
|
||||
# theme_pack.delete=
|
||||
# theme_pack.delete.confirm=
|
||||
# theme_pack.delete.failed=
|
||||
# theme_pack.delete.success=
|
||||
# theme_pack.directory=
|
||||
# theme_pack.empty=
|
||||
# theme_pack.export=
|
||||
# theme_pack.export.author=
|
||||
# theme_pack.export.failed=
|
||||
# theme_pack.export.id=
|
||||
# theme_pack.export.name=
|
||||
# theme_pack.export.subtitle=
|
||||
# theme_pack.export.success=
|
||||
# theme_pack.export.version=
|
||||
# theme_pack.export.title=
|
||||
# theme_pack.file=
|
||||
# theme_pack.import=
|
||||
# theme_pack.import.failed=
|
||||
# theme_pack.import.success=
|
||||
# theme_pack.import.title=
|
||||
# theme_pack.load.failed=
|
||||
# theme_pack.manage=
|
||||
# theme_pack.theme=
|
||||
# theme_pack.theme.id=
|
||||
# theme_pack.themes=
|
||||
# theme_pack.version=
|
||||
|
||||
unofficial.hint=أنت تستخدم إصداراً غير رسمي من HMCL. لا نستطيع ضمان أمانه.
|
||||
|
||||
update=تحديث
|
||||
@@ -1530,15 +1788,21 @@ update.channel.nightly.hint=أنت تستخدم حالياً إصدار القن
|
||||
update.channel.nightly.title=إشعار القناة الليلية
|
||||
update.channel.stable=رسمي
|
||||
update.checking=جارٍ التحقق من التحديثات
|
||||
# update.check_failed=
|
||||
# update.disable_auto_show_update_dialog=
|
||||
# update.disable_auto_show_update_dialog.subtitle=
|
||||
update.failed=فشل التحديث
|
||||
update.found=تحديث متاح!
|
||||
update.newest_version=أحدث إصدار: %s
|
||||
update.bubble.title=تحديث متاح: %s
|
||||
update.bubble.subtitle=انقر هنا للتحديث
|
||||
# update.note.stable=
|
||||
# update.note.dev=
|
||||
update.latest=هذا هو أحدث إصدار
|
||||
update.tooltip=تحديث
|
||||
update.preview=الاطلاع المبكر على إصدارات HMCL
|
||||
update.preview.subtitle=فعّل هذا الخيار لاستقبال إصدارات HMCL الجديدة مبكراً للاختبار قبل إصدارها الرسمي.
|
||||
# update.unverified=
|
||||
|
||||
warning.java_interpreted_mode=يعمل HMCL في بيئة Java مُترجمة تفسيرياً، مما سيؤثر بشكل كبير على الأداء.\n\
|
||||
\n\
|
||||
|
||||
@@ -160,8 +160,6 @@ account.skin.upload.failed=Fehler beim Hochladen des Skins.
|
||||
account.skin.invalid_skin=Ungültige Skin-Datei.
|
||||
account.username=Benutzername
|
||||
|
||||
addon.broken_dependency.title=Fehlerhafte Abhängigkeit
|
||||
addon.broken_dependency.desc=Diese Abhängigkeit existierte vorher, ist aber nicht mehr vorhanden. Versuchen Sie, eine andere Download-Quelle zu verwenden.
|
||||
addon.category=Kategorie
|
||||
addon.changelog=Änderungsprotokoll
|
||||
addon.changelog.empty=Derzeit kein Änderungsprotokoll
|
||||
@@ -186,10 +184,16 @@ addon.dependency.tool=Erforderliche Abhängigkeiten (Müssen separat heruntergel
|
||||
addon.dependency.include=Integrierte Abhängigkeiten (Bereits in der Addon-Datei vom Autor gepackt. Muss nicht separat heruntergeladen werden)
|
||||
addon.dependency.incompatible=Inkompatible Addons (Wenn diese Addons gleichzeitig installiert werden, kann das Spiel nicht gestartet werden)
|
||||
addon.dependency.broken=Fehlerhafte Abhängigkeiten (Dieses Addon existierte vorher, ist aber nicht mehr vorhanden. Versuchen Sie, eine andere Download-Quelle zu verwenden.)
|
||||
# addon.dependencies.has_broken=
|
||||
addon.download.recommend=Empfohlene Version für Minecraft %1s
|
||||
addon.download.title.release=Minecraft %s
|
||||
addon.download.title.snapshot=Minecraft %s (Snapshots)
|
||||
addon.modrinth=Modrinth
|
||||
addon.sort.date_created=Erstellungsdatum
|
||||
addon.sort.last_updated=Zuletzt aktualisiert
|
||||
addon.sort.popularity=Beliebtheit
|
||||
# addon.sort.relevancy=
|
||||
addon.sort.total_downloads=Downloads insgesamt
|
||||
|
||||
archive.author=Autor(en)
|
||||
archive.date=Veröffentlichungsdatum
|
||||
@@ -371,13 +375,6 @@ curse.category.6553=Realistisch
|
||||
curse.category.6554=Fantasie
|
||||
curse.category.6555=Vanilla
|
||||
|
||||
curse.sort.author=Autor
|
||||
curse.sort.date_created=Erstellungsdatum
|
||||
curse.sort.last_updated=Zuletzt aktualisiert
|
||||
curse.sort.name=Name
|
||||
curse.sort.popularity=Beliebtheit
|
||||
curse.sort.total_downloads=Downloads insgesamt
|
||||
|
||||
datetime.format=dd.MM.yyyy, HH\:mm\:ss
|
||||
|
||||
download=Herunterladen
|
||||
@@ -433,32 +430,32 @@ extension.resourcepack=Ressourcenpaket-Archiv
|
||||
extension.schematic=Schematik-Datei
|
||||
extension.world=Welt-Archiv
|
||||
|
||||
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher kann das HMCL-Verzeichnis (%s) nicht erstellen. Bitte verschieben Sie HMCL an einen anderen Speicherort und öffnen Sie es erneut.
|
||||
fatal.create_hmcl_current_directory_failure=HMCL kann das HMCL-Verzeichnis (%s) nicht erstellen. Bitte verschieben Sie HMCL an einen anderen Speicherort und öffnen Sie es erneut.
|
||||
fatal.javafx.incomplete=Die JavaFX-Umgebung ist unvollständig.\n\
|
||||
Bitte versuchen Sie, Ihr Java zu ersetzen oder OpenJFX neu zu installieren.
|
||||
fatal.javafx.missing=Fehlende JavaFX-Umgebung. Bitte öffnen Sie den Hello Minecraft! Launcher mit Java, welches OpenJFX enthält.
|
||||
fatal.config_change_owner_root=Sie verwenden das Root-Konto, um den Hello Minecraft! Launcher zu öffnen. Dies könnte Sie daran hindern, HMCL in Zukunft mit einem anderen Konto zu öffnen.\n\
|
||||
fatal.javafx.missing=Fehlende JavaFX-Umgebung. Bitte öffnen Sie den HMCL mit Java, welches OpenJFX enthält.
|
||||
fatal.config_change_owner_root=Sie verwenden das Root-Konto, um den HMCL zu öffnen. Dies könnte Sie daran hindern, HMCL in Zukunft mit einem anderen Konto zu öffnen.\n\
|
||||
Möchten Sie trotzdem fortfahren?
|
||||
fatal.config_in_temp_dir=Sie öffnen den Hello Minecraft! Launcher in einem temporären Verzeichnis. Ihre Einstellungen und Spieldaten könnten verloren gehen.\n\
|
||||
fatal.config_in_temp_dir=Sie öffnen den HMCL in einem temporären Verzeichnis. Ihre Einstellungen und Spieldaten könnten verloren gehen.\n\
|
||||
Es wird empfohlen, HMCL an einen anderen Ort zu verschieben und erneut zu öffnen.\n\
|
||||
Möchten Sie trotzdem fortfahren?
|
||||
fatal.config_loading_failure=Konfigurationsdateien können nicht geladen werden.\n\
|
||||
Bitte stellen Sie sicher, dass der Hello Minecraft! Launcher Lese- und Schreibzugriff auf "%s" und die darin enthaltenen Dateien hat.\n\
|
||||
Bitte stellen Sie sicher, dass der HMCL Lese- und Schreibzugriff auf "%s" und die darin enthaltenen Dateien hat.\n\
|
||||
Versuchen Sie unter macOS, HMCL an einen Ort mit anderen Berechtigungen als "Schreibtisch", "Downloads" und "Dokumente" zu legen und versuchen Sie es erneut.
|
||||
fatal.config_loading_failure.unix=Hello Minecraft! Launcher konnte die Konfigurationsdatei nicht laden, da sie vom Benutzer "%1$s" erstellt wurde.\n\
|
||||
fatal.config_loading_failure.unix=HMCL konnte die Konfigurationsdatei nicht laden, da sie vom Benutzer "%1$s" erstellt wurde.\n\
|
||||
Bitte öffnen Sie HMCL als Root-Benutzer (nicht empfohlen) oder führen Sie den folgenden Befehl im Terminal aus, um den Besitz der Konfigurationsdatei auf den aktuellen Benutzer zu übertragen:\n%2$s
|
||||
fatal.config_unsupported_version=Die aktuelle Konfigurationsdatei wurde von einer neueren Version des Hello Minecraft! Launchers erstellt, und diese Version von HMCL kann sie nicht richtig laden.\n\
|
||||
fatal.config_unsupported_version=Die aktuelle Konfigurationsdatei wurde von einer neueren Version des HMCLs erstellt, und diese Version von HMCL kann sie nicht richtig laden.\n\
|
||||
Bitte aktualisieren Sie HMCL und starten Sie es neu.\n\
|
||||
Vor dem Aktualisieren des Launchers werden keine von Ihnen geänderten Einstellungen gespeichert.
|
||||
fatal.mac_app_translocation=Hello Minecraft! Launcher wurde aufgrund von macOS-Sicherheitsmechanismen vom Betriebssystem in ein temporäres Verzeichnis isoliert.\n\
|
||||
fatal.mac_app_translocation=HMCL wurde aufgrund von macOS-Sicherheitsmechanismen vom Betriebssystem in ein temporäres Verzeichnis isoliert.\n\
|
||||
Bitte verschieben Sie HMCL in ein anderes Verzeichnis, bevor Sie versuchen, es zu öffnen. Andernfalls könnten Ihre Einstellungen und Spieldaten nach einem Neustart verloren gehen.\n\
|
||||
Möchten Sie trotzdem fortfahren?
|
||||
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher wurde aktualisiert. Bitte starten Sie den Launcher neu.
|
||||
fatal.apply_update_failure=Es tut uns leid, aber der Hello Minecraft! Launcher kann nicht automatisch aktualisiert werden.\n\
|
||||
fatal.migration_requires_manual_reboot=HMCL wurde aktualisiert. Bitte starten Sie den Launcher neu.
|
||||
fatal.apply_update_failure=Es tut uns leid, aber der HMCL kann nicht automatisch aktualisiert werden.\n\
|
||||
\n\
|
||||
Sie können manuell aktualisieren, indem Sie eine neuere Launcher-Version von %s herunterladen.\n\
|
||||
Wenn das Problem weiterhin besteht, melden Sie es uns bitte.
|
||||
fatal.apply_update_need_win7=Der Hello Minecraft! Launcher kann unter Windows XP/Vista nicht automatisch aktualisiert werden.\n\
|
||||
fatal.apply_update_need_win7=Der HMCL kann unter Windows XP/Vista nicht automatisch aktualisiert werden.\n\
|
||||
\n\
|
||||
Sie können manuell aktualisieren, indem Sie eine neuere Launcher-Version von %s herunterladen.
|
||||
fatal.deprecated_java_version=HMCL wird in Zukunft Java 17 oder neuer benötigen, um zu laufen, wird aber weiterhin das Starten von Spielen mit Java 6~16 unterstützen.\n\
|
||||
@@ -467,14 +464,14 @@ Es wird empfohlen, die neueste Java-Version zu installieren, um sicherzustellen,
|
||||
\n\
|
||||
Sie können die alte Java-Version weiterhin behalten. HMCL kann mehrere Java-Versionen erkennen und verwalten und wählt basierend auf der Spielversion automatisch das entsprechende Java für Sie aus.
|
||||
fatal.deprecated_java_version.download_link=Java %d herunterladen
|
||||
fatal.samba=Wenn Sie den Hello Minecraft! Launcher von einem Samba-Netzlaufwerk geöffnet haben, funktionieren einige Funktionen möglicherweise nicht. Bitte versuchen Sie, Ihr Java zu aktualisieren oder den Launcher in ein anderes Verzeichnis zu verschieben.
|
||||
fatal.samba=Wenn Sie den HMCL von einem Samba-Netzlaufwerk geöffnet haben, funktionieren einige Funktionen möglicherweise nicht. Bitte versuchen Sie, Ihr Java zu aktualisieren oder den Launcher in ein anderes Verzeichnis zu verschieben.
|
||||
fatal.illegal_char=Ihr Arbeitsverzeichnis enthält ein ungültiges Zeichen "=". Sie können authlib-injector nicht verwenden oder den Skin Ihres Offline-Kontos nicht ändern.
|
||||
fatal.unsupported_platform=Minecraft wird auf Ihrer Plattform noch nicht vollständig unterstützt, daher fehlen möglicherweise Funktionen oder Sie können das Spiel überhaupt nicht starten.\n\
|
||||
\n\
|
||||
Wenn Sie Minecraft 1.17 und neuer nicht starten können, können Sie versuchen, den "Renderer" unter "Allgemeine/Instanzspezifische Einstellungen → Grafikeinstellungen" auf "Mesa LLVMpipe" oder "Mesa Lavapipe" umzustellen, um CPU-Rendering für bessere Kompatibilität zu verwenden.
|
||||
fatal.unsupported_platform.loongarch=Der Hello Minecraft! Launcher bietet nun Unterstützung für die Loongson-Plattform.\n\
|
||||
fatal.unsupported_platform.loongarch=Der HMCL bietet nun Unterstützung für die Loongson-Plattform.\n\
|
||||
Wenn beim Spielen Probleme auftreten, können Sie https://docs.hmcl.net/groups.html für Hilfe besuchen.
|
||||
fatal.unsupported_platform.windows_arm64=Der Hello Minecraft! Launcher bietet native Unterstützung für die Windows-on-Arm-Plattform. Wenn beim Spielen Probleme auftreten, versuchen Sie bitte, das Spiel mit Java basierend auf der x86-Architektur zu starten.\n\
|
||||
fatal.unsupported_platform.windows_arm64=Der HMCL bietet native Unterstützung für die Windows-on-Arm-Plattform. Wenn beim Spielen Probleme auftreten, versuchen Sie bitte, das Spiel mit Java basierend auf der x86-Architektur zu starten.\n\
|
||||
\n\
|
||||
Wenn Sie die <b>Qualcomm</b>-Plattform verwenden, müssen Sie möglicherweise das <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL-Kompatibilitätspaket</a> installieren, bevor Sie Spiele spielen.\n\
|
||||
Klicken Sie auf den Link, um zum Microsoft Store zu navigieren und das Kompatibilitätspaket zu installieren.
|
||||
@@ -732,9 +729,10 @@ game_directory.instance_directory.choose=Spielverzeichnis auswählen
|
||||
game_directory.name=Name
|
||||
game_directory.new=Neues Verzeichnis
|
||||
game_directory.use_relative_path=Verwenden Sie, wenn möglich, einen relativen Pfad für das Spielverzeichnis
|
||||
# game_directory.root=
|
||||
|
||||
help=Hilfe
|
||||
help.doc=Hello Minecraft! Launcher Dokumentation
|
||||
help.doc=HMCL Dokumentation
|
||||
help.detail=Für Hersteller von Datenpaketen und Modpacks.
|
||||
|
||||
input.email=Der Benutzername muss eine E-Mail-Adresse sein.
|
||||
@@ -744,6 +742,7 @@ input.url=Die Eingabe muss eine gültige URL sein.
|
||||
|
||||
install=Neue Instanz
|
||||
install.change_version=Version ändern
|
||||
# install.change_version.title=
|
||||
install.change_version.confirm=Sind Sie sicher, dass Sie %1$s von Version %2$s zu %3$s wechseln möchten?
|
||||
install.change_version.process=Versionsänderungsprozess
|
||||
install.failed=Installation fehlgeschlagen
|
||||
@@ -752,6 +751,7 @@ install.failed.downloading.detail=Datei konnte nicht heruntergeladen werden: %s
|
||||
install.failed.downloading.timeout=Download-Timeout beim Abrufen von: %s
|
||||
install.failed.install_online=Die bereitgestellte Datei konnte nicht identifiziert werden. Wenn Sie einen Mod installieren, navigieren Sie zur Seite "Mods".
|
||||
install.failed.malformed=Die heruntergeladenen Dateien sind beschädigt. Sie können versuchen, dieses Problem zu beheben, indem Sie unter "Einstellungen → Herunterladen → Downloadquelle" zu einer anderen Downloadquelle wechseln.
|
||||
# install.failed.cleanroom_not_compatible_with_forge=
|
||||
install.failed.optifine_conflict=OptiFine und Fabric können auf Minecraft 1.13 oder neuer nicht gleichzeitig installiert werden.
|
||||
install.failed.optifine_forge_1.17=Für Minecraft 1.17.1 ist Forge nur mit OptiFine H1 pre2 oder neuer kompatibel. Sie können sie installieren, indem Sie "Snapshots" aktivieren, wenn Sie eine OptiFine-Version in HMCL auswählen.
|
||||
install.failed.version_mismatch=Dieser Loader benötigt die Spielversion %1$s, aber die installierte ist %2$s.
|
||||
@@ -886,6 +886,7 @@ launch.advice.forge28_2_2_optifine=Forge Version 28.2.2 oder neuer ist nicht mit
|
||||
launch.advice.forge37_0_60=Forge-Versionen vor 37.0.60 sind nicht mit Java 17 kompatibel. Bitte aktualisieren Sie Forge auf 37.0.60 oder neuer, oder starten Sie das Spiel mit Java 16.
|
||||
launch.advice.java8_51_1_13=Minecraft 1.13 kann auf Java 8-Versionen vor 1.8.0_51 abstürzen. Bitte installieren Sie die neueste Java 8-Version.
|
||||
launch.advice.java9=Sie können Minecraft 1.12 oder früher nicht mit Java 9 oder neuer starten. Bitte verwenden Sie stattdessen Java 8.
|
||||
# launch.advice.lwjgl_3_4_1=
|
||||
# launch.advice.lwjgl_3_4_1=Die aktuelle Spielversion hat ein bekanntes Problem, das zu Ruckeln im Spiel führen kann.\nHMCL kann die Spielleistung automatisch optimieren (mittels eines Java-Agenten).\nErlauben Sie HMCL, das Spiel automatisch zu optimieren?\nWenn Probleme auftreten, können Sie die Option "HMCL erlauben, das Spiel zu modifizieren" unter "Einstellungen → Allgemein" deaktivieren.
|
||||
launch.advice.lwjgl_3_4_1.title=HMCL erlauben, das Spiel automatisch zu optimieren?
|
||||
launch.advice.modded_java=Einige Mods sind möglicherweise nicht mit neueren Java-Versionen kompatibel. Es wird empfohlen, Java %1$s zu verwenden, um Minecraft %2$s zu starten.
|
||||
@@ -951,9 +952,9 @@ launcher.cache_directory.default=Standard ("%APPDATA%/.minecraft" oder "~/.minec
|
||||
launcher.cache_directory.disabled=Deaktiviert
|
||||
launcher.cache_directory.invalid=Fehler beim Erstellen des Cache-Verzeichnisses, Rückgriff auf Standard.
|
||||
launcher.contact=Kontaktiere uns
|
||||
launcher.crash=Hello Minecraft! Launcher ist auf einen fatalen Fehler gestoßen! Bitte kopieren Sie das folgende Protokoll und bitten Sie in unserem Discord, der QQ-Gruppe, auf GitHub oder in einem anderen Minecraft-Forum um Hilfe.
|
||||
launcher.crash.java_internal_error=Hello Minecraft! Launcher ist auf einen fatalen Fehler gestoßen, da Ihre Java-Installation beschädigt ist. Bitte deinstallieren Sie Java und laden Sie eine geeignete Java-Version <a href="https://bell-sw.com/pages/downloads/#downloads">hier</a> herunter.
|
||||
launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher ist auf einen fatalen Fehler gestoßen! Ihr Launcher ist veraltet. Bitte aktualisieren Sie Ihren Launcher!
|
||||
launcher.crash=HMCL ist auf einen fatalen Fehler gestoßen! Bitte kopieren Sie das folgende Protokoll und bitten Sie in unserem Discord, der QQ-Gruppe, auf GitHub oder in einem anderen Minecraft-Forum um Hilfe.
|
||||
launcher.crash.java_internal_error=HMCL ist auf einen fatalen Fehler gestoßen, da Ihre Java-Installation beschädigt ist. Bitte deinstallieren Sie Java und laden Sie eine geeignete Java-Version <a href="https://bell-sw.com/pages/downloads/#downloads">hier</a> herunter.
|
||||
launcher.crash.hmcl_out_dated=HMCL ist auf einen fatalen Fehler gestoßen! Ihr Launcher ist veraltet. Bitte aktualisieren Sie Ihren Launcher!
|
||||
launcher.update_java=Bitte aktualisieren Sie Ihre Java-Version.
|
||||
|
||||
libraries.download=Bibliotheken werden heruntergeladen
|
||||
@@ -1022,6 +1023,9 @@ modpack.installing=Modpack wird installiert
|
||||
modpack.installing.given=Installiere %s Modpack
|
||||
modpack.invalid=Ungültiges Modpack, Sie können versuchen, es erneut herunterzuladen.
|
||||
modpack.mismatched_type=Modpack-Typ stimmt nicht überein, die aktuelle Instanz ist ein(e) %1$s Typ, aber der bereitgestellte Typ ist %2$s.
|
||||
modpack.optional_files=Optionale Dateien
|
||||
modpack.retry_optional_files=Optionale Dateien konnten nicht geladen werden. Hier klicken, um es erneut zu versuchen.
|
||||
modpack.unknown_optional_file=Unbekannte Datei
|
||||
modpack.name=Modpack-Name
|
||||
modpack.origin=Quelle
|
||||
modpack.origin.url=Offizielle Website
|
||||
@@ -1035,11 +1039,11 @@ modpack.type.curse.error=Fehler beim Herunterladen von Abhängigkeiten. Bitte ve
|
||||
modpack.type.curse.not_found=Einige Abhängigkeiten sind nicht mehr verfügbar. Bitte versuchen Sie, eine neuere Modpack-Version zu installieren.
|
||||
modpack.type.manual.warning=Das Modpack wurde vom Herausgeber manuell verpackt, was möglicherweise bereits einen Launcher enthält. Es wird empfohlen, das Modpack zu entpacken und das Spiel mit dem eigenen Launcher zu starten. HMCL kann es dennoch importieren, jedoch ohne Garantie auf Nutzbarkeit. Trotzdem fortfahren?
|
||||
modpack.type.mcbbs=MCBBS
|
||||
modpack.type.mcbbs.export=Kann durch Hello Minecraft! Launcher importiert werden
|
||||
modpack.type.mcbbs.export=Kann durch HMCL importiert werden
|
||||
modpack.type.modrinth=Modrinth
|
||||
modpack.type.modrinth.export=Kann von gängigen Drittanbieter-Launchern importiert werden
|
||||
modpack.type.multimc=MultiMC
|
||||
modpack.type.multimc.export=Kann durch Hello Minecraft! Launcher und MultiMC importiert werden
|
||||
modpack.type.multimc.export=Kann durch HMCL und MultiMC importiert werden
|
||||
modpack.type.server=Modpack vom Server automatisch aktualisieren
|
||||
modpack.type.server.export=Ermöglicht dem Serverbesitzer, die Spielinstanz remote zu aktualisieren
|
||||
modpack.type.server.malformed=Ungültiges Modpack-Manifest. Bitte kontaktieren Sie den Modpack-Ersteller, um dieses Problem zu beheben.
|
||||
@@ -1328,7 +1332,6 @@ schematics=Schematiken
|
||||
schematics.add=Hinzufügen
|
||||
schematics.add.failed=Fehler beim Hinzufügen der Schematik-Dateien
|
||||
schematics.add.title=Wählen Sie die Schematik-Datei, die Sie importieren möchten
|
||||
schematics.back_to=Zurück zu "%s"
|
||||
schematics.create_directory=Neues Verzeichnis
|
||||
schematics.create_directory.prompt=Bitte geben Sie den neuen Verzeichnisnamen ein
|
||||
schematics.create_directory.failed=Fehler beim Erstellen des Verzeichnisses
|
||||
@@ -1344,7 +1347,13 @@ schematics.info.time_created=Erstellungszeit
|
||||
schematics.info.time_modified=Änderungszeit
|
||||
schematics.info.total_blocks=Gesamte Blöcke
|
||||
schematics.info.total_volume=Gesamtes Volumen
|
||||
# schematics.info.type=
|
||||
# schematics.info.type.litematic=
|
||||
# schematics.info.type.nbt_structure=
|
||||
# schematics.info.type.schem=
|
||||
schematics.info.version=Schematik-Version
|
||||
# schematics.info.mc_data_version=
|
||||
# schematics.install_mod=
|
||||
schematics.manage=Schematiken
|
||||
schematics.sub_items=%d Unterelement(e)
|
||||
|
||||
@@ -1477,6 +1486,7 @@ settings.advanced.server_ip.prompt=Nach Spielstart automatisch beitreten
|
||||
settings.advanced.unsupported_system_options=Für das aktuelle System nicht zutreffende Einstellungen
|
||||
settings.advanced.use_native_glfw_or_sdl=System-GLFW / SDL verwenden
|
||||
settings.advanced.use_native_openal=System-OpenAL verwenden
|
||||
# settings.advanced.use_native_sdl=
|
||||
settings.advanced.linux_freebsd_only=Nur Linux/FreeBSD
|
||||
settings.advanced.windows_only=Nur Windows
|
||||
settings.advanced.wrapper_launcher=Wrapper-Befehl
|
||||
@@ -1780,6 +1790,7 @@ update.channel.nightly.hint=Sie verwenden derzeit einen Nightly-Kanal-Build des
|
||||
update.channel.nightly.title=Hinweis zum Nightly-Kanal
|
||||
update.channel.stable=Stabil
|
||||
update.checking=Suche nach Updates
|
||||
# update.check_failed=
|
||||
update.disable_auto_show_update_dialog=Update-Dialog nicht automatisch anzeigen
|
||||
update.disable_auto_show_update_dialog.subtitle=Aktivieren Sie diese Option, um zu verhindern, dass HMCL den Update-Dialog automatisch anzeigt.
|
||||
update.failed=Fehler beim Aktualisieren
|
||||
@@ -1793,6 +1804,7 @@ update.latest=Dies ist die neueste Version
|
||||
update.tooltip=Aktualisieren
|
||||
update.preview=Vorschau auf HMCL-Veröffentlichungen frühzeitig erhalten
|
||||
update.preview.subtitle=Aktivieren Sie diese Option, um neue Versionen von HMCL frühzeitig zum Testen vor ihrer offiziellen Veröffentlichung zu erhalten.
|
||||
# update.unverified=
|
||||
|
||||
warning.java_interpreted_mode=HMCL wird in einer interpretierten Java-Umgebung ausgeführt, was die Leistung erheblich beeinträchtigt.\n\
|
||||
\n\
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -100,12 +100,10 @@ account.copy_uuid=Копіювати UUID облікового запису
|
||||
account.methods=Тип входу
|
||||
account.methods.authlib_injector=authlib-injector
|
||||
account.methods.microsoft=Microsoft
|
||||
account.methods.microsoft.birth=Як змінити дату народження облікового запису
|
||||
account.methods.microsoft.code=Код (Скопійовано в буфер обміну)
|
||||
account.methods.microsoft.close_page=Авторизація облікового запису Microsoft тепер завершена.\n\
|
||||
\n\
|
||||
Нам залишилося виконати ще трохи роботи, але ви можете безпечно закрити цю вкладку зараз.
|
||||
account.methods.microsoft.deauthorize=Скасувати авторизацію
|
||||
account.methods.microsoft.error.add_family=Будь ласка, натисніть <a href="https://support.microsoft.com/account-billing/837badbc-999e-54d2-2617-d19206b9540a">тут</a>, щоб змінити дату народження вашого акаунта на старше 18 років, або додайте свій акаунт до сім'ї.
|
||||
account.methods.microsoft.error.country_unavailable=XBOX Live недоступний у вашій поточній країні/регіоні.
|
||||
account.methods.microsoft.error.missing_xbox_account=Будь ласка, підтвердіть, що ви придбали Minecraft: Java Edition. Якщо так, натисніть <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">тут</a>, щоб створити свій ігровий профіль.
|
||||
@@ -115,8 +113,6 @@ account.methods.microsoft.error.banned=Ваш акаунт міг бути за
|
||||
Ви можете натиснути <a href="https://enforcement.xbox.com/enforcement/showenforcementhistory">тут</a>, щоб перевірити статус блокування вашого акаунта.
|
||||
account.methods.microsoft.error.unknown=Не вдалося увійти, код помилки: %d.
|
||||
account.methods.microsoft.error.wrong_verify_method=Не вдалося увійти. Будь ласка, спробуйте увійти у свій акаунт, використовуючи ПАРОЛЬ, замість інших методів входу.
|
||||
account.methods.microsoft.logging_in=Вхід...
|
||||
account.methods.microsoft.makegameidsettings=Створити профіль / Редагувати ім'я профілю
|
||||
account.methods.microsoft.hint=Натисніть кнопку "Увійти", щоб почати додавання вашого акаунта Microsoft.
|
||||
account.methods.microsoft.methods.device=Увійти за допомогою QR-коду
|
||||
account.methods.microsoft.methods.device.hint=Відскануйте QR-код або перейдіть за посиланням <a href="%s">%s</a>, щоб завершити вхід, введіть <b>%s</b> на відкритій сторінці.
|
||||
@@ -128,7 +124,6 @@ account.methods.microsoft.profile=Профіль облікового запис
|
||||
account.methods.microsoft.purchase=Купити Minecraft
|
||||
account.methods.microsoft.snapshot=Ви використовуєте неофіційну збірку HMCL. Будь ласка, завантажте <a href="https://hmcl.huangyuhui.net/download">офіційну збірку</a> для входу.
|
||||
account.methods.microsoft.snapshot.tooltip=Ви використовуєте неофіційну збірку HMCL. Будь ласка, завантажте офіційну збірку для оновлення акаунта.
|
||||
account.methods.microsoft.snapshot.website=Офіційний сайт
|
||||
account.methods.offline=Автономно
|
||||
account.methods.offline.name.special_characters=Використовуйте лише літери, цифри та підкреслення (макс. 16 символів)
|
||||
account.methods.offline.name.invalid=Рекомендується використовувати лише англійські літери, цифри та підкреслення для імені користувача, довжина не повинна перевищувати 16 символів.\n\
|
||||
@@ -191,10 +186,16 @@ addon.dependency.tool=Обов'язкові залежності (Необхід
|
||||
addon.dependency.include=Вбудовані залежності (Вже упаковані у файл доповнення автором. Не потрібно завантажувати окремо)
|
||||
addon.dependency.incompatible=Несумісні доповнення (Одночасне встановлення цих доповнень перешкодить запуску гри)
|
||||
addon.dependency.broken=Зламані залежності (Це доповнення існувало раніше, але його більше немає. Спробуйте використати інше джерело завантаження.)
|
||||
# addon.dependencies.has_broken=
|
||||
addon.download.recommend=Рекомендована версія для Minecraft %1s
|
||||
addon.download.title.release=Minecraft %s
|
||||
addon.download.title.snapshot=Minecraft %s (Снапшоти)
|
||||
addon.modrinth=Modrinth
|
||||
addon.sort.date_created=Дата створення
|
||||
addon.sort.last_updated=Останнє оновлення
|
||||
addon.sort.popularity=Популярність
|
||||
# addon.sort.relevancy=
|
||||
addon.sort.total_downloads=Всього завантажень
|
||||
|
||||
archive.author=Автор(и)
|
||||
archive.date=Дата публікації
|
||||
@@ -371,13 +372,6 @@ curse.category.6553=Реалістично
|
||||
curse.category.6554=Фентезі
|
||||
curse.category.6555=Ваніла
|
||||
|
||||
curse.sort.author=Автор
|
||||
curse.sort.date_created=Дата створення
|
||||
curse.sort.last_updated=Останнє оновлення
|
||||
curse.sort.name=Назва
|
||||
curse.sort.popularity=Популярність
|
||||
curse.sort.total_downloads=Всього завантажень
|
||||
|
||||
datetime.format=dd.MM.yyyy, HH:mm:ss
|
||||
|
||||
download=Завантажити
|
||||
@@ -446,7 +440,7 @@ fatal.config_loading_failure=Не вдається завантажити фай
|
||||
Для macOS спробуйте помістити HMCL кудись із дозволами, відмінними від "Робочий стіл", "Завантаження" та "Документи", і спробуйте знову.
|
||||
fatal.config_loading_failure.unix=Hello Minecraft! Лаунчер не зміг завантажити файл конфігурації, оскільки він був створений користувачем "%1$s".\n\
|
||||
Відкрийте HMCL як користувач root (не рекомендується) або виконайте наступну команду в терміналі, щоб змінити власника файлу конфігурації на поточного користувача:\n%2$s
|
||||
fatal.config_unsupported_version=Поточний файл конфігурації був створений новішою версією Hello Minecraft! Launcher, і ця версія HMCL не може завантажити його належним чином.\n\
|
||||
fatal.config_unsupported_version=Поточний файл конфігурації був створений новішою версією HMCL, і ця версія HMCL не може завантажити його належним чином.\n\
|
||||
Будь ласка, оновіть та перезапустіть HMCL.\n\
|
||||
До оновлення лаунчера будь-які змінені вами налаштування не будуть збережені.
|
||||
fatal.mac_app_translocation=Hello Minecraft! Лаунчер ізольовано до тимчасового каталогу ОС через механізми безпеки macOS.\n\
|
||||
@@ -459,7 +453,6 @@ fatal.apply_update_failure=Вибачте, але Hello Minecraft! Лаунче
|
||||
fatal.apply_update_need_win7=Hello Minecraft! Лаунчер не може автоматично оновитися на Windows XP/Vista. \n\
|
||||
Ви можете оновити вручну, завантаживши новішу версію лаунчера з %s.
|
||||
fatal.deprecated_java_version=У майбутньому HMCL потребуватиме Java 17 або новішої версії для роботи, але все ще підтримуватиме запуск ігор на Java 6~16.\n\nРекомендується встановити останню версію Java, щоб гарантувати належну роботу HMCL.\n\nВи можете продовжувати зберігати стару версію Java. HMCL може розпізнавати та керувати декількома версіями Java і автоматично вибере відповідну Java для вас на основі версії гри.
|
||||
fatal.deprecated_java_version.update=У майбутньому HMCL потребуватиме Java 17 або новішої версії для роботи. Будь ласка, встановіть останню версію Java, щоб HMCL міг завершити оновлення.\n\nВи можете продовжувати зберігати стару версію Java. HMCL може розпізнавати та керувати декількома версіями Java і автоматично вибере відповідну Java для вас на основі версії гри.
|
||||
fatal.deprecated_java_version.download_link=Завантажити Java %d
|
||||
fatal.samba=Якщо ви відкрили Hello Minecraft! Лаунчер з мережевого диска Samba, деякі функції можуть не працювати. Спробуйте оновити вашу Java або перемістити лаунчер в інший каталог.
|
||||
fatal.illegal_char=Ваш шлях користувача містить недопустимий символ "=". Ви не зможете використовувати authlib-injector або змінювати скін вашого автономного облікового запису.
|
||||
@@ -691,6 +684,7 @@ game_directory.instance_directory.choose=Вибрати каталог гри
|
||||
game_directory.name=Назва
|
||||
game_directory.new=Новий каталог
|
||||
game_directory.use_relative_path=Використовувати відносний шлях для каталогу гри, якщо можливо
|
||||
# game_directory.root=
|
||||
|
||||
help=Допомога
|
||||
help.doc=Документація Hello Minecraft! Лаунчера
|
||||
@@ -703,6 +697,7 @@ input.url=Введення має бути дійсною URL-адресою.
|
||||
|
||||
install=Новий екземпляр
|
||||
install.change_version=Змінити версію
|
||||
# install.change_version.title=
|
||||
install.change_version.confirm=Ви впевнені, що хочете переключити %s з версії %s на %s?
|
||||
install.change_version.process=Процес зміни версії
|
||||
install.failed=Не вдалося встановити
|
||||
@@ -711,18 +706,19 @@ install.failed.downloading.detail=Не вдалося завантажити ф
|
||||
install.failed.downloading.timeout=Час очікування завантаження при отриманні: %s
|
||||
install.failed.install_online=Не вдалося ідентифікувати наданий файл. Якщо ви встановлюєте мод, перейдіть на сторінку "Моди".
|
||||
install.failed.malformed=Завантажені файли пошкоджені. Ви можете спробувати вирішити цю проблему, переключившись на інше джерело завантаження у "Налаштування → Завантаження → Джерело завантаження".
|
||||
# install.failed.cleanroom_not_compatible_with_forge=
|
||||
install.failed.optifine_conflict=Не можна встановити OptiFine та Fabric одночасно на Minecraft 1.13 або новішому.
|
||||
install.failed.optifine_forge_1.17=Для Minecraft 1.17.1 Forge сумісний лише з OptiFine H1 pre2 або новішою. Ви можете встановити їх, позначивши "Снапшоти" при виборі версії OptiFine в HMCL.
|
||||
install.failed.version_mismatch=Цей завантажувач вимагає версію гри %s, але встановлена %s.
|
||||
install.installer.change_version=%s Несумісний
|
||||
install.installer.choose=Виберіть вашу версію %s
|
||||
install.installer.do_not_install=Не встановлювати
|
||||
install.installer.cleanroom=Cleanroom
|
||||
install.installer.do_not_install=Не встановлювати
|
||||
install.installer.fabric=Fabric
|
||||
install.installer.fabric-api=Fabric API
|
||||
install.installer.fabric-quilt-api.warning=%1$s - це мод, і він буде встановлений у каталог модів екземпляра гри. Не змінюйте робочий каталог гри, інакше %1$s не працюватиме. Якщо ви все ж хочете змінити каталог, вам слід перевстановити його.
|
||||
install.installer.legacyfabric=Legacy Fabric
|
||||
install.installer.legacyfabric-api=Legacy Fabric API
|
||||
install.installer.fabric-quilt-api.warning=%1$s - це мод, і він буде встановлений у каталог модів екземпляра гри. Не змінюйте робочий каталог гри, інакше %1$s не працюватиме. Якщо ви все ж хочете змінити каталог, вам слід перевстановити його.
|
||||
install.installer.forge=Forge
|
||||
install.installer.neoforge=NeoForge
|
||||
install.installer.game=Minecraft
|
||||
@@ -779,8 +775,8 @@ instance.manage=Усі екземпляри
|
||||
instance.manage.clean=Видалити файли журналів
|
||||
instance.manage.clean.tooltip=Видалити файли в каталогах "logs" та "crash-reports".
|
||||
instance.manage.duplicate=Дублювати екземпляр
|
||||
instance.manage.duplicate.prompt=Введіть нову назву екземпляра
|
||||
instance.manage.duplicate.duplicate_save=Дублювати світи
|
||||
instance.manage.duplicate.prompt=Введіть нову назву екземпляра
|
||||
instance.manage.duplicate.confirm=Дубльований екземпляр матиме копію всіх файлів у каталозі екземпляра (".minecraft/versions/<назва екземпляра>"), з ізольованим робочим каталогом та налаштуваннями.
|
||||
instance.manage.manage=Редагувати екземпляр
|
||||
instance.manage.manage.title=Редагувати екземпляр - %1s
|
||||
@@ -845,6 +841,9 @@ launch.advice.forge28_2_2_optifine=Forge версії 28.2.2 або новішо
|
||||
launch.advice.forge37_0_60=Forge версій до 37.0.60 несумісний з Java 17. Оновіть Forge до 37.0.60 або новішої, або запустіть гру з Java 16.
|
||||
launch.advice.java8_51_1_13=Minecraft 1.13 може зазнати збою на версіях Java 8 до 1.8.0_51. Встановіть останню версію Java 8.
|
||||
launch.advice.java9=Ви не можете запустити Minecraft 1.12 або раніші з Java 9 або новішою. Використовуйте Java 8.
|
||||
# launch.advice.lwjgl_3_4_1=
|
||||
# launch.advice.lwjgl_3_4_1=Поточна версія гри має відому проблему, яка може спричинити підвисання у грі.\nHMCL може автоматично оптимізувати продуктивність гри (використовуючи Java Agent).\nЧи дозволяєте ви HMCL автоматично оптимізувати гру?\nЯкщо у вас виникнуть проблеми, ви можете вимкнути опцію "Дозволити HMCL змінювати гру" у "Налаштування → Загальні".
|
||||
launch.advice.lwjgl_3_4_1.title=Дозволити HMCL автоматично оптимізувати гру?
|
||||
launch.advice.modded_java=Деякі моди можуть бути несумісні з новішими версіями Java. Рекомендується використовувати Java %s для запуску Minecraft %s.
|
||||
launch.advice.modlauncher8=Версія Forge, яку ви використовуєте, несумісна з поточною версією Java. Спробуйте оновити Forge.
|
||||
launch.advice.newer_java=Ви використовуєте старішу версію Java для запуску гри. Рекомендується оновити до Java 8, інакше деякі моди можуть призвести до збою гри.
|
||||
@@ -853,8 +852,6 @@ launch.advice.require_newer_java_version=Поточна версія гри ви
|
||||
launch.advice.too_large_memory_for_32bit=Ви виділили розмір пам'яті, більший за обмеження пам'яті 32-бітної інсталяції Java. Ви можете не змогти запустити гру.
|
||||
launch.advice.vanilla_linux_java_8=Minecraft 1.12.2 або раніші підтримують лише Java 8 для платформи Linux x86-64, оскільки новіші версії не можуть завантажити 32-бітні нативні бібліотеки, такі як liblwjgl.so. Завантажте її з java.com або встановіть OpenJDK 8.
|
||||
launch.advice.vanilla_x86.translation=Minecraft не повністю підтримується на вашій платформі, тому ви можете зіткнутися з відсутністю функцій або навіть не зможете запустити гру. Ви можете завантажити Java для архітектури <b>x86-64</b> <a href="https://learn.microsoft.com/java/openjdk/download">тут</a> для повного ігрового досвіду.
|
||||
# launch.advice.lwjgl_3_4_1=Поточна версія гри має відому проблему, яка може спричинити підвисання у грі.\nHMCL може автоматично оптимізувати продуктивність гри (використовуючи Java Agent).\nЧи дозволяєте ви HMCL автоматично оптимізувати гру?\nЯкщо у вас виникнуть проблеми, ви можете вимкнути опцію "Дозволити HMCL змінювати гру" у "Налаштування → Загальні".
|
||||
launch.advice.lwjgl_3_4_1.title=Дозволити HMCL автоматично оптимізувати гру?
|
||||
launch.advice.unknown=Гра не може бути запущена з наступних причин:
|
||||
launch.failed=Не вдалося запустити
|
||||
launch.failed.cannot_create_jvm=Ми не можемо створити JVM. Це може бути через неправильні аргументи JVM. Ви можете спробувати вирішити це, видаливши всі аргументи, які ви додали у "Глобальні/Специфічні налаштування екземпляра → Розширені налаштування → Параметри JVM".
|
||||
@@ -900,8 +897,6 @@ launcher.background.load_policy.wait_for_background=Очікувати на тл
|
||||
launcher.background.loading=Завантаження тла
|
||||
launcher.background.network=З URL
|
||||
launcher.background.network.cache=Кешувати зображення з URL-адрес
|
||||
launcher.background.network.cache.disabled=Вимкнено
|
||||
launcher.background.network.cache.enabled=Увімкнено
|
||||
launcher.background.paint=Суцільний колір
|
||||
launcher.background.theme=Слідувати темі
|
||||
launcher.background.theme_color=За кольором теми
|
||||
@@ -983,6 +978,9 @@ modpack.installing=Встановлення модпака
|
||||
modpack.installing.given=Встановлення модпака %s
|
||||
modpack.invalid=Недійсний модпак, ви можете спробувати завантажити його знову.
|
||||
modpack.mismatched_type=Тип модпака не відповідає, поточний екземпляр має тип %s, але наданий має тип %s.
|
||||
modpack.optional_files=Необов’язкові файли
|
||||
modpack.retry_optional_files=Не вдалося завантажити необов’язкові файли. Натисніть, щоб повторити.
|
||||
modpack.unknown_optional_file=Невідомий файл
|
||||
modpack.name=Назва модпака
|
||||
modpack.origin=Джерело
|
||||
modpack.origin.url=Офіційний сайт
|
||||
@@ -1113,7 +1111,6 @@ modrinth.category.velocity=Velocity
|
||||
modrinth.category.waterfall=Waterfall
|
||||
modrinth.category.worldgen=Генерація світу
|
||||
modrinth.category.8x-=×8 і менше
|
||||
modrinth.category.16x=×16
|
||||
modrinth.category.32x=×32
|
||||
modrinth.category.48x=×48
|
||||
modrinth.category.64x=×64
|
||||
@@ -1291,7 +1288,6 @@ schematics=Схематики
|
||||
schematics.add=Додати
|
||||
schematics.add.failed=Не вдалося додати файли схематик
|
||||
schematics.add.title=Виберіть файл схеми, який ви хочете імпортувати
|
||||
schematics.back_to=Назад до "%s"
|
||||
schematics.create_directory=Новий каталог
|
||||
schematics.create_directory.prompt=Введіть нову назву каталогу
|
||||
schematics.create_directory.failed=Не вдалося створити каталог
|
||||
@@ -1307,7 +1303,13 @@ schematics.info.time_created=Час створення
|
||||
schematics.info.time_modified=Час зміни
|
||||
schematics.info.total_blocks=Всього блоків
|
||||
schematics.info.total_volume=Загальний об'єм
|
||||
# schematics.info.type=
|
||||
# schematics.info.type.litematic=
|
||||
# schematics.info.type.nbt_structure=
|
||||
# schematics.info.type.schem=
|
||||
schematics.info.version=Версія схематики
|
||||
# schematics.info.mc_data_version=
|
||||
# schematics.install_mod=
|
||||
schematics.manage=Схематики
|
||||
schematics.sub_items=%d підпунктів
|
||||
|
||||
@@ -1369,14 +1371,9 @@ settings.advanced.launcher_visibility.keep=Залишити лаунчер ви
|
||||
settings.advanced.launcher_visible=Видимість лаунчера
|
||||
settings.advanced.minecraft_arguments=Аргументи запуску
|
||||
settings.advanced.minecraft_arguments.prompt=Типово
|
||||
settings.advanced.natives=Нативні бібліотеки
|
||||
settings.advanced.natives_settings=Налаштування нативних бібліотек
|
||||
settings.advanced.natives_directory=Шлях до нативних бібліотек
|
||||
settings.advanced.natives_directory.choose=Виберіть місце розташування бажаних нативних бібліотек
|
||||
settings.advanced.natives_directory.custom=Власний
|
||||
settings.advanced.natives_directory.custom.enabled=Використовувати власні нативні бібліотеки
|
||||
settings.advanced.natives_directory.default=Типово
|
||||
settings.advanced.natives_directory.default.version_id=<Назва екземпляра>
|
||||
settings.advanced.no_jvm_args=Не додавати типові аргументи JVM
|
||||
settings.advanced.no_optimizing_jvm_args=Не додавати типові аргументи оптимізації JVM
|
||||
settings.advanced.precall_command=Команда перед запуском
|
||||
@@ -1443,6 +1440,7 @@ settings.advanced.server_ip.prompt=Автоматично приєднатися
|
||||
settings.advanced.unsupported_system_options=Налаштування, що не застосовуються до поточної системи
|
||||
settings.advanced.use_native_glfw_or_sdl=Використовувати системний GLFW / SDL
|
||||
settings.advanced.use_native_openal=Використовувати системний OpenAL
|
||||
# settings.advanced.use_native_sdl=
|
||||
settings.advanced.linux_freebsd_only=Лише Linux/FreeBSD
|
||||
settings.advanced.windows_only=Тільки для Windows
|
||||
settings.advanced.wrapper_launcher=Команда обгортки
|
||||
@@ -1491,9 +1489,7 @@ settings.game.window_type=Тип вікна гри
|
||||
settings.game.window_type.fullscreen=На весь екран
|
||||
settings.game.window_type.maximized=Розгорнуте
|
||||
settings.game.window_type.windowed=Віконне
|
||||
settings.game.working_directory=Робочий каталог
|
||||
settings.game.working_directory.choose=Вибрати робочий каталог
|
||||
settings.game.working_directory.hint=Увімкніть опцію "Ізольовано" в "Робоча директорія", щоб дозволити поточному екземпляру зберігати свої налаштування, світи та моди в окремій директорії.\n\ \n\ Рекомендується увімкнути цю опцію, щоб уникнути конфліктів модів, але вам доведеться перенести ваші світи вручну.
|
||||
|
||||
settings.icon=Іконка
|
||||
settings.game_directories.read_only=Файл директорії гри був збережений новішою версією HMCL. Поточна версія HMCL не може змінювати директорії гри.
|
||||
@@ -1507,7 +1503,6 @@ settings.launcher.brightness=Режим теми
|
||||
settings.launcher.brightness.auto=Слідувати системним налаштуванням
|
||||
settings.launcher.brightness.dark=Темний режим
|
||||
settings.launcher.brightness.light=Світлий режим
|
||||
settings.launcher.common_path.tooltip=HMCL розмістить усі ігрові ресурси та залежності тут. Якщо в каталозі гри вже є існуючі бібліотеки, тоді HMCL спочатку надасть перевагу їх використанню.
|
||||
settings.launcher.debug=Налагодження
|
||||
settings.launcher.disable_april_fools=Не вмикати першоквітневі функції
|
||||
settings.launcher.disable_auto_game_options=Не змінювати мову гри
|
||||
@@ -1746,6 +1741,7 @@ update.channel.nightly.hint=Ви зараз використовуєте збі
|
||||
update.channel.nightly.title=Повідомлення каналу Нічний
|
||||
update.channel.stable=Реліз
|
||||
update.checking=Перевірка оновлень
|
||||
# update.check_failed=
|
||||
update.disable_auto_show_update_dialog=Не показувати діалогове вікно оновлення автоматично
|
||||
update.disable_auto_show_update_dialog.subtitle=Увімкніть цю опцію, щоб HMCL автоматично не показував діалогове вікно оновлення.
|
||||
update.failed=Не вдалося оновити
|
||||
@@ -1759,6 +1755,7 @@ update.latest=Це остання версія
|
||||
update.tooltip=Оновити
|
||||
update.preview=Ранній попередній перегляд випусків HMCL
|
||||
update.preview.subtitle=Увімкніть цю опцію, щоб отримувати нові версії HMCL раніше для тестування перед їх офіційним випуском.
|
||||
# update.unverified=
|
||||
|
||||
warning.java_interpreted_mode=HMCL працює в інтерпретованому середовищі Java, що значно вплине на продуктивність.\n\ \n\ Для найкращого досвіду ми рекомендуємо використовувати Java з підтримкою JIT для запуску HMCL.
|
||||
warning.software_rendering=Наразі HMCL використовує програмний рендеринг, що значно вплине на продуктивність.
|
||||
|
||||
+76
-66
@@ -190,6 +190,11 @@ addon.download.recommend=推荐版本 - Minecraft %1s
|
||||
addon.download.title.release=Minecraft %s
|
||||
addon.download.title.snapshot=Minecraft %s (快照)
|
||||
addon.modrinth=Modrinth
|
||||
addon.sort.date_created=创建日期
|
||||
addon.sort.last_updated=最近更新
|
||||
addon.sort.popularity=热度
|
||||
addon.sort.relevancy=相关度
|
||||
addon.sort.total_downloads=下载量
|
||||
|
||||
archive.author=作者
|
||||
archive.date=发布日期
|
||||
@@ -209,8 +214,8 @@ button.delete=删除
|
||||
button.disable=禁用
|
||||
button.do_not_show_again=不再显示
|
||||
button.edit=修改
|
||||
button.install=安装
|
||||
button.enable=启用
|
||||
button.install=安装
|
||||
button.export=导出
|
||||
button.no=否
|
||||
button.ok=确定
|
||||
@@ -226,20 +231,20 @@ button.select_all=全选
|
||||
button.view=查看
|
||||
button.yes=是
|
||||
|
||||
color.recent=推荐
|
||||
color.custom=自定义颜色
|
||||
|
||||
contact=反馈
|
||||
contact.chat=官方群组
|
||||
contact.chat.discord=Discord
|
||||
contact.chat.discord.statement=欢迎加入 Discord 服务器,加入后请遵守讨论区规定
|
||||
contact.chat.qq_group=用户 QQ 群
|
||||
contact.chat.qq_group.statement=欢迎加入 HMCL 官方 QQ 群,加入后请遵守群规
|
||||
contact.feedback=提交反馈
|
||||
contact.feedback.github=GitHub Issue
|
||||
contact.feedback.github.statement=提交一个 GitHub Issue
|
||||
contact.chat=官方群组
|
||||
contact.chat.qq_group=用户 QQ 群
|
||||
contact.chat.qq_group.statement=欢迎加入 HMCL 官方 QQ 群,加入后请遵守群规
|
||||
contact.chat.discord=Discord
|
||||
contact.chat.discord.statement=欢迎加入 Discord 服务器,加入后请遵守讨论区规定
|
||||
|
||||
crash.NoClassDefFound=请确认 Hello Minecraft! Launcher 本体是否完整,或更新你的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
color.recent=推荐
|
||||
color.custom=自定义颜色
|
||||
|
||||
crash.NoClassDefFound=请确认 HMCL 本体是否完整,或更新你的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
crash.user_fault=你的系统或 Java 环境可能安装不当导致本软件崩溃,请检查你的 Java 环境或你的电脑。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
|
||||
curse.category.0=全部
|
||||
@@ -270,14 +275,14 @@ curse.category.5299=教育
|
||||
curse.category.5232=额外行星
|
||||
curse.category.5129=原版增强
|
||||
curse.category.5189=实用与 QOL
|
||||
curse.category.5190=QOL
|
||||
curse.category.5191=实用与 QOL
|
||||
curse.category.5192=梦幻菜单
|
||||
curse.category.6145=空岛
|
||||
curse.category.6814=性能
|
||||
curse.category.6954=动态联合/集成动力 (Integrated Dynamics)
|
||||
curse.category.6484=机械动力 (Create)
|
||||
curse.category.6821=错误修复
|
||||
curse.category.6145=空岛
|
||||
curse.category.5190=QOL
|
||||
curse.category.5191=实用与 QOL
|
||||
curse.category.5192=梦幻菜单
|
||||
curse.category.423=信息展示
|
||||
curse.category.426=模组扩展
|
||||
curse.category.434=装备武器
|
||||
@@ -288,10 +293,10 @@ curse.category.429=工业 (Industrialcraft)
|
||||
curse.category.419=魔法
|
||||
curse.category.412=科技
|
||||
curse.category.4557=红石
|
||||
curse.category.428=匠魂
|
||||
curse.category.8937=ModJam 2025
|
||||
curse.category.9026=创造模式
|
||||
curse.category.10775=恐怖
|
||||
curse.category.428=匠魂
|
||||
curse.category.414=交通运输
|
||||
curse.category.4486=幸运方块 (Lucky Blocks)
|
||||
curse.category.432=建筑 (Buildcraft)
|
||||
@@ -371,13 +376,6 @@ curse.category.6553=写实
|
||||
curse.category.6554=幻想
|
||||
curse.category.6555=原生
|
||||
|
||||
curse.sort.author=作者
|
||||
curse.sort.date_created=创建日期
|
||||
curse.sort.last_updated=最近更新
|
||||
curse.sort.name=名称
|
||||
curse.sort.popularity=热度
|
||||
curse.sort.total_downloads=下载量
|
||||
|
||||
datetime.format=yyyy 年 MM 月 dd 日 HH:mm:ss
|
||||
|
||||
download=下载
|
||||
@@ -430,27 +428,27 @@ 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.create_hmcl_current_directory_failure=HMCL 无法创建 HMCL 文件夹 (%s),请将 HMCL 移动至其他位置再启动。\n如遇到问题,你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.javafx.incomplete=JavaFX 运行环境不完整。请尝试更换你的 Java 或者重新安装 OpenJFX。
|
||||
fatal.javafx.missing=缺少 JavaFX 运行环境。请使用包含 OpenJFX 的 Java 运行环境启动 Hello Minecraft! Launcher。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.config_change_owner_root=你正在使用 root 账户启动 Hello Minecraft! Launcher,这可能导致你未来无法正常使用其他账户正常启动 HMCL。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。\n是否继续启动?
|
||||
fatal.config_in_temp_dir=你正在临时文件夹中启动 Hello Minecraft! Launcher,你的设置和游戏数据可能会丢失。建议将 HMCL 移动至其他位置再启动。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。\n是否继续启动?
|
||||
fatal.config_loading_failure=Hello Minecraft! Launcher 无法加载配置文件。\n请确保 HMCL 对“%s”文件夹及该文件夹下的文件拥有读写权限。\n对于 macOS,尝试将 HMCL 放在除“桌面”“下载”“文稿”之外的有权限的地方再试。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.config_loading_failure.unix=Hello Minecraft! Launcher 无法加载配置文件,因为配置文件是由用户“%1$s”创建的。\n请使用 root 账户启动 HMCL (不推荐),或在终端中执行以下命令将配置文件的所有权变更为当前用户:\n%2$s\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.config_unsupported_version=当前配置文件是由更高版本的 Hello Minecraft! Launcher 创建的,当前版本的 HMCL 无法正常加载。请更新并重启 HMCL。\n在更新启动器之前,你修改的所有设置都不会被保存。\n如果遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
fatal.mac_app_translocation=由于 macOS 的安全机制,Hello Minecraft! Launcher 被系统隔离至临时文件夹中。\n请将 HMCL 移动到其他文件夹后再尝试启动,否则你的设置和游戏数据可能会在重启后丢失。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。\n是否继续启动?
|
||||
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即将完成升级,请重新打开 HMCL。\n如遇到问题,你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.apply_update_failure=我们很抱歉 Hello Minecraft! Launcher 无法自动完成升级,因为出现了一些问题。\n但你依可以从 %s 手动下载 HMCL 来完成升级。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.apply_update_need_win7=Hello Minecraft! Launcher 无法在 Windows XP/Vista 上进行自动更新。请从 %s 手动下载 HMCL 来完成升级。
|
||||
fatal.javafx.missing=缺少 JavaFX 运行环境。请使用包含 OpenJFX 的 Java 运行环境启动 HMCL。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.config_change_owner_root=你正在使用 root 账户启动 HMCL,这可能导致你未来无法正常使用其他账户正常启动 HMCL。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。\n是否继续启动?
|
||||
fatal.config_in_temp_dir=你正在临时文件夹中启动 HMCL,你的设置和游戏数据可能会丢失。建议将 HMCL 移动至其他位置再启动。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。\n是否继续启动?
|
||||
fatal.config_loading_failure=HMCL 无法加载配置文件。\n请确保 HMCL 对“%s”文件夹及该文件夹下的文件拥有读写权限。\n对于 macOS,尝试将 HMCL 放在除“桌面”“下载”“文稿”之外的有权限的地方再试。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.config_loading_failure.unix=HMCL 无法加载配置文件,因为配置文件是由用户“%1$s”创建的。\n请使用 root 账户启动 HMCL (不推荐),或在终端中执行以下命令将配置文件的所有权变更为当前用户:\n%2$s\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.config_unsupported_version=当前配置文件是由更高版本的 HMCL 创建的,当前版本的 HMCL 无法正常加载。请更新并重启 HMCL。\n在更新启动器之前,你修改的所有设置都不会被保存。\n如果遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
fatal.mac_app_translocation=由于 macOS 的安全机制,HMCL 被系统隔离至临时文件夹中。\n请将 HMCL 移动到其他文件夹后再尝试启动,否则你的设置和游戏数据可能会在重启后丢失。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。\n是否继续启动?
|
||||
fatal.migration_requires_manual_reboot=HMCL 即将完成升级,请重新打开 HMCL。\n如遇到问题,你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.apply_update_failure=我们很抱歉 HMCL 无法自动完成升级,因为出现了一些问题。\n但你依可以从 %s 手动下载 HMCL 来完成升级。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.apply_update_need_win7=HMCL 无法在 Windows XP/Vista 上进行自动更新。请从 %s 手动下载 HMCL 来完成升级。
|
||||
fatal.deprecated_java_version=HMCL 未来需要 Java 17 或更高版本才能运行,但依然支持使用 Java 6~16 启动游戏。建议安装最新版本的 Java 以确保 HMCL 能正常工作。\n你可以继续保留旧版本 Java。HMCL 能够识别与管理多个 Java,并会自动根据游戏版本为你选择合适的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.deprecated_java_version.download_link=下载 Java %d
|
||||
fatal.samba=如果你正在通过 Samba 共享的文件夹中运行 Hello Minecraft! Launcher,启动器可能无法正常工作。请尝试更新你的 Java 或在本地文件夹内运行 HMCL。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.samba=如果你正在通过 Samba 共享的文件夹中运行 HMCL,启动器可能无法正常工作。请尝试更新你的 Java 或在本地文件夹内运行 HMCL。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.illegal_char=由于你的运行路径中存在非法字符“=”,你将无法使用外置登录账户以及离线登录更换皮肤功能。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.unsupported_platform=Minecraft 尚未对你的平台提供完善支持,所以可能影响游戏体验或无法启动游戏。\n\
|
||||
若无法启动 Minecraft 1.17 及更高版本,可以尝试在“(全局/实例特定) 游戏设置 → 图形设置”中将“渲染器”切换为“Mesa LLVMpipe” / “Mesa Lavapipe”,以获得更好的兼容性。\n\
|
||||
如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已为龙芯提供支持。\n如果遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher 已为 Windows on Arm 平台提供原生支持。如果你在游戏中遇到问题,请尝试使用 x86 架构的 Java 启动游戏。\n如果你正在使用 <b>高通</b> 平台,你可能需要安装 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 兼容包</a> 后才能进行游戏。点击链接前往 Microsoft Store 安装兼容包。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
fatal.unsupported_platform.loongarch=HMCL 已为龙芯提供支持。\n如果遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
fatal.unsupported_platform.windows_arm64=HMCL 已为 Windows on Arm 平台提供原生支持。如果你在游戏中遇到问题,请尝试使用 x86 架构的 Java 启动游戏。\n如果你正在使用 <b>高通</b> 平台,你可能需要安装 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 兼容包</a> 后才能进行游戏。点击链接前往 Microsoft Store 安装兼容包。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
# When translating the following string into non-Latin languages, please retain the English translation
|
||||
fatal.wine_warning=HMCL is running under Wine or one of its distributions.\nAs a cross-platform application, HMCL is best experienced natively on your host OS.\nPress "OK" to continue, but please note that some issues may arise.\n\n您目前正在使用 Wine 或其发行版运行 HMCL。\nHMCL 是跨平台的,我们建议您直接在宿主系统上运行。\n按下“确定”以继续,但请注意,此操作可能会引发一些问题。
|
||||
|
||||
@@ -473,14 +471,21 @@ game.crash.feedback=<b>请不要将本界面截图或拍照给他人!</b>如
|
||||
game.crash.info=游戏信息
|
||||
game.crash.reason=崩溃原因
|
||||
game.crash.reason.analyzing=分析中……
|
||||
game.crash.reason.multiple=检测到多个原因:\n\n
|
||||
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你可以尝试使用地图编辑工具编辑世界删除该实体,或者直接删除对应的模组。\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.modmixin_failure=当前游戏由于某些模组注入失败,无法继续运行。\n这一般代表着该模组存在问题,或与当前环境不兼容。\n你可以查看日志寻找出错模组。
|
||||
game.crash.reason.forge_error=Forge/NeoForge 可能已经提供了错误信息。\n你可以查看日志,并根据错误报告中的日志信息进行对应处理。\n如果没有看到报错信息,可以查看错误报告了解错误具体是如何发生的。\n%1$s
|
||||
game.crash.reason.mod_resolution0=当前游戏由于一些模组出现问题,无法继续运行。\n你可以查看日志寻找出错模组。
|
||||
game.crash.reason.mixin_apply_mod_failed=当前游戏由于 Mixin 无法应用于“%1$s”模组,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
|
||||
game.crash.reason.java_version_is_too_high=当前游戏由于 Java 版本过高,无法继续运行。\n请在“(全局/实例特定) 游戏设置 → 游戏 Java”中改用较低版本的 Java,然后再启动游戏。\n如果没有,可以从 <a href="https://www.java.com/download/">java.com (Java 8)</a> 或 <a href="https://bell-sw.com/pages/downloads/#downloads">BellSoft Liberica Full JRE (Java 17)</a> 等平台下载、安装一个 (安装完后需重启启动器)。
|
||||
game.crash.reason.need_jdk11=当前游戏由于 Java 版本不合适,无法继续运行。\n你需要下载安装 <a href="https://bell-sw.com/pages/downloads/#downloads">Java 11</a>,并在“(全局/实例特定) 游戏设置 → 游戏 Java”中将 Java 设置为 11.x 的版本。
|
||||
game.crash.reason.mod_name=当前游戏由于模组文件名称问题,无法继续运行。\n模组文件名称应只使用半角的大小写字母 (Aa~Zz)、数字 (0~9)、横线 (-)、下划线 (_)和点 (.)。\n请到模组文件夹中将所有不合规的模组文件名修改为上述合规字符。
|
||||
game.crash.reason.incomplete_forge_installation=当前游戏由于 Forge/NeoForge 安装不完整,无法继续运行。\n请在“实例管理 → 自动安装”中卸载 Forge 并重新安装。
|
||||
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如果你认为这个文件可以删除,你可以在备份这个文件后尝试删除它,并重新启动游戏。
|
||||
@@ -494,38 +499,31 @@ game.crash.reason.graphics_driver=当前游戏由于显卡驱动问题而崩溃
|
||||
game.crash.reason.macos_failed_to_find_service_port_for_display=当前游戏由于 Apple Silicon 平台下初始化 OpenGL 窗口失败,无法继续运行。\n对于该问题,HMCL 暂无直接性的解决方案。请你尝试任意打开一个浏览器并全屏,然后再回到 HMCL 启动游戏。在弹出游戏窗口前<b>迅速切回浏览器页面</b>,等待游戏窗口出现后再切回游戏窗口。
|
||||
game.crash.reason.illegal_access_error=当前游戏由于某些模组的问题,无法继续运行。\n如果你认识“%1$s”,你可以更新或删除对应模组再试。
|
||||
game.crash.reason.install_mixinbootstrap=当前游戏由于缺失 MixinBootstrap,无法继续运行。\n你可以尝试安装 <a href="https://www.curseforge.com/minecraft/mc-mods/mixinbootstrap">MixinBootstrap</a> 解决该问题。若安装后崩溃,可以尝试在该模组的文件名开头添加半角感叹号 (!) 解决。
|
||||
game.crash.reason.need_jdk11=当前游戏由于 Java 版本不合适,无法继续运行。\n你需要下载安装 <a href="https://bell-sw.com/pages/downloads/#downloads">Java 11</a>,并在“(全局/实例特定) 游戏设置 → 游戏 Java”中将 Java 设置为 11.x 的版本。
|
||||
game.crash.reason.optifine_is_not_compatible_with_forge=当前游戏由于 OptiFine 与当前版本的 Forge 不兼容,导致游戏崩溃。\n点击 <a href="https://zkitefly.github.io/optifine-forge-support-list">此处</a> 查看 OptiFine 所兼容的 Forge 版本,并严格按照对应版本重新安装游戏或在“实例管理 → 自动安装”中更换版本。\n经测试,Forge 版本过高或过低都可能导致崩溃。
|
||||
game.crash.reason.mod_files_are_decompressed=当前游戏由于模组文件被解压了,无法继续运行。\n请直接把整个模组文件放进模组文件夹中即可。\n解压模组文件会导致游戏出错。请删除模组文件夹中已被解压的模组,然后再启动游戏。
|
||||
game.crash.reason.shaders_mod=当前游戏由于同时安装了 OptiFine 和 Shaders 模组,无法继续运行。\n由于 OptiFine 已集成 Shaders 模组的功能,所以只需删除 Shaders 模组即可。
|
||||
game.crash.reason.rtss_forest_sodium=当前游戏由于 RivaTuner Statistics Server (RTSS) 与 Sodium 不兼容,导致游戏崩溃。\n点击 <a href="https://github.com/CaffeineMC/sodium-fabric/wiki/Known-Issues#rtss-incompatible">此处</a> 查看详情。
|
||||
game.crash.reason.too_many_mods_lead_to_exceeding_the_id_limit=当前游戏由于你所安装的模组过多,超出了游戏的 ID 限制,无法继续运行。\n请尝试安装 <a href="https://www.curseforge.com/minecraft/mc-mods/jeid">JEID</a> 等修复模组,或删除部分大型模组。
|
||||
game.crash.reason.night_config_fixes=当前游戏由于 Night Config 库的一些问题,无法继续运行。\n你可以尝试安装 <a href="https://www.curseforge.com/minecraft/mc-mods/night-config-fixes">Night Config Fixes</a> 模组,这或许能帮助你解决这个问题。\n更多详情,可访问该模组的 <a href="https://github.com/Fuzss/nightconfigfixes">GitHub 仓库</a>。
|
||||
game.crash.reason.optifine_causes_the_world_to_fail_to_load=当前游戏可能由于 OptiFine 而无法继续运行。\n该问题只在特定 OptiFine 版本中出现,你可以尝试在“实例管理 → 自动安装”中更换 OptiFine 的版本。
|
||||
game.crash.reason.jdk_9=当前游戏由于 Java 版本过高,无法继续运行。\n你需要下载安装 <a href="https://bell-sw.com/pages/downloads/#downloads">Java 8</a>,并在“(全局/实例特定) 游戏设置 → 游戏 Java”中将 Java 设置为 1.8.x 的版本。
|
||||
game.crash.reason.jvm_32bit=当前游戏由于内存分配过大,超过了 32 位 Java 虚拟机内存限制,无法继续运行。\n如果你的电脑是 64 位系统,请下载安装并更换 64 位 Java。<a href="https://bell-sw.com/pages/downloads/#downloads">下载 Java</a>\n如果你的电脑是 32 位系统,你或许可以重新安装 64 位系统,或换一台新电脑。\n或者,你可以在“(全局/实例特定) 游戏设置 → 游戏内存”中关闭“自动分配内存”,并把内存分配值调整为 1024 MiB 或以下。
|
||||
game.crash.reason.loading_crashed_forge=当前游戏由于模组“%1$s (%2$s)”错误,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
|
||||
game.crash.reason.loading_crashed_fabric=当前游戏由于模组“%1$s”错误,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
|
||||
game.crash.reason.memory_exceeded=当前游戏由于内存分配过大,无法继续运行。\n该问题是由于系统页面文件太小导致的。\n你需要在“(全局/实例特定) 游戏设置”中关闭“自动分配内存”,并将内存分配值调低至游戏能正常启动为止。\n你还可以尝试将虚拟内存设置调整为“自动管理所有驱动器分页文件大小”,<a href="https://docs.hmcl.net/assets/img/hmcl/自动管理所有驱动器分页文件大小.webp">详情</a>。
|
||||
game.crash.reason.mac_jdk_8u261=当前游戏由于你所使用的 Forge/OptiFine 与 Java 冲突而崩溃。\n请尝试更新 Forge/OptiFine,或使用 Java 8u251 及更早版本启动。
|
||||
game.crash.reason.forge_repeat_installation=当前游戏由于 Forge 重复安装,无法继续运行。<a href="https://github.com/HMCL-dev/HMCL/issues/1880">此为已知问题</a>\n建议将日志上传并反馈至 GitHub,以便我们找到更多线索并修复此问题。\n目前你可以在“实例管理 → 自动安装”中卸载 Forge 并重新安装。
|
||||
game.crash.reason.optifine_repeat_installation=当前游戏由于 OptiFine 重复安装,无法继续运行。\n请删除模组文件夹下的 OptiFine 或前往“实例管理 → 自动安装”卸载安装的 OptiFine。
|
||||
game.crash.reason.memory_exceeded=当前游戏由于内存分配过大,无法继续运行。\n该问题是由于系统页面文件太小导致的。\n你需要在“(全局/实例特定) 游戏设置”中关闭“自动分配内存”,并将内存分配值调低至游戏能正常启动为止。\n你还可以尝试将虚拟内存设置调整为“自动管理所有驱动器分页文件大小”,<a href="https://docs.hmcl.net/assets/img/hmcl/自动管理所有驱动器分页文件大小.webp">详情</a>。
|
||||
game.crash.reason.mod=当前游戏由于“%1$s”的问题,无法继续运行。\n你可以更新或删除已经安装的“%1$s”再试。
|
||||
game.crash.reason.mod_resolution=当前游戏由于模组依赖问题,无法继续运行。Fabric 提供了如下信息:\n%1$s
|
||||
game.crash.reason.forgemod_resolution=当前游戏由于模组依赖问题,无法继续运行。Forge/NeoForge 提供了如下信息:\n%1$s
|
||||
game.crash.reason.forge_found_duplicate_mods=当前游戏由于模组重复安装,无法继续运行。Forge/NeoForge 提供了如下信息:\n%1$s
|
||||
game.crash.reason.mod_resolution_collection=当前游戏由于前置模组版本不匹配,无法继续运行。\n“%1$s”需要前置模组“%2$s”才能继续运行。\n这表示你需要更新或降级前置模组。你可以前往“下载 → 模组”页面或网上下载“%3$s”。
|
||||
game.crash.reason.mod_resolution_conflict=当前游戏由于模组冲突,无法继续运行。\n“%1$s”与“%2$s”不兼容。
|
||||
game.crash.reason.mod_resolution_missing=当前游戏由于缺少前置模组,无法继续运行。\n“%1$s”需要前置模组“%2$s”才能继续运行。\n这表示有一些必需的模组没有安装,或该模组版本不匹配。你可以前往“下载 → 模组”页面或网上下载“%3$s”。
|
||||
game.crash.reason.mod_resolution_missing_minecraft=当前游戏由于模组和 Minecraft 游戏版本不匹配,无法继续运行。\n“%1$s”需要 Minecraft %2$s 才能运行。\n如果你要继续使用你已经安装的模组,你可以选择安装对应的 Minecraft 版本。如果你要继续使用当前 Minecraft 版本,你需要安装对应版本的模组。
|
||||
game.crash.reason.mod_resolution_mod_version=%1$s (版本号 %2$s)
|
||||
game.crash.reason.mod_resolution_mod_version.any=%1$s (任意版本)
|
||||
game.crash.reason.forge_repeat_installation=当前游戏由于 Forge 重复安装,无法继续运行。<a href="https://github.com/HMCL-dev/HMCL/issues/1880">此为已知问题</a>\n建议将日志上传并反馈至 GitHub,以便我们找到更多线索并修复此问题。\n目前你可以在“实例管理 → 自动安装”中卸载 Forge 并重新安装。
|
||||
game.crash.reason.optifine_repeat_installation=当前游戏由于 OptiFine 重复安装,无法继续运行。\n请删除模组文件夹下的 OptiFine 或前往“实例管理 → 自动安装”卸载安装的 OptiFine。
|
||||
game.crash.reason.forgemod_resolution=当前游戏由于模组依赖问题,无法继续运行。Forge/NeoForge 提供了如下信息:\n%1$s
|
||||
game.crash.reason.forge_found_duplicate_mods=当前游戏由于模组重复安装,无法继续运行。Forge/NeoForge 提供了如下信息:\n%1$s
|
||||
game.crash.reason.modmixin_failure=当前游戏由于某些模组注入失败,无法继续运行。\n这一般代表着该模组存在问题,或与当前环境不兼容。\n你可以查看日志寻找出错模组。
|
||||
game.crash.reason.night_config_fixes=当前游戏由于 Night Config 库的一些问题,无法继续运行。\n你可以尝试安装 <a href="https://www.curseforge.com/minecraft/mc-mods/night-config-fixes">Night Config Fixes</a> 模组,这或许能帮助你解决这个问题。\n更多详情,可访问该模组的 <a href="https://github.com/Fuzss/nightconfigfixes">GitHub 仓库</a>。
|
||||
game.crash.reason.forge_error=Forge/NeoForge 可能已经提供了错误信息。\n你可以查看日志,并根据错误报告中的日志信息进行对应处理。\n如果没有看到报错信息,可以查看错误报告了解错误具体是如何发生的。\n%1$s
|
||||
game.crash.reason.mod_resolution0=当前游戏由于一些模组出现问题,无法继续运行。\n你可以查看日志寻找出错模组。
|
||||
game.crash.reason.java_version_is_too_high=当前游戏由于 Java 版本过高,无法继续运行。\n请在“(全局/实例特定) 游戏设置 → 游戏 Java”中改用较低版本的 Java,然后再启动游戏。\n如果没有,可以从 <a href="https://www.java.com/download/">java.com (Java 8)</a> 或 <a href="https://bell-sw.com/pages/downloads/#downloads">BellSoft Liberica Full JRE (Java 17)</a> 等平台下载、安装一个 (安装完后需重启启动器)。
|
||||
game.crash.reason.mod_name=当前游戏由于模组文件名称问题,无法继续运行。\n模组文件名称应只使用半角的大小写字母 (Aa~Zz)、数字 (0~9)、横线 (-)、下划线 (_)和点 (.)。\n请到模组文件夹中将所有不合规的模组文件名修改为上述合规字符。
|
||||
game.crash.reason.incomplete_forge_installation=当前游戏由于 Forge/NeoForge 安装不完整,无法继续运行。\n请在“实例管理 → 自动安装”中卸载 Forge 并重新安装。
|
||||
game.crash.reason.optifine_is_not_compatible_with_forge=当前游戏由于 OptiFine 与当前版本的 Forge 不兼容,导致游戏崩溃。\n点击 <a href="https://zkitefly.github.io/optifine-forge-support-list">此处</a> 查看 OptiFine 所兼容的 Forge 版本,并严格按照对应版本重新安装游戏或在“实例管理 → 自动安装”中更换版本。\n经测试,Forge 版本过高或过低都可能导致崩溃。
|
||||
game.crash.reason.mod_files_are_decompressed=当前游戏由于模组文件被解压了,无法继续运行。\n请直接把整个模组文件放进模组文件夹中即可。\n解压模组文件会导致游戏出错。请删除模组文件夹中已被解压的模组,然后再启动游戏。
|
||||
game.crash.reason.shaders_mod=当前游戏由于同时安装了 OptiFine 和 Shaders 模组,无法继续运行。\n由于 OptiFine 已集成 Shaders 模组的功能,所以只需删除 Shaders 模组即可。
|
||||
game.crash.reason.rtss_forest_sodium=当前游戏由于 RivaTuner Statistics Server (RTSS) 与 Sodium 不兼容,导致游戏崩溃。\n点击 <a href="https://github.com/CaffeineMC/sodium-fabric/wiki/Known-Issues#rtss-incompatible">此处</a> 查看详情。
|
||||
game.crash.reason.too_many_mods_lead_to_exceeding_the_id_limit=当前游戏由于你所安装的模组过多,超出了游戏的 ID 限制,无法继续运行。\n请尝试安装 <a href="https://www.curseforge.com/minecraft/mc-mods/jeid">JEID</a> 等修复模组,或删除部分大型模组。
|
||||
game.crash.reason.optifine_causes_the_world_to_fail_to_load=当前游戏可能由于 OptiFine 而无法继续运行。\n该问题只在特定 OptiFine 版本中出现,你可以尝试在“实例管理 → 自动安装”中更换 OptiFine 的版本。
|
||||
game.crash.reason.modlauncher_8=当前游戏由于你所使用的 Forge 版本与当前使用的 Java 冲突而崩溃。请尝试更新 Forge 到 36.2.26 或更高版本或换用版本低于 1.8.0.320 的 Java。<a href="https://bell-sw.com/pages/downloads/?version=java-8&package=jre-full">Liberica JDK 8u312+7</a>
|
||||
game.crash.reason.no_class_def_found_error=当前游戏由于代码不完整,无法继续运行。\n你的游戏可能缺失了某个模组,或者某些模组文件不完整,或者模组与游戏的版本不匹配。\n你可能需要重新安装游戏和模组,或寻求他人帮助。\n缺失:\n%1$s
|
||||
game.crash.reason.no_such_method_error=当前游戏由于代码不完整,无法继续运行。\n你的游戏可能缺失了某个模组,或者某些模组文件不完整,或者模组与游戏的版本不匹配。\n你可能需要重新安装游戏和模组,或寻求他人帮助。
|
||||
@@ -553,7 +551,7 @@ game_directory.use_relative_path=若可能,游戏文件夹使用相对路径
|
||||
game_directory.root=使用磁盘根目录作为游戏文件夹可能会导致无法启动游戏,并会在磁盘根目录下创建多个文件(夹)。\n\n确定要继续吗?
|
||||
|
||||
help=帮助
|
||||
help.doc=Hello Minecraft! Launcher 帮助文档
|
||||
help.doc=HMCL 帮助文档
|
||||
help.detail=可查阅数据包、整合包制作指南等内容
|
||||
|
||||
input.email=用户名必须是邮箱
|
||||
@@ -581,9 +579,9 @@ install.installer.choose=选择 %s 版本
|
||||
install.installer.cleanroom=Cleanroom
|
||||
install.installer.do_not_install=不安装
|
||||
install.installer.fabric=Fabric
|
||||
install.installer.fabric-api=Fabric API
|
||||
install.installer.legacyfabric=Legacy Fabric
|
||||
install.installer.legacyfabric-api=Legacy Fabric API
|
||||
install.installer.fabric-api=Fabric API
|
||||
install.installer.fabric-quilt-api.warning=%1$s 是一个模组,将会被安装到新游戏的模组文件夹。请你在安装游戏后不要修改当前游戏的“版本隔离”设置。如果你在之后修改了相关设置,则需要重新安装 %1$s。
|
||||
install.installer.forge=Forge
|
||||
install.installer.neoforge=NeoForge
|
||||
@@ -772,9 +770,9 @@ launcher.cache_directory.default=默认 ("%APPDATA%/.minecraft" 或 "~/.minecraf
|
||||
launcher.cache_directory.disabled=禁用 (总是使用游戏文件夹路径)
|
||||
launcher.cache_directory.invalid=无法创建自定义的缓存文件夹。已经恢复到默认设置。
|
||||
launcher.contact=联系我们
|
||||
launcher.crash=Hello Minecraft! Launcher 遇到了无法处理的错误。请复制下列内容并点击右下角的按钮反馈问题。
|
||||
launcher.crash.java_internal_error=Hello Minecraft! Launcher 由于当前 Java 损坏而无法继续运行。请卸载当前 Java,点击 <a href="https://bell-sw.com/pages/downloads/#downloads">此处</a> 安装合适的 Java 版本。
|
||||
launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher 遇到了无法处理的错误。已检测到你的启动器不是最新版本,请更新后再试。
|
||||
launcher.crash=HMCL 遇到了无法处理的错误。请复制下列内容并点击右下角的按钮反馈问题。
|
||||
launcher.crash.java_internal_error=HMCL 由于当前 Java 损坏而无法继续运行。请卸载当前 Java,点击 <a href="https://bell-sw.com/pages/downloads/#downloads">此处</a> 安装合适的 Java 版本。
|
||||
launcher.crash.hmcl_out_dated=HMCL 遇到了无法处理的错误。已检测到你的启动器不是最新版本,请更新后再试。
|
||||
launcher.update_java=请更新你的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
|
||||
libraries.download=下载依赖库
|
||||
@@ -792,6 +790,7 @@ logwindow.export_game_crash_logs=导出游戏崩溃信息
|
||||
logwindow.export_dump=导出游戏运行栈
|
||||
logwindow.export_dump.no_dependency=你的 Java 不包含用于创建游戏运行栈的依赖。请前往 HMCL QQ 群或 Discord 寻求帮助。
|
||||
|
||||
|
||||
message.cancelled=操作被取消
|
||||
message.confirm=提示
|
||||
message.copied=已复制到剪贴板
|
||||
@@ -842,6 +841,9 @@ modpack.installing=安装整合包
|
||||
modpack.installing.given=安装 %s 整合包
|
||||
modpack.invalid=无效的整合包升级文件。可能是下载时出现问题。
|
||||
modpack.mismatched_type=整合包类型不匹配。当前游戏为“%s”整合包,但是提供的整合包更新文件为“%s”整合包。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
modpack.optional_files=可选文件
|
||||
modpack.retry_optional_files=可选文件加载失败,点击重试
|
||||
modpack.unknown_optional_file=未知文件
|
||||
modpack.name=整合包名称
|
||||
modpack.origin=来源
|
||||
modpack.origin.url=官方网站
|
||||
@@ -855,15 +857,15 @@ modpack.type.curse.error=未能完成该整合包所需的依赖下载,请再
|
||||
modpack.type.curse.not_found=部分必需文件已经在网络中被删除并且再也无法下载。请尝试该整合包的最新版本或者安装其他整合包。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
modpack.type.manual.warning=该整合包由发布者手动打包,其中可能已经包含启动器。建议尝试解压后使用其自带的启动器运行游戏。\n如遇到问题,你可以点击右上角帮助按钮进行求助。\nHMCL 可以尝试导入该整合包,但不保证可用性。是否继续?
|
||||
modpack.type.mcbbs=MCBBS 整合包 (推荐)
|
||||
modpack.type.mcbbs.export=可以被 Hello Minecraft! Launcher 导入
|
||||
modpack.type.mcbbs.export=可以被 HMCL 导入
|
||||
modpack.type.modrinth=Modrinth
|
||||
modpack.type.modrinth.export=可以被主流第三方启动器导入
|
||||
modpack.type.multimc=MultiMC
|
||||
modpack.type.multimc.export=可以被 Hello Minecraft! Launcher 和 MultiMC 导入
|
||||
modpack.type.multimc.export=可以被 HMCL 和 MultiMC 导入
|
||||
modpack.type.server=服务器自动更新整合包
|
||||
modpack.type.server.export=允许服务器管理员远程更新游戏客户端
|
||||
modpack.type.server.malformed=服务器整合包配置格式错误,请联系服务器管理员解决此问题。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
modpack.unsupported=Hello Minecraft! Launcher 不支持该整合包格式
|
||||
modpack.unsupported=HMCL 不支持该整合包格式
|
||||
modpack.update=正在升级整合包
|
||||
modpack.wizard=导出整合包向导
|
||||
modpack.wizard.step.1=基本设置
|
||||
@@ -1143,7 +1145,6 @@ schematics=原理图
|
||||
schematics.add=添加原理图
|
||||
schematics.add.failed=添加原理图失败
|
||||
schematics.add.title=选择要添加的原理图文件
|
||||
schematics.back_to=返回到“%s”
|
||||
schematics.create_directory=创建文件夹
|
||||
schematics.create_directory.prompt=请输入新文件夹名称
|
||||
schematics.create_directory.failed=创建文件夹失败
|
||||
@@ -1159,7 +1160,13 @@ schematics.info.time_created=创建时间
|
||||
schematics.info.time_modified=修改时间
|
||||
schematics.info.total_blocks=总方块数
|
||||
schematics.info.total_volume=总体积
|
||||
schematics.info.type=原理图类型
|
||||
schematics.info.type.litematic=Litematic (Litematica)
|
||||
schematics.info.type.nbt_structure=NBT 结构 (原版, 机械动力)
|
||||
schematics.info.type.schem=Schem (Schematica, WE, MCEdit)
|
||||
schematics.info.version=原理图版本
|
||||
schematics.info.mc_data_version=Minecraft 数据版本
|
||||
schematics.install_mod=安装投影模组
|
||||
schematics.manage=原理图管理
|
||||
schematics.sub_items=%d 个子项
|
||||
|
||||
@@ -1455,6 +1462,7 @@ settings.type.global.preset.auto_name=预设 %d
|
||||
|
||||
shaderpack.download.title=光影下载 - %1s
|
||||
|
||||
|
||||
system.architecture=架构
|
||||
system.operating_system=操作系统
|
||||
|
||||
@@ -1591,6 +1599,7 @@ update.channel.nightly.hint=你正在使用 HMCL 预览版。预览版更新较
|
||||
update.channel.nightly.title=预览版提示
|
||||
update.channel.stable=稳定版
|
||||
update.checking=正在检查更新
|
||||
update.check_failed=检查更新失败 %s
|
||||
update.disable_auto_show_update_dialog=不自动显示更新弹窗
|
||||
update.disable_auto_show_update_dialog.subtitle=启用此选项,HMCL 将不会自动弹出更新弹窗。
|
||||
update.failed=更新失败
|
||||
@@ -1604,6 +1613,7 @@ update.latest=当前版本为最新版本
|
||||
update.tooltip=更新
|
||||
update.preview=提前预览 HMCL 版本
|
||||
update.preview.subtitle=启用此选项,你将可以提前获取 HMCL 的新版本,以便在正式发布前进行测试。
|
||||
update.unverified=更新不可用,因为无法验证此启动器实例完整性
|
||||
|
||||
warning.java_interpreted_mode=HMCL 正在运行在解释器模式的 Java 环境中,性能将会受到很大影响。\n我们建议你使用支持 JIT 的 Java 启动 HMCL 以获取最佳体验。
|
||||
warning.software_rendering=HMCL 正在使用软件渲染,性能将会受到很大影响。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
+55
-47
@@ -188,6 +188,11 @@ addon.download.recommend=推薦版本 - Minecraft %1s
|
||||
addon.download.title.release=Minecraft %s
|
||||
addon.download.title.snapshot=Minecraft %s (快照)
|
||||
addon.modrinth=Modrinth
|
||||
addon.sort.date_created=建立日期
|
||||
addon.sort.last_updated=最近更新
|
||||
addon.sort.popularity=熱度
|
||||
addon.sort.relevancy=相關度
|
||||
addon.sort.total_downloads=下載量
|
||||
|
||||
archive.author=作者
|
||||
archive.date=發布日期
|
||||
@@ -207,8 +212,8 @@ button.delete=刪除
|
||||
button.disable=停用
|
||||
button.do_not_show_again=不再顯示
|
||||
button.edit=編輯
|
||||
button.install=安裝
|
||||
button.enable=啟用
|
||||
button.install=安裝
|
||||
button.export=匯出
|
||||
button.no=否
|
||||
button.ok=確定
|
||||
@@ -237,7 +242,7 @@ contact.feedback.github.statement=提交一個 GitHub Issue
|
||||
color.recent=建議
|
||||
color.custom=自訂顏色
|
||||
|
||||
crash.NoClassDefFound=請確認 Hello Minecraft! Launcher 本體是否完整,或更新你的 Java。
|
||||
crash.NoClassDefFound=請確認 HMCL 本體是否完整,或更新你的 Java。
|
||||
crash.user_fault=你的系統或 Java 環境可能安裝不當導致本軟體當機,請檢查你的 Java 環境或你的電腦!可以嘗試重新安裝 Java。
|
||||
|
||||
curse.category.0=全部
|
||||
@@ -286,10 +291,10 @@ curse.category.429=工業 (Industrialcraft)
|
||||
curse.category.419=魔法
|
||||
curse.category.412=科技
|
||||
curse.category.4557=紅石
|
||||
curse.category.428=匠魂
|
||||
curse.category.8937=ModJam 2025
|
||||
curse.category.9026=創造模式
|
||||
curse.category.10775=恐怖
|
||||
curse.category.428=匠魂
|
||||
curse.category.414=交通運輸
|
||||
curse.category.4486=幸運方塊 (Lucky Blocks)
|
||||
curse.category.432=建築 (Buildcraft)
|
||||
@@ -369,13 +374,6 @@ curse.category.6553=寫實
|
||||
curse.category.6554=幻想
|
||||
curse.category.6555=原生
|
||||
|
||||
curse.sort.author=作者
|
||||
curse.sort.date_created=建立日期
|
||||
curse.sort.last_updated=最近更新
|
||||
curse.sort.name=名稱
|
||||
curse.sort.popularity=熱度
|
||||
curse.sort.total_downloads=下載量
|
||||
|
||||
datetime.format=yyyy 年 MM 月 dd 日 HH:mm:ss
|
||||
|
||||
download=下載
|
||||
@@ -427,26 +425,26 @@ extension.resourcepack=資源包壓縮檔
|
||||
extension.schematic=原理圖檔案
|
||||
extension.world=世界壓縮檔
|
||||
|
||||
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher 無法建立 HMCL 資料夾 (%s),請將 HMCL 移動至其他位置再開啟。
|
||||
fatal.create_hmcl_current_directory_failure=HMCL 無法建立 HMCL 資料夾 (%s),請將 HMCL 移動至其他位置再開啟。
|
||||
fatal.javafx.incomplete=JavaFX 執行環境不完整。請嘗試更換你的 Java 或者重新安裝 OpenJFX。
|
||||
fatal.javafx.missing=缺少 JavaFX 執行環境。請使用包含 OpenJFX 的 Java 執行環境開啟 Hello Minecraft! Launcher。
|
||||
fatal.config_change_owner_root=你正在使用 root 帳戶開啟 Hello Minecraft! Launcher,這可能導致你未來無法使用其他帳戶正常開啟 HMCL。\n是否繼續開啟?
|
||||
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.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 來完成升級。
|
||||
fatal.apply_update_need_win7=Hello Minecraft! Launcher 無法在 Windows XP/Vista 上進行自動更新。請從 %s 處手動下載 HMCL 來完成升級。
|
||||
fatal.javafx.missing=缺少 JavaFX 執行環境。請使用包含 OpenJFX 的 Java 執行環境開啟 HMCL。
|
||||
fatal.config_change_owner_root=你正在使用 root 帳戶開啟 HMCL,這可能導致你未來無法使用其他帳戶正常開啟 HMCL。\n是否繼續開啟?
|
||||
fatal.config_in_temp_dir=你正在暫存目錄中開啟 HMCL,你的設定和遊戲資料可能會遺失。建議將 HMCL 移動至其他位置再開啟。\n是否繼續開啟?
|
||||
fatal.config_loading_failure=HMCL 無法載入設定檔案。\n請確保 HMCL 對「%s」目錄及該目錄下的檔案擁有讀寫權限。
|
||||
fatal.config_loading_failure.unix=HMCL 無法載入設定檔案,因為設定檔案是由使用者「%1$s」建立的。\n請使用 root 帳戶開啟 HMCL (不推薦),或在終端中執行以下指令將設定檔案的所有權變更為目前使用者:\n%2$s
|
||||
fatal.config_unsupported_version=目前設定檔案是由更高版本的 HMCL 建立的,目前版本的 HMCL 無法正常載入。請更新並重新啟動 HMCL。\n在更新啟動器之前,你所做的所有設定更改都不會被儲存。
|
||||
fatal.mac_app_translocation=由於 macOS 的安全機制,HMCL 被系統隔離至暫存目錄中。\n請將 HMCL 移動到其他目錄後再嘗試開啟,否則你的設定和遊戲資料可能會在重啟後遺失。\n是否繼續開啟?
|
||||
fatal.migration_requires_manual_reboot=HMCL 即將升級完成,請重新開啟 HMCL。
|
||||
fatal.apply_update_failure=我們很抱歉 HMCL 無法自動完成升級程式,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 HMCL 來完成升級。
|
||||
fatal.apply_update_need_win7=HMCL 無法在 Windows XP/Vista 上進行自動更新。請從 %s 處手動下載 HMCL 來完成升級。
|
||||
fatal.deprecated_java_version=HMCL 未來需要 Java 17 或更高版本才能執行,但依然支援使用 Java 6~16 啟動遊戲。建議安裝最新版本的 Java 以確保 HMCL 正常執行。\n你可以繼續保留舊版本 Java。HMCL 能夠識別與管理多個 Java,並會自動根據遊戲版本為你選取合適的 Java。
|
||||
fatal.deprecated_java_version.download_link=下載 Java %d
|
||||
fatal.samba=如果您正在透過 Samba 共亯的目錄中開啟 Hello Minecraft! Launcher,啟動器可能無法正常工作,請嘗試更新您的 Java 或在本機目錄內開啟 HMCL。
|
||||
fatal.samba=如果您正在透過 Samba 共亯的目錄中開啟 HMCL,啟動器可能無法正常工作,請嘗試更新您的 Java 或在本機目錄內開啟 HMCL。
|
||||
fatal.illegal_char=由於您的執行路徑中存在無效字元『=』,您將無法使用外部登入帳戶以及離線登入更換外觀功能。
|
||||
fatal.unsupported_platform=Minecraft 尚未對您的平臺提供完善支援,所以可能影響遊戲體驗或無法啟動遊戲。\n\
|
||||
若無法啟動 Minecraft 1.17 及更高版本,可以嘗試在「(全域/實例特定) 遊戲設定 → 圖形設定」中將「繪製器」切換為「Mesa LLVMpipe」/「Mesa Lavapipe」,以獲得更好的相容性。
|
||||
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已為龍芯提供支援。\n如果遇到問題,你可以點擊右上角幫助按鈕進行求助。
|
||||
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher 已為 Windows on Arm 平臺提供原生支援。如果你在遊戲中遭遇問題,請嘗試使用 x86 架構的 Java 啟動遊戲。\n\n如果你正在使用<b>高通</b>平臺,你可能需要安裝 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 相容包</a>後才能進行遊戲。點擊連結前往 Microsoft Store 安裝相容包。
|
||||
fatal.unsupported_platform.loongarch=HMCL 已為龍芯提供支援。\n如果遇到問題,你可以點擊右上角幫助按鈕進行求助。
|
||||
fatal.unsupported_platform.windows_arm64=HMCL 已為 Windows on Arm 平臺提供原生支援。如果你在遊戲中遭遇問題,請嘗試使用 x86 架構的 Java 啟動遊戲。\n\n如果你正在使用<b>高通</b>平臺,你可能需要安裝 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 相容包</a>後才能進行遊戲。點擊連結前往 Microsoft Store 安裝相容包。
|
||||
# When translating the following string into non-Latin languages, please retain the English translation
|
||||
fatal.wine_warning=HMCL is running under Wine or one of its distributions.\nAs a cross-platform application, HMCL is best experienced natively on your host OS.\nPress "OK" to continue, but please note that some issues may arise.\n\n您目前正在使用 Wine 或其衍生版本執行 HMCL。\nHMCL 是跨平臺的,我們建議您直接在主機系統上執行。\n按下「確定」以繼續,但請注意,此操作可能會引發一些問題。
|
||||
|
||||
@@ -472,20 +470,20 @@ game.crash.reason.analyzing=分析中……
|
||||
game.crash.reason.multiple=檢測到多個原因:\n\n
|
||||
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你可以嘗試使用地圖編輯工具編輯世界刪除該實體,或者直接刪除對應的模組。\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你可以查看日誌尋找出錯模組。
|
||||
game.crash.reason.forge_error=Forge/NeoForge 可能已經提供了錯誤資訊。\n你可以查看日誌,並根據錯誤報告中的日誌資訊進行對應處。\n如果沒有看到報錯資訊,可以查看錯誤報告了解錯誤具體是如何發生的。\n%1$s
|
||||
game.crash.reason.mod_resolution0=目前遊戲由於一些模組出現問題,無法繼續執行。\n你可以查看日誌尋找出錯模組。
|
||||
game.crash.reason.need_jdk11=目前遊戲由於 Java 版本不合適,無法繼續執行。\n你需要下載安裝 Java 11,並在「(全域/實例特定) 遊戲設定 → 遊戲 Java」中將 Java 設定為 11.x 的版本。
|
||||
game.crash.reason.mixin_apply_mod_failed=目前遊戲由於 Mixin 無法應用於「%1$s」模組,無法繼續執行。\n你可以嘗試刪除或更新該模組以解決問題。
|
||||
game.crash.reason.java_version_is_too_high=目前遊戲由於使用的 Java 版本過高而崩潰了,無法繼續執行。\n請在「(全域/實例特定) 遊戲設定 → 遊戲 Java」中改用較低版本的 Java,然後再啟動遊戲。\n如果沒有,可以從 <a href="https://www.java.com/download/">java.com (Java 8)</a> 或 <a href="https://bell-sw.com/pages/downloads/#downloads">BellSoft Liberica Full JRE (Java 17)</a> 等平台下載、安裝一個 (安裝完後需重啟啟動器)。
|
||||
game.crash.reason.need_jdk11=目前遊戲由於 Java 版本不合適,無法繼續執行。\n你需要下載安裝 Java 11,並在「(全域/實例特定) 遊戲設定 → 遊戲 Java」中將 Java 設定為 11.x 的版本。
|
||||
game.crash.reason.mod_name=目前遊戲由於模組檔案名稱問題,無法繼續執行。\n模組檔案名稱應只使用半型的大小寫字母 (Aa~Zz)、數位 (0~9)、橫線 (-)、底線 (_)和點 (.)。\n請到模組目錄中將所有不合規的模組檔案名稱修正為上述合規字元。
|
||||
game.crash.reason.incomplete_forge_installation=目前遊戲由於 Forge 安裝不完整,無法繼續執行。\n請在「實例管理 - 自動安裝」中移除 Forge 並重新安裝。
|
||||
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如果你認為這個檔案可以刪除,你可以在備份這個檔案後嘗試刪除它,並重新啟動遊戲。
|
||||
game.crash.reason.file_changed=目前遊戲由於檔案校驗失敗,無法繼續執行。\n如果你手動修改了 Minecraft.jar 檔案,你需要回退修改,或者重新下載遊戲。
|
||||
game.crash.reason.gl_operation_failure=目前遊戲由於你使用的某些模組/光影包/資源包/紋理包出現問題,無法繼續執行。\n請先嘗試停用你所使用的模組/光影包/資源包/紋理包再試。
|
||||
@@ -495,11 +493,20 @@ game.crash.reason.graphics_driver=目前遊戲由於你的顯示卡驅動存在
|
||||
game.crash.reason.macos_failed_to_find_service_port_for_display=目前遊戲由於 Apple Silicon 平台下初始化 OpenGL 視窗失敗,無法繼續執行。\n對於該問題,HMCL 暫無直接性的解決方案。請你嘗試任意開啟一個瀏覽器並切換為全螢幕,然後再回到 HMCL 啟動遊戲。在彈出遊戲視窗前<b>迅速切回瀏覽器頁面</b>,等待遊戲視窗出現後再切回遊戲視窗。
|
||||
game.crash.reason.illegal_access_error=目前遊戲由於某些模組的問題,無法繼續執行。\n如果你認識「%1$s」,你可以更新或刪除對應模組再試。
|
||||
game.crash.reason.install_mixinbootstrap=目前遊戲由於缺失 MixinBootstrap,無法繼續執行。\n你可以嘗試安裝 <a href="https://www.curseforge.com/minecraft/mc-mods/mixinbootstrap">MixinBootstrap</a> 解決該問題。若安裝後崩潰,可以嘗試在該模組的檔案名前新增半形驚嘆號 (!) 解決。
|
||||
game.crash.reason.optifine_is_not_compatible_with_forge=目前遊戲由於 OptiFine 與目前版本的 Forge 不相容,導致了遊戲崩潰。\n請前往 <a href="https://optifine.net/downloads">OptiFine 官網</a>查看 OptiFine 所相容的 Forge 版本,並嚴格按照對應版本重新安裝遊戲,或在「實例管理 → 自動安裝」中更換版本。\n經測試,Forge 版本過高或過低都可能導致崩潰。
|
||||
game.crash.reason.mod_files_are_decompressed=目前遊戲由於模組檔案被解壓了,無法繼續執行。\n請直接把整個模組檔案放進模組目錄中即可。\n解壓模組檔案會導致遊戲出錯。請刪除模組目錄中已被解壓的模組,然後再啟動遊戲。
|
||||
game.crash.reason.shaders_mod=目前遊戲由於同時安裝了 OptiFine 和 Shaders 模組,無法繼續執行。 \n由於 OptiFine 已整合 Shaders 模組的功能,只需刪除 Shaders 模組即可。
|
||||
game.crash.reason.rtss_forest_sodium=目前遊戲由於 RivaTuner Statistics Server (RTSS) 與 Sodium 不相容,導致遊戲崩潰。\n點擊 <a href="https://github.com/CaffeineMC/sodium-fabric/wiki/Known-Issues#rtss-incompatible">此處</a> 查看詳情。
|
||||
game.crash.reason.too_many_mods_lead_to_exceeding_the_id_limit=目前遊戲由於你所安裝的模組過多,超出了遊戲的 ID 限制,無法繼續執行。\n請嘗試安裝<a href="https://www.curseforge.com/minecraft/mc-mods/jeid">JEID</a>等修正模組,或刪除部分大型模組。
|
||||
game.crash.reason.night_config_fixes=目前遊戲由於 Night Config 庫的一些問題,無法繼續執行。\n你可以嘗試安裝 <a href="https://www.curseforge.com/minecraft/mc-mods/night-config-fixes">Night Config Fixes</a> 模組,這或許能幫助你解決這個問題。\n了解更多,可訪問該模組的 <a href="https://github.com/Fuzss/nightconfigfixes">GitHub 儲存庫</a>。
|
||||
game.crash.reason.optifine_causes_the_world_to_fail_to_load=目前遊戲可能由於 OptiFine 而無法繼續執行。\n該問題只在特定 OptiFine 版本中出現,你可以嘗試在「實例管理 → 自動安裝」中更換 OptiFine 的版本。
|
||||
game.crash.reason.jdk_9=目前遊戲由於 Java 版本過高,無法繼續執行。\n你需要下載安裝 Java 8,並在「(全域/實例特定) 遊戲設定 → 遊戲 Java」中將 Java 設定為 1.8.x 的版本。
|
||||
game.crash.reason.jvm_32bit=目前遊戲由於記憶體分配過大,超過了 32 位 Java 虛擬機記憶體限制,無法繼續執行。\n如果你的電腦是 64 位系統,請下載安裝並更換 64 位 Java。\n如果你的電腦是 32 位系統,你或許可以重新安裝 64 位系統,或換一台新電腦。\n或者,你可以關閉「(全域/實例特定) 遊戲設定 → 遊戲記憶體」中的「自動分配」,並且把記憶體分配值調節為 1024 MiB 或以下。
|
||||
game.crash.reason.loading_crashed_forge=目前遊戲由於模組「%1$s (%2$s)」錯誤,無法繼續執行。\n你可以嘗試刪除或更新該模組以解決問題。
|
||||
game.crash.reason.loading_crashed_fabric=目前遊戲由於模組「%1$s」錯誤,無法繼續執行。\n你可以嘗試刪除或更新該模組以解決問題。
|
||||
game.crash.reason.mac_jdk_8u261=目前遊戲由於你所使用的 Forge 或 OptiFine 與 Java 衝突而崩潰。\n請嘗試更新 Forge 和 OptiFine,或使用 Java 8u251 及更早版本啟動。
|
||||
game.crash.reason.forge_repeat_installation=目前遊戲由於 Forge 重複安裝,無法繼續執行。<a href="https://github.com/HMCL-dev/HMCL/issues/1880">此為已知問題</a>\n建議將日誌上傳並回報至 GitHub,以便我們找到更多線索並修復此問題。\n目前你可以在「實例管理 → 自動安裝」中移除 Forge 並重新安裝。
|
||||
game.crash.reason.optifine_repeat_installation=目前遊戲由於重複安裝 OptiFine,無法繼續執行。\n請刪除模組目錄下的 OptiFine 或前往「實例管理 → 自動安裝」移除安裝的 OptiFine。
|
||||
game.crash.reason.memory_exceeded=目前遊戲由於記憶體分配過大,無法繼續執行。\n該問題是由於系統分頁檔太小導致的。\n你需要在「(全域/實例特定) 遊戲設定 → 遊戲記憶體」中關閉「自動分配」,並將記憶體分配值調低至遊戲能正常啟動為止。\n你還可以嘗試調大系統的分頁檔大小。
|
||||
game.crash.reason.mod=目前遊戲由於 %1$s 的問題,無法繼續執行。\n你可以更新或刪除已經安裝的「%1$s」再試。
|
||||
game.crash.reason.mod_resolution=目前遊戲由於相依模組問題,無法繼續執行。Fabric 提供了如下訊息:\n%1$s
|
||||
@@ -511,16 +518,7 @@ game.crash.reason.mod_resolution_missing=目前遊戲由於缺少相依模組,
|
||||
game.crash.reason.mod_resolution_missing_minecraft=目前遊戲由於模組和 Minecraft 遊戲版本不匹配,無法繼續執行。\n「%1$s」需要 Minecraft %2$s 才能執行。\n如果你要繼續使用你已經安裝的模組,你可以選取安裝對應的 Minecraft 版本。如果你要繼續使用目前的 Minecraft 版本,你需要安裝對應版本的模組。
|
||||
game.crash.reason.mod_resolution_mod_version=%1$s (版本號 %2$s)
|
||||
game.crash.reason.mod_resolution_mod_version.any=%1$s (任意版本)
|
||||
game.crash.reason.forge_repeat_installation=目前遊戲由於 Forge 重複安裝,無法繼續執行。<a href="https://github.com/HMCL-dev/HMCL/issues/1880">此為已知問題</a>\n建議將日誌上傳並回報至 GitHub,以便我們找到更多線索並修復此問題。\n目前你可以在「實例管理 → 自動安裝」中移除 Forge 並重新安裝。
|
||||
game.crash.reason.optifine_repeat_installation=目前遊戲由於重複安裝 OptiFine,無法繼續執行。\n請刪除模組目錄下的 OptiFine 或前往「實例管理 → 自動安裝」移除安裝的 OptiFine。
|
||||
game.crash.reason.optifine_is_not_compatible_with_forge=目前遊戲由於 OptiFine 與目前版本的 Forge 不相容,導致了遊戲崩潰。\n請前往 <a href="https://optifine.net/downloads">OptiFine 官網</a>查看 OptiFine 所相容的 Forge 版本,並嚴格按照對應版本重新安裝遊戲,或在「實例管理 → 自動安裝」中更換版本。\n經測試,Forge 版本過高或過低都可能導致崩潰。
|
||||
game.crash.reason.night_config_fixes=目前遊戲由於 Night Config 庫的一些問題,無法繼續執行。\n你可以嘗試安裝 <a href="https://www.curseforge.com/minecraft/mc-mods/night-config-fixes">Night Config Fixes</a> 模組,這或許能幫助你解決這個問題。\n了解更多,可訪問該模組的 <a href="https://github.com/Fuzss/nightconfigfixes">GitHub 儲存庫</a>。
|
||||
game.crash.reason.mod_files_are_decompressed=目前遊戲由於模組檔案被解壓了,無法繼續執行。\n請直接把整個模組檔案放進模組目錄中即可。\n解壓模組檔案會導致遊戲出錯。請刪除模組目錄中已被解壓的模組,然後再啟動遊戲。
|
||||
game.crash.reason.too_many_mods_lead_to_exceeding_the_id_limit=目前遊戲由於你所安裝的模組過多,超出了遊戲的 ID 限制,無法繼續執行。\n請嘗試安裝<a href="https://www.curseforge.com/minecraft/mc-mods/jeid">JEID</a>等修正模組,或刪除部分大型模組。
|
||||
game.crash.reason.optifine_causes_the_world_to_fail_to_load=目前遊戲可能由於 OptiFine 而無法繼續執行。\n該問題只在特定 OptiFine 版本中出現,你可以嘗試在「實例管理 → 自動安裝」中更換 OptiFine 的版本。
|
||||
game.crash.reason.modlauncher_8=目前遊戲由於你所使用的 Forge 版本與目前使用的 Java 衝突崩潰。請嘗試更新 Forge。
|
||||
game.crash.reason.shaders_mod=目前遊戲由於同時安裝了 OptiFine 和 Shaders 模組,無法繼續執行。 \n由於 OptiFine 已整合 Shaders 模組的功能,只需刪除 Shaders 模組即可。
|
||||
game.crash.reason.rtss_forest_sodium=目前遊戲由於 RivaTuner Statistics Server (RTSS) 與 Sodium 不相容,導致遊戲崩潰。\n點擊 <a href="https://github.com/CaffeineMC/sodium-fabric/wiki/Known-Issues#rtss-incompatible">此處</a> 查看詳情。
|
||||
game.crash.reason.no_class_def_found_error=目前遊戲由於程式碼不完整,無法繼續執行。\n你的遊戲可能缺失了某個模組,或者某些模組檔案不完整,或者模組與遊戲的版本不匹配。\n你可能需要重新安裝遊戲和模組,或請求他人幫助。\n缺失:%1$s
|
||||
game.crash.reason.no_such_method_error=目前遊戲由於程式碼不完整,無法繼續執行。\n你的遊戲可能缺失了某個模組,或者某些模組檔案不完整,或者模組與遊戲的版本不匹配。\n你可能需要重新安裝遊戲和模組,或請求他人幫助。
|
||||
game.crash.reason.opengl_not_supported=目前遊戲由於你的顯示卡驅動存在問題,無法繼續執行。\n原因是 OpenGL 不受支援,你現在是否在遠端桌面或者串流模式下?如果是,請直接使用原電腦啟動遊戲。\n或者嘗試升級你的顯示卡驅動到最新版本後再嘗試啟動遊戲。如果你的電腦同時存在獨立顯示卡與整合式顯示卡,你需要檢查遊戲是否使用整合式顯示卡啟動。如果是,請嘗試使用獨立顯示卡開啟 HMCL 與遊戲。如果仍有問題,你可能需要考慮換一個新顯示卡或新電腦。
|
||||
@@ -547,7 +545,7 @@ game_directory.use_relative_path=如可行,則對於遊戲目錄使用相對
|
||||
game_directory.root=使用磁碟根目錄作為遊戲資料夾可能會導致無法啟動遊戲,並會在磁碟根目錄下建立多個檔案和資料夾。\n\n確定要繼續嗎?
|
||||
|
||||
help=說明
|
||||
help.doc=Hello Minecraft! Launcher 說明文件
|
||||
help.doc=HMCL 說明文件
|
||||
help.detail=可查閱資料包、模組包製作教學等內容
|
||||
|
||||
input.email=[使用者名稱] 必須是電子信箱格式
|
||||
@@ -766,9 +764,9 @@ launcher.cache_directory.default=預設 ("%APPDATA%/.minecraft" 或 "~/.minecraf
|
||||
launcher.cache_directory.disabled=停用
|
||||
launcher.cache_directory.invalid=無法建立自訂的快取目錄。已還原至預設設定。
|
||||
launcher.contact=聯絡我們
|
||||
launcher.crash=Hello Minecraft! Launcher 遇到了無法處理的錯誤。請複製下列內容並透過 GitHub、Discord 或 HMCL QQ 群回報問題。
|
||||
launcher.crash.java_internal_error=Hello Minecraft! Launcher 由於目前 Java 損壞而無法繼續執行。請移除目前 Java,點擊 <a href="https://bell-sw.com/pages/downloads/#downloads">此處</a> 安裝合適的 Java 版本。
|
||||
launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher 遇到了無法處理的錯誤。已偵測到你的啟動器不是最新版本,請更新後重試!
|
||||
launcher.crash=HMCL 遇到了無法處理的錯誤。請複製下列內容並透過 GitHub、Discord 或 HMCL QQ 群回報問題。
|
||||
launcher.crash.java_internal_error=HMCL 由於目前 Java 損壞而無法繼續執行。請移除目前 Java,點擊 <a href="https://bell-sw.com/pages/downloads/#downloads">此處</a> 安裝合適的 Java 版本。
|
||||
launcher.crash.hmcl_out_dated=HMCL 遇到了無法處理的錯誤。已偵測到你的啟動器不是最新版本,請更新後重試!
|
||||
launcher.update_java=請更新你的 Java
|
||||
|
||||
libraries.download=下載依賴庫
|
||||
@@ -837,6 +835,9 @@ modpack.installing=安裝模組包
|
||||
modpack.installing.given=安裝 %s 模組包
|
||||
modpack.invalid=無效的模組包升級檔案。可能是下載時出現問題。
|
||||
modpack.mismatched_type=模組包類型不符。目前遊戲是「%s」模組包,但是提供的模組包更新檔案是「%s」模組包。
|
||||
modpack.optional_files=可選檔案
|
||||
modpack.retry_optional_files=可選檔案載入失敗,點擊重試
|
||||
modpack.unknown_optional_file=未知檔案
|
||||
modpack.name=模組包名稱
|
||||
modpack.origin=來源
|
||||
modpack.origin.url=官方網站
|
||||
@@ -850,15 +851,15 @@ modpack.type.curse.error=無法完成該模組包所需的相依元件下載,
|
||||
modpack.type.curse.not_found=部分必需檔案已經從網路中被刪除並且再也無法下載。請嘗試該模組包的最新版本或者安裝其他模組包。
|
||||
modpack.type.manual.warning=該模組包由發佈者手動打包,其中可能已經包含啟動器。建議嘗試解壓後使用其內建的啟動器執行遊戲。\nHMCL 可以嘗試匯入該模組包,但不保證可用性。是否繼續?
|
||||
modpack.type.mcbbs=MCBBS 模組包
|
||||
modpack.type.mcbbs.export=可以被 Hello Minecraft! Launcher 匯入
|
||||
modpack.type.mcbbs.export=可以被 HMCL 匯入
|
||||
modpack.type.modrinth=Modrinth
|
||||
modpack.type.modrinth.export=可以被主流第三方啟動器匯入
|
||||
modpack.type.multimc=MultiMC
|
||||
modpack.type.multimc.export=可以被 Hello Minecraft! Launcher 和 MultiMC 匯入
|
||||
modpack.type.multimc.export=可以被 HMCL 和 MultiMC 匯入
|
||||
modpack.type.server=伺服器自動更新模組包
|
||||
modpack.type.server.export=允許伺服器管理員遠端更新遊戲用戶端
|
||||
modpack.type.server.malformed=伺服器模組包配置格式錯誤,請聯絡伺服器管理員解決此問題
|
||||
modpack.unsupported=Hello Minecraft! Launcher 不支援該模組包格式
|
||||
modpack.unsupported=HMCL 不支援該模組包格式
|
||||
modpack.update=正在升級模組包
|
||||
modpack.wizard=匯出模組包引導
|
||||
modpack.wizard.step.1=基本設定
|
||||
@@ -1056,6 +1057,7 @@ world.export=匯出此世界
|
||||
world.export.title=選取該世界的儲存位置
|
||||
world.export.location=儲存到
|
||||
world.export.wizard=匯出世界「%s」
|
||||
world.game_version=遊戲版本
|
||||
world.icon=世界圖示
|
||||
world.icon.change=修改世界圖示
|
||||
world.icon.change.fail.load.title=圖片解析失敗
|
||||
@@ -1101,7 +1103,6 @@ world.info.time.format=%d 天 %d 小時 %d 分鐘
|
||||
world.load.fail=世界載入失敗
|
||||
world.locked=使用中
|
||||
world.locked.failed=該世界正在使用中,請關閉遊戲後重試。
|
||||
world.game_version=遊戲版本
|
||||
world.manage=世界管理
|
||||
world.manage.button=管理此世界
|
||||
world.manage.title=世界 - %s
|
||||
@@ -1138,7 +1139,6 @@ schematics=原理圖
|
||||
schematics.add=新增原理圖
|
||||
schematics.add.failed=新增原理圖失敗
|
||||
schematics.add.title=選取要新增的原理圖檔案
|
||||
schematics.back_to=返回到「%s」
|
||||
schematics.create_directory=建立目錄
|
||||
schematics.create_directory.prompt=請輸入新目錄名稱
|
||||
schematics.create_directory.failed=建立目錄失敗
|
||||
@@ -1154,7 +1154,13 @@ schematics.info.time_created=建立時間
|
||||
schematics.info.time_modified=修改時間
|
||||
schematics.info.total_blocks=總方塊數
|
||||
schematics.info.total_volume=總體積
|
||||
schematics.info.type=原理圖類型
|
||||
schematics.info.type.litematic=Litematic (Litematica)
|
||||
schematics.info.type.nbt_structure=NBT 結構 (原版, 機械動力)
|
||||
schematics.info.type.schem=Schem (Schematica, WE, MCEdit)
|
||||
schematics.info.version=原理圖版本
|
||||
schematics.info.mc_data_version=Minecraft 資料版本
|
||||
schematics.install_mod=安裝投影模組
|
||||
schematics.manage=原理圖管理
|
||||
schematics.sub_items=%d 個子項
|
||||
|
||||
@@ -1536,8 +1542,8 @@ terracotta.unsupported.arch.32bit=多人聯機功能不支援 32 位元系統。
|
||||
terracotta.unsupported.arch.loongarch64_ow=多人聯機功能不支援 Linux LoongArch64 舊世界發行版,請更新至新世界發行版 (如 AOSC OC)。
|
||||
terracotta.unsupported.region=多人聯機功能目前僅為中國內地使用者提供服務,在您所在地區可能不可用。
|
||||
|
||||
theme_pack.appearance.custom.tooltip=目前使用自訂啟動器設定。點擊後跟隨所選主題。
|
||||
theme_pack.appearance.follow_theme.tooltip=目前跟隨所選主題。點擊後使用自訂啟動器設定。
|
||||
theme_pack.appearance.custom.tooltip=目前使用自訂啟動器設定。點擊後跟隨所選主題。
|
||||
theme_pack.apply=套用主題
|
||||
theme_pack.apply.confirm=確定要套用主題「%s」嗎?
|
||||
theme_pack.apply.failed=套用主題失敗
|
||||
@@ -1587,6 +1593,7 @@ update.channel.nightly.hint=你正在使用 HMCL 預覽版。預覽版更新較
|
||||
update.channel.nightly.title=預覽版提示
|
||||
update.channel.stable=穩定版
|
||||
update.checking=正在檢查更新
|
||||
update.check_failed=檢查更新失敗 %s
|
||||
update.disable_auto_show_update_dialog=不自動顯示更新彈窗
|
||||
update.disable_auto_show_update_dialog.subtitle=啟用此選項,HMCL 將不會自動彈出更新彈窗。
|
||||
update.failed=更新失敗
|
||||
@@ -1600,6 +1607,7 @@ update.latest=目前版本為最新版本
|
||||
update.tooltip=更新
|
||||
update.preview=提前測試 HMCL 預覽版本
|
||||
update.preview.subtitle=啟用此選項,你將可以提前取得 HMCL 的新版本,以便在正式發布前進行測試。
|
||||
update.unverified=更新不可用,因為無法驗證此啟動器例項完整性
|
||||
|
||||
warning.java_interpreted_mode=HMCL 正在執行於直譯器模式的 Java 環境中,效能將會受到很大影響。\n我們建議你使用支援 JIT 的 Java 開啟 HMCL 以獲得最佳體驗。
|
||||
warning.software_rendering=HMCL 正在使用軟體繪製,效能將會受到很大影響。
|
||||
File diff suppressed because it is too large.
Load diff
@@ -40,7 +40,7 @@ gregblock;32;;格雷空岛;Gregblock;
|
||||
sevtech-ages;33;;赛文科技;SevTech: Ages;STA
|
||||
;34;;伊卡洛斯号迫降;Icarus Landing;IL
|
||||
greedycraft;35;;贪婪整合包;GreedyCraft;GC
|
||||
stoneblock;36;;StoneBlock;;
|
||||
stoneblock;36;;StoneBlock;;SB
|
||||
ftb-presents-skyfactory-3;37;;天空工厂3;FTB Presents SkyFactory 3;SF3
|
||||
space-astronomy;38;;Space Astronomy;;
|
||||
world-of-dragons;39;;World of Dragons;;
|
||||
@@ -52,7 +52,7 @@ farming-valley-lite;44;;Farming Valley - Lite;;
|
||||
atm-all-the-magic;45;;All the Mods: All the Magic;;
|
||||
hr-new-beginnings;46;;HR: New Beginnings;;
|
||||
valhelsia-2;47;;Valhelsia 2;;
|
||||
ftb-presents-stoneblock-2;48;;FTB Presents Stoneblock 2;;
|
||||
ftb-presents-stoneblock-2;48;;FTB Presents Stoneblock 2;;SB2
|
||||
ftb-sky-odyssey;49;;天空奥德赛;FTB Sky Odyssey;
|
||||
all-of-fabric-3;50;;All of Fabric 3;;AOF3
|
||||
terrafirma-rescue;51;;群峦:救援;Terrafirma:Rescue;TFR
|
||||
@@ -515,7 +515,7 @@ tankoptimization;521;;坦坦优化;Tankoptimization;
|
||||
pick-the-stars;523;;摘星;Pick The Stars;PTS
|
||||
earth-technology;524;;Earth Technology;;
|
||||
;525;;核乐不为;;ZMC
|
||||
ftb-stoneblock-3;526;;FTB StoneBlock 3;;
|
||||
ftb-stoneblock-3;526;;FTB StoneBlock 3;;SB3
|
||||
create-mod-plus;527;;Create+;;
|
||||
multiplayer-optimization;528;;RawDiamondMC的优化包;RawDiamondMC's optimization pack;MPO
|
||||
chroma-endless;529;;Chroma Endless;;
|
||||
@@ -701,13 +701,12 @@ create-perfect-world;711;;Create: Perfect World;;
|
||||
new-simple-mods;712;;New Simple Mods - Easy to Understand;;
|
||||
;713;;灾变录;Catastrophe Record;CR
|
||||
sky-greg;714;;Sky Greg;;
|
||||
deceasedcraft;715;;亡者世界;DeceasedCraft - Urban Zombie Apocalypse;
|
||||
deceasedcraft;715;;亡者世界;DeceasedCraft - Urban Zombie Apocalypse;DC
|
||||
;716;;生欲:求生;Vivere The Zombie Apocalypse;VZA
|
||||
create-planetary;717;;Create Planetary;;
|
||||
master-mabaoguo;718;;Master mabaoguo;;
|
||||
gregorian-nightmares;719;;Gregorian Nightmares Industrially Fabricated;;
|
||||
;720;;史诗的地下城;Dungeons Of Fantasy;
|
||||
;721;;Emotional Damaged Minecraft;;EmDM
|
||||
;722;;脆骨症;No Flesh Within Chest;NFWC
|
||||
all-in-one-create;723;;All in One: Create;;AIOC
|
||||
the-backrooms-survival;724;;The Backrooms Survival;;
|
||||
@@ -754,7 +753,7 @@ modecube;765;;ModéCube;;
|
||||
electrictechnocraft;766;;ElectricTechnoCraft;;
|
||||
create-2-mekanism;767;;Create 2 Mekanism;;C2M
|
||||
randblock;768;;随机空岛;RandBlock;RD
|
||||
;769;;格雷科技休闲版;GregTech Leisure;GTL
|
||||
gregtech-leisure;769;;格雷科技休闲版;GregTech Leisure;GTL
|
||||
frozenopolis;770;;Frozenopolis;;
|
||||
infernal-rpg;771;;Infernal Origins;;
|
||||
;772;;天狼星;Sirius;
|
||||
@@ -793,7 +792,7 @@ chemillas-reborn;804;;化学:重生;Chemilla's Reborn;CR
|
||||
milkyway;806;;Create: Milkyway;;
|
||||
farming-crossing-4;807;;农业森友会 4;Farming Crossing 4;
|
||||
hardrock-terrafirmacraft-4-realistic-extreme;808;;HardRock TerraFirmaCraft;;TFCH/HTFC
|
||||
terrafirma-rebirth;809;;群峦:重生;TerraFirma: Rebirth;TFCR
|
||||
terrafirma-rebirth;809;;群峦重生2;TerraFirma Rebirth 2;TFCR2
|
||||
create-live-5-skyblock;810;;Create Live 5;;
|
||||
;811;;万象征程;Cosmos of Endless Allegory;CEA
|
||||
;812;;远古之遗;;
|
||||
@@ -867,7 +866,7 @@ aeon-automata;881;;Aeon Automata;;AA
|
||||
;882;;被恐惧支配2:收容失效;Dominated by Fear 2 Containment Breach;DoF2
|
||||
;883;;剑拔弩张之时;;TC
|
||||
project-architect-2;884;;Project Architect 2;;
|
||||
;885;;接入铁路网络:我的世界铁路;MTR Railway Network Access;RNA-MTR
|
||||
block-railway-engineering;885;;像素铁路工程;Block Railway Engineering;BRE
|
||||
create-mekanized;886;;Create: Mekanized;;
|
||||
create-perfect-world-2;887;;Create: Perfect World 2;;
|
||||
create-stellar;888;;Create Stellar;;
|
||||
@@ -1037,7 +1036,7 @@ minecraft-legendary-edition;1050;;Legendary Edition;;
|
||||
forge-frontier;1055;;Create - Forge Frontier;;
|
||||
create-workshop-uwu;1056;;Create: Workshop UwU Edition;;
|
||||
;1057;;古钠重铸;Oldium Optimized;OO
|
||||
;1058;;越寰;Leaping across the world;LTW
|
||||
;1058;;越渡;Leaping across the world;LTW
|
||||
ftb-oceanblock-2;1059;;FTB OceanBlock 2;;
|
||||
;1060;;伊甸空天2;Eden Aerospace 2;EAS2
|
||||
create-infinity-mechanism;1061;;机械动力:无限构件;Create:Infinity Mechanism;CIM
|
||||
@@ -1073,7 +1072,7 @@ roguelike-adventures-and-dungeons-3;1090;;冒险与地牢3;Roguelike Adventures
|
||||
wheelchair-gregtech;1093;;Wheelchair Gregtech;;
|
||||
egged-up;1094;;Egged Up;;
|
||||
;1095;;香草纪元:食旅纪行;VanillaEra: FaresChron;VEFC
|
||||
the-fool;1096;;愚者;The Fool;TF
|
||||
;1096;;愚者;The Fool;TF
|
||||
;1097;;时间之书2:星梦指南;BookofTime II Stardream Compass;BKSC
|
||||
;1098;;群峦之上:无垠伊土;EndlessFirma;EnF
|
||||
;1099;;血月低语:暗夜惊魂;Bloodmoon's Whisper;BW
|
||||
@@ -1117,12 +1116,12 @@ life-in-the-village-4;1136;;Life in the village 4;;LITV4
|
||||
real-island-2-modular-craft;1137;;真实空岛2:模块化之旅;Real Island 2 - Modular Craft;RI2
|
||||
;1138;;奥术咖啡馆;Arcane Cofferia;
|
||||
unyielding;1139;;Unyielding;;
|
||||
;1140;;剑与王国;;sword_and_kingdom
|
||||
;1140;;剑与王国;Sword and Kingdom;
|
||||
the-fungal-infection-coop;1141;;The Fungal Infection | Co-Op Base Defense;;
|
||||
mc-eternal-2;1142;;永恒的MC 2;MC Eternal 2;MCE2
|
||||
solo-leveling-reawakening;1143;;Solo Leveling - Reawakening;;
|
||||
create-ultimate-selection;1144;;Create: Ultimate Selection;;
|
||||
;1145;;元素觉醒;Elemental Awakening;
|
||||
;1145;;元素觉醒;Elemental Awakening;ELA
|
||||
endless-horror-the-ultimate-horror-modpack;1146;;Endless Horror - The ULTIMATE Horror Experience;;
|
||||
;1147;;锻造之旅;;
|
||||
picky;1148;;Picky;;
|
||||
@@ -1238,7 +1237,7 @@ skyopolis-evolved;1255;;Skyopolis: Evolved;;
|
||||
;1260;;登神者:天阶咏叹;The God Ascender;TGA
|
||||
;1261;;虚饰作品;Fictional;
|
||||
;1262;;无尽工坊:奇境传说;Infinity Workshop: Legends of Wonderland;IWLoW
|
||||
;1263;;大群方舟;Arknights: The Great Tide;AGT
|
||||
arknights-the-great-tide;1263;;大群方舟;Arknights: The Great Tide;AGT
|
||||
arcadia-rpg;1264;;Arcadia[RPG];;
|
||||
ozone-skyblock-reborn-2;1265;;Ozone Skyblock Reborn 2;;
|
||||
witarcland;1266;;维塔克兰;Witarcland;WTL
|
||||
@@ -1267,7 +1266,6 @@ ultramodern-optimization;1289;;超现代优化;Ultramodern optimization;UO
|
||||
reverie-foundry;1290;;遐想铸造厂;Reverie Foundry;RF
|
||||
;1291;;神秘时代4·全家桶;All the thaumcraft's mod;AllTC
|
||||
over-stars;1292;;Over Stars;;
|
||||
pandemonium-nexus-anarchic-aeon;1293;;万象枢纽:无序纪元;Pandemonium Nexus: Anarchic Aeon;PNAA
|
||||
;1294;;菌染亵渎;Fungal Infection Blasphemy;FIB
|
||||
;1295;;Ice and M:Sky;;IAMS
|
||||
;1296;;TUFF PERFORMANCE;;
|
||||
@@ -1299,12 +1297,12 @@ chosens-create-integrated;1320;;Chosen's Create: Integrated;;
|
||||
;1322;;MineZ v.无尽苦寒之地;MineZ v.I.C.E;
|
||||
;1323;;森语田园;;
|
||||
;1324;;子弹呼啸;Bullet Whizz;BW
|
||||
forgemechtrek;1325;;锻械游行;ForgeMechTrek;FMT
|
||||
;1325;;锻械游行;ForgeMechTrek;FMT
|
||||
;1326;;生电与优化整合包;Electricity Generation Integration Package;EGIP
|
||||
create-perfect-world-post-production;1327;;Create - Perfect World Post Production;;
|
||||
terrafirmagreg-modern;1328;;群峦格雷:现代版;TerraFirmaGreg: Modern;TFG
|
||||
terrafirmagreg-modern;1328;;群峦格雷:现代版;TerraFirmaGreg-Modern;TFGM
|
||||
nanomagic;1329;;NanoMagic;;
|
||||
;1330;;接入铁路网络:沉浸铁路;IR Railway Network Access (Immersive Railroading);RNA-IR
|
||||
;1330;;接入铁路网络:沉浸铁路;IR Railway Network Access;rRNA
|
||||
biohazard-project-genesis;1331;;Biohazard: Project Genesis - The Ultimate Apocalypse;;
|
||||
;1332;;齐柏林的启示录;Apocalypse of Zeppelin;AoZ
|
||||
;1333;;遗尘往事;Story of Forgotten Land;SOFL
|
||||
@@ -1331,13 +1329,13 @@ the-last-world-shines;1352;;末世生辉;The Last World Shines;TLWS
|
||||
journey-to-infinity;1354;;Journey To Infinity;;
|
||||
;1355;;混元登仙;;HATI
|
||||
flying-path;1356;;飞舞之路;Flying Path;
|
||||
create-craft-quiet;1357;;机械动力:匠途闲境;Create:Craft&Quiet;ccq
|
||||
create-craft-quiet;1357;;机械动力:匠途闲境;Create : Craft & Quiet;CCQ
|
||||
sky-new-horizons;1358;;Sky New Horizons;;SkyNH
|
||||
;1359;;天启之路;The Path Of Revelation;TPOR
|
||||
mana-and-mechanisms;1360;;Mana & Mechanisms;;
|
||||
;1361;;胶兽异界之旅Ⅱ;;
|
||||
mechanical-dragon;1362;;龙之机械;Dragon create;DC
|
||||
;1363;;香草精华;;VE
|
||||
;1363;;香草精华;Vanilla Essence;VE
|
||||
submerged-2;1364;;Seaopolis Submerged 2;;
|
||||
;1365;;天煞孤星;A STAR DOOMED TO LONELINESS;ALONG
|
||||
;1366;;全亡灵启示录;All Undead Apocalypse;AUA
|
||||
@@ -1352,14 +1350,14 @@ mysticalblockreloaded;1374;;Mystical Block Reloaded;;
|
||||
cozy-mizuno-minecraft;1375;;A Cozy Mizuno World;;
|
||||
;1376;;新生异土;Tensura Neo Otherworld;
|
||||
extraordinary-energy-modern;1377;;Extraordinary Energy : Modern;;
|
||||
;1378;;铁砧工艺-七天创世;;
|
||||
;1378;;铁砧工艺-归途;;
|
||||
avaritia-project;1379;;Avaritia Project;;
|
||||
after-the-nukes;1380;;After The Nukes;;
|
||||
abyssal-ascent;1381;;Abyssal Ascent;;
|
||||
desolate-planet;1382;;Desolate Planet;;
|
||||
;1383;;德雷马尔:神化;Drehmal: Apotheosis;DHA
|
||||
;1383;;德雷马尔:神化;Drehmal: Apotheosis;
|
||||
project-arc-light-the-hanging-pavilion;1384;;弧光计划:空中楼阁;Project Arc Light: The Hanging Pavilion;PAHP
|
||||
;1385;;文明:创世;Civilization Isolaria;
|
||||
civilization-isolaria;1385;;文明:创世;Civilization Isolaria;
|
||||
blockitecture;1386;;方块建筑学;Blockitecture (Block Architecture);
|
||||
;1388;;曙光空岛;;
|
||||
;1389;;千界一梦;The Oneiropolis;TOP
|
||||
@@ -1399,7 +1397,7 @@ enigmatic-skies;1419;;Enigmatic Skies;;
|
||||
cook-in-skyland-recaked;1423;;天空厨房:重制版;Cook in Skyland Recaked;CiSR
|
||||
rlcraft-fanmade;1424;;RLCraft Fanmade;;
|
||||
unaltered;1425;;Unaltered+;;
|
||||
sendims-banforges;1426;;千界万锻;Myriad Dimensional Forge;SDBF
|
||||
sendims-banforges;1426;;千界※万锻;Myriad Dimensional Forge;SDBF
|
||||
;1427;;超级优化;Super Optimized;SO
|
||||
society-sunlit-cobblemon;1428;;Society: Sunlit Cobblemon;;
|
||||
create-aeronautica;1429;;Create Aeronautica;;
|
||||
@@ -1432,3 +1430,71 @@ dungeon-train;1455;;地牢列车;Dungeon train;
|
||||
project-arc-light-dimensional-rift;1456;;弧光计划:维度裂隙;Project Arc Light - Dimensional Rift;PA2
|
||||
;1457;;恶意世界;HostileCraft;HC
|
||||
adj;1458;;A Distant Journey;;ADJ
|
||||
;1459;;枪械:先锋;TACZ: Pioneer;
|
||||
;1460;;莱特兰:魂影与恶域;L2Land: Souls & Evil Domains;
|
||||
;1461;;终末幸存者;Last One;LO
|
||||
soa3-f;1462;;Slow Start AoA3 [FORGE];;SoA3-f
|
||||
;1463;;KessokuTeaTime Bingopack;;KTTBP
|
||||
;1464;;更多模组的Bingo;More Mods Bingo;MMB
|
||||
blast-off-reborn;1465;;Blast off-Reborn;;
|
||||
new-age-branch-line;1466;;新时代支线;new age branch line;NABL
|
||||
;1467;;工业溯源;Industrial Genesis;
|
||||
sky-of-grind;1468;;Sky of Grind;;SoG
|
||||
ntnewhorizons;1469;;Nuclear Tech New Horizons;;NTNH
|
||||
;1470;;秘法漫纪;Chronicles of Mystic Arcana;CMA
|
||||
;1471;;爱之诗;Poem Amour;POA
|
||||
;1472;;从一无所有到所有;Nothing To All;
|
||||
;1473;;剑痕纪元;;
|
||||
;1474;;魔女之旅;Wondering Witch:The Journey of Elaina;WWTJE
|
||||
;1475;;丧尸危机:生存;Zombie Crisis: Survival;ZCS
|
||||
;1476;;星之谷;StarValley;
|
||||
journey-through-middle-earth;1477;;Journey Through Middle-Earth;;
|
||||
;1478;;魔法金属优化美化包;;
|
||||
;1479;;锈蚀天穹;;
|
||||
xiaozi-craft;1480;;xiaozi craft;;XC
|
||||
;1481;;渎圣织罪;;BLSW
|
||||
;1482;;无限模板;;IT
|
||||
flat-factory-f2;1483;;平原化工厂;Flat Factory;F2
|
||||
;1484;;下界潜兵2:自由升级;Netherdivers™ 2 Escalation of Freedom;ND2
|
||||
;1485;;落雪长冬;The Winter;
|
||||
;1486;;空中厕所2026;PoopSky2026;
|
||||
;1487;;通天之路;;
|
||||
;1488;;钻石大陆加豪版;Diamond Continent — Plus&Plus Edition;DC++
|
||||
;1489;;地下酒吧;Underground Speakeasy;
|
||||
all-of-mcreator-6-aomc6;1490;;All of Mcreator 6;;AOMC6
|
||||
;1491;;宝可梦:轻快之旅;Pokémon: Journey of Agility;PJA
|
||||
;1492;;直至永恒;Until Eternity;UE
|
||||
;1493;;猪咪大冒险;;
|
||||
;1494;;诡厄使徒:诸神黄昏;;GK
|
||||
;1495;;永恒恶意;Eternal Hostility;EH
|
||||
vault-hunters-third-edition-remastered;1496;;Vault Hunters Third Edition - Remastered;;
|
||||
;1497;;万般闲暇;All The Leisures;ATL
|
||||
minecraft-or-not;1498;;Minecraft or not;;MON
|
||||
;1499;;忆婷乐章;;
|
||||
;1500;;格雷笙息空岛;;GTSK
|
||||
things-addonpack;1501;;Things/All Of The Addons;;
|
||||
;1502;;旅行时光:饥饿;Traveltime: Hunger;TTH
|
||||
;1503;;脑叶世界:收容重置;LobotoCraft Recontainment;LCR
|
||||
;1504;;闲居物语:重制版;The Serene Homestead 2;TSH2
|
||||
i-wanna-make-a-sink;1505;;I wanna make a sink;;IWMS
|
||||
;1506;;神之征伐:涤罪;Apothic Craft;ATC
|
||||
shitblock;1507;;ShitBlock;;
|
||||
;1508;;麦神之旅;Journey of Wheat;JoW
|
||||
;1509;;永无止境:超越无界;Create: Above and Beyond Community Refresh-Ex;CABREx
|
||||
;1510;;炒饭食堂;Skyblock:Fried Rice;
|
||||
;1511;;幻梦境;The Dreamlands;TD
|
||||
;1512;;摄影奇境;Photo Wanderer;
|
||||
;1513;;登神者·登神之长阶;;TLA
|
||||
;1514;;越渡 贰;Leaping across the world 2;LTW2
|
||||
arknights-lucky-block;1515;;明日方舟幸运方块;Arknights Lucky Block;ALB
|
||||
donnas-odyssey-fantasy-rpg;1516;;Donna's Odyssey Fantasy RPG;;
|
||||
matrix-of-materia;1517;;Matrix of Materia;;
|
||||
on-ancient-ground;1518;;On Ancient Ground;;
|
||||
;1519;;末地颂歌;;TEO
|
||||
;1520;;荒垣兴筑:启垣;BUILDING A ROUGH WRLL:STARTING THE WALL;BARW
|
||||
;1521;;工业复兴;;
|
||||
statech-industry-2;1522;;StaTech Industry 2;;
|
||||
;1523;;魔女之泉:归寂;;
|
||||
create-village-to-sky;1524;;机械动力:下村上天;Create Village to Sky;
|
||||
essence-odyssey;1525;;本源远征;Essence Odyssey;
|
||||
ftb-presents-architects-exodus;1526;;FTB Presents Architect's Exodus;;
|
||||
@@ -1045,6 +1045,138 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl/3.4.3/lwjgl-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl/3.4.3/lwjgl-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "101c753b129dbdeed565798d259b2b9544ef3878",
|
||||
"size": 126343
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-jemalloc:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-jemalloc:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-jemalloc/3.4.3/lwjgl-jemalloc-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-jemalloc/3.4.3/lwjgl-jemalloc-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "b558d38d8eaa32831bb66835aae163428fe69646",
|
||||
"size": 235928
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-openal:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-openal:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-openal/3.4.3/lwjgl-openal-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-openal/3.4.3/lwjgl-openal-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "f2083556a7d8a02e4abeb2b933642250ef06c61d",
|
||||
"size": 866036
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-opengl:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-opengl:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-opengl/3.4.3/lwjgl-opengl-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-opengl/3.4.3/lwjgl-opengl-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "84f229d793388be57ae5555b0cfee09b5990fa20",
|
||||
"size": 80850
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-glfw:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-glfw:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-glfw/3.4.3/lwjgl-glfw-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-glfw/3.4.3/lwjgl-glfw-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "e2f84903e71f7f2c5fe11a0f6de9576da270c503",
|
||||
"size": 144552
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-stb:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-stb:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-stb/3.4.3/lwjgl-stb-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-stb/3.4.3/lwjgl-stb-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "74f1cd22ccf4fd7432b982372b8a34c93cd9aacf",
|
||||
"size": 258996
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-tinyfd:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-tinyfd:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-tinyfd/3.4.3/lwjgl-tinyfd-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-tinyfd/3.4.3/lwjgl-tinyfd-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "97296e394d3d7dfebcc68754ab8273d44d8e9602",
|
||||
"size": 45345
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-freetype:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-freetype:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-freetype/3.4.3/lwjgl-freetype-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-freetype/3.4.3/lwjgl-freetype-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "6babd5ddf9b4850e91a952d494cfd22b895ce200",
|
||||
"size": 1300898
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-shaderc:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-shaderc:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-shaderc/3.4.3/lwjgl-shaderc-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.3/lwjgl-shaderc-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "f17eb9b12086809a31e61734dabc1dea80f6b7b2",
|
||||
"size": 3540777
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-spvc:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-spvc:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-spvc/3.4.3/lwjgl-spvc-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.3/lwjgl-spvc-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "85d2636eb1368a3864fb10faec02a4269020b31e",
|
||||
"size": 1166740
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-vma:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-vma:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-vma/3.4.3/lwjgl-vma-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.3/lwjgl-vma-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "0a9e20e7d8ae4d0106cd23ae58a1b72ca151c49f",
|
||||
"size": 60863
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-sdl:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-sdl:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-sdl/3.4.3/lwjgl-sdl-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-sdl/3.4.3/lwjgl-sdl-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "577e41b6d0006f3d3f841c14abe19736e695a760",
|
||||
"size": 1460461
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl.lwjgl:lwjgl-platform:2.9.0:natives": {
|
||||
"name": "org.glavo.hmcl:lwjgl2-natives:2.9.3",
|
||||
"downloads": {
|
||||
@@ -2956,6 +3088,181 @@
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-sdl:3.4.2:natives-linux": null,
|
||||
"org.lwjgl:lwjgl:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl/3.4.1/lwjgl-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl/3.4.1/lwjgl-3.4.1.jar",
|
||||
"sha1": "6105690714874b995aa7812e4f7732a21109276c",
|
||||
"size": 1157957
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl:3.4.3:natives-linux": {
|
||||
"name": "org.glavo.hmcl:lwjgl3-natives:3.4.1-rc1",
|
||||
"downloads": {
|
||||
"classifiers": {
|
||||
"linux-loongarch64": {
|
||||
"path": "org/glavo/hmcl/lwjgl3-natives/3.4.1-rc1/lwjgl3-natives-3.4.1-rc1-linux-loongarch64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/glavo/hmcl/lwjgl3-natives/3.4.1-rc1-linux-loongarch64/lwjgl3-natives-3.4.1-rc1-linux-loongarch64.jar",
|
||||
"sha1": "3fed872fc2f9929d2be25389dd312dfb48e9f89e",
|
||||
"size": 22854580
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract": {
|
||||
"exclude": [
|
||||
"META-INF/"
|
||||
]
|
||||
},
|
||||
"natives": {
|
||||
"linux": "linux-loongarch64"
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-jemalloc:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-jemalloc:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-jemalloc/3.4.1/lwjgl-jemalloc-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-jemalloc/3.4.1/lwjgl-jemalloc-3.4.1.jar",
|
||||
"sha1": "16b663092854fc6adfe0a941e61cae2bb2e437b6",
|
||||
"size": 47872
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-jemalloc:3.4.3:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-openal:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-openal:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-openal/3.4.1/lwjgl-openal-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-openal/3.4.1/lwjgl-openal-3.4.1.jar",
|
||||
"sha1": "8508bd60589de0539aa465b78fcf838efd07ae9a",
|
||||
"size": 153919
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-openal:3.4.3:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-opengl:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-opengl:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-opengl/3.4.1/lwjgl-opengl-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-opengl/3.4.1/lwjgl-opengl-3.4.1.jar",
|
||||
"sha1": "830412cab823c029cda6b9729f9d2ed36cf79959",
|
||||
"size": 937660
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-opengl:3.4.3:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-glfw:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-glfw:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-glfw/3.4.1/lwjgl-glfw-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-glfw/3.4.1/lwjgl-glfw-3.4.1.jar",
|
||||
"sha1": "a782b1ddd175c9107bf300fd92920579ea65e2a4",
|
||||
"size": 151893
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-glfw:3.4.3:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-stb:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-stb:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-stb/3.4.1/lwjgl-stb-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-stb/3.4.1/lwjgl-stb-3.4.1.jar",
|
||||
"sha1": "f077c3dafc31924fbe8acb0b913f08977ba27f8e",
|
||||
"size": 142927
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-stb:3.4.3:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-tinyfd:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-tinyfd:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-tinyfd/3.4.1/lwjgl-tinyfd-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-tinyfd/3.4.1/lwjgl-tinyfd-3.4.1.jar",
|
||||
"sha1": "562697d40dbd2ed0424bf24dcf65d51170f81adf",
|
||||
"size": 15931
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-tinyfd:3.4.3:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-freetype:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-freetype:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-freetype/3.4.1/lwjgl-freetype-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-freetype/3.4.1/lwjgl-freetype-3.4.1.jar",
|
||||
"sha1": "caa75a37e857efc9b0686e434d708d6420c5b6ea",
|
||||
"size": 464342
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-freetype:3.4.3:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-shaderc:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-shaderc:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1.jar",
|
||||
"sha1": "5c02c88f0d879529014c78463b76f43fdecfd272",
|
||||
"size": 153051
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-shaderc:3.4.3:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-spvc:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-spvc:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1.jar",
|
||||
"sha1": "188553592e97b5ab2dba54a1b403ee80b48dad4c",
|
||||
"size": 148285
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-spvc:3.4.3:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-vma:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-vma:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1.jar",
|
||||
"sha1": "2c54ce10154feb1ede4d23c5947438cdbfdcc003",
|
||||
"size": 109209
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-vma:3.4.3:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-vulkan:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-vulkan:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-vulkan/3.4.1/lwjgl-vulkan-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vulkan/3.4.1/lwjgl-vulkan-3.4.1.jar",
|
||||
"sha1": "d0601fdc38890755e5638048c14cc0f7b990964b",
|
||||
"size": 8356700
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-sdl:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-sdl:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-sdl/3.4.1/lwjgl-sdl-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-sdl/3.4.1/lwjgl-sdl-3.4.1.jar",
|
||||
"sha1": "ec28db6e949478044c649fea25eae43b1b161971",
|
||||
"size": 983498
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-sdl:3.4.3:natives-linux": null,
|
||||
"net.java.dev.jna:jna:5.8.0": {
|
||||
"name": "net.java.dev.jna:jna:5.13.0",
|
||||
"downloads": {
|
||||
@@ -4857,6 +5164,138 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl/3.4.3/lwjgl-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl/3.4.3/lwjgl-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "38374c04f498a9b214f7cb28cfd3fcc9737b8065",
|
||||
"size": 127129
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-jemalloc:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-jemalloc:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-jemalloc/3.4.3/lwjgl-jemalloc-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-jemalloc/3.4.3/lwjgl-jemalloc-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "79b62b3efdcdba28d2f89e6a7d99d88cf3cda48f",
|
||||
"size": 248363
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-openal:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-openal:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-openal/3.4.3/lwjgl-openal-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-openal/3.4.3/lwjgl-openal-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "f4d3d1050f244b3a7fe9ddc02e94fbe0c507fc4d",
|
||||
"size": 938685
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-opengl:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-opengl:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-opengl/3.4.3/lwjgl-opengl-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-opengl/3.4.3/lwjgl-opengl-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "56ecca3a33c7966f3d678737757f5feb3efd03c2",
|
||||
"size": 79430
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-glfw:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-glfw:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-glfw/3.4.3/lwjgl-glfw-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-glfw/3.4.3/lwjgl-glfw-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "7f88d98db5c2fdd33063b5810df8a9809fd0983b",
|
||||
"size": 141071
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-stb:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-stb:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-stb/3.4.3/lwjgl-stb-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-stb/3.4.3/lwjgl-stb-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "8f0aeb06e1d532fd9411d08dcff6c0c3572d71a6",
|
||||
"size": 268102
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-tinyfd:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-tinyfd:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-tinyfd/3.4.3/lwjgl-tinyfd-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-tinyfd/3.4.3/lwjgl-tinyfd-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "f9b6c895de7f43caf50c12b8d30fa3aef13e82ff",
|
||||
"size": 56940
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-freetype:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-freetype:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-freetype/3.4.3/lwjgl-freetype-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-freetype/3.4.3/lwjgl-freetype-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "809c6df123ae9ea36aa3117a368e7d300ed712eb",
|
||||
"size": 1409111
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-shaderc:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-shaderc:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-shaderc/3.4.3/lwjgl-shaderc-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.3/lwjgl-shaderc-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "1cb0fc92286da27d5782140a8ba72af5458eb1cf",
|
||||
"size": 3702544
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-spvc:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-spvc:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-spvc/3.4.3/lwjgl-spvc-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.3/lwjgl-spvc-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "38610faedc2a7c44f02c0bc7246ef1f8357a2846",
|
||||
"size": 1221331
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-vma:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-vma:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-vma/3.4.3/lwjgl-vma-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.3/lwjgl-vma-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "e4dc73c846d42eb63c39c2b30d7cb5983a59bdbe",
|
||||
"size": 66532
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-sdl:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-sdl:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-sdl/3.4.3/lwjgl-sdl-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-sdl/3.4.3/lwjgl-sdl-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "30cd5ef9665905be2c0c7325dc848296d4788b49",
|
||||
"size": 1496337
|
||||
}
|
||||
}
|
||||
},
|
||||
"com.github.oshi:oshi-core:6.6.5": {
|
||||
"name": "com.github.oshi:oshi-core:6.8.0",
|
||||
"downloads": {
|
||||
@@ -7054,6 +7493,138 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl/3.4.3/lwjgl-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl/3.4.3/lwjgl-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "fbab1de5df7db20be4c74bf6cf14750f599e6d79",
|
||||
"size": 110742
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-jemalloc:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-jemalloc:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-jemalloc/3.4.3/lwjgl-jemalloc-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-jemalloc/3.4.3/lwjgl-jemalloc-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "30a905bf255a0eac294da13cdfddb2963bda4541",
|
||||
"size": 160297
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-openal:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-openal:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-openal/3.4.3/lwjgl-openal-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-openal/3.4.3/lwjgl-openal-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "6f72cdd56726eacaaa1e4f5824f9fc88f0bc2272",
|
||||
"size": 851111
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-opengl:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-opengl:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-opengl/3.4.3/lwjgl-opengl-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-opengl/3.4.3/lwjgl-opengl-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "97992b72536b99ad922ac1318f397e049de50223",
|
||||
"size": 83069
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-glfw:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-glfw:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-glfw/3.4.3/lwjgl-glfw-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-glfw/3.4.3/lwjgl-glfw-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "783d56acc7b0bc5690f4d139d843d5cc083a7ef9",
|
||||
"size": 108309
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-stb:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-stb:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-stb/3.4.3/lwjgl-stb-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-stb/3.4.3/lwjgl-stb-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "0422952f19f24b9a10a29fcc44ac77ed8b842b6a",
|
||||
"size": 225716
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-tinyfd:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-tinyfd:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-tinyfd/3.4.3/lwjgl-tinyfd-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-tinyfd/3.4.3/lwjgl-tinyfd-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "5650a5b1cddce100f5c13761f7e4a5a6cf248320",
|
||||
"size": 40582
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-freetype:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-freetype:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-freetype/3.4.3/lwjgl-freetype-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-freetype/3.4.3/lwjgl-freetype-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "3938689ef674b5894b41af23de67aa53cf5c6a58",
|
||||
"size": 1217752
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-shaderc:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-shaderc:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-shaderc/3.4.3/lwjgl-shaderc-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.3/lwjgl-shaderc-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "843c2c6b1588175e28eefe260d3833244119607b",
|
||||
"size": 3063282
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-spvc:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-spvc:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-spvc/3.4.3/lwjgl-spvc-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.3/lwjgl-spvc-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "f66777d0f5e784c203fda965e28e6f41f6287eed",
|
||||
"size": 1122077
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-vma:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-vma:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-vma/3.4.3/lwjgl-vma-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.3/lwjgl-vma-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "b160d451c6224139bcb47d565dac76b1d242cf2b",
|
||||
"size": 63336
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-sdl:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-sdl:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-sdl/3.4.3/lwjgl-sdl-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-sdl/3.4.3/lwjgl-sdl-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "aad4d48eb5b3b72d550589fa6d07bbc6113b442e",
|
||||
"size": 1242611
|
||||
}
|
||||
}
|
||||
},
|
||||
"net.java.jinput:jinput-platform:2.0.5:natives": null,
|
||||
"com.mojang:text2speech:1.10.3:natives": null,
|
||||
"com.mojang:text2speech:1.11.3:natives": null,
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* 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.application.Platform;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Region;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.junit.jupiter.api.condition.EnabledIf;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/// Verifies tag sizing in parents that allocate only the item's preferred width.
|
||||
@NotNullByDefault
|
||||
@EnabledIf("org.jackhuang.hmcl.JavaFXLauncher#isStarted")
|
||||
public final class TwoLineListItemTest {
|
||||
|
||||
/// Verifies tags remain visible as they are added, resized, removed, and added again.
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
public void testPreferredWidthIncludesTags(boolean wrapText) {
|
||||
onFxThread(() -> {
|
||||
TwoLineListItem item = new TwoLineListItem("Example Mod", "a.jar");
|
||||
item.getTitleLabel().setWrapText(wrapText);
|
||||
HBox parent = createParent(item);
|
||||
layout(parent, 800);
|
||||
double originalWidth = item.getWidth();
|
||||
|
||||
item.addTag("Fabric");
|
||||
layout(parent, 800);
|
||||
assertTagsVisible(item);
|
||||
assertTrue(item.getWidth() > originalWidth);
|
||||
|
||||
item.addTag("NeoForge");
|
||||
item.addTag("Quilt");
|
||||
layout(parent, 800);
|
||||
assertTagsVisible(item);
|
||||
double taggedWidth = item.getWidth();
|
||||
|
||||
item.getTags().get(0).setText("A longer loader tag");
|
||||
layout(parent, 800);
|
||||
assertTagsVisible(item);
|
||||
assertTrue(item.getWidth() > taggedWidth);
|
||||
|
||||
item.getTags().clear();
|
||||
layout(parent, 800);
|
||||
assertEquals(originalWidth, item.getWidth(), 0.001);
|
||||
|
||||
item.addTag("Fabric");
|
||||
item.setSubtitle("A subtitle that is wider than the title and its loader tag");
|
||||
layout(parent, 800);
|
||||
assertTagsVisible(item);
|
||||
assertTrue(item.getSubtitleLabel().getWidth() >= item.getSubtitleLabel().prefWidth(-1));
|
||||
});
|
||||
}
|
||||
|
||||
/// Verifies constrained rows give the title priority while allowing truncation or wrapping.
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
public void testTitleRetainsPriority(boolean wrapText) {
|
||||
onFxThread(() -> {
|
||||
TwoLineListItem item = new TwoLineListItem("A very long mod title ".repeat(20), "a.jar");
|
||||
item.getTitleLabel().setWrapText(wrapText);
|
||||
item.addTag("Fabric");
|
||||
item.addTag("NeoForge");
|
||||
HBox parent = createParent(item);
|
||||
layout(parent, 300);
|
||||
|
||||
assertEquals(252, item.getWidth(), 0.001);
|
||||
assertEquals(item.getWidth(), item.getTitleLabel().getWidth(), 0.001);
|
||||
assertTrue(item.getBoundsInParent().getMaxX() <= parent.getWidth());
|
||||
if (wrapText) {
|
||||
assertTrue(item.getTitleLabel().getHeight() > item.getTitleLabel().prefHeight(-1));
|
||||
}
|
||||
|
||||
item.setTitle("Mod");
|
||||
layout(parent, 300);
|
||||
assertTagsVisible(item);
|
||||
});
|
||||
}
|
||||
|
||||
/// Creates a scene with an icon and an item without a horizontal growth constraint.
|
||||
private static HBox createParent(TwoLineListItem item) {
|
||||
Region icon = new Region();
|
||||
icon.setMinSize(40, 40);
|
||||
icon.setPrefSize(40, 40);
|
||||
icon.setMaxSize(40, 40);
|
||||
HBox parent = new HBox(8, icon, item);
|
||||
new Scene(parent);
|
||||
return parent;
|
||||
}
|
||||
|
||||
/// Applies CSS and lays out the parent at the given width and its preferred height.
|
||||
private static void layout(HBox parent, double width) {
|
||||
parent.applyCss();
|
||||
parent.resize(width, parent.prefHeight(width));
|
||||
parent.layout();
|
||||
}
|
||||
|
||||
/// Verifies every tag fits within the region that clips its contents.
|
||||
private static void assertTagsVisible(TwoLineListItem item) {
|
||||
for (Label tag : item.getTags()) {
|
||||
Region container = (Region) tag.getParent();
|
||||
assertTrue(tag.getBoundsInParent().getMaxX() <= container.getWidth(), "Tag is clipped");
|
||||
assertTrue(tag.getWidth() >= tag.prefWidth(-1), "Tag text is truncated");
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs assertions on the JavaFX thread and waits up to 30 seconds for completion.
|
||||
private static void onFxThread(Runnable action) {
|
||||
FutureTask<Boolean> task = new FutureTask<>(action, true);
|
||||
Platform.runLater(task);
|
||||
try {
|
||||
task.get(30, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new AssertionError(e);
|
||||
} catch (ExecutionException | TimeoutException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 296 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user