Compare commits
66
Commits
v3.16.0.347
...
v3.16.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8210c77de9 | ||
|
|
b11b298312 | ||
|
|
5c605c58d2 | ||
|
|
01d4100a2f | ||
|
|
d07eb2f748 | ||
|
|
345bb176fd | ||
|
|
83a38fadf0 | ||
|
|
9980dc8aea | ||
|
|
7dfbf76b69 | ||
|
|
7f2f77f54f | ||
|
|
2dccd8099d | ||
|
|
d8a03cfed8 | ||
|
|
26a874a030 | ||
|
|
7c5c3e2fb4 | ||
|
|
62d7b36248 | ||
|
|
dec6837528 | ||
|
|
3484792e81 | ||
|
|
2b310e035d | ||
|
|
6e960612dd | ||
|
|
73a776b021 | ||
|
|
31a2f49ed2 | ||
|
|
ab268cfe48 | ||
|
|
721aeccfc0 | ||
|
|
e4348f727a | ||
|
|
dfc96f10da | ||
|
|
a5c7fe08d8 | ||
|
|
a6e0acca45 | ||
|
|
3d312f9247 | ||
|
|
18dc5b69c0 | ||
|
|
98af2c85b6 | ||
|
|
c4c0b1a1ce | ||
|
|
5e621ce340 | ||
|
|
1d1c8dfdc9 | ||
|
|
9b68b8825d | ||
|
|
5c17783223 | ||
|
|
a921be45f1 | ||
|
|
2dd1c0f85e | ||
|
|
7ba229b3e5 | ||
|
|
59a299630b | ||
|
|
b41b89e233 | ||
|
|
af137e5ff1 | ||
|
|
74ddbf5623 | ||
|
|
accef4a93f | ||
|
|
9c3a955fad | ||
|
|
1c0e506d34 | ||
|
|
6374ec0de7 | ||
|
|
a6b3872af4 | ||
|
|
ad98a5a6d4 | ||
|
|
05393af537 | ||
|
|
3438da2ab9 | ||
|
|
3163b465ec | ||
|
|
573dac471c | ||
|
|
12f54f353a | ||
|
|
246699d571 | ||
|
|
3480e6974b | ||
|
|
6d14351833 | ||
|
|
fd0d83257d | ||
|
|
0eb1d3cf4b | ||
|
|
f9b8dad3d2 | ||
|
|
57fbd0910e | ||
|
|
7767a57f50 | ||
|
|
f024dba940 | ||
|
|
52736d3c86 | ||
|
|
f75b472068 | ||
|
|
d8e0c9e9c7 | ||
|
|
9601c1c1ea |
No files matched your search
@@ -38,7 +38,6 @@ import javafx.scene.paint.Paint;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu;
|
||||
|
||||
@@ -68,14 +67,18 @@ public class JFXPasswordField extends PasswordField {
|
||||
|
||||
private void initialize() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
if ("dalvik".equals(System.getProperty("java.vm.name").toLowerCase(Locale.ROOT))) {
|
||||
this.setStyle("-fx-skin: \"com.jfoenix.android.skins.JFXPasswordFieldSkinAndroid\";");
|
||||
}
|
||||
|
||||
|
||||
useJFXContextMenu(this);
|
||||
}
|
||||
|
||||
/// Prevents the legacy JFoenix skin from continuously requesting another layout pass.
|
||||
// https://github.com/HMCL-dev/HMCL/issues/5822
|
||||
// TODO: This method may no longer be needed after we update JFXTextFieldSkin
|
||||
@Override
|
||||
protected void layoutChildren() {
|
||||
super.layoutChildren();
|
||||
this.setNeedsLayout(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the style class to 'jfx-password-field'.
|
||||
* <p>
|
||||
@@ -294,4 +297,3 @@ public class JFXPasswordField extends PasswordField {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,9 +15,13 @@ import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.event.EventTarget;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ComboBoxBase;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
@@ -25,6 +29,8 @@ import javafx.scene.text.Text;
|
||||
import javafx.scene.transform.Scale;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class JFXComboBoxListViewSkin<T> extends ComboBoxListViewSkin<T> {
|
||||
protected final ObjectProperty<Paint> promptTextFill;
|
||||
private boolean invalid = true;
|
||||
@@ -150,6 +156,30 @@ public class JFXComboBoxListViewSkin<T> extends ComboBoxListViewSkin<T> {
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
for (Node child : getChildren()) {
|
||||
if (child instanceof ListView<?> listView && "list-view".equals(listView.getId())) {
|
||||
// The default hideOnClick logic of ComboBoxListViewSkin doesn't work well with FloatScrollBarSkin.
|
||||
// We implement hideOnClick ourselves to prevent the popup from
|
||||
// hiding when releasing the mouse on the scrollbar.
|
||||
|
||||
this.setHideOnClick(false);
|
||||
listView.addEventFilter(MouseEvent.MOUSE_RELEASED, t -> {
|
||||
EventTarget target = t.getTarget();
|
||||
if (target instanceof Node targetNode) {
|
||||
List<String> s = targetNode.getStyleClass();
|
||||
if (s.contains("thumb")
|
||||
|| s.contains("track")
|
||||
|| s.contains("decrement-arrow")
|
||||
|| s.contains("increment-arrow")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
comboBox.hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void layoutChildren(double x, double y, double w, double h) {
|
||||
|
||||
@@ -242,6 +242,19 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
clean(getRunDirectory(id));
|
||||
}
|
||||
|
||||
/// Removes a version from disk and drops any cached instance settings for that version.
|
||||
@Override
|
||||
public boolean removeVersionFromDisk(String id) {
|
||||
boolean removed = super.removeVersionFromDisk(id);
|
||||
if (removed) {
|
||||
instanceGameSettings.remove(id);
|
||||
loadedInstanceGameSettings.remove(id);
|
||||
readOnlyInstanceGameSettings.remove(id);
|
||||
beingModpackVersions.remove(id);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
|
||||
Path srcDir = getVersionRoot(srcId);
|
||||
Path dstDir = getVersionRoot(dstId);
|
||||
@@ -284,7 +297,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
newGameSettings.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
|
||||
newGameSettings.runningDirectoryProperty().setValue("");
|
||||
initInstanceGameSettings(dstId, newGameSettings);
|
||||
saveGameSettings(dstId);
|
||||
saveGameSettingsSync(dstId);
|
||||
|
||||
Path dstGameDir = getRunDirectory(dstId);
|
||||
|
||||
@@ -337,10 +350,15 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
return;
|
||||
}
|
||||
|
||||
@Nullable GameSettingsPresetID legacyParent = gameDirectory.getLegacyGameSettings();
|
||||
if (SettingsManager.getGameSettings(legacyParent) == null) {
|
||||
legacyParent = null;
|
||||
}
|
||||
|
||||
LegacyGameSettingsMigrator.InstanceMigrationResult migrationResult =
|
||||
LegacyGameSettingsMigrator.migrateInstanceGameSettings(
|
||||
this, id,
|
||||
getParentGameSettings(null).idProperty().getValue());
|
||||
legacyParent);
|
||||
if (migrationResult != null) {
|
||||
initInstanceGameSettings(id, migrationResult.setting());
|
||||
try {
|
||||
@@ -351,14 +369,6 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
GameSettings.Preset gameDirectoryPreset = SettingsManager.getGameSettings(gameDirectory.getLegacyGameSettings());
|
||||
if (gameDirectoryPreset != null && gameDirectoryPreset.defaultIsolationTypeProperty().getValue() == DefaultIsolationType.ALWAYS) {
|
||||
GameSettings.Instance setting = new GameSettings.Instance();
|
||||
setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
|
||||
initInstanceGameSettings(id, setting);
|
||||
saveGameSettings(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads a new-format instance game settings file.
|
||||
@@ -385,8 +395,9 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
+ file + ", Actual: " + schemaResult.actual());
|
||||
case UNEXPECTED_ID -> LOG.warning("Unexpected instance game settings schema. Expected: "
|
||||
+ GameSettings.Instance.CURRENT_SCHEMA + ", Actual: " + schemaResult.actual());
|
||||
case UNSUPPORTED_MAJOR, READ_ONLY_PRESERVE_SCHEMA -> LOG.warning("Unsupported instance game settings schema. Expected: "
|
||||
+ GameSettings.Instance.CURRENT_SCHEMA + ", Actual: " + schemaResult.actual());
|
||||
case UNSUPPORTED_MAJOR, READ_ONLY_PRESERVE_SCHEMA ->
|
||||
LOG.warning("Unsupported instance game settings schema. Expected: "
|
||||
+ GameSettings.Instance.CURRENT_SCHEMA + ", Actual: " + schemaResult.actual());
|
||||
case READ_WRITE, READ_WRITE_PRESERVE_SCHEMA -> {
|
||||
}
|
||||
}
|
||||
@@ -516,40 +527,44 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the explicit parent preset of the instance, falling back to the default preset.
|
||||
public GameSettings.Preset getParentGameSettings(@Nullable GameSettings.Instance instance) {
|
||||
@Nullable GameSettingsPresetID parent = instance != null && instance.parentProperty().getValue() != null
|
||||
? instance.parentProperty().getValue()
|
||||
: gameDirectory.getLegacyGameSettings();
|
||||
@Nullable GameSettingsPresetID parent = instance != null ? instance.parentProperty().getValue() : null;
|
||||
GameSettings.Preset parentSetting = SettingsManager.getGameSettings(parent);
|
||||
return parentSetting != null ? parentSetting : SettingsManager.getDefaultGameSettingsPresetOrCreate();
|
||||
}
|
||||
|
||||
/// Returns whether a new instance should use an isolated running directory under the default isolation settings.
|
||||
public boolean shouldIsolateNewInstance(boolean modded) {
|
||||
GameSettings.Preset preset = getParentGameSettings(null);
|
||||
DefaultIsolationType type = Lang.requireNonNullElse(preset.defaultIsolationTypeProperty().getValue(), DefaultIsolationType.MODDED);
|
||||
return switch (type) {
|
||||
case NEVER -> false;
|
||||
case ALWAYS -> true;
|
||||
case MODDED -> modded;
|
||||
};
|
||||
}
|
||||
|
||||
/// Applies default isolation to a new instance before the version metadata is saved.
|
||||
public void applyDefaultIsolationSettingForNewInstance(String id, boolean modded) {
|
||||
if (!shouldIsolateNewInstance(modded) || readOnlyInstanceGameSettings.contains(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameSettings.Instance setting = getInstanceGameSettings(id);
|
||||
if (setting == null) {
|
||||
setting = initInstanceGameSettings(id, new GameSettings.Instance());
|
||||
}
|
||||
if (setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY)) {
|
||||
saveGameSettings(id);
|
||||
}
|
||||
}
|
||||
|
||||
public GameSettings.Effective getEffectiveGameSettings(String id) {
|
||||
GameSettings.Instance instance = getInstanceGameSettings(id);
|
||||
return GameSettings.resolve(getParentGameSettings(instance), instance);
|
||||
}
|
||||
|
||||
public void applyDefaultIsolationSetting(String id) {
|
||||
if (!hasVersion(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameSettings.Preset preset = getParentGameSettings(null);
|
||||
DefaultIsolationType type = Lang.requireNonNullElse(preset.defaultIsolationTypeProperty().getValue(), DefaultIsolationType.MODDED);
|
||||
boolean isolated = switch (type) {
|
||||
case NEVER -> false;
|
||||
case ALWAYS -> true;
|
||||
case MODDED -> LibraryAnalyzer.isModded(this, getVersion(id).resolve(this));
|
||||
};
|
||||
|
||||
if (isolated) {
|
||||
GameSettings.Instance setting = getInstanceGameSettingsOrCreate(id);
|
||||
if (setting != null) {
|
||||
setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<Path> getVersionIconFile(String id) {
|
||||
Path root = getVersionRoot(id);
|
||||
|
||||
@@ -687,7 +702,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
|
||||
/// Result of loading an instance-specific game settings file.
|
||||
///
|
||||
/// @param setting the loaded instance settings, or `null` when unavailable
|
||||
/// @param setting the loaded instance settings, or `null` when unavailable
|
||||
/// @param allowSave whether the file may be overwritten
|
||||
private record InstanceGameSettingsLoadResult(
|
||||
@Nullable GameSettings.Instance setting,
|
||||
@@ -697,7 +712,17 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
public LaunchOptions.Builder getLaunchOptions(String version, JavaRuntime javaVersion, Path gameDir, List<String> javaAgents, List<String> javaArguments, boolean makeLaunchScript) {
|
||||
GameSettings.Effective vs = getEffectiveGameSettings(version);
|
||||
boolean noJVMOptions = vs.getInheritable(GameSettings::noJVMOptionsProperty);
|
||||
boolean autoMemory = vs.get(GameSettings::autoMemoryProperty);
|
||||
boolean autoMemory = vs.getInheritable(GameSettings::autoMemoryProperty);
|
||||
GameVersionNumber gameVersionNumber = GameVersionNumber.asGameVersion(getGameVersion(version));
|
||||
|
||||
@Nullable Integer maxMemory;
|
||||
if (autoMemory) {
|
||||
maxMemory = noJVMOptions
|
||||
? null
|
||||
: Math.toIntExact(getAutoAllocatedMemory(SystemInfo.getPhysicalMemoryStatus().available()) / 1024L / 1024L);
|
||||
} else {
|
||||
maxMemory = vs.getMaxMemory();
|
||||
}
|
||||
|
||||
LaunchOptions.Builder builder = new LaunchOptions.Builder()
|
||||
.setGameDir(gameDir)
|
||||
@@ -705,17 +730,13 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
.setVersionType(Metadata.TITLE)
|
||||
.setVersionName(version)
|
||||
.setProfileName(Metadata.TITLE)
|
||||
.setGameArguments(StringUtils.tokenize(vs.get(GameSettings::gameArgumentsProperty)))
|
||||
.setOverrideJavaArguments(StringUtils.tokenize(vs.get(GameSettings::jvmOptionsProperty)))
|
||||
.setMaxMemory(noJVMOptions && autoMemory ? null : (int) (getAllocatedMemory(
|
||||
vs.getMaxMemory() * 1024L * 1024L,
|
||||
SystemInfo.getPhysicalMemoryStatus().getAvailable(),
|
||||
autoMemory
|
||||
) / 1024 / 1024))
|
||||
.setMinMemory(vs.get(GameSettings::minMemoryProperty))
|
||||
.setMetaspace(Lang.toIntOrNull(vs.get(GameSettings::permSizeProperty)))
|
||||
.setGameArguments(StringUtils.tokenize(vs.getInheritable(GameSettings::gameArgumentsProperty)))
|
||||
.setOverrideJavaArguments(StringUtils.tokenize(vs.getInheritable(GameSettings::jvmOptionsProperty)))
|
||||
.setMaxMemory(maxMemory)
|
||||
.setMinMemory(vs.getInheritable(GameSettings::minMemoryProperty))
|
||||
.setMetaspace(Lang.toIntOrNull(vs.getInheritable(GameSettings::permSizeProperty)))
|
||||
.setEnvironmentVariables(
|
||||
Lang.mapOf(StringUtils.tokenize(vs.get(GameSettings::environmentVariablesProperty))
|
||||
Lang.mapOf(StringUtils.tokenize(vs.getInheritable(GameSettings::environmentVariablesProperty))
|
||||
.stream()
|
||||
.map(it -> {
|
||||
int idx = it.indexOf('=');
|
||||
@@ -733,16 +754,16 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
.setPostExitCommand(vs.getInheritable(GameSettings::postExitCommandProperty))
|
||||
.setNoGeneratedJVMArgs(noJVMOptions)
|
||||
.setNoGeneratedOptimizingJVMArgs(vs.getInheritable(GameSettings::noOptimizingJVMOptionsProperty))
|
||||
.setUseCustomNatives(vs.get(GameSettings::useCustomNativesProperty))
|
||||
.setNativesDir(vs.get(GameSettings::nativesDirectoryProperty))
|
||||
.setUseCustomNatives(vs.getInheritable(GameSettings::useCustomNativesProperty))
|
||||
.setNativesDir(vs.getInheritable(GameSettings::nativesDirectoryProperty))
|
||||
.setProcessPriority(vs.getInheritable(GameSettings::processPriorityProperty))
|
||||
.setGraphicsBackend(vs.getInheritable(GameSettings::graphicsBackendProperty))
|
||||
.setRenderer(vs.getRenderer())
|
||||
.setRenderer(vs.getRenderer(gameVersionNumber))
|
||||
.setEnableDebugLogOutput(vs.getInheritable(GameSettings::enableDebugLogOutputProperty))
|
||||
.setAllowAutoAgent(vs.getInheritable(GameSettings::allowAutoAgentProperty))
|
||||
.setDisableAutoGameOptions(vs.getInheritable(GameSettings::disableAutoGameOptionsProperty))
|
||||
.setUseNativeGLFW(vs.get(GameSettings::useNativeGLFWProperty))
|
||||
.setUseNativeOpenAL(vs.get(GameSettings::useNativeOpenALProperty))
|
||||
.setUseNativeGLFW(vs.getInheritable(GameSettings::useNativeGLFWProperty))
|
||||
.setUseNativeOpenAL(vs.getInheritable(GameSettings::useNativeOpenALProperty))
|
||||
.setDaemon(!makeLaunchScript && vs.getInheritable(GameSettings::launcherVisibilityProperty).isDaemon())
|
||||
.setJavaAgents(javaAgents)
|
||||
.setJavaArguments(javaArguments);
|
||||
@@ -840,22 +861,21 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public static long getAllocatedMemory(long minimum, long available, boolean auto) {
|
||||
if (auto) {
|
||||
available -= 512 * 1024 * 1024; // Reserve 512 MiB memory for off-heap memory and HMCL itself
|
||||
if (available <= 0) {
|
||||
return minimum;
|
||||
}
|
||||
|
||||
final long threshold = 8L * 1024 * 1024 * 1024; // 8 GiB
|
||||
final long suggested = Math.min(available <= threshold
|
||||
? (long) (available * 0.8)
|
||||
: (long) (threshold * 0.8 + (available - threshold) * 0.2),
|
||||
16L * 1024 * 1024 * 1024);
|
||||
return Math.max(minimum, suggested);
|
||||
} else {
|
||||
return minimum;
|
||||
public static long getAutoAllocatedMemory(long available) {
|
||||
long usable = available - 512 * 1024 * 1024; // Reserve 512 MiB memory for off-heap memory and HMCL itself
|
||||
if (usable <= 0) {
|
||||
return available;
|
||||
}
|
||||
|
||||
final long threshold = 8L * 1024 * 1024 * 1024; // 8 GiB
|
||||
final long suggested;
|
||||
if (usable <= threshold)
|
||||
suggested = (long) (usable * 0.8);
|
||||
else
|
||||
suggested = Math.min(
|
||||
(long) (threshold * 0.8 + (usable - threshold) * 0.2),
|
||||
16L * 1024 * 1024 * 1024);
|
||||
return suggested;
|
||||
}
|
||||
|
||||
public static ProxyOption getProxyOption() {
|
||||
|
||||
@@ -34,7 +34,10 @@ 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.*;
|
||||
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;
|
||||
@@ -57,6 +60,7 @@ import java.net.SocketTimeoutException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -71,6 +75,7 @@ import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
|
||||
import static org.jackhuang.hmcl.util.Lang.resolveException;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.TIME_FORMATTER;
|
||||
import static org.jackhuang.hmcl.util.platform.Platform.SYSTEM_PLATFORM;
|
||||
|
||||
public final class LauncherHelper {
|
||||
@@ -172,7 +177,7 @@ public final class LauncherHelper {
|
||||
}),
|
||||
Task.composeAsync(() -> {
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS
|
||||
|| !(setting.getRenderer() instanceof Renderer.Driver renderer)
|
||||
|| !(setting.getRenderer(GameVersionNumber.asGameVersion(gameVersion)) instanceof Renderer.Driver renderer)
|
||||
|| renderer.mesaDriverName() == null)
|
||||
return null;
|
||||
|
||||
@@ -539,7 +544,7 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA_LINUX_JAVA_8)) {
|
||||
if (!setting.get(GameSettings::useCustomNativesProperty)) {
|
||||
if (!setting.getInheritable(GameSettings::useCustomNativesProperty)) {
|
||||
FXUtils.runInFX(() -> Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction));
|
||||
return result;
|
||||
} else {
|
||||
@@ -576,7 +581,7 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
// 32-bit JVM cannot make use of too much memory.
|
||||
if (java.getBits() == Bits.BIT_32 && setting.getMaxMemory() > 1.5 * 1024) {
|
||||
if (java.getBits() == Bits.BIT_32 && !setting.getInheritable(GameSettings::autoMemoryProperty) && setting.getMaxMemory() > 1.5 * 1024) {
|
||||
// 1.5 * 1024 is an inaccurate number.
|
||||
// Actual memory limit depends on operating system and memory.
|
||||
suggestions.add(i18n("launch.advice.too_large_memory_for_32bit"));
|
||||
@@ -623,7 +628,7 @@ public final class LauncherHelper {
|
||||
suggestions.add(i18n("launch.advice.modlauncher8"));
|
||||
break;
|
||||
case VANILLA_X86:
|
||||
if (!setting.get(GameSettings::useCustomNativesProperty)
|
||||
if (!setting.getInheritable(GameSettings::useCustomNativesProperty)
|
||||
&& Platform.isSupportedTranslationX86_64()) {
|
||||
suggestions.add(i18n("launch.advice.vanilla_x86.translation"));
|
||||
}
|
||||
@@ -635,7 +640,7 @@ public final class LauncherHelper {
|
||||
|
||||
// Cannot allocate too much memory exceeding free space.
|
||||
long totalMemorySizeMB = (long) MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize());
|
||||
if (totalMemorySizeMB > 0 && totalMemorySizeMB < setting.getMaxMemory()) {
|
||||
if (totalMemorySizeMB > 0 && !setting.getInheritable(GameSettings::autoMemoryProperty) && totalMemorySizeMB < setting.getMaxMemory()) {
|
||||
suggestions.add(i18n("launch.advice.not_enough_space", totalMemorySizeMB));
|
||||
}
|
||||
|
||||
@@ -964,7 +969,7 @@ public final class LauncherHelper {
|
||||
@Override
|
||||
public void onExit(int exitCode, ExitType exitType) {
|
||||
if (showLogs) {
|
||||
logBuffer.add(new Log(String.format("[HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", exitCode, exitCode, exitType), Log4jLevel.INFO));
|
||||
logBuffer.add(new Log(String.format("[%s] [HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", TIME_FORMATTER.format(Instant.now()), exitCode, exitCode, exitType), Log4jLevel.INFO));
|
||||
submitLogThread.interrupt();
|
||||
try {
|
||||
submitLogThread.join();
|
||||
|
||||
@@ -26,6 +26,8 @@ import java.nio.file.FileSystem;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class ManuallyCreatedModpackInstallTask extends Task<Path> {
|
||||
|
||||
private final Path zipFile;
|
||||
@@ -36,6 +38,8 @@ public class ManuallyCreatedModpackInstallTask extends Task<Path> {
|
||||
this.zipFile = zipFile;
|
||||
this.charset = charset;
|
||||
this.name = name;
|
||||
|
||||
setName(i18n("modpack.installing"));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -358,10 +358,10 @@ public final class ModpackHelper {
|
||||
GameSettings.PROPERTY_MAX_MEMORY,
|
||||
GameSettings.PROPERTY_PERM_SIZE
|
||||
));
|
||||
setting.autoMemoryProperty().setValue(effective.get(GameSettings::autoMemoryProperty));
|
||||
setting.minMemoryProperty().setValue(effective.get(GameSettings::minMemoryProperty));
|
||||
setting.autoMemoryProperty().setValue(effective.getInheritable(GameSettings::autoMemoryProperty));
|
||||
setting.minMemoryProperty().setValue(effective.getInheritable(GameSettings::minMemoryProperty));
|
||||
setting.maxMemoryProperty().setValue(manifest.getLaunchInfo().getMinMemory());
|
||||
setting.permSizeProperty().setValue(effective.get(GameSettings::permSizeProperty));
|
||||
setting.permSizeProperty().setValue(effective.getInheritable(GameSettings::permSizeProperty));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,7 +45,10 @@ import java.io.InputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -65,21 +68,10 @@ public final class TexturesLoader {
|
||||
}
|
||||
|
||||
// ==== Texture Loading ====
|
||||
public static class LoadedTexture {
|
||||
private final Image image;
|
||||
private final Map<String, String> metadata;
|
||||
|
||||
public LoadedTexture(Image image, Map<String, String> metadata) {
|
||||
this.image = requireNonNull(image);
|
||||
this.metadata = requireNonNull(metadata);
|
||||
}
|
||||
|
||||
public Image getImage() {
|
||||
return image;
|
||||
}
|
||||
|
||||
public Map<String, String> getMetadata() {
|
||||
return metadata;
|
||||
public record LoadedTexture(Image image, Map<String, String> metadata) {
|
||||
public LoadedTexture {
|
||||
requireNonNull(image);
|
||||
requireNonNull(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +150,7 @@ public final class TexturesLoader {
|
||||
}
|
||||
|
||||
public static TextureModel getDefaultModel(UUID uuid) {
|
||||
return TextureModel.WIDE.modelName.equals(getDefaultSkin(uuid).getMetadata().get("model"))
|
||||
return TextureModel.WIDE.modelName.equals(getDefaultSkin(uuid).metadata().get("model"))
|
||||
? TextureModel.WIDE
|
||||
: TextureModel.SLIM;
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
update.put("component", gameJavaVersion.component());
|
||||
|
||||
Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
|
||||
result.remoteFiles().getFiles().forEach((path, file) -> {
|
||||
result.remoteFiles().files().forEach((path, file) -> {
|
||||
if (file instanceof MojangJavaRemoteFiles.RemoteFile) {
|
||||
DownloadInfo downloadInfo = ((MojangJavaRemoteFiles.RemoteFile) file).getDownloads().get("raw");
|
||||
if (downloadInfo != null) {
|
||||
@@ -190,12 +190,12 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
public Task<JavaRuntime> getInstallJavaTask(Platform platform, String name, Map<String, Object> update, Path archiveFile) {
|
||||
Path javaDir = getJavaDir(platform, name);
|
||||
return new JavaInstallTask(javaDir, update, archiveFile).thenApplyAsync(result -> {
|
||||
if (!result.getInfo().getPlatform().equals(platform))
|
||||
throw new IOException("Platform is mismatch: expected " + platform + " but got " + result.getInfo().getPlatform());
|
||||
if (!result.info().getPlatform().equals(platform))
|
||||
throw new IOException("Platform is mismatch: expected " + platform + " but got " + result.info().getPlatform());
|
||||
|
||||
Path executable = javaDir.resolve("bin").resolve(platform.getOperatingSystem().getJavaExecutable()).toRealPath();
|
||||
JsonUtils.writeToJsonFile(getManifestFile(platform, name), result);
|
||||
return JavaRuntime.of(executable, result.getInfo(), true);
|
||||
return JavaRuntime.of(executable, result.info(), true);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -34,33 +34,7 @@ import static org.jackhuang.hmcl.util.gson.JsonUtils.mapTypeOf;
|
||||
* @author Glavo
|
||||
*/
|
||||
@JsonAdapter(JavaManifest.Serializer.class)
|
||||
public final class JavaManifest {
|
||||
|
||||
private final JavaInfo info;
|
||||
|
||||
@Nullable
|
||||
private final Map<String, Object> update;
|
||||
|
||||
@Nullable
|
||||
private final Map<String, JavaLocalFiles.Local> files;
|
||||
|
||||
public JavaManifest(JavaInfo info, @Nullable Map<String, Object> update, @Nullable Map<String, JavaLocalFiles.Local> files) {
|
||||
this.info = info;
|
||||
this.update = update;
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
public JavaInfo getInfo() {
|
||||
return info;
|
||||
}
|
||||
|
||||
public Map<String, Object> getUpdate() {
|
||||
return update;
|
||||
}
|
||||
|
||||
public Map<String, JavaLocalFiles.Local> getFiles() {
|
||||
return files;
|
||||
}
|
||||
public record JavaManifest(JavaInfo info, @Nullable Map<String, Object> update, @Nullable Map<String, JavaLocalFiles.Local> files) {
|
||||
|
||||
public static final class Serializer implements JsonSerializer<JavaManifest>, JsonDeserializer<JavaManifest> {
|
||||
|
||||
@@ -69,16 +43,16 @@ public final class JavaManifest {
|
||||
@Override
|
||||
public JsonElement serialize(JavaManifest src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonObject res = new JsonObject();
|
||||
res.addProperty("os.name", src.getInfo().getPlatform().getOperatingSystem().getCheckedName());
|
||||
res.addProperty("os.arch", src.getInfo().getPlatform().getArchitecture().getCheckedName());
|
||||
res.addProperty("java.version", src.getInfo().getVersion());
|
||||
res.addProperty("java.vendor", src.getInfo().getVendor());
|
||||
res.addProperty("os.name", src.info().getPlatform().getOperatingSystem().getCheckedName());
|
||||
res.addProperty("os.arch", src.info().getPlatform().getArchitecture().getCheckedName());
|
||||
res.addProperty("java.version", src.info().getVersion());
|
||||
res.addProperty("java.vendor", src.info().getVendor());
|
||||
|
||||
if (src.getUpdate() != null)
|
||||
res.add("update", context.serialize(src.getUpdate()));
|
||||
if (src.update() != null)
|
||||
res.add("update", context.serialize(src.update()));
|
||||
|
||||
if (src.getFiles() != null)
|
||||
res.add("files", context.serialize(src.getFiles(), LOCAL_FILES_TYPE));
|
||||
if (src.files() != null)
|
||||
res.add("files", context.serialize(src.files(), LOCAL_FILES_TYPE));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.*;
|
||||
@@ -54,6 +55,14 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@NotNullByDefault
|
||||
public final class GameDirectoryManager {
|
||||
|
||||
/// The stable ID shared by newly created and migrated current-workspace game directories.
|
||||
static final GameDirectoryID DEFAULT_GAME_DIRECTORY_ID =
|
||||
new GameDirectoryID(new UUID(0x7105bc1f490e5e8cL, 0x878cf5844c3d4bc3L));
|
||||
|
||||
/// The stable ID shared by newly created and migrated user-home game directories.
|
||||
static final GameDirectoryID HOME_GAME_DIRECTORY_ID =
|
||||
new GameDirectoryID(new UUID(0xf3eafde8506e5a77L, 0xbc88f24b4728dfb2L));
|
||||
|
||||
/// The default current-workspace game directory path.
|
||||
private static final PortablePath CURRENT_GAME_DIRECTORY_PATH = PortablePath.of(".minecraft");
|
||||
|
||||
@@ -154,12 +163,14 @@ public final class GameDirectoryManager {
|
||||
if (localGameDirectories().isNewlyCreated() && localGameDirectories().getGameDirectories().isEmpty()) {
|
||||
needRebuildGameDirectories = true;
|
||||
GameDirectories gameDirectories = localGameDirectories();
|
||||
gameDirectories.getGameDirectories().add(new GameDirectory(newGameDirectoryId(), null, CURRENT_GAME_DIRECTORY_PATH));
|
||||
gameDirectories.getGameDirectories().add(new GameDirectory(
|
||||
DEFAULT_GAME_DIRECTORY_ID, null, CURRENT_GAME_DIRECTORY_PATH));
|
||||
}
|
||||
if (userGameDirectories().isNewlyCreated() && userGameDirectories().getGameDirectories().isEmpty()) {
|
||||
needRebuildGameDirectories = true;
|
||||
GameDirectories gameDirectories = userGameDirectories();
|
||||
gameDirectories.getGameDirectories().add(new GameDirectory(newGameDirectoryId(), null, HOME_GAME_DIRECTORY_PATH));
|
||||
gameDirectories.getGameDirectories().add(new GameDirectory(
|
||||
HOME_GAME_DIRECTORY_ID, null, HOME_GAME_DIRECTORY_PATH));
|
||||
}
|
||||
|
||||
needRebuildGameDirectories |= createDefaultGameDirectoriesIfEmpty();
|
||||
@@ -216,9 +227,11 @@ public final class GameDirectoryManager {
|
||||
if (localGameDirectories().getGameDirectories().isEmpty()
|
||||
&& userGameDirectories().getGameDirectories().isEmpty()) {
|
||||
localGameDirectories().getGameDirectories()
|
||||
.add(new GameDirectory(newGameDirectoryId(), null, CURRENT_GAME_DIRECTORY_PATH));
|
||||
.add(new GameDirectory(
|
||||
DEFAULT_GAME_DIRECTORY_ID, null, CURRENT_GAME_DIRECTORY_PATH));
|
||||
userGameDirectories().getGameDirectories()
|
||||
.add(new GameDirectory(newGameDirectoryId(), null, HOME_GAME_DIRECTORY_PATH));
|
||||
.add(new GameDirectory(
|
||||
HOME_GAME_DIRECTORY_ID, null, HOME_GAME_DIRECTORY_PATH));
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
||||
@@ -67,7 +67,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@NotNullByDefault
|
||||
public sealed abstract class GameSettings extends ObservableSetting {
|
||||
/// Suggested maximum heap memory in MiB.
|
||||
static final int SUGGESTED_MEMORY;
|
||||
public static final int SUGGESTED_MEMORY;
|
||||
|
||||
static {
|
||||
double totalMemoryMB = MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize());
|
||||
@@ -331,6 +331,11 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
}
|
||||
|
||||
/// Creates a new inheritable property.
|
||||
protected final <T extends @Nullable Object> InheritableProperty<@Nullable T> newInheritableProperty(String name) {
|
||||
return new SimpleInheritableProperty<>(this, name, null);
|
||||
}
|
||||
|
||||
/// Creates a new inheritable property with the given default value.
|
||||
protected final <T extends @UnknownNullability Object> InheritableProperty<T> newInheritableProperty(String name, T defaultValue) {
|
||||
return new SimpleInheritableProperty<>(this, name, defaultValue);
|
||||
}
|
||||
@@ -388,10 +393,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
|
||||
/// User customized JVM options.
|
||||
@SerializedName(PROPERTY_JVM_OPTIONS)
|
||||
private final SettingProperty<String> jvmOptions = newSettingProperty(PROPERTY_JVM_OPTIONS, "");
|
||||
private final InheritableProperty<String> jvmOptions = newInheritableProperty(PROPERTY_JVM_OPTIONS, "");
|
||||
|
||||
/// Returns the user customized JVM options property.
|
||||
public SettingProperty<String> jvmOptionsProperty() {
|
||||
public InheritableProperty<String> jvmOptionsProperty() {
|
||||
return jvmOptions;
|
||||
}
|
||||
|
||||
@@ -448,10 +453,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
|
||||
/// If `true`, HMCL will automatically adjust memory allocation.
|
||||
@SerializedName(PROPERTY_AUTO_MEMORY)
|
||||
private final SettingProperty<Boolean> autoMemory = newSettingProperty(PROPERTY_AUTO_MEMORY, true);
|
||||
private final InheritableProperty<Boolean> autoMemory = newInheritableProperty(PROPERTY_AUTO_MEMORY, true);
|
||||
|
||||
/// Returns the automatic memory allocation property.
|
||||
public SettingProperty<Boolean> autoMemoryProperty() {
|
||||
public InheritableProperty<Boolean> autoMemoryProperty() {
|
||||
return autoMemory;
|
||||
}
|
||||
|
||||
@@ -460,10 +465,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
|
||||
/// The minimum heap memory in MiB.
|
||||
@SerializedName(PROPERTY_MIN_MEMORY)
|
||||
private final SettingProperty<@Nullable Integer> minMemory = newSettingProperty(PROPERTY_MIN_MEMORY);
|
||||
private final InheritableProperty<@Nullable Integer> minMemory = newInheritableProperty(PROPERTY_MIN_MEMORY);
|
||||
|
||||
/// Returns the minimum heap memory property.
|
||||
public SettingProperty<@Nullable Integer> minMemoryProperty() {
|
||||
public InheritableProperty<@Nullable Integer> minMemoryProperty() {
|
||||
return minMemory;
|
||||
}
|
||||
|
||||
@@ -472,10 +477,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
|
||||
/// The maximum heap memory in MiB.
|
||||
@SerializedName(PROPERTY_MAX_MEMORY)
|
||||
private final SettingProperty<@Nullable Integer> maxMemory = newSettingProperty(PROPERTY_MAX_MEMORY, SUGGESTED_MEMORY);
|
||||
private final InheritableProperty<@Nullable Integer> maxMemory = newInheritableProperty(PROPERTY_MAX_MEMORY, SUGGESTED_MEMORY);
|
||||
|
||||
/// Returns the maximum heap memory property.
|
||||
public SettingProperty<@Nullable Integer> maxMemoryProperty() {
|
||||
public InheritableProperty<@Nullable Integer> maxMemoryProperty() {
|
||||
return maxMemory;
|
||||
}
|
||||
|
||||
@@ -484,10 +489,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
|
||||
/// The permanent generation or metaspace size in MiB.
|
||||
@SerializedName(PROPERTY_PERM_SIZE)
|
||||
private final SettingProperty<String> permSize = newSettingProperty(PROPERTY_PERM_SIZE, "");
|
||||
private final InheritableProperty<String> permSize = newInheritableProperty(PROPERTY_PERM_SIZE, "");
|
||||
|
||||
/// Returns the permanent generation or metaspace size property.
|
||||
public SettingProperty<String> permSizeProperty() {
|
||||
public InheritableProperty<String> permSizeProperty() {
|
||||
return permSize;
|
||||
}
|
||||
|
||||
@@ -593,10 +598,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
|
||||
/// User customized arguments passed to the game.
|
||||
@SerializedName(PROPERTY_GAME_ARGS)
|
||||
private final SettingProperty<String> gameArguments = newSettingProperty(PROPERTY_GAME_ARGS, "");
|
||||
private final InheritableProperty<String> gameArguments = newInheritableProperty(PROPERTY_GAME_ARGS, "");
|
||||
|
||||
/// Returns the customized game arguments property.
|
||||
public SettingProperty<String> gameArgumentsProperty() {
|
||||
public InheritableProperty<String> gameArgumentsProperty() {
|
||||
return gameArguments;
|
||||
}
|
||||
|
||||
@@ -643,10 +648,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
|
||||
/// User customized environment variables passed to the game.
|
||||
@SerializedName(PROPERTY_ENVIRONMENT_VARIABLES)
|
||||
private final SettingProperty<String> environmentVariables = newSettingProperty(PROPERTY_ENVIRONMENT_VARIABLES, "");
|
||||
private final InheritableProperty<String> environmentVariables = newInheritableProperty(PROPERTY_ENVIRONMENT_VARIABLES, "");
|
||||
|
||||
/// Returns the customized environment variables property.
|
||||
public SettingProperty<String> environmentVariablesProperty() {
|
||||
public InheritableProperty<String> environmentVariablesProperty() {
|
||||
return environmentVariables;
|
||||
}
|
||||
|
||||
@@ -763,10 +768,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
|
||||
/// If `true`, HMCL does not patch native libraries.
|
||||
@SerializedName(PROPERTY_NOT_PATCH_NATIVES)
|
||||
private final SettingProperty<Boolean> notPatchNatives = newSettingProperty(PROPERTY_NOT_PATCH_NATIVES, false);
|
||||
private final InheritableProperty<Boolean> notPatchNatives = newInheritableProperty(PROPERTY_NOT_PATCH_NATIVES, false);
|
||||
|
||||
/// Returns the native library patching property.
|
||||
public SettingProperty<Boolean> notPatchNativesProperty() {
|
||||
public InheritableProperty<Boolean> notPatchNativesProperty() {
|
||||
return notPatchNatives;
|
||||
}
|
||||
|
||||
@@ -775,10 +780,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
|
||||
/// If `true`, HMCL leaves native library management to the user.
|
||||
@SerializedName(PROPERTY_USE_CUSTOM_NATIVES)
|
||||
private final SettingProperty<Boolean> useCustomNatives = newSettingProperty(PROPERTY_USE_CUSTOM_NATIVES, false);
|
||||
private final InheritableProperty<Boolean> useCustomNatives = newInheritableProperty(PROPERTY_USE_CUSTOM_NATIVES, false);
|
||||
|
||||
/// Returns the custom native libraries property.
|
||||
public SettingProperty<Boolean> useCustomNativesProperty() {
|
||||
public InheritableProperty<Boolean> useCustomNativesProperty() {
|
||||
return useCustomNatives;
|
||||
}
|
||||
|
||||
@@ -787,10 +792,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
|
||||
/// The path to the native library directory, or an empty string for the default path.
|
||||
@SerializedName(PROPERTY_NATIVES_DIRECTORY)
|
||||
private final SettingProperty<String> nativesDirectory = newSettingProperty(PROPERTY_NATIVES_DIRECTORY, "");
|
||||
private final InheritableProperty<String> nativesDirectory = newInheritableProperty(PROPERTY_NATIVES_DIRECTORY, "");
|
||||
|
||||
/// Returns the native library directory property.
|
||||
public SettingProperty<String> nativesDirectoryProperty() {
|
||||
public InheritableProperty<String> nativesDirectoryProperty() {
|
||||
return nativesDirectory;
|
||||
}
|
||||
|
||||
@@ -799,10 +804,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
|
||||
/// If `true`, HMCL will use native GLFW.
|
||||
@SerializedName(PROPERTY_USE_NATIVE_GLFW)
|
||||
private final SettingProperty<Boolean> useNativeGLFW = newSettingProperty(PROPERTY_USE_NATIVE_GLFW, false);
|
||||
private final InheritableProperty<Boolean> useNativeGLFW = newInheritableProperty(PROPERTY_USE_NATIVE_GLFW, false);
|
||||
|
||||
/// Returns the native GLFW property.
|
||||
public SettingProperty<Boolean> useNativeGLFWProperty() {
|
||||
public InheritableProperty<Boolean> useNativeGLFWProperty() {
|
||||
return useNativeGLFW;
|
||||
}
|
||||
|
||||
@@ -811,10 +816,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
|
||||
/// If `true`, HMCL will use native OpenAL.
|
||||
@SerializedName(PROPERTY_USE_NATIVE_OPENAL)
|
||||
private final SettingProperty<Boolean> useNativeOpenAL = newSettingProperty(PROPERTY_USE_NATIVE_OPENAL, false);
|
||||
private final InheritableProperty<Boolean> useNativeOpenAL = newInheritableProperty(PROPERTY_USE_NATIVE_OPENAL, false);
|
||||
|
||||
/// Returns the native OpenAL property.
|
||||
public SettingProperty<Boolean> useNativeOpenALProperty() {
|
||||
public InheritableProperty<Boolean> useNativeOpenALProperty() {
|
||||
return useNativeOpenAL;
|
||||
}
|
||||
|
||||
@@ -898,11 +903,6 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// Returns the effective value for a property whose override state is tracked by the property itself.
|
||||
public <T extends @UnknownNullability Object> T get(Function<GameSettings, SettingProperty<T>> propertyGetter) {
|
||||
return inherited(preset, instance, propertyGetter);
|
||||
}
|
||||
|
||||
/// Returns the effective value for an inheritable property.
|
||||
public <T extends @UnknownNullability Object> T getInheritable(
|
||||
Function<GameSettings, InheritableProperty<T>> propertyGetter) {
|
||||
@@ -981,7 +981,7 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
|
||||
/// Returns the effective maximum heap memory in MiB.
|
||||
public int getMaxMemory() {
|
||||
Integer value = get(GameSettings::maxMemoryProperty);
|
||||
Integer value = getInheritable(GameSettings::maxMemoryProperty);
|
||||
return value != null && value > 0 ? value : SUGGESTED_MEMORY;
|
||||
}
|
||||
|
||||
@@ -996,11 +996,16 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
}
|
||||
|
||||
/// Returns the effective renderer.
|
||||
public Renderer getRenderer() {
|
||||
return switch (getInheritable(GameSettings::graphicsBackendProperty)) {
|
||||
public Renderer getRenderer(GameVersionNumber version) {
|
||||
GraphicsAPI graphicsAPI = getInheritable(GameSettings::graphicsBackendProperty);
|
||||
if (graphicsAPI == GraphicsAPI.DEFAULT) {
|
||||
graphicsAPI = GraphicsAPI.getDefaultGraphicsAPI(version);
|
||||
}
|
||||
|
||||
return switch (graphicsAPI) {
|
||||
case DEFAULT -> Renderer.DEFAULT;
|
||||
case OPENGL -> selectRenderer(GraphicsAPI.OPENGL, getInheritable(GameSettings::openGLRendererProperty));
|
||||
case VULKAN -> selectRenderer(GraphicsAPI.VULKAN, getInheritable(GameSettings::vulkanRendererProperty));
|
||||
case DEFAULT -> Renderer.DEFAULT;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -129,6 +129,21 @@ public final class GameSettingsPresets extends ObservableSetting implements Json
|
||||
return id;
|
||||
}
|
||||
|
||||
/// Returns the first automatic preset number that is not used by existing presets.
|
||||
public int newPresetAutoNameNumber() {
|
||||
mainLoop:
|
||||
for (int index = 1; ; index++) {
|
||||
for (GameSettings.Preset setting : getPresets()) {
|
||||
Integer autoNameNumber = setting.autoNameNumberProperty().getValue();
|
||||
if ((autoNameNumber != null && autoNameNumber == index)) {
|
||||
continue mainLoop;
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the preset with the given ID.
|
||||
public GameSettings.@Nullable Preset getPreset(@Nullable GameSettingsPresetID id) {
|
||||
if (id == null) {
|
||||
|
||||
@@ -87,6 +87,9 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
|
||||
/// Theme appearance override key for title-bar transparency.
|
||||
public static final String THEME_APPEARANCE_TITLE_BAR_TRANSPARENT = "titleBarTransparent";
|
||||
|
||||
/// Theme appearance override key for window transparency.
|
||||
public static final String THEME_APPEARANCE_WINDOW_TRANSPARENT = "windowTransparent";
|
||||
|
||||
/// Theme appearance override key for the primary background source.
|
||||
public static final String THEME_APPEARANCE_BACKGROUND = "background";
|
||||
|
||||
@@ -336,6 +339,15 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
|
||||
return titleBarTransparent;
|
||||
}
|
||||
|
||||
/// Whether translucent launcher backgrounds reveal content behind the window.
|
||||
@SerializedName("windowTransparent")
|
||||
private final BooleanProperty windowTransparent = new SimpleBooleanProperty(false);
|
||||
|
||||
/// Returns the window transparency property.
|
||||
public BooleanProperty windowTransparentProperty() {
|
||||
return windowTransparent;
|
||||
}
|
||||
|
||||
// Background source
|
||||
|
||||
/// The launcher background source type.
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.theme.NetworkBackgroundImageCachePolicy;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSchema;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -80,12 +81,6 @@ public final class LegacyConfigMigrator {
|
||||
/// The legacy built-in profile name for the user-home game directory.
|
||||
private static final String LEGACY_HOME_PROFILE = "Home";
|
||||
|
||||
/// The legacy built-in current-workspace profile ID.
|
||||
private static final GameDirectoryID LEGACY_DEFAULT_PROFILE_ID = getLegacyProfileID(LEGACY_DEFAULT_PROFILE);
|
||||
|
||||
/// The legacy built-in user-home profile ID.
|
||||
private static final GameDirectoryID LEGACY_HOME_PROFILE_ID = getLegacyProfileID(LEGACY_HOME_PROFILE);
|
||||
|
||||
/// The legacy Windows and portable configuration file name used before HMCL 3.16.
|
||||
private static final String LEGACY_CONFIG_FILENAME = "hmcl.json";
|
||||
|
||||
@@ -260,6 +255,10 @@ public final class LegacyConfigMigrator {
|
||||
renameMember(jsonObject, "bgpaint", "customBackgroundPaint");
|
||||
renameMember(jsonObject, "backgroundPaint", "customBackgroundPaint");
|
||||
migrateBackgroundOpacity(jsonObject, themeAppearanceOverrides);
|
||||
jsonObject.addProperty("windowTransparent", true);
|
||||
addThemeAppearanceOverride(
|
||||
themeAppearanceOverrides,
|
||||
LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT);
|
||||
writeThemeAppearanceOverrides(jsonObject, themeAppearanceOverrides);
|
||||
renameMember(jsonObject, "proxyUserName", "proxyUser");
|
||||
migrateLegacySelectedVersions(jsonObject);
|
||||
@@ -276,6 +275,7 @@ public final class LegacyConfigMigrator {
|
||||
|
||||
GameSettingsPresets gameSettingsPresets = new GameSettingsPresets();
|
||||
migrateLegacyPresetSettings(gameDirectories, gameSettingsPresets, legacyConfigurations);
|
||||
migrateLegacyInstanceGameSettings(gameDirectories, gameSettingsPresets);
|
||||
migrateLegacyAllowAutoAgent(deserialized, gameSettingsPresets, legacyAllowAutoAgent);
|
||||
migrateLegacyDisableAutoGameOptions(deserialized, gameSettingsPresets, legacyDisableAutoGameOptions);
|
||||
DetachedSettings detachedSettings = new DetachedSettings(gameDirectories, gameSettingsPresets,
|
||||
@@ -1272,18 +1272,95 @@ public final class LegacyConfigMigrator {
|
||||
continue;
|
||||
}
|
||||
|
||||
legacyParent = LegacyGameSettingsMigrator.toPreset(legacyGameSettings, profileName, legacySettingObject);
|
||||
legacyParent = LegacyGameSettingsMigrator.toPreset(legacyGameSettings, gameSettingsPresets.newPresetAutoNameNumber(), legacySettingObject);
|
||||
gameSettingsPresets.getPresets().add(legacyParent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrates existing instances under migrated legacy game directories to explicit instance settings.
|
||||
static void migrateLegacyInstanceGameSettings(
|
||||
GameDirectories gameDirectories,
|
||||
GameSettingsPresets gameSettingsPresets) {
|
||||
Objects.requireNonNull(gameDirectories);
|
||||
Objects.requireNonNull(gameSettingsPresets);
|
||||
|
||||
for (GameDirectory gameDirectory : gameDirectories.getGameDirectories()) {
|
||||
@Nullable GameSettingsPresetID legacyGameSettings = gameDirectory.getLegacyGameSettings();
|
||||
if (legacyGameSettings == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
GameSettings.Preset legacyParent = gameSettingsPresets.getPreset(legacyGameSettings);
|
||||
if (legacyParent == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
migrateLegacyInstanceGameSettings(gameDirectory.getPath().toPath(), legacyGameSettings, legacyParent);
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrates existing instances under one game directory to explicit instance settings.
|
||||
private static void migrateLegacyInstanceGameSettings(
|
||||
Path gameDirectory,
|
||||
GameSettingsPresetID legacyGameSettings,
|
||||
GameSettings.Preset legacyParent) {
|
||||
Path versionsDirectory = gameDirectory.resolve("versions");
|
||||
if (!Files.isDirectory(versionsDirectory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (var stream = Files.list(versionsDirectory)) {
|
||||
stream.filter(Files::isDirectory)
|
||||
.forEach(instanceRoot -> migrateLegacyInstanceGameSettingsInVersionRoot(
|
||||
instanceRoot,
|
||||
legacyGameSettings,
|
||||
legacyParent));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to migrate legacy instance game settings under " + versionsDirectory, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrates one existing instance to explicit instance settings.
|
||||
private static void migrateLegacyInstanceGameSettingsInVersionRoot(
|
||||
Path instanceRoot,
|
||||
GameSettingsPresetID legacyGameSettings,
|
||||
GameSettings.Preset legacyParent) {
|
||||
@Nullable Path instanceId = instanceRoot.getFileName();
|
||||
if (instanceId == null || !Files.isRegularFile(instanceRoot.resolve(instanceId.toString() + ".json"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
Path currentSettings = instanceRoot.resolve(LegacyGameSettingsMigrator.INSTANCE_METADATA_DIRECTORY)
|
||||
.resolve(LegacyGameSettingsMigrator.INSTANCE_CONFIG_DIRECTORY)
|
||||
.resolve(LegacyGameSettingsMigrator.INSTANCE_GAME_SETTINGS_FILENAME);
|
||||
if (Files.exists(currentSettings)) {
|
||||
return;
|
||||
}
|
||||
if (Files.exists(instanceRoot.resolve(LegacyGameSettingsMigrator.LEGACY_INSTANCE_SETTINGS_FILENAME))) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameSettings.Instance setting = new GameSettings.Instance();
|
||||
setting.parentProperty().setValue(legacyGameSettings);
|
||||
if (legacyParent.defaultIsolationTypeProperty().getValue() == DefaultIsolationType.ALWAYS
|
||||
&& StringUtils.isBlank(legacyParent.runningDirectoryProperty().getValue())) {
|
||||
setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
|
||||
}
|
||||
|
||||
try {
|
||||
FileUtils.saveSafely(currentSettings, LauncherSettings.SETTINGS_GSON.toJson(setting));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to save migrated instance game settings " + currentSettings, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the legacy profile name used in `configurations`.
|
||||
private static @Nullable String getLegacyProfileName(GameDirectory gameDirectory) {
|
||||
if (LEGACY_DEFAULT_PROFILE_ID.equals(gameDirectory.getId())) {
|
||||
if (GameDirectoryManager.DEFAULT_GAME_DIRECTORY_ID.equals(gameDirectory.getId())) {
|
||||
return LEGACY_DEFAULT_PROFILE;
|
||||
}
|
||||
if (LEGACY_HOME_PROFILE_ID.equals(gameDirectory.getId())) {
|
||||
if (GameDirectoryManager.HOME_GAME_DIRECTORY_ID.equals(gameDirectory.getId())) {
|
||||
return LEGACY_HOME_PROFILE;
|
||||
}
|
||||
return GameDirectoryManager.getGameDirectoryCustomName(gameDirectory);
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.setting.property.InheritableProperty;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.LocalizedText;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
@@ -43,12 +42,24 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@NotNullByDefault
|
||||
public final class LegacyGameSettingsMigrator {
|
||||
/// Legacy file name used by old per-version `VersionSetting` data.
|
||||
private static final String LEGACY_INSTANCE_SETTINGS_FILENAME = "hmclversion.cfg";
|
||||
static final String LEGACY_INSTANCE_SETTINGS_FILENAME = "hmclversion.cfg";
|
||||
|
||||
/// Receipt file name used to record successful legacy per-version settings migration.
|
||||
private static final String LEGACY_INSTANCE_SETTINGS_MIGRATION_RECEIPT_FILENAME =
|
||||
"instance-game-settings.migration-receipt.json";
|
||||
|
||||
/// Directory under the version root that stores HMCL-managed instance metadata.
|
||||
static final String INSTANCE_METADATA_DIRECTORY = ".hmcl";
|
||||
|
||||
/// Directory under the instance metadata directory that stores instance configuration files.
|
||||
static final String INSTANCE_CONFIG_DIRECTORY = "config";
|
||||
|
||||
/// Directory under the instance metadata directory that stores instance state files.
|
||||
private static final String INSTANCE_STATE_DIRECTORY = "state";
|
||||
|
||||
/// Current file name for instance-specific game settings.
|
||||
static final String INSTANCE_GAME_SETTINGS_FILENAME = "instance-game-settings.json";
|
||||
|
||||
/// Legacy game directory modes stored by old configuration files.
|
||||
private enum GameDirectoryType {
|
||||
/// Use the root `.minecraft` folder.
|
||||
@@ -101,9 +112,9 @@ public final class LegacyGameSettingsMigrator {
|
||||
}
|
||||
|
||||
/// Converts a legacy profile-level setting JSON object into a preset with the given ID.
|
||||
public static GameSettings.Preset toPreset(GameSettingsPresetID id, String name, @Nullable JsonObject source) {
|
||||
public static GameSettings.Preset toPreset(GameSettingsPresetID id, int autoNameNumber, @Nullable JsonObject source) {
|
||||
GameSettings.Preset target = new GameSettings.Preset(id);
|
||||
target.nameProperty().setValue(LocalizedText.plain(name));
|
||||
target.autoNameNumberProperty().setValue(autoNameNumber);
|
||||
if (getLegacyGameDirType(source, GameDirectoryType.ROOT_FOLDER) == GameDirectoryType.VERSION_FOLDER) {
|
||||
target.defaultIsolationTypeProperty().setValue(DefaultIsolationType.ALWAYS);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ public enum ThemeColorType {
|
||||
/// Uses the built-in launcher default theme color.
|
||||
DEFAULT,
|
||||
|
||||
/// Follows the accent color provided by the operating system.
|
||||
SYSTEM,
|
||||
|
||||
/// Uses the custom launcher theme color selected by the user.
|
||||
CUSTOM,
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.theme;
|
||||
|
||||
import javafx.scene.layout.Background;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/// A launcher background and the opacity applied by its rendering node.
|
||||
///
|
||||
/// @param background the JavaFX background rendered by the launcher
|
||||
/// @param opacity the node opacity in the range `[0, 1]`
|
||||
@NotNullByDefault
|
||||
public record LauncherBackground(Background background, double opacity) {
|
||||
/// Creates a launcher background value.
|
||||
public LauncherBackground {
|
||||
Objects.requireNonNull(background);
|
||||
if (!Double.isFinite(opacity) || opacity < 0.0 || opacity > 1.0) {
|
||||
throw new IllegalArgumentException("Launcher background opacity must be between 0 and 1: " + opacity);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -43,6 +43,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
/// @param contrast the MonetFX contrast level, or `null` when inherited
|
||||
/// @param background the background settings, or `null` when inherited
|
||||
/// @param titleBar the title-bar settings, or `null` when inherited
|
||||
/// @param windowTransparent whether the launcher window should be transparent, or `null` when inherited
|
||||
@NotNullByDefault
|
||||
public record ThemeAppearance(
|
||||
@Nullable ThemeColorSource color,
|
||||
@@ -50,7 +51,8 @@ public record ThemeAppearance(
|
||||
@Nullable ColorStyle colorStyle,
|
||||
@Nullable Contrast contrast,
|
||||
@Nullable ThemeBackgroundSettings background,
|
||||
@Nullable ThemeTitleBar titleBar) {
|
||||
@Nullable ThemeTitleBar titleBar,
|
||||
@Nullable Boolean windowTransparent) {
|
||||
|
||||
/// JSON member name for the color seed.
|
||||
static final String FIELD_COLOR = "color";
|
||||
@@ -70,6 +72,9 @@ public record ThemeAppearance(
|
||||
/// JSON member name for title-bar settings.
|
||||
static final String FIELD_TITLE_BAR = "titleBar";
|
||||
|
||||
/// JSON member name for window transparency.
|
||||
static final String FIELD_WINDOW_TRANSPARENT = "windowTransparent";
|
||||
|
||||
/// Creates an appearance patch.
|
||||
///
|
||||
/// @param color the color seed source, or `null` when inherited
|
||||
@@ -78,6 +83,7 @@ public record ThemeAppearance(
|
||||
/// @param contrast the MonetFX contrast level, or `null` when inherited
|
||||
/// @param background the background settings, or `null` when inherited
|
||||
/// @param titleBar the title-bar settings, or `null` when inherited
|
||||
/// @param windowTransparent whether the launcher window should be transparent, or `null` when inherited
|
||||
public ThemeAppearance {
|
||||
if (background != null && background.isEmpty()) {
|
||||
background = null;
|
||||
@@ -100,7 +106,8 @@ public record ThemeAppearance(
|
||||
readColorStyle(object),
|
||||
readContrast(object),
|
||||
readBackground(object),
|
||||
readTitleBar(object));
|
||||
readTitleBar(object),
|
||||
readWindowTransparent(object));
|
||||
}
|
||||
|
||||
/// Returns whether this appearance contains no concrete fields.
|
||||
@@ -112,7 +119,8 @@ public record ThemeAppearance(
|
||||
&& colorStyle == null
|
||||
&& contrast == null
|
||||
&& background == null
|
||||
&& titleBar == null;
|
||||
&& titleBar == null
|
||||
&& windowTransparent == null;
|
||||
}
|
||||
|
||||
/// Adds this appearance patch's concrete fields to a JSON object.
|
||||
@@ -139,6 +147,9 @@ public record ThemeAppearance(
|
||||
if (titleBar != null) {
|
||||
object.add(FIELD_TITLE_BAR, titleBar.toJsonObject());
|
||||
}
|
||||
if (windowTransparent != null) {
|
||||
object.addProperty(FIELD_WINDOW_TRANSPARENT, windowTransparent);
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts this appearance patch to its JSON representation.
|
||||
@@ -167,7 +178,8 @@ public record ThemeAppearance(
|
||||
: patch.background != null ? patch.background : background,
|
||||
titleBar != null && patch.titleBar != null
|
||||
? titleBar.merge(patch.titleBar)
|
||||
: patch.titleBar != null ? patch.titleBar : titleBar);
|
||||
: patch.titleBar != null ? patch.titleBar : titleBar,
|
||||
patch.windowTransparent != null ? patch.windowTransparent : windowTransparent);
|
||||
}
|
||||
|
||||
/// Converts this appearance to concrete launcher theme values.
|
||||
@@ -309,6 +321,19 @@ public record ThemeAppearance(
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads the optional window transparency field.
|
||||
private static @Nullable Boolean readWindowTransparent(JsonObject object) {
|
||||
JsonElement element = object.get(FIELD_WINDOW_TRANSPARENT);
|
||||
if (element == null) {
|
||||
return null;
|
||||
}
|
||||
if (!(element instanceof JsonPrimitive primitive) || !primitive.isBoolean()) {
|
||||
LOG.warning("Ignored invalid theme windowTransparent: expected a boolean, got " + element);
|
||||
return null;
|
||||
}
|
||||
return primitive.getAsBoolean();
|
||||
}
|
||||
|
||||
/// Reads an optional string field.
|
||||
private static @Nullable String readString(JsonObject object, String field) {
|
||||
JsonElement element = object.get(field);
|
||||
|
||||
@@ -77,7 +77,7 @@ public final class ThemePackManager {
|
||||
private static final Path USER_THEME_PACKS_DIRECTORY = Metadata.HMCL_USER_HOME.resolve("themes");
|
||||
|
||||
/// Default version used when exporting the current launcher appearance.
|
||||
private static final String CURRENT_THEME_PACK_VERSION = "1.0.0";
|
||||
public static final String CURRENT_THEME_PACK_VERSION = "1.0.0";
|
||||
|
||||
/// IDs of launcher-bundled theme packs in display order.
|
||||
private static final @Unmodifiable List<String> BUILTIN_THEME_PACK_IDS = List.of(
|
||||
@@ -615,13 +615,14 @@ public final class ThemePackManager {
|
||||
///
|
||||
/// @param outputFile the target theme-pack file
|
||||
/// @param packId the exported package identifier
|
||||
/// @param version the exported package version
|
||||
/// @param packName the exported package display name
|
||||
/// @param authorName the exported package author name
|
||||
/// @throws IOException if the current appearance cannot be exported
|
||||
public static void exportCurrent(Path outputFile, String packId, String packName, String authorName) throws IOException {
|
||||
public static void exportCurrent(Path outputFile, String packId, String version, String packName, String authorName) throws IOException {
|
||||
Objects.requireNonNull(outputFile);
|
||||
|
||||
ExportedThemePack themePack = createCurrent(packId, packName, authorName);
|
||||
ExportedThemePack themePack = createCurrent(packId, version, packName, authorName);
|
||||
try {
|
||||
ThemePackExporter.export(themePack.manifest(), themePack.assets(), outputFile);
|
||||
} finally {
|
||||
@@ -638,12 +639,14 @@ public final class ThemePackManager {
|
||||
/// Creates an exportable theme pack from the current launcher appearance.
|
||||
///
|
||||
/// @param packId the exported package identifier
|
||||
/// @param version the exported package version
|
||||
/// @param packName the exported package display name
|
||||
/// @param authorName the exported package author name
|
||||
/// @return the exportable theme-pack descriptor
|
||||
/// @throws IOException if the current appearance cannot be represented as a theme pack
|
||||
public static ExportedThemePack createCurrent(String packId, String packName, String authorName) throws IOException {
|
||||
public static ExportedThemePack createCurrent(String packId, String version, String packName, String authorName) throws IOException {
|
||||
packId = ThemePackManifest.requirePackageId(packId);
|
||||
version = requireNonBlank(version, "version");
|
||||
packName = requireNonBlank(packName, "packName");
|
||||
authorName = requireNonBlank(authorName, "authorName");
|
||||
|
||||
@@ -657,7 +660,8 @@ public final class ThemePackManager {
|
||||
currentColorStyle(),
|
||||
null,
|
||||
background,
|
||||
new ThemeTitleBar(currentTitleBarTransparent()));
|
||||
new ThemeTitleBar(currentTitleBarTransparent()),
|
||||
currentWindowTransparent());
|
||||
Theme theme = new Theme(
|
||||
null,
|
||||
null,
|
||||
@@ -668,7 +672,7 @@ public final class ThemePackManager {
|
||||
List.of());
|
||||
ThemePackManifest manifest = new ThemePackManifest(
|
||||
packId,
|
||||
CURRENT_THEME_PACK_VERSION,
|
||||
version,
|
||||
LocalizedText.plain(packName),
|
||||
List.of(new ThemePackAuthor(LocalizedText.plain(authorName))),
|
||||
background.source() instanceof ThemeBackground.Image image ? image.path() : null,
|
||||
@@ -708,6 +712,9 @@ public final class ThemePackManager {
|
||||
if (themeColorType == ThemeColorType.DEFAULT) {
|
||||
return ThemeColorSource.DEFAULT;
|
||||
}
|
||||
if (themeColorType == ThemeColorType.SYSTEM) {
|
||||
return ThemeColorSource.custom(Themes.getSystemThemeColor());
|
||||
}
|
||||
if (themeColorType == ThemeColorType.BACKGROUND) {
|
||||
if (backgroundType == BackgroundType.THEME_COLOR) {
|
||||
return ThemeColorSource.DEFAULT;
|
||||
@@ -752,6 +759,13 @@ public final class ThemePackManager {
|
||||
: Objects.requireNonNullElse(resolveCurrentTitleBarTransparent(currentResolveContext()), false);
|
||||
}
|
||||
|
||||
/// Returns whether the current launcher window should be transparent.
|
||||
private static boolean currentWindowTransparent() throws IOException {
|
||||
return SettingsManager.settings().getThemeAppearanceOverrides().contains(LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT)
|
||||
? settings().windowTransparentProperty().get()
|
||||
: Objects.requireNonNullElse(resolveCurrentWindowTransparent(currentResolveContext()), false);
|
||||
}
|
||||
|
||||
/// Resolves the selected theme's controlled brightness.
|
||||
///
|
||||
/// @param context the condition resolution context
|
||||
@@ -792,6 +806,16 @@ public final class ThemePackManager {
|
||||
return appearance != null && appearance.titleBar() != null ? appearance.titleBar().transparent() : null;
|
||||
}
|
||||
|
||||
/// Resolves the selected theme window transparency directive.
|
||||
///
|
||||
/// @param context the condition resolution context
|
||||
/// @return the selected theme window transparency directive, or `null` when unavailable
|
||||
/// @throws IOException if the selected theme pack cannot be read
|
||||
public static @Nullable Boolean resolveCurrentWindowTransparent(ThemeResolveContext context) throws IOException {
|
||||
@Nullable ThemeAppearance appearance = resolveCurrentThemeAppearance(context);
|
||||
return appearance != null ? appearance.windowTransparent() : null;
|
||||
}
|
||||
|
||||
/// Resolves the current selected theme color source without extracting any wallpaper pixels.
|
||||
///
|
||||
/// @param context the condition resolution context
|
||||
|
||||
@@ -32,9 +32,6 @@ import javafx.collections.SetChangeListener;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelReader;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.BackgroundImage;
|
||||
@@ -115,6 +112,9 @@ public final class Themes {
|
||||
if (FXUtils.DARK_MODE != null) {
|
||||
observables.add(FXUtils.DARK_MODE);
|
||||
}
|
||||
if (FXUtils.ACCENT_COLOR != null) {
|
||||
observables.add(FXUtils.ACCENT_COLOR);
|
||||
}
|
||||
bind(observables.toArray(new Observable[0]));
|
||||
}
|
||||
|
||||
@@ -195,6 +195,7 @@ public final class Themes {
|
||||
}
|
||||
return switch (themeColorType) {
|
||||
case DEFAULT -> ThemeColor.DEFAULT;
|
||||
case SYSTEM -> getSystemThemeColor();
|
||||
case CUSTOM -> fallback;
|
||||
case BACKGROUND -> resolveWallpaperThemeColor(fallback, backgroundType);
|
||||
};
|
||||
@@ -282,9 +283,11 @@ public final class Themes {
|
||||
colorScheme
|
||||
);
|
||||
|
||||
/// The current JavaFX launcher background.
|
||||
private static final ReadOnlyObjectWrapper<Background> background = new ReadOnlyObjectWrapper<>(
|
||||
new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
/// The current JavaFX launcher background and its node opacity.
|
||||
private static final ReadOnlyObjectWrapper<LauncherBackground> background = new ReadOnlyObjectWrapper<>(
|
||||
new LauncherBackground(
|
||||
new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY)),
|
||||
1.0));
|
||||
|
||||
/// The last loaded background source used to rebuild the JavaFX background without reloading images.
|
||||
private static @Nullable LoadedBackground loadedBackground;
|
||||
@@ -300,8 +303,8 @@ public final class Themes {
|
||||
|
||||
/// A loaded JavaFX background source and the wallpaper color extracted while loading it.
|
||||
private sealed interface LoadedBackground {
|
||||
/// Returns the loaded JavaFX background.
|
||||
Background background();
|
||||
/// Returns the loaded JavaFX background and its node opacity.
|
||||
LauncherBackground background();
|
||||
|
||||
/// Returns whether this background is the configured fallback background.
|
||||
boolean fallbackBackground();
|
||||
@@ -316,7 +319,7 @@ public final class Themes {
|
||||
/// @param wallpaperThemeColor the seed color extracted from [#image], or `null` when unavailable
|
||||
/// @param fallbackBackground whether this background is the configured fallback background
|
||||
record Image(
|
||||
Background background,
|
||||
LauncherBackground background,
|
||||
javafx.scene.image.Image image,
|
||||
@Nullable ThemeColor wallpaperThemeColor,
|
||||
boolean fallbackBackground) implements LoadedBackground {
|
||||
@@ -339,7 +342,7 @@ public final class Themes {
|
||||
/// @param wallpaperThemeColor the seed color represented by [#paint], or `null` when unavailable
|
||||
/// @param fallbackBackground whether this background is the configured fallback background
|
||||
record Paint(
|
||||
Background background,
|
||||
LauncherBackground background,
|
||||
@Nullable javafx.scene.paint.Paint paint,
|
||||
@Nullable ThemeColor wallpaperThemeColor,
|
||||
boolean fallbackBackground) implements LoadedBackground {
|
||||
@@ -359,7 +362,7 @@ public final class Themes {
|
||||
/// @param background the loaded JavaFX background
|
||||
/// @param fallbackBackground whether this background is the configured fallback background
|
||||
record ThemeColorFill(
|
||||
Background background,
|
||||
LauncherBackground background,
|
||||
boolean fallbackBackground) implements LoadedBackground {
|
||||
/// Creates a loaded theme-color background.
|
||||
///
|
||||
@@ -528,6 +531,15 @@ public final class Themes {
|
||||
return defaultBrightness = brightness;
|
||||
}
|
||||
|
||||
/// Returns the current operating system accent color, or the launcher default when unavailable.
|
||||
public static ThemeColor getSystemThemeColor() {
|
||||
if (FXUtils.ACCENT_COLOR == null) {
|
||||
return ThemeColor.DEFAULT;
|
||||
}
|
||||
@Nullable Color accentColor = FXUtils.ACCENT_COLOR.get();
|
||||
return accentColor != null ? ThemeColor.of(accentColor) : ThemeColor.DEFAULT;
|
||||
}
|
||||
|
||||
/// Returns the current resolved launcher theme.
|
||||
public static ResolvedTheme getTheme() {
|
||||
return theme.get();
|
||||
@@ -543,8 +555,8 @@ public final class Themes {
|
||||
return colorScheme.get();
|
||||
}
|
||||
|
||||
/// Returns the current JavaFX launcher background property.
|
||||
public static ReadOnlyObjectProperty<Background> backgroundProperty() {
|
||||
/// Returns the current JavaFX launcher background and node opacity property.
|
||||
public static ReadOnlyObjectProperty<LauncherBackground> backgroundProperty() {
|
||||
startBackgroundUpdates();
|
||||
return background.getReadOnlyProperty();
|
||||
}
|
||||
@@ -824,7 +836,8 @@ public final class Themes {
|
||||
}
|
||||
@Nullable LoadedBackground loaded = loadedBackground;
|
||||
if (loaded instanceof LoadedBackground.ThemeColorFill themeColorFill) {
|
||||
Background newBackground = createThemeColorBackground(getLoadedBackgroundOpacity(loaded.fallbackBackground()));
|
||||
LauncherBackground newBackground =
|
||||
createThemeColorBackground(getLoadedBackgroundOpacity(loaded.fallbackBackground()));
|
||||
loadedBackground = new LoadedBackground.ThemeColorFill(newBackground, themeColorFill.fallbackBackground());
|
||||
background.set(newBackground);
|
||||
}
|
||||
@@ -858,17 +871,23 @@ public final class Themes {
|
||||
LoadedBackground refreshed;
|
||||
if (loaded instanceof LoadedBackground.Image imageBackground) {
|
||||
refreshed = new LoadedBackground.Image(
|
||||
createBackgroundWithOpacity(imageBackground.image(), opacity),
|
||||
new LauncherBackground(
|
||||
imageBackground.background().background(),
|
||||
MathUtils.clamp(opacity, 0., 1.)),
|
||||
imageBackground.image(),
|
||||
imageBackground.wallpaperThemeColor(),
|
||||
imageBackground.fallbackBackground());
|
||||
} else if (loaded instanceof LoadedBackground.ThemeColorFill themeColorFill) {
|
||||
refreshed = new LoadedBackground.ThemeColorFill(
|
||||
createThemeColorBackground(opacity),
|
||||
new LauncherBackground(
|
||||
themeColorFill.background().background(),
|
||||
MathUtils.clamp(opacity, 0., 1.)),
|
||||
themeColorFill.fallbackBackground());
|
||||
} else if (loaded instanceof LoadedBackground.Paint paintBackground) {
|
||||
refreshed = new LoadedBackground.Paint(
|
||||
createPaintBackground(paintBackground.paint(), opacity),
|
||||
new LauncherBackground(
|
||||
paintBackground.background().background(),
|
||||
MathUtils.clamp(opacity, 0., 1.)),
|
||||
paintBackground.paint(),
|
||||
paintBackground.wallpaperThemeColor(),
|
||||
paintBackground.fallbackBackground());
|
||||
@@ -917,60 +936,37 @@ public final class Themes {
|
||||
return FXUtils.loadImage(new CacheFileTask(url).run());
|
||||
}
|
||||
|
||||
/// Creates a JavaFX paint background with the requested opacity.
|
||||
private static Background createPaintBackground(@Nullable Paint paint, double opacity) {
|
||||
opacity = MathUtils.clamp(opacity, 0., 1.);
|
||||
if (paint instanceof Color || paint == null) {
|
||||
Color color = (Color) paint;
|
||||
if (color == null) {
|
||||
color = Color.WHITE;
|
||||
}
|
||||
if (opacity < 1.) {
|
||||
color = new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getOpacity() * opacity);
|
||||
}
|
||||
return new Background(new BackgroundFill(color, CornerRadii.EMPTY, Insets.EMPTY));
|
||||
} else {
|
||||
return new Background(new BackgroundFill(paint, CornerRadii.EMPTY, Insets.EMPTY));
|
||||
}
|
||||
/// Creates a JavaFX paint background with a separately stored node opacity.
|
||||
private static LauncherBackground createPaintBackground(@Nullable Paint paint, double opacity) {
|
||||
return new LauncherBackground(
|
||||
new Background(new BackgroundFill(
|
||||
Objects.requireNonNullElse(paint, Color.WHITE),
|
||||
CornerRadii.EMPTY,
|
||||
Insets.EMPTY)),
|
||||
MathUtils.clamp(opacity, 0., 1.));
|
||||
}
|
||||
|
||||
/// Creates a JavaFX background from the current theme color scheme surface container.
|
||||
private static Background createThemeColorBackground(double opacity) {
|
||||
private static LauncherBackground createThemeColorBackground(double opacity) {
|
||||
return createPaintBackground(getColorScheme().getColor(ColorRole.SURFACE_CONTAINER), opacity);
|
||||
}
|
||||
|
||||
/// Creates a JavaFX image background with the requested opacity.
|
||||
private static Background createBackgroundWithOpacity(Image image, double opacity) {
|
||||
PixelReader pixelReader = image.getPixelReader();
|
||||
if (opacity <= 0) {
|
||||
return new Background(new BackgroundFill(new Color(1, 1, 1, 0), CornerRadii.EMPTY, Insets.EMPTY));
|
||||
} else if (opacity >= 1. || pixelReader == null) {
|
||||
return new Background(new BackgroundImage(
|
||||
image,
|
||||
BackgroundRepeat.NO_REPEAT,
|
||||
BackgroundRepeat.NO_REPEAT,
|
||||
BackgroundPosition.DEFAULT,
|
||||
new BackgroundSize(800, 480, false, false, true, true)
|
||||
));
|
||||
} else {
|
||||
WritableImage tempImage = new WritableImage((int) image.getWidth(), (int) image.getHeight());
|
||||
PixelWriter pixelWriter = tempImage.getPixelWriter();
|
||||
for (int y = 0; y < image.getHeight(); y++) {
|
||||
for (int x = 0; x < image.getWidth(); x++) {
|
||||
Color color = pixelReader.getColor(x, y);
|
||||
Color newColor = new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getOpacity() * opacity);
|
||||
pixelWriter.setColor(x, y, newColor);
|
||||
}
|
||||
}
|
||||
|
||||
return new Background(new BackgroundImage(
|
||||
tempImage,
|
||||
BackgroundRepeat.NO_REPEAT,
|
||||
BackgroundRepeat.NO_REPEAT,
|
||||
BackgroundPosition.DEFAULT,
|
||||
new BackgroundSize(800, 480, false, false, true, true)
|
||||
));
|
||||
}
|
||||
/// Creates a JavaFX image background with a separately stored node opacity.
|
||||
private static LauncherBackground createBackgroundWithOpacity(Image image, double opacity) {
|
||||
Background imageBackground = new Background(new BackgroundImage(
|
||||
image,
|
||||
BackgroundRepeat.NO_REPEAT,
|
||||
BackgroundRepeat.NO_REPEAT,
|
||||
BackgroundPosition.DEFAULT,
|
||||
new BackgroundSize(
|
||||
BackgroundSize.AUTO,
|
||||
BackgroundSize.AUTO,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true)
|
||||
));
|
||||
return new LauncherBackground(imageBackground, MathUtils.clamp(opacity, 0., 1.));
|
||||
}
|
||||
|
||||
/// Loads a default launcher background image from local workspace files.
|
||||
@@ -1073,6 +1069,27 @@ public final class Themes {
|
||||
FXUtils.DARK_MODE != null ? FXUtils.DARK_MODE : settings().themeBrightnessModeProperty()
|
||||
);
|
||||
|
||||
/// Whether the launcher window should be transparent after applying launcher and theme settings.
|
||||
private static final BooleanBinding windowTransparent = Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
if (settings().getThemeAppearanceOverrides().contains(LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT)) {
|
||||
return settings().windowTransparentProperty().get();
|
||||
}
|
||||
try {
|
||||
return Objects.requireNonNullElse(
|
||||
ThemePackManager.resolveCurrentWindowTransparent(ThemePackManager.currentResolveContext()),
|
||||
false);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
settings().windowTransparentProperty(),
|
||||
settings().getThemeAppearanceOverrides(),
|
||||
settings().selectedThemeProperty(),
|
||||
settings().themeBrightnessModeProperty(),
|
||||
FXUtils.DARK_MODE != null ? FXUtils.DARK_MODE : settings().themeBrightnessModeProperty()
|
||||
);
|
||||
|
||||
/// The title text fill derived from the current color scheme.
|
||||
private static final ObjectBinding<Color> titleFill = Bindings.createObjectBinding(
|
||||
() -> titleBarTransparent.get()
|
||||
@@ -1092,6 +1109,11 @@ public final class Themes {
|
||||
return titleBarTransparent;
|
||||
}
|
||||
|
||||
/// Returns whether the launcher window should be transparent after applying launcher and theme settings.
|
||||
public static BooleanBinding windowTransparentProperty() {
|
||||
return windowTransparent;
|
||||
}
|
||||
|
||||
/// Returns whether the current color scheme uses dark brightness.
|
||||
public static BooleanBinding darkModeProperty() {
|
||||
return darkMode;
|
||||
|
||||
@@ -92,11 +92,7 @@ import org.xml.sax.SAXException;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.StringReader;
|
||||
import java.io.*;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
@@ -1691,6 +1687,7 @@ public final class FXUtils {
|
||||
|
||||
control.setOnContextMenuRequested(e -> {
|
||||
boolean hasNoSelection = control.getSelectedText().isEmpty();
|
||||
boolean isPassword = control instanceof PasswordField;
|
||||
|
||||
IconedMenuItem undo = new IconedMenuItem(SVG.UNDO, i18n("menu.undo"), control::undo, popup);
|
||||
IconedMenuItem redo = new IconedMenuItem(SVG.REDO, i18n("menu.redo"), control::redo, popup);
|
||||
@@ -1704,9 +1701,9 @@ public final class FXUtils {
|
||||
|
||||
undo.setDisable(!control.isUndoable());
|
||||
redo.setDisable(!control.isRedoable());
|
||||
cut.setDisable(hasNoSelection);
|
||||
cut.setDisable(hasNoSelection || isPassword);
|
||||
delete.setDisable(hasNoSelection);
|
||||
copy.setDisable(hasNoSelection);
|
||||
copy.setDisable(hasNoSelection || isPassword);
|
||||
paste.setDisable(!Clipboard.getSystemClipboard().hasString());
|
||||
selectall.setDisable(control.getText() == null || control.getText().isEmpty());
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ public class GameCrashWindow extends Stage {
|
||||
Set<String> keywords = new HashSet<>();
|
||||
for (Pair<Set<CrashReportAnalyzer.Result>, Set<String>> pair : (List<Pair<Set<CrashReportAnalyzer.Result>, Set<String>>>) (List<?>) taskResult) {
|
||||
for (CrashReportAnalyzer.Result result : pair.getKey()) {
|
||||
results.put(result.getRule(), result);
|
||||
results.put(result.rule(), result);
|
||||
}
|
||||
keywords.addAll(pair.getValue());
|
||||
}
|
||||
@@ -184,22 +184,22 @@ public class GameCrashWindow extends Stage {
|
||||
|
||||
for (CrashReportAnalyzer.Result result : results.values()) {
|
||||
String message;
|
||||
switch (result.getRule()) {
|
||||
switch (result.rule()) {
|
||||
case TOO_OLD_JAVA:
|
||||
message = i18n("game.crash.reason.too_old_java", CrashReportAnalyzer.getJavaVersionFromMajorVersion(Integer.parseInt(result.getMatcher().group("expected"))));
|
||||
message = i18n("game.crash.reason.too_old_java", CrashReportAnalyzer.getJavaVersionFromMajorVersion(Integer.parseInt(result.matcher().group("expected"))));
|
||||
break;
|
||||
case MOD_RESOLUTION_CONFLICT:
|
||||
case MOD_RESOLUTION_MISSING:
|
||||
case MOD_RESOLUTION_COLLECTION:
|
||||
message = i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
|
||||
translateFabricModId(result.getMatcher().group("sourcemod")),
|
||||
parseFabricModId(result.getMatcher().group("destmod")),
|
||||
parseFabricModId(result.getMatcher().group("destmod")));
|
||||
message = i18n("game.crash.reason." + result.rule().name().toLowerCase(Locale.ROOT),
|
||||
translateFabricModId(result.matcher().group("sourcemod")),
|
||||
parseFabricModId(result.matcher().group("destmod")),
|
||||
parseFabricModId(result.matcher().group("destmod")));
|
||||
break;
|
||||
case MOD_RESOLUTION_MISSING_MINECRAFT:
|
||||
message = i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
|
||||
translateFabricModId(result.getMatcher().group("mod")),
|
||||
result.getMatcher().group("version"));
|
||||
message = i18n("game.crash.reason." + result.rule().name().toLowerCase(Locale.ROOT),
|
||||
translateFabricModId(result.matcher().group("mod")),
|
||||
result.matcher().group("version"));
|
||||
break;
|
||||
case MOD_FOREST_OPTIFINE:
|
||||
case TWILIGHT_FOREST_OPTIFINE:
|
||||
@@ -207,15 +207,15 @@ public class GameCrashWindow extends Stage {
|
||||
case JADE_FOREST_OPTIFINE:
|
||||
case NEOFORGE_FOREST_OPTIFINE:
|
||||
message = i18n("game.crash.reason.mod", "OptiFine");
|
||||
LOG.info("Crash cause: " + result.getRule() + ": " + i18n("game.crash.reason.mod", "OptiFine"));
|
||||
LOG.info("Crash cause: " + result.rule() + ": " + i18n("game.crash.reason.mod", "OptiFine"));
|
||||
break;
|
||||
default:
|
||||
message = i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
|
||||
Arrays.stream(result.getRule().getGroupNames()).map(groupName -> result.getMatcher().group(groupName))
|
||||
message = i18n("game.crash.reason." + result.rule().name().toLowerCase(Locale.ROOT),
|
||||
Arrays.stream(result.rule().getGroupNames()).map(groupName -> result.matcher().group(groupName))
|
||||
.toArray());
|
||||
break;
|
||||
}
|
||||
LOG.info("Crash cause: " + result.getRule() + ": " + message);
|
||||
LOG.info("Crash cause: " + result.rule() + ": " + message);
|
||||
segments.addAll(FXUtils.parseSegment(message, Controllers::onHyperlinkAction));
|
||||
segments.add(new Text("\n\n"));
|
||||
}
|
||||
|
||||
@@ -338,6 +338,13 @@ public class InstallerItem extends Control {
|
||||
throw new AssertionError("Unknown state type: " + state.getClass());
|
||||
}
|
||||
}, control.resolvedStateProperty));
|
||||
FXUtils.onChangeAndOperate(
|
||||
control.resolvedStateProperty,
|
||||
state -> statusLabel.pseudoClassStateChanged(
|
||||
PseudoClass.getPseudoClass("incompatible"),
|
||||
state instanceof IncompatibleState
|
||||
)
|
||||
);
|
||||
BorderPane.setMargin(statusLabel, new Insets(0, 0, 0, 8));
|
||||
BorderPane.setAlignment(statusLabel, Pos.CENTER_LEFT);
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ public final class MemoryStatusBar extends Control {
|
||||
}
|
||||
|
||||
PhysicalMemoryStatus status = getSkinnable().getMemoryStatus();
|
||||
double total = status.getTotal();
|
||||
double total = status.total();
|
||||
double used = status.getUsed();
|
||||
double allocated = getSkinnable().getMemoryAllocated();
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
@@ -86,6 +87,7 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
|
||||
center.getChildren().setAll(canvas, item);
|
||||
root.setCenter(center);
|
||||
BorderPane.setMargin(center, new Insets(0, 0, 0, 8));
|
||||
|
||||
HBox right = new HBox();
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
@@ -119,6 +121,7 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
JFXButton btnRefresh = FXUtils.newToggleButton4(SVG.REFRESH);
|
||||
SpinnerPane spinnerRefresh = new SpinnerPane();
|
||||
spinnerRefresh.getStyleClass().setAll("small-spinner-pane");
|
||||
spinnerRefresh.setPrefSize(30.0, 30.0);
|
||||
if (skinnable.getAccount() instanceof MicrosoftAccount && Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
|
||||
btnRefresh.setDisable(true);
|
||||
FXUtils.installFastTooltip(spinnerRefresh, i18n("account.methods.microsoft.snapshot.tooltip"));
|
||||
@@ -157,12 +160,9 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
right.getChildren().add(spinnerUpload);
|
||||
|
||||
JFXButton btnCopyUUID = FXUtils.newToggleButton4(SVG.CONTENT_COPY);
|
||||
SpinnerPane spinnerCopyUUID = new SpinnerPane();
|
||||
spinnerCopyUUID.getStyleClass().add("small-spinner-pane");
|
||||
btnCopyUUID.setOnAction(e -> FXUtils.copyText(skinnable.getAccount().getProfileID().toString()));
|
||||
FXUtils.installFastTooltip(btnCopyUUID, i18n("account.copy_uuid"));
|
||||
spinnerCopyUUID.setContent(btnCopyUUID);
|
||||
right.getChildren().add(spinnerCopyUUID);
|
||||
right.getChildren().add(btnCopyUUID);
|
||||
|
||||
JFXButton btnRemove = FXUtils.newToggleButton4(SVG.DELETE_FOREVER);
|
||||
btnRemove.setOnAction(e -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), skinnable::remove, null));
|
||||
|
||||
@@ -87,8 +87,22 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
|
||||
actions.getChildren().setAll(cancel, nextPane);
|
||||
}
|
||||
|
||||
addServerPane.setBody(txtServerUrl);
|
||||
addServerPane.setActions(lblCreationWarning, actions);
|
||||
VBox body = new VBox();
|
||||
body.setSpacing(8);
|
||||
|
||||
lblCreationWarning.setMinWidth(0);
|
||||
lblCreationWarning.setPrefWidth(0);
|
||||
lblCreationWarning.maxWidthProperty().bind(body.widthProperty());
|
||||
lblCreationWarning.visibleProperty().bind(lblCreationWarning.textProperty().isNotEmpty());
|
||||
lblCreationWarning.managedProperty().bind(lblCreationWarning.visibleProperty());
|
||||
|
||||
body.getChildren().addAll(
|
||||
txtServerUrl,
|
||||
lblCreationWarning
|
||||
);
|
||||
|
||||
addServerPane.setBody(body);
|
||||
addServerPane.setActions(actions);
|
||||
|
||||
txtServerUrl.getValidators().addAll(new RequiredValidator(), new URLValidator());
|
||||
FXUtils.setValidateWhileTextChanged(txtServerUrl, true);
|
||||
@@ -154,7 +168,6 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
|
||||
|
||||
this.setContent(addServerPane, ContainerAnimations.NONE);
|
||||
|
||||
lblCreationWarning.maxWidthProperty().bind(((FlowPane) lblCreationWarning.getParent()).widthProperty());
|
||||
nextPane.hideSpinner();
|
||||
|
||||
onEscPressed(this, this::onAddCancel);
|
||||
|
||||
@@ -148,14 +148,14 @@ public class OfflineAccountSkinPane extends StackPane {
|
||||
UUID uuid = this.account.getProfileID();
|
||||
if (result == null || result.skin() == null && result.cape() == null) {
|
||||
canvas.updateSkin(
|
||||
TexturesLoader.getDefaultSkin(uuid).getImage(),
|
||||
TexturesLoader.getDefaultSkin(uuid).image(),
|
||||
TexturesLoader.getDefaultModel(uuid) == TextureModel.SLIM,
|
||||
null
|
||||
);
|
||||
return;
|
||||
}
|
||||
canvas.updateSkin(
|
||||
result.skin() != null ? result.skin().image() : TexturesLoader.getDefaultSkin(uuid).getImage(),
|
||||
result.skin() != null ? result.skin().image() : TexturesLoader.getDefaultSkin(uuid).image(),
|
||||
result.model() == TextureModel.SLIM,
|
||||
result.cape() != null ? result.cape().image() : null);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,10 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class ComponentList extends Control implements NoPaddingComponent {
|
||||
|
||||
@@ -136,13 +139,25 @@ public class ComponentList extends Control implements NoPaddingComponent {
|
||||
}
|
||||
|
||||
public static Node createComponentListTitle(String title) {
|
||||
HBox node = new HBox();
|
||||
return createComponentListTitle(title, null);
|
||||
}
|
||||
|
||||
public static Node createComponentListTitle(String title, @Nullable String helpMessage) {
|
||||
HBox node = new HBox(4);
|
||||
node.setAlignment(Pos.CENTER_LEFT);
|
||||
node.setPadding(new Insets(8, 0, 0, 0));
|
||||
|
||||
{
|
||||
Label advanced = new Label(title);
|
||||
node.getChildren().setAll(advanced);
|
||||
}
|
||||
|
||||
if (helpMessage != null) {
|
||||
var helpIcon = new StackPane(SVG.HELP.createIcon(16));
|
||||
FXUtils.installFastTooltip(helpIcon, helpMessage);
|
||||
node.getChildren().add(helpIcon);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
@@ -228,4 +229,30 @@ public class ComponentSublist extends Control implements NoPaddingComponent {
|
||||
public void setTip(String tip) {
|
||||
this.tip.set(tip);
|
||||
}
|
||||
|
||||
/// Whether the sublist header should use a larger title font.
|
||||
private static final PseudoClass PSEUDO_LARGE_TITLE = PseudoClass.getPseudoClass("large-title");
|
||||
|
||||
/// The property controlling whether the sublist header uses a larger title font.
|
||||
private final BooleanProperty largeTitle = new SimpleBooleanProperty(this, "largeTitle", false) {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
pseudoClassStateChanged(PSEUDO_LARGE_TITLE, get());
|
||||
}
|
||||
};
|
||||
|
||||
/// Returns the property controlling whether the sublist header uses a larger title font.
|
||||
public BooleanProperty largeTitleProperty() {
|
||||
return largeTitle;
|
||||
}
|
||||
|
||||
/// Returns whether the sublist header uses a larger title font.
|
||||
public final boolean isLargeTitle() {
|
||||
return largeTitle.get();
|
||||
}
|
||||
|
||||
/// Sets whether the sublist header uses a larger title font.
|
||||
public final void setLargeTitle(boolean value) {
|
||||
largeTitle.set(value);
|
||||
}
|
||||
}
|
||||
@@ -145,10 +145,7 @@ final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
|
||||
});
|
||||
});
|
||||
|
||||
Node headerLeft = sublist.getHeaderLeft();
|
||||
if (headerLeft != null) {
|
||||
header.setTitleContent(headerLeft);
|
||||
}
|
||||
FXUtils.onChangeAndOperate(sublist.largeTitleProperty(), header::setLargeTitle);
|
||||
|
||||
InvalidationListener updateTitleTrailing = observable -> {
|
||||
Node titleTrailing = sublist.getTitleTrailing();
|
||||
|
||||
@@ -23,8 +23,8 @@ import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.NumberBinding;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.Node;
|
||||
@@ -43,11 +43,10 @@ import org.jackhuang.hmcl.util.MathUtils;
|
||||
public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
private ScrollBar scrollBar;
|
||||
private Region group;
|
||||
private Rectangle track = new Rectangle();
|
||||
private Rectangle thumb = new Rectangle();
|
||||
private final Rectangle track = new Rectangle();
|
||||
private final Rectangle thumb = new Rectangle();
|
||||
|
||||
@SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal"})
|
||||
private ChangeListener<Boolean> thumbHoverListener;
|
||||
private BooleanBinding hoverOrPressed;
|
||||
private Animation thumbHoverAnimation;
|
||||
|
||||
public FloatScrollBarSkin(final ScrollBar scrollBar) {
|
||||
@@ -214,7 +213,8 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
}
|
||||
};
|
||||
|
||||
this.thumbHoverListener = FXUtils.onWeakChangeAndOperate(thumb.hoverProperty(), newValue -> {
|
||||
this.hoverOrPressed = thumb.hoverProperty().or(thumb.pressedProperty());
|
||||
FXUtils.onChangeAndOperate(hoverOrPressed, newValue -> {
|
||||
if (thumbHoverAnimation != null) {
|
||||
thumbHoverAnimation.stop();
|
||||
thumbHoverAnimation = null;
|
||||
@@ -249,7 +249,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
thumbHoverAnimation.stop();
|
||||
thumbHoverAnimation = null;
|
||||
}
|
||||
thumbHoverListener = null;
|
||||
hoverOrPressed = null;
|
||||
scrollBar = null;
|
||||
group = null;
|
||||
}
|
||||
|
||||
@@ -26,10 +26,12 @@ import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -274,6 +276,8 @@ public abstract class LineComponent extends StackPane implements NoPaddingCompon
|
||||
|
||||
/// Updates the fixed preferred height after the row width or subtitle content changes.
|
||||
private void updatePreferredHeight() {
|
||||
if (FXUtils.getLimitHeight(this) > 0) return;
|
||||
|
||||
double width = getWidth();
|
||||
if (width <= 0) {
|
||||
setMinHeight(MIN_HEIGHT);
|
||||
|
||||
@@ -53,8 +53,13 @@ public abstract class MDListCell<T> extends ListCell<T> {
|
||||
|
||||
@Override
|
||||
protected void updateItem(T item, boolean empty) {
|
||||
T oldItem = getItem();
|
||||
boolean oldEmpty = isEmpty();
|
||||
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (oldItem == item && oldEmpty == empty) return;
|
||||
|
||||
updateControl(item, empty);
|
||||
if (empty) {
|
||||
setGraphic(null);
|
||||
|
||||
@@ -85,8 +85,8 @@ public final class RadioChoiceList<T extends @UnknownNullability Object> extends
|
||||
|
||||
group.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
|
||||
Choice<T> choice = newValue != null ? choiceByToggle.get(newValue) : null;
|
||||
selectedChoice.set(choice);
|
||||
selectedValue.set(choice != null ? choice.getValue() : null);
|
||||
selectedChoice.set(choice);
|
||||
});
|
||||
|
||||
selectedValue.addListener((observable, oldValue, newValue) -> {
|
||||
@@ -96,6 +96,12 @@ public final class RadioChoiceList<T extends @UnknownNullability Object> extends
|
||||
});
|
||||
}
|
||||
|
||||
/// Replaces the displayed choices.
|
||||
@SafeVarargs
|
||||
public final void setChoices(Choice<T>... choices) {
|
||||
setChoices(List.of(choices));
|
||||
}
|
||||
|
||||
/// Replaces the displayed choices.
|
||||
public void setChoices(Collection<? extends Choice<T>> choices) {
|
||||
this.choices.setAll(choices);
|
||||
|
||||
@@ -40,6 +40,7 @@ import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.theme.LauncherBackground;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
@@ -49,7 +50,9 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
public class Decorator extends Control {
|
||||
private final ListProperty<Node> content = new SimpleListProperty<>(FXCollections.observableArrayList());
|
||||
private final ListProperty<Node> container = new SimpleListProperty<>(FXCollections.observableArrayList());
|
||||
private final ObjectProperty<Background> contentBackground = new SimpleObjectProperty<>();
|
||||
|
||||
/// The launcher background and opacity rendered by this decorator's skin.
|
||||
private final ObjectProperty<LauncherBackground> contentBackground = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<DecoratorPage.State> state = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<EventHandler<ActionEvent>> onCloseNavButtonAction = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<EventHandler<ActionEvent>> onBackNavButtonAction = new SimpleObjectProperty<>();
|
||||
@@ -151,15 +154,18 @@ public class Decorator extends Control {
|
||||
this.container.set(container);
|
||||
}
|
||||
|
||||
public Background getContentBackground() {
|
||||
/// Returns the launcher background rendered behind the content.
|
||||
public LauncherBackground getContentBackground() {
|
||||
return contentBackground.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Background> contentBackgroundProperty() {
|
||||
/// Returns the launcher background property rendered by this decorator's skin.
|
||||
public ObjectProperty<LauncherBackground> contentBackgroundProperty() {
|
||||
return contentBackground;
|
||||
}
|
||||
|
||||
public void setContentBackground(Background contentBackground) {
|
||||
/// Sets the launcher background rendered behind the content.
|
||||
public void setContentBackground(LauncherBackground contentBackground) {
|
||||
this.contentBackground.set(contentBackground);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,27 +44,11 @@ public interface DecoratorPage extends Refreshable {
|
||||
((Node) this).getStyleClass().add("content-background");
|
||||
}
|
||||
|
||||
class State {
|
||||
private final String title;
|
||||
private final Node titleNode;
|
||||
private final boolean backable;
|
||||
private final boolean refreshable;
|
||||
private final boolean animate;
|
||||
private final double leftPaneWidth;
|
||||
|
||||
record State(String title, Node titleNode, boolean backable, boolean refreshable, boolean animate, double leftPaneWidth) {
|
||||
public State(String title, Node titleNode, boolean backable, boolean refreshable, boolean animate) {
|
||||
this(title, titleNode, backable, refreshable, animate, 0);
|
||||
}
|
||||
|
||||
public State(String title, Node titleNode, boolean backable, boolean refreshable, boolean animate, double leftPaneWidth) {
|
||||
this.title = title;
|
||||
this.titleNode = titleNode;
|
||||
this.backable = backable;
|
||||
this.refreshable = refreshable;
|
||||
this.animate = animate;
|
||||
this.leftPaneWidth = leftPaneWidth;
|
||||
}
|
||||
|
||||
public static State fromTitle(String title) {
|
||||
return new State(title, null, true, false, true);
|
||||
}
|
||||
@@ -76,29 +60,5 @@ public interface DecoratorPage extends Refreshable {
|
||||
public static State fromTitleNode(Node titleNode) {
|
||||
return new State(null, titleNode, true, false, true);
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public Node getTitleNode() {
|
||||
return titleNode;
|
||||
}
|
||||
|
||||
public boolean isBackable() {
|
||||
return backable;
|
||||
}
|
||||
|
||||
public boolean isRefreshable() {
|
||||
return refreshable;
|
||||
}
|
||||
|
||||
public boolean isAnimate() {
|
||||
return animate;
|
||||
}
|
||||
|
||||
public double getLeftPaneWidth() {
|
||||
return leftPaneWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import javafx.scene.shape.Rectangle;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import javafx.util.Duration;
|
||||
import org.glavo.monetfx.ColorRole;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -134,9 +135,33 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
root.getChildren().setAll(shadowContainer);
|
||||
|
||||
StackPane wrapper = new StackPane();
|
||||
wrapper.backgroundProperty().bind(Bindings.createObjectBinding(
|
||||
() -> Themes.windowTransparentProperty().get()
|
||||
? null
|
||||
: new Background(new BackgroundFill(
|
||||
Themes.getColorScheme().getColor(ColorRole.SURFACE_CONTAINER),
|
||||
CornerRadii.EMPTY,
|
||||
Insets.EMPTY)),
|
||||
Themes.windowTransparentProperty(),
|
||||
Themes.colorSchemeProperty()));
|
||||
|
||||
Region backgroundNode = new Region();
|
||||
backgroundNode.setMouseTransparent(true);
|
||||
backgroundNode.backgroundProperty().bind(Bindings.createObjectBinding(
|
||||
() -> skinnable.getContentBackground() == null
|
||||
? null
|
||||
: skinnable.getContentBackground().background(),
|
||||
skinnable.contentBackgroundProperty()));
|
||||
backgroundNode.opacityProperty().bind(Bindings.createDoubleBinding(
|
||||
() -> skinnable.getContentBackground() == null
|
||||
? 1.0
|
||||
: skinnable.getContentBackground().opacity(),
|
||||
skinnable.contentBackgroundProperty()));
|
||||
StackPane.setAlignment(backgroundNode, Pos.BOTTOM_CENTER);
|
||||
|
||||
BorderPane frame = new BorderPane();
|
||||
frame.getStyleClass().addAll("jfx-decorator");
|
||||
wrapper.getChildren().setAll(frame);
|
||||
wrapper.getChildren().setAll(backgroundNode, frame);
|
||||
skinnable.setDrawerWrapper(wrapper);
|
||||
|
||||
parent.getChildren().add(wrapper);
|
||||
@@ -178,20 +203,20 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
titleContainer = new StackPane();
|
||||
titleContainer.setPickOnBounds(false);
|
||||
titleContainer.getStyleClass().addAll("jfx-tool-bar");
|
||||
backgroundNode.maxHeightProperty().bind(Bindings.createDoubleBinding(
|
||||
() -> Math.max(0.0, wrapper.getHeight()
|
||||
- (skinnable.isTitleTransparent() ? 0.0 : titleContainer.getHeight())),
|
||||
wrapper.heightProperty(),
|
||||
skinnable.titleTransparentProperty(),
|
||||
titleContainer.heightProperty()));
|
||||
|
||||
// Maybe, we can automatically identify whether the top part of the picture is light-coloured or dark when the title is transparent,
|
||||
// and decide whether the whole top bar should be rendered in white or black. TODO
|
||||
FXUtils.onChangeAndOperate(skinnable.titleTransparentProperty(), titleTransparent -> {
|
||||
if (titleTransparent) {
|
||||
wrapper.backgroundProperty().bind(skinnable.contentBackgroundProperty());
|
||||
container.backgroundProperty().unbind();
|
||||
container.setBackground(null);
|
||||
titleContainer.getStyleClass().remove("background");
|
||||
titleContainer.getStyleClass().add("gray-background");
|
||||
} else {
|
||||
container.backgroundProperty().bind(skinnable.contentBackgroundProperty());
|
||||
wrapper.backgroundProperty().unbind();
|
||||
wrapper.setBackground(null);
|
||||
titleContainer.getStyleClass().add("background");
|
||||
titleContainer.getStyleClass().remove("gray-background");
|
||||
}
|
||||
@@ -208,8 +233,8 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
navBarPane.setId("decoratorTitleTransitionPane");
|
||||
FXUtils.onChangeAndOperate(skinnable.stateProperty(), s -> {
|
||||
if (s == null) return;
|
||||
Node node = createNavBar(skinnable, s.getLeftPaneWidth(), s.isBackable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.isRefreshable(), s.getTitle(), s.getTitleNode());
|
||||
if (s.isAnimate()) {
|
||||
Node node = createNavBar(skinnable, s.leftPaneWidth(), s.backable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.refreshable(), s.title(), s.titleNode());
|
||||
if (s.animate()) {
|
||||
TransitionPane.AnimationProducer animation = switch (skinnable.getNavigationDirection()) {
|
||||
case NEXT -> NavBarAnimations.NEXT;
|
||||
case PREVIOUS -> NavBarAnimations.PREVIOUS;
|
||||
|
||||
@@ -57,7 +57,7 @@ public abstract class DecoratorTransitionPage extends Control implements Decorat
|
||||
if (to instanceof DecoratorPage) {
|
||||
state.bind(Bindings.createObjectBinding(() -> {
|
||||
State state = ((DecoratorPage) to).stateProperty().get();
|
||||
return new State(state.getTitle(), state.getTitleNode(), backable.get(), state.isRefreshable(), true, leftPaneWidth.get());
|
||||
return new State(state.title(), state.titleNode(), backable.get(), state.refreshable(), true, leftPaneWidth.get());
|
||||
}, ((DecoratorPage) to).stateProperty()));
|
||||
} else {
|
||||
state.unbind();
|
||||
|
||||
@@ -318,9 +318,9 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
builder.version(remoteVersion);
|
||||
});
|
||||
|
||||
repository.applyDefaultIsolationSettingForNewInstance(name, settings.isInstallingModdedVersion());
|
||||
return builder.buildAsync().whenComplete(any -> {
|
||||
repository.refreshVersions();
|
||||
repository.applyDefaultIsolationSetting(name);
|
||||
}).thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name));
|
||||
}
|
||||
|
||||
|
||||
@@ -61,11 +61,12 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
|
||||
{
|
||||
var archiveNamePane = new LinePane();
|
||||
{
|
||||
FXUtils.setLimitHeight(archiveNamePane, 75);
|
||||
|
||||
archiveNamePane.setTitle(i18n("version.name"));
|
||||
|
||||
txtModpackName = new JFXTextField();
|
||||
txtModpackName.setPrefWidth(300);
|
||||
FXUtils.setLimitHeight(archiveNamePane, 75);
|
||||
// BorderPane.setMargin(txtModpackName, new Insets(0, 0, 8, 32));
|
||||
BorderPane.setAlignment(txtModpackName, Pos.CENTER_RIGHT);
|
||||
archiveNamePane.setRight(txtModpackName);
|
||||
|
||||
@@ -61,9 +61,9 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
|
||||
builder.version(remoteVersion);
|
||||
});
|
||||
|
||||
repository.applyDefaultIsolationSettingForNewInstance(name, settings.isInstallingModdedVersion());
|
||||
return builder.buildAsync().whenComplete(any -> {
|
||||
repository.refreshVersions();
|
||||
repository.applyDefaultIsolationSetting(name);
|
||||
})
|
||||
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name));
|
||||
}
|
||||
|
||||
@@ -213,6 +213,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
@Override
|
||||
public void updateItem(RemoteVersion remoteVersion, boolean empty) {
|
||||
RemoteVersion oldRemoteVersion = getItem();
|
||||
|
||||
super.updateItem(remoteVersion, empty);
|
||||
|
||||
if (empty) {
|
||||
@@ -221,6 +222,8 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
}
|
||||
setGraphic(pane);
|
||||
|
||||
if (oldRemoteVersion == remoteVersion) return;
|
||||
|
||||
twoLineListItem.setTitle(I18n.getDisplayVersion(remoteVersion));
|
||||
if (remoteVersion.getReleaseDate() != null) {
|
||||
twoLineListItem.setSubtitle(I18n.formatDateTime(remoteVersion.getReleaseDate()));
|
||||
@@ -228,67 +231,65 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
twoLineListItem.setSubtitle(null);
|
||||
}
|
||||
|
||||
if (oldRemoteVersion != remoteVersion) {
|
||||
twoLineListItem.getTags().clear();
|
||||
twoLineListItem.getTags().clear();
|
||||
|
||||
if (remoteVersion instanceof GameRemoteVersion) {
|
||||
RemoteVersion.Type versionType = remoteVersion.getVersionType();
|
||||
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(remoteVersion.getGameVersion());
|
||||
if (remoteVersion instanceof GameRemoteVersion) {
|
||||
RemoteVersion.Type versionType = remoteVersion.getVersionType();
|
||||
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(remoteVersion.getGameVersion());
|
||||
|
||||
switch (versionType) {
|
||||
case RELEASE -> {
|
||||
twoLineListItem.addTag(i18n("version.game.release"));
|
||||
imageView.setImage(VersionIconType.GRASS.getIcon());
|
||||
}
|
||||
case SNAPSHOT, PENDING, UNOBFUSCATED -> {
|
||||
if (versionType == RemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) {
|
||||
twoLineListItem.addTag(i18n("version.game.april_fools"));
|
||||
imageView.setImage(VersionIconType.APRIL_FOOLS.getIcon());
|
||||
} else {
|
||||
twoLineListItem.addTag(i18n("version.game.snapshot"));
|
||||
imageView.setImage(VersionIconType.COMMAND.getIcon());
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
twoLineListItem.addTag(i18n("version.game.old"));
|
||||
imageView.setImage(VersionIconType.CRAFT_TABLE.getIcon());
|
||||
switch (versionType) {
|
||||
case RELEASE -> {
|
||||
twoLineListItem.addTag(i18n("version.game.release"));
|
||||
imageView.setImage(VersionIconType.GRASS.getIcon());
|
||||
}
|
||||
case SNAPSHOT, PENDING, UNOBFUSCATED -> {
|
||||
if (versionType == RemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) {
|
||||
twoLineListItem.addTag(i18n("version.game.april_fools"));
|
||||
imageView.setImage(VersionIconType.APRIL_FOOLS.getIcon());
|
||||
} else {
|
||||
twoLineListItem.addTag(i18n("version.game.snapshot"));
|
||||
imageView.setImage(VersionIconType.COMMAND.getIcon());
|
||||
}
|
||||
}
|
||||
|
||||
switch (NativePatcher.checkSupportedStatus(gameVersion, Platform.SYSTEM_PLATFORM, OperatingSystem.SYSTEM_VERSION)) {
|
||||
case UNTESTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.untested"));
|
||||
case UNSUPPORTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.unsupported"));
|
||||
default -> {
|
||||
twoLineListItem.addTag(i18n("version.game.old"));
|
||||
imageView.setImage(VersionIconType.CRAFT_TABLE.getIcon());
|
||||
}
|
||||
} else {
|
||||
VersionIconType iconType;
|
||||
if (remoteVersion instanceof LiteLoaderRemoteVersion)
|
||||
iconType = VersionIconType.CHICKEN;
|
||||
else if (remoteVersion instanceof OptiFineRemoteVersion)
|
||||
iconType = VersionIconType.OPTIFINE;
|
||||
else if (remoteVersion instanceof ForgeRemoteVersion)
|
||||
iconType = VersionIconType.FORGE;
|
||||
else if (remoteVersion instanceof CleanroomRemoteVersion)
|
||||
iconType = VersionIconType.CLEANROOM;
|
||||
else if (remoteVersion instanceof NeoForgeRemoteVersion)
|
||||
iconType = VersionIconType.NEO_FORGE;
|
||||
else if (remoteVersion instanceof LegacyFabricRemoteVersion || remoteVersion instanceof LegacyFabricAPIRemoteVersion)
|
||||
iconType = VersionIconType.LEGACY_FABRIC;
|
||||
else if (remoteVersion instanceof FabricRemoteVersion || remoteVersion instanceof FabricAPIRemoteVersion)
|
||||
iconType = VersionIconType.FABRIC;
|
||||
else if (remoteVersion instanceof QuiltRemoteVersion || remoteVersion instanceof QuiltAPIRemoteVersion)
|
||||
iconType = VersionIconType.QUILT;
|
||||
else
|
||||
iconType = VersionIconType.COMMAND;
|
||||
|
||||
imageView.setImage(iconType.getIcon());
|
||||
String displayGameVersion = I18n.getDisplayVersion(GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()));
|
||||
|
||||
if (twoLineListItem.getSubtitle() == null)
|
||||
twoLineListItem.setSubtitle(displayGameVersion);
|
||||
else
|
||||
twoLineListItem.addTag(displayGameVersion);
|
||||
}
|
||||
|
||||
switch (NativePatcher.checkSupportedStatus(gameVersion, Platform.SYSTEM_PLATFORM, OperatingSystem.SYSTEM_VERSION)) {
|
||||
case UNTESTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.untested"));
|
||||
case UNSUPPORTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.unsupported"));
|
||||
}
|
||||
} else {
|
||||
VersionIconType iconType;
|
||||
if (remoteVersion instanceof LiteLoaderRemoteVersion)
|
||||
iconType = VersionIconType.CHICKEN;
|
||||
else if (remoteVersion instanceof OptiFineRemoteVersion)
|
||||
iconType = VersionIconType.OPTIFINE;
|
||||
else if (remoteVersion instanceof ForgeRemoteVersion)
|
||||
iconType = VersionIconType.FORGE;
|
||||
else if (remoteVersion instanceof CleanroomRemoteVersion)
|
||||
iconType = VersionIconType.CLEANROOM;
|
||||
else if (remoteVersion instanceof NeoForgeRemoteVersion)
|
||||
iconType = VersionIconType.NEO_FORGE;
|
||||
else if (remoteVersion instanceof LegacyFabricRemoteVersion || remoteVersion instanceof LegacyFabricAPIRemoteVersion)
|
||||
iconType = VersionIconType.LEGACY_FABRIC;
|
||||
else if (remoteVersion instanceof FabricRemoteVersion || remoteVersion instanceof FabricAPIRemoteVersion)
|
||||
iconType = VersionIconType.FABRIC;
|
||||
else if (remoteVersion instanceof QuiltRemoteVersion || remoteVersion instanceof QuiltAPIRemoteVersion)
|
||||
iconType = VersionIconType.QUILT;
|
||||
else
|
||||
iconType = VersionIconType.COMMAND;
|
||||
|
||||
imageView.setImage(iconType.getIcon());
|
||||
String displayGameVersion = I18n.getDisplayVersion(GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()));
|
||||
|
||||
if (twoLineListItem.getSubtitle() == null)
|
||||
twoLineListItem.setSubtitle(displayGameVersion);
|
||||
else
|
||||
twoLineListItem.addTag(displayGameVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -390,7 +391,12 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
list = new JFXListView<>();
|
||||
list.getStyleClass().add("jfx-list-view-float");
|
||||
|
||||
list.setFixedCellSize(65);
|
||||
RemoteVersionListCell dummyCell = new RemoteVersionListCell(control);
|
||||
dummyCell.twoLineListItem.setTitle("Dummy");
|
||||
dummyCell.twoLineListItem.setSubtitle("Dummy");
|
||||
dummyCell.imageView.setImage(VersionIconType.GRASS.getIcon());
|
||||
prepareNode(dummyCell.pane);
|
||||
list.setFixedCellSize(dummyCell.pane.prefHeight(-1));
|
||||
|
||||
VBox.setVgrow(list, Priority.ALWAYS);
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
"OneSix",
|
||||
exportInfo.getName() + "-" + exportInfo.getVersion(),
|
||||
null,
|
||||
Lang.toIntOrNull(setting.get(GameSettings::permSizeProperty)),
|
||||
Lang.toIntOrNull(setting.getInheritable(GameSettings::permSizeProperty)),
|
||||
setting.getInheritable(GameSettings::commandWrapperProperty),
|
||||
setting.getInheritable(GameSettings::preLaunchCommandProperty),
|
||||
null,
|
||||
|
||||
@@ -101,9 +101,9 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
author.set(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getProfileName).orElse(""));
|
||||
|
||||
GameSettings.Effective versionSetting = gameRepository.getEffectiveGameSettings(versionName);
|
||||
minMemory.set(Optional.ofNullable(versionSetting.get(GameSettings::minMemoryProperty)).orElse(0));
|
||||
launchArguments.set(versionSetting.get(GameSettings::gameArgumentsProperty));
|
||||
javaArguments.set(versionSetting.get(GameSettings::jvmOptionsProperty));
|
||||
minMemory.set(Optional.ofNullable(versionSetting.getInheritable(GameSettings::minMemoryProperty)).orElse(0));
|
||||
launchArguments.set(versionSetting.getInheritable(GameSettings::gameArgumentsProperty));
|
||||
javaArguments.set(versionSetting.getInheritable(GameSettings::jvmOptionsProperty));
|
||||
|
||||
canIncludeLauncher = JarUtils.thisJarPath() != null;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXSlider;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
@@ -32,10 +35,7 @@ import javafx.geometry.Pos;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.FileChooser;
|
||||
@@ -70,7 +70,6 @@ import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -101,9 +100,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
/// The current setting.
|
||||
private final ObjectProperty<@Nullable S> currentSetting = new SimpleObjectProperty<>(this, "setting");
|
||||
|
||||
private final ObjectProperty<GameVersionNumber> currentGameVersionNumber = new SimpleObjectProperty<>(this, "currentGameVersionNumber", GameVersionNumber.unknown());
|
||||
|
||||
private boolean updatingJavaSetting = false;
|
||||
private boolean updatingSelectedJava = false;
|
||||
private boolean updatingParentSetting = false;
|
||||
private final ObjectProperty<GameSettings.@Nullable Preset> activeParentSetting =
|
||||
new SimpleObjectProperty<>(this, "activeParentSetting");
|
||||
|
||||
// GUI
|
||||
private final ScrollPane scrollPane;
|
||||
@@ -130,6 +133,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
assert settingType == GameSettings.Preset.class || settingType == GameSettings.Instance.class;
|
||||
|
||||
this.isPresetSetting = settingType == GameSettings.Preset.class;
|
||||
if (!isPresetSetting) {
|
||||
bindActiveParentSetting();
|
||||
}
|
||||
|
||||
this.scrollPane = new ScrollPane();
|
||||
scrollPane.setFitToHeight(true);
|
||||
@@ -155,11 +161,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
{
|
||||
if (isPresetSetting) {
|
||||
var presetSettings = new ComponentList();
|
||||
rootPane.getChildren().addAll(
|
||||
ComponentList.createComponentListTitle(i18n("settings.type.global.preset")),
|
||||
presetSettings,
|
||||
ComponentList.createComponentListTitle(i18n("settings.game.section.basic")),
|
||||
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.game.section.basic")),
|
||||
basicSettings,
|
||||
ComponentList.createComponentListTitle(
|
||||
i18n("settings.type.global.preset"),
|
||||
i18n("settings.type.global.preset.help")
|
||||
),
|
||||
presetSettings,
|
||||
ComponentList.createComponentListTitle(i18n("settings.game.section.game")),
|
||||
gameSettings,
|
||||
ComponentList.createComponentListTitle(i18n("settings.launcher")),
|
||||
@@ -188,9 +196,10 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
var parentGameSettingsPane = new LineSelectButton<GameSettings.@Nullable Preset>();
|
||||
basicSettings.getContent().add(parentGameSettingsPane);
|
||||
parentGameSettingsPane.setTitle(i18n("settings.type.global.preset"));
|
||||
parentGameSettingsPane.setSubtitle(i18n("settings.type.global.preset.subtitle"));
|
||||
parentGameSettingsPane.setConverter(setting -> setting != null
|
||||
? PresetManagementPane.getPresetDisplayName(setting)
|
||||
: getImplicitParentGameSettingsDisplayName());
|
||||
: i18n("settings.type.global.preset.default"));
|
||||
bindInstanceParentSetting(parentGameSettingsPane);
|
||||
}
|
||||
|
||||
@@ -271,9 +280,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
|
||||
refreshJavaSettings();
|
||||
});
|
||||
SettingsManager.getGameSettings().addListener(weakJavaListener);
|
||||
settings().defaultGameSettingsPresetProperty().addListener(weakJavaListener);
|
||||
|
||||
activeParentSetting.addListener(weakJavaListener);
|
||||
javaItem.selectedChoiceProperty().addListener((observable, oldChoice, newChoice) -> {
|
||||
S setting = currentSetting.get();
|
||||
if (setting == null || updatingSelectedJava) {
|
||||
@@ -339,15 +346,15 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
defaultIsolationTypePane.setSubtitle(i18n("settings.game.default_isolation.subtitle"));
|
||||
defaultIsolationTypePane.setItems(DefaultIsolationType.values());
|
||||
defaultIsolationTypePane.setNullSafeConverter(type -> switch (type) {
|
||||
case NEVER -> i18n("settings.game.default_isolation.never");
|
||||
case ALWAYS -> i18n("settings.game.default_isolation.always");
|
||||
case MODDED -> i18n("settings.game.default_isolation.modded");
|
||||
});
|
||||
case NEVER -> i18n("settings.game.default_isolation.never");
|
||||
case ALWAYS -> i18n("settings.game.default_isolation.always");
|
||||
case MODDED -> i18n("settings.game.default_isolation.modded");
|
||||
});
|
||||
defaultIsolationTypePane.setDescriptionConverter(type -> switch (type) {
|
||||
case NEVER -> i18n("settings.game.default_isolation.never.desc");
|
||||
case ALWAYS -> i18n("settings.game.default_isolation.always.desc");
|
||||
case MODDED -> i18n("settings.game.default_isolation.modded.desc");
|
||||
});
|
||||
case NEVER -> i18n("settings.game.default_isolation.never.desc");
|
||||
case ALWAYS -> i18n("settings.game.default_isolation.always.desc");
|
||||
case MODDED -> i18n("settings.game.default_isolation.modded.desc");
|
||||
});
|
||||
bindPresetBidirectional(defaultIsolationTypePane.valueProperty(), GameSettings.Preset::defaultIsolationTypeProperty);
|
||||
} else {
|
||||
var isolationButton = new LineToggleButton();
|
||||
@@ -359,6 +366,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
|
||||
// Memory Setting
|
||||
@Nullable JFXButton autoMemoryButton = !isPresetSetting ? createInheritanceButton() : null;
|
||||
ObjectProperty<Boolean> memorySelectedValue = new SimpleObjectProperty<>(true);
|
||||
var memorySublist = new ComponentSublist(() -> {
|
||||
var memoryItem = new RadioChoiceList<Boolean>();
|
||||
memoryItem.setFallbackValue(true);
|
||||
@@ -378,16 +386,18 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
maxMemoryButton = createInheritanceButton();
|
||||
}
|
||||
|
||||
var options = new ArrayList<RadioChoiceList.Choice<Boolean>>();
|
||||
options.add(new RadioChoiceList.Choice<>(i18n("settings.memory.auto_allocate"), true));
|
||||
var manualMemoryEditor = new HBox(8);
|
||||
manualMemoryEditor.setAlignment(Pos.CENTER_RIGHT);
|
||||
manualMemoryEditor.getChildren().setAll(
|
||||
maxMemorySlider,
|
||||
maxMemoryTextField,
|
||||
new Label(i18n("settings.memory.unit.mib")));
|
||||
options.add(new EditorChoice<>(i18n("settings.memory.manual_allocate"), false, manualMemoryEditor, maxMemoryButton));
|
||||
memoryItem.setChoices(options);
|
||||
memoryItem.setChoices(List.of(
|
||||
new RadioChoiceList.Choice<>(i18n("settings.memory.auto_allocate"), true),
|
||||
new EditorChoice<>(i18n("settings.memory.manual_allocate"), false, manualMemoryEditor, maxMemoryButton)
|
||||
));
|
||||
memoryItem.selectedValueProperty().addListener((observable, oldValue, newValue) ->
|
||||
memorySelectedValue.set(newValue));
|
||||
|
||||
var memoryStatusBar = new MemoryStatusBar();
|
||||
|
||||
@@ -415,7 +425,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
maxMemoryButton,
|
||||
GameSettingsPage::updateInheritanceButton,
|
||||
holder,
|
||||
this::getEffectiveParentGameSettings);
|
||||
this::getEffectiveParentGameSettings,
|
||||
activeParentSetting);
|
||||
|
||||
return List.of(memoryItem, memoryStatusPane);
|
||||
});
|
||||
@@ -425,7 +436,10 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
gameSettings.getContent().add(memorySublist);
|
||||
memorySublist.setTitle(i18n("settings.memory"));
|
||||
memorySublist.setHasSubtitle(true);
|
||||
memorySublist.setDescription(i18n("settings.memory.auto_allocate"));
|
||||
memorySelectedValue.addListener((observable, oldValue, newValue) ->
|
||||
memorySublist.setDescription(getMemoryAllocationDisplayName(newValue)));
|
||||
memorySublist.setDescription(getMemoryAllocationDisplayName(memorySelectedValue.get()));
|
||||
bindMemorySelectedValue(memorySelectedValue);
|
||||
|
||||
// Launcher Visibility Setting
|
||||
var launcherVisibilityPane = createInheritableButton(
|
||||
@@ -653,6 +667,10 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
{
|
||||
var txtMinMemory = new JFXTextField();
|
||||
txtMinMemory.setPrefWidth(160);
|
||||
txtMinMemory.setTextFormatter(new TextFormatter<>(change -> {
|
||||
change.setText(change.getText().replaceAll("[^0-9]", ""));
|
||||
return change;
|
||||
}));
|
||||
|
||||
var rightBox = new HBox(8, txtMinMemory, new Label(i18n("settings.memory.unit.mib")));
|
||||
rightBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
@@ -666,6 +684,10 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
{
|
||||
var txtMetaspace = new JFXTextField();
|
||||
txtMetaspace.setPrefWidth(160);
|
||||
txtMetaspace.setTextFormatter(new TextFormatter<>(change -> {
|
||||
change.setText(change.getText().replaceAll("[^0-9]", ""));
|
||||
return change;
|
||||
}));
|
||||
|
||||
var rightBox = new HBox(8, txtMetaspace, new Label(i18n("settings.memory.unit.mib")));
|
||||
rightBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
@@ -740,6 +762,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
graphicsSettings.getContent().add(graphicsBackendPane);
|
||||
graphicsBackendPane.setTitle(i18n("settings.advanced.graphics_backend"));
|
||||
|
||||
if (isPresetSetting)
|
||||
graphicsBackendPane.setSubtitle(i18n("settings.advanced.graphics_backend.desc"));
|
||||
|
||||
var openGLRendererPane = createInheritableButton(
|
||||
GameSettings::openGLRendererProperty,
|
||||
e -> i18n("settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT)),
|
||||
@@ -762,6 +787,19 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
graphicsSettings.getContent().add(vulkanRendererPane);
|
||||
vulkanRendererPane.setTitle(i18n("settings.advanced.renderer.vulkan"));
|
||||
|
||||
this.currentGameVersionNumber.addListener((o, oldValue, newValue) -> {
|
||||
boolean showBackendChoose = isPresetSetting || newValue.compareTo("26.2-snapshot-2") >= 0;
|
||||
graphicsBackendPane.setVisible(showBackendChoose);
|
||||
graphicsBackendPane.setManaged(showBackendChoose);
|
||||
|
||||
boolean showOpenGL = GraphicsAPI.OPENGL.isSupported(newValue);
|
||||
openGLRendererPane.setVisible(showOpenGL);
|
||||
openGLRendererPane.setManaged(showOpenGL);
|
||||
|
||||
boolean showVulkan = GraphicsAPI.VULKAN.isSupported(newValue);
|
||||
vulkanRendererPane.setVisible(showVulkan);
|
||||
vulkanRendererPane.setManaged(showVulkan);
|
||||
});
|
||||
}
|
||||
|
||||
var nativeLibrarySettings = new ComponentList();
|
||||
@@ -810,18 +848,19 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
|
||||
private void bindInstanceParentSetting(LineSelectButton<GameSettings.@Nullable Preset> button) {
|
||||
ObservableList<GameSettings.Preset> items = FXCollections.observableArrayList();
|
||||
ObservableList<GameSettings.Preset> presets = getAvailableParentPresets();
|
||||
InvalidationListener updateItems = observable -> {
|
||||
@Nullable GameSettings.Preset selected = button.getValue();
|
||||
items.setAll((GameSettings.Preset) null);
|
||||
items.addAll(SettingsManager.getGameSettings());
|
||||
items.addAll(presets);
|
||||
refreshInstanceParentSettingConverter(button);
|
||||
if (selected != null && SettingsManager.getGameSettings(selected.idProperty().getValue()) == null) {
|
||||
if (selected != null && getAvailableParentPreset(selected.idProperty().getValue()) == null) {
|
||||
button.setValue(null);
|
||||
}
|
||||
};
|
||||
InvalidationListener weakUpdateItems = holder.weak(updateItems);
|
||||
updateItems.invalidated(SettingsManager.getGameSettings());
|
||||
SettingsManager.getGameSettings().addListener(weakUpdateItems);
|
||||
updateItems.invalidated(presets);
|
||||
presets.addListener(weakUpdateItems);
|
||||
button.setItems(items);
|
||||
refreshInstanceParentSettingConverter(button);
|
||||
|
||||
@@ -838,7 +877,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
try {
|
||||
refreshInstanceParentSettingConverter(button);
|
||||
GameSettingsPresetID parent = setting.parentProperty().getValue();
|
||||
button.setValue(parent != null ? SettingsManager.getGameSettings(parent) : null);
|
||||
button.setValue(parent != null ? getAvailableParentPreset(parent) : null);
|
||||
} finally {
|
||||
updatingParentSetting = false;
|
||||
}
|
||||
@@ -846,28 +885,54 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
});
|
||||
}
|
||||
|
||||
/// Refreshes parent preset display text because the implicit parent depends on the current game directory.
|
||||
/// Refreshes parent preset display text after the preset list changes.
|
||||
private void refreshInstanceParentSettingConverter(LineSelectButton<GameSettings.@Nullable Preset> button) {
|
||||
button.setConverter(setting -> setting != null
|
||||
? PresetManagementPane.getPresetDisplayName(setting)
|
||||
: getImplicitParentGameSettingsDisplayName());
|
||||
: i18n("settings.type.global.preset.default"));
|
||||
}
|
||||
|
||||
/// Returns the label for the implicit parent preset selected by a null instance parent.
|
||||
private String getImplicitParentGameSettingsDisplayName() {
|
||||
GameSettings.Preset legacyParent = getGameDirectoryLegacyGameSettings();
|
||||
return legacyParent != null
|
||||
? PresetManagementPane.getPresetDisplayName(legacyParent)
|
||||
: i18n("settings.type.global.preset.default");
|
||||
/// Returns the presets that can be selected as an instance parent.
|
||||
private ObservableList<GameSettings.Preset> getAvailableParentPresets() {
|
||||
return SettingsManager.gameSettingsPresets().getPresets();
|
||||
}
|
||||
|
||||
/// Returns the migrated game-directory-level parent preset, or `null` when this game directory uses the default preset.
|
||||
private @Nullable GameSettings.Preset getGameDirectoryLegacyGameSettings() {
|
||||
if (gameDirectory == null || gameDirectory.getLegacyGameSettings() == null) {
|
||||
return null;
|
||||
/// Returns the preset selected by an instance parent ID.
|
||||
private GameSettings.@Nullable Preset getAvailableParentPreset(@Nullable GameSettingsPresetID id) {
|
||||
return SettingsManager.gameSettingsPresets().getPreset(id);
|
||||
}
|
||||
|
||||
/// Keeps [#activeParentSetting] synchronized with the current instance's resolved parent preset.
|
||||
private void bindActiveParentSetting() {
|
||||
InvalidationListener refresh = observable -> refreshActiveParentSetting();
|
||||
InvalidationListener weakRefresh = holder.weak(refresh);
|
||||
|
||||
getAvailableParentPresets().addListener(weakRefresh);
|
||||
SettingsManager.settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
|
||||
currentSetting.addListener((observable, oldValue, newValue) -> {
|
||||
if (oldValue != null) {
|
||||
oldValue.removeListener(weakRefresh);
|
||||
}
|
||||
|
||||
if (newValue != null) {
|
||||
newValue.addListener(weakRefresh);
|
||||
}
|
||||
|
||||
refreshActiveParentSetting();
|
||||
});
|
||||
|
||||
S setting = currentSetting.get();
|
||||
if (setting != null) {
|
||||
setting.addListener(weakRefresh);
|
||||
}
|
||||
refreshActiveParentSetting();
|
||||
}
|
||||
|
||||
return SettingsManager.getGameSettings(gameDirectory.getLegacyGameSettings());
|
||||
/// Refreshes the current instance's resolved parent preset.
|
||||
private void refreshActiveParentSetting() {
|
||||
activeParentSetting.set(currentSetting.get() instanceof GameSettings.Instance instance
|
||||
? getEffectiveParentGameSettings(instance)
|
||||
: null);
|
||||
}
|
||||
|
||||
/// Adds the title-line inheritance button for the Java selection sublist.
|
||||
@@ -1023,7 +1088,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
this::createInheritanceButton,
|
||||
GameSettingsPage::updateInheritanceButton,
|
||||
holder,
|
||||
this::getEffectiveParentGameSettings);
|
||||
this::getEffectiveParentGameSettings,
|
||||
activeParentSetting);
|
||||
}
|
||||
|
||||
/// Binds an integer text field to a setting property with independent override state.
|
||||
@@ -1040,7 +1106,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
this::createInheritanceButton,
|
||||
GameSettingsPage::updateInheritanceButton,
|
||||
holder,
|
||||
this::getEffectiveParentGameSettings);
|
||||
this::getEffectiveParentGameSettings,
|
||||
activeParentSetting);
|
||||
}
|
||||
|
||||
/// Binds the windowed resolution selector to the width and height settings.
|
||||
@@ -1083,6 +1150,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
};
|
||||
InvalidationListener weakPropertyListener = holder.weak(propertyListener);
|
||||
refreshHolder.value = weakPropertyListener;
|
||||
activeParentSetting.addListener(weakPropertyListener);
|
||||
|
||||
ChangeListener<@Nullable Boolean> focusedListener = (observable, oldValue, newValue) -> {
|
||||
if (!newValue) {
|
||||
@@ -1168,8 +1236,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
widthProperty.addListener(weakPropertyListener);
|
||||
heightProperty.addListener(weakPropertyListener);
|
||||
}
|
||||
SettingsManager.getGameSettings().addListener(weakPropertyListener);
|
||||
settings().defaultGameSettingsPresetProperty().addListener(weakPropertyListener);
|
||||
propertyListener.invalidated(setting);
|
||||
}
|
||||
|
||||
@@ -1406,7 +1472,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
|
||||
InvalidationListener refresh = observable -> {
|
||||
GameSettings setting = currentSetting.get();
|
||||
updateParentInheritablePropertyListener(setting, activeParentProperty, propertyGetter, refreshHolder.value);
|
||||
updateParentInheritablePropertyListener(
|
||||
setting,
|
||||
activeParentProperty,
|
||||
propertyGetter,
|
||||
refreshHolder.value);
|
||||
InheritableProperty<String> property = activeProperty.get();
|
||||
if (setting == null || property == null || updating.value) {
|
||||
return;
|
||||
@@ -1424,6 +1494,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
};
|
||||
InvalidationListener weakRefresh = holder.weak(refresh);
|
||||
refreshHolder.value = weakRefresh;
|
||||
activeParentSetting.addListener(weakRefresh);
|
||||
|
||||
textProperty.addListener((observable, oldValue, newValue) -> {
|
||||
GameSettings setting = currentSetting.get();
|
||||
@@ -1489,9 +1560,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
refresh.invalidated(newValue);
|
||||
});
|
||||
|
||||
SettingsManager.getGameSettings().addListener(weakRefresh);
|
||||
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
|
||||
|
||||
S setting = currentSetting.get();
|
||||
if (setting != null) {
|
||||
InheritableProperty<String> property = propertyGetter.apply(setting);
|
||||
@@ -1514,7 +1582,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
|
||||
InvalidationListener refresh = observable -> {
|
||||
GameSettings setting = currentSetting.get();
|
||||
updateParentInheritablePropertyListener(setting, activeParentProperty, propertyGetter, refreshHolder.value);
|
||||
updateParentInheritablePropertyListener(
|
||||
setting,
|
||||
activeParentProperty,
|
||||
propertyGetter,
|
||||
refreshHolder.value);
|
||||
InheritableProperty<String> property = activeProperty.get();
|
||||
if (setting == null || property == null || updating.value) {
|
||||
return;
|
||||
@@ -1532,6 +1604,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
};
|
||||
InvalidationListener weakRefresh = holder.weak(refresh);
|
||||
refreshHolder.value = weakRefresh;
|
||||
activeParentSetting.addListener(weakRefresh);
|
||||
|
||||
textProperty.addListener((observable, oldValue, newValue) -> {
|
||||
GameSettings setting = currentSetting.get();
|
||||
@@ -1597,9 +1670,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
refresh.invalidated(newValue);
|
||||
});
|
||||
|
||||
SettingsManager.getGameSettings().addListener(weakRefresh);
|
||||
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
|
||||
|
||||
S setting = currentSetting.get();
|
||||
if (setting != null) {
|
||||
InheritableProperty<String> property = propertyGetter.apply(setting);
|
||||
@@ -1650,6 +1720,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
updating.value = false;
|
||||
}
|
||||
refresh.invalidated(instance);
|
||||
|
||||
fireEvent(new VersionPage.WorkingDirChangedEvent());
|
||||
});
|
||||
|
||||
currentSetting.addListener((observable, oldValue, newValue) -> {
|
||||
@@ -1718,6 +1790,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
};
|
||||
InvalidationListener weakRefresh = holder.weak(refresh);
|
||||
refreshHolder.value = weakRefresh;
|
||||
activeParentSetting.addListener(weakRefresh);
|
||||
|
||||
inheritButton.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
GameSettings setting = currentSetting.get();
|
||||
@@ -1737,6 +1810,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
updating.value = false;
|
||||
}
|
||||
refresh.invalidated(instance);
|
||||
|
||||
fireEvent(new VersionPage.WorkingDirChangedEvent());
|
||||
|
||||
event.consume();
|
||||
});
|
||||
|
||||
@@ -1756,6 +1832,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
} finally {
|
||||
updating.value = false;
|
||||
}
|
||||
|
||||
fireEvent(new VersionPage.WorkingDirChangedEvent());
|
||||
});
|
||||
|
||||
currentSetting.addListener((observable, oldValue, newValue) -> {
|
||||
@@ -1769,9 +1847,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
refresh.invalidated(newValue);
|
||||
});
|
||||
|
||||
SettingsManager.getGameSettings().addListener(weakRefresh);
|
||||
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
|
||||
|
||||
S setting = currentSetting.get();
|
||||
if (setting instanceof GameSettings.Instance instance) {
|
||||
instance.addListener(weakRefresh);
|
||||
@@ -1815,13 +1890,96 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
}
|
||||
}
|
||||
|
||||
/// Keeps a listener attached to the current instance's parent preset setting property.
|
||||
private <T extends @UnknownNullability Object> void updateParentSettingPropertyListener(
|
||||
@Nullable GameSettings setting,
|
||||
ObjectProperty<@Nullable SettingProperty<T>> activeParentProperty,
|
||||
Function<GameSettings, ? extends SettingProperty<T>> propertyGetter,
|
||||
InvalidationListener listener) {
|
||||
SettingProperty<T> oldParentProperty = activeParentProperty.get();
|
||||
SettingProperty<T> newParentProperty = setting instanceof GameSettings.Instance instance
|
||||
? propertyGetter.apply(getEffectiveParentGameSettings(instance))
|
||||
: null;
|
||||
if (oldParentProperty == newParentProperty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldParentProperty != null) {
|
||||
oldParentProperty.removeListener(listener);
|
||||
}
|
||||
activeParentProperty.set(newParentProperty);
|
||||
if (newParentProperty != null) {
|
||||
newParentProperty.addListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/// Binds the memory sublist description source to the effective automatic memory setting.
|
||||
private void bindMemorySelectedValue(ObjectProperty<Boolean> memorySelectedValue) {
|
||||
ObjectProperty<@Nullable SettingProperty<Boolean>> activeAutoMemoryProperty = new SimpleObjectProperty<>();
|
||||
ObjectProperty<@Nullable SettingProperty<Boolean>> activeParentAutoMemoryProperty = new SimpleObjectProperty<>();
|
||||
final Holder<InvalidationListener> refreshHolder = new Holder<>();
|
||||
|
||||
InvalidationListener refresh = observable -> {
|
||||
S setting = currentSetting.get();
|
||||
updateParentSettingPropertyListener(
|
||||
setting,
|
||||
activeParentAutoMemoryProperty,
|
||||
GameSettings::autoMemoryProperty,
|
||||
refreshHolder.value);
|
||||
if (setting == null) {
|
||||
memorySelectedValue.set(true);
|
||||
return;
|
||||
}
|
||||
|
||||
memorySelectedValue.set(resolveEffectiveSetting(setting)
|
||||
.getInheritable(GameSettings::autoMemoryProperty));
|
||||
};
|
||||
InvalidationListener weakRefresh = holder.weak(refresh);
|
||||
refreshHolder.value = weakRefresh;
|
||||
activeParentSetting.addListener(weakRefresh);
|
||||
|
||||
currentSetting.addListener((observable, oldValue, newValue) -> {
|
||||
if (oldValue != null) {
|
||||
oldValue.removeListener(weakRefresh);
|
||||
}
|
||||
|
||||
SettingProperty<Boolean> oldAutoMemoryProperty = activeAutoMemoryProperty.get();
|
||||
if (oldAutoMemoryProperty != null) {
|
||||
oldAutoMemoryProperty.removeListener(weakRefresh);
|
||||
}
|
||||
|
||||
SettingProperty<Boolean> newAutoMemoryProperty = newValue != null
|
||||
? newValue.autoMemoryProperty()
|
||||
: null;
|
||||
activeAutoMemoryProperty.set(newAutoMemoryProperty);
|
||||
if (newValue != null) {
|
||||
newValue.addListener(weakRefresh);
|
||||
}
|
||||
if (newAutoMemoryProperty != null) {
|
||||
newAutoMemoryProperty.addListener(weakRefresh);
|
||||
}
|
||||
refresh.invalidated(newValue);
|
||||
});
|
||||
|
||||
S setting = currentSetting.get();
|
||||
if (setting != null) {
|
||||
SettingProperty<Boolean> autoMemoryProperty = setting.autoMemoryProperty();
|
||||
activeAutoMemoryProperty.set(autoMemoryProperty);
|
||||
setting.addListener(weakRefresh);
|
||||
autoMemoryProperty.addListener(weakRefresh);
|
||||
}
|
||||
refresh.invalidated(setting);
|
||||
}
|
||||
|
||||
/// Binds a radio choice list to an inheritable setting property.
|
||||
private <T> void bindInheritableRadioChoiceList(
|
||||
ComponentSublist sublist,
|
||||
RadioChoiceList<T> item,
|
||||
Function<GameSettings, InheritableProperty<T>> propertyGetter) {
|
||||
ObjectProperty<@Nullable InheritableProperty<T>> activeProperty = new SimpleObjectProperty<>();
|
||||
ObjectProperty<@Nullable InheritableProperty<T>> activeParentProperty = new SimpleObjectProperty<>();
|
||||
final Holder<Boolean> updating = new Holder<>(false);
|
||||
final Holder<InvalidationListener> refreshHolder = new Holder<>();
|
||||
@Nullable JFXButton inheritButton = null;
|
||||
if (!isPresetSetting) {
|
||||
inheritButton = createInheritanceButton();
|
||||
@@ -1831,6 +1989,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
|
||||
InvalidationListener propertyListener = observable -> {
|
||||
GameSettings setting = currentSetting.get();
|
||||
updateParentInheritablePropertyListener(
|
||||
setting,
|
||||
activeParentProperty,
|
||||
propertyGetter,
|
||||
refreshHolder.value);
|
||||
InheritableProperty<T> property = activeProperty.get();
|
||||
if (setting == null || property == null || updating.value) {
|
||||
return;
|
||||
@@ -1847,6 +2010,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
}
|
||||
};
|
||||
InvalidationListener weakPropertyListener = holder.weak(propertyListener);
|
||||
refreshHolder.value = weakPropertyListener;
|
||||
activeParentSetting.addListener(weakPropertyListener);
|
||||
|
||||
ChangeListener<@Nullable T> itemListener = (observable, oldValue, newValue) -> {
|
||||
GameSettings setting = currentSetting.get();
|
||||
@@ -1911,9 +2076,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
}
|
||||
propertyListener.invalidated(newProperty);
|
||||
});
|
||||
SettingsManager.getGameSettings().addListener(weakPropertyListener);
|
||||
settings().defaultGameSettingsPresetProperty().addListener(weakPropertyListener);
|
||||
|
||||
S setting = currentSetting.get();
|
||||
if (setting != null) {
|
||||
InheritableProperty<T> property = propertyGetter.apply(setting);
|
||||
@@ -1927,7 +2089,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
/// Binds the window type editor to the window type setting.
|
||||
private void bindWindowSettings(ComponentSublist sublist, RadioChoiceList<GameWindowType> item) {
|
||||
ObjectProperty<@Nullable InheritableProperty<GameWindowType>> activeWindowTypeProperty = new SimpleObjectProperty<>();
|
||||
ObjectProperty<@Nullable InheritableProperty<GameWindowType>> activeParentWindowTypeProperty = new SimpleObjectProperty<>();
|
||||
final Holder<Boolean> updating = new Holder<>(false);
|
||||
final Holder<InvalidationListener> refreshHolder = new Holder<>();
|
||||
@Nullable JFXButton inheritButton = null;
|
||||
if (!isPresetSetting) {
|
||||
inheritButton = createInheritanceButton();
|
||||
@@ -1937,6 +2101,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
|
||||
InvalidationListener propertyListener = observable -> {
|
||||
GameSettings setting = currentSetting.get();
|
||||
updateParentInheritablePropertyListener(
|
||||
setting,
|
||||
activeParentWindowTypeProperty,
|
||||
GameSettings::windowTypeProperty,
|
||||
refreshHolder.value);
|
||||
InheritableProperty<GameWindowType> property = activeWindowTypeProperty.get();
|
||||
if (setting == null || property == null || updating.value) {
|
||||
return;
|
||||
@@ -1953,6 +2122,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
}
|
||||
};
|
||||
InvalidationListener weakPropertyListener = holder.weak(propertyListener);
|
||||
refreshHolder.value = weakPropertyListener;
|
||||
activeParentSetting.addListener(weakPropertyListener);
|
||||
|
||||
item.selectedValueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
GameSettings setting = currentSetting.get();
|
||||
@@ -2012,9 +2183,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
propertyListener.invalidated(newValue);
|
||||
});
|
||||
|
||||
SettingsManager.getGameSettings().addListener(weakPropertyListener);
|
||||
settings().defaultGameSettingsPresetProperty().addListener(weakPropertyListener);
|
||||
|
||||
S setting = currentSetting.get();
|
||||
if (setting != null) {
|
||||
activeWindowTypeProperty.set(setting.windowTypeProperty());
|
||||
@@ -2047,8 +2215,19 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
private <T> void bindInheritableSublistDescription(ComponentSublist sublist,
|
||||
Function<GameSettings, InheritableProperty<T>> propertyGetter,
|
||||
Function<T, String> converter) {
|
||||
InvalidationListener propertyListener = observable -> initInheritableSublistDescription(sublist, propertyGetter, converter);
|
||||
ObjectProperty<@Nullable InheritableProperty<T>> activeParentProperty = new SimpleObjectProperty<>();
|
||||
final Holder<InvalidationListener> refreshHolder = new Holder<>();
|
||||
InvalidationListener propertyListener = observable -> {
|
||||
updateParentInheritablePropertyListener(
|
||||
currentSetting.get(),
|
||||
activeParentProperty,
|
||||
propertyGetter,
|
||||
refreshHolder.value);
|
||||
initInheritableSublistDescription(sublist, propertyGetter, converter);
|
||||
};
|
||||
InvalidationListener weakPropertyListener = holder.weak(propertyListener);
|
||||
refreshHolder.value = weakPropertyListener;
|
||||
activeParentSetting.addListener(weakPropertyListener);
|
||||
|
||||
currentSetting.addListener((observable, oldValue, newValue) -> {
|
||||
if (oldValue != null) {
|
||||
@@ -2059,16 +2238,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
propertyGetter.apply(newValue).addListener(weakPropertyListener);
|
||||
}
|
||||
|
||||
initInheritableSublistDescription(sublist, propertyGetter, converter);
|
||||
propertyListener.invalidated(newValue);
|
||||
});
|
||||
SettingsManager.getGameSettings().addListener(weakPropertyListener);
|
||||
settings().defaultGameSettingsPresetProperty().addListener(weakPropertyListener);
|
||||
|
||||
S setting = currentSetting.get();
|
||||
if (setting != null) {
|
||||
propertyGetter.apply(setting).addListener(weakPropertyListener);
|
||||
}
|
||||
initInheritableSublistDescription(sublist, propertyGetter, converter);
|
||||
propertyListener.invalidated(setting);
|
||||
}
|
||||
|
||||
private <T> void initInheritableSublistDescription(ComponentSublist sublist,
|
||||
@@ -2083,6 +2259,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
sublist.setDescription(converter.apply(getEffectiveValue(setting, propertyGetter)));
|
||||
}
|
||||
|
||||
/// Returns the localized memory allocation mode display name.
|
||||
private static String getMemoryAllocationDisplayName(Boolean autoMemory) {
|
||||
return i18n(Boolean.TRUE.equals(autoMemory)
|
||||
? "settings.memory.auto_allocate"
|
||||
: "settings.memory.manual_allocate");
|
||||
}
|
||||
|
||||
private static String getWindowTypeDisplayName(GameWindowType type) {
|
||||
return switch (type) {
|
||||
case FULLSCREEN -> i18n("settings.game.window_type.fullscreen");
|
||||
@@ -2129,7 +2312,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
button.setOverriddenTooltip(i18n("settings.game.override_global"));
|
||||
button.setInheritAvailable(!isPresetSetting);
|
||||
|
||||
IndependentSettingBinder.bindToggleButton(currentSetting, button, propertyGetter, holder, this::getEffectiveParentGameSettings);
|
||||
IndependentSettingBinder.bindToggleButton(
|
||||
currentSetting,
|
||||
button,
|
||||
propertyGetter,
|
||||
holder,
|
||||
this::getEffectiveParentGameSettings,
|
||||
activeParentSetting);
|
||||
return button;
|
||||
}
|
||||
|
||||
@@ -2150,7 +2339,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
LineSelectButton<T> button,
|
||||
Function<GameSettings, InheritableProperty<T>> propertyGetter) {
|
||||
ObjectProperty<@Nullable InheritableProperty<T>> activeProperty = new SimpleObjectProperty<>();
|
||||
ObjectProperty<@Nullable InheritableProperty<T>> activeParentProperty = new SimpleObjectProperty<>();
|
||||
final Holder<Boolean> updating = new Holder<>(false);
|
||||
final Holder<InvalidationListener> refreshHolder = new Holder<>();
|
||||
@Nullable JFXButton inheritButton = null;
|
||||
if (!isPresetSetting) {
|
||||
inheritButton = createInheritanceButton();
|
||||
@@ -2160,6 +2351,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
|
||||
InvalidationListener refresh = observable -> {
|
||||
GameSettings setting = currentSetting.get();
|
||||
updateParentInheritablePropertyListener(setting, activeParentProperty, propertyGetter, refreshHolder.value);
|
||||
InheritableProperty<T> property = activeProperty.get();
|
||||
if (setting == null || property == null || updating.value) {
|
||||
return;
|
||||
@@ -2176,6 +2368,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
}
|
||||
};
|
||||
InvalidationListener weakRefresh = holder.weak(refresh);
|
||||
refreshHolder.value = weakRefresh;
|
||||
activeParentSetting.addListener(weakRefresh);
|
||||
|
||||
button.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
GameSettings setting = currentSetting.get();
|
||||
@@ -2241,9 +2435,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
refresh.invalidated(newValue);
|
||||
});
|
||||
|
||||
SettingsManager.getGameSettings().addListener(weakRefresh);
|
||||
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
|
||||
|
||||
S setting = currentSetting.get();
|
||||
if (setting != null) {
|
||||
activeProperty.set(propertyGetter.apply(setting));
|
||||
@@ -2258,10 +2449,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
LineInheritableToggleButton button,
|
||||
Function<GameSettings, InheritableProperty<Boolean>> propertyGetter) {
|
||||
ObjectProperty<@Nullable InheritableProperty<Boolean>> activeProperty = new SimpleObjectProperty<>();
|
||||
ObjectProperty<@Nullable InheritableProperty<Boolean>> activeParentProperty = new SimpleObjectProperty<>();
|
||||
final Holder<Boolean> updating = new Holder<>(false);
|
||||
final Holder<InvalidationListener> refreshHolder = new Holder<>();
|
||||
|
||||
InvalidationListener refresh = observable -> {
|
||||
GameSettings setting = currentSetting.get();
|
||||
updateParentInheritablePropertyListener(setting, activeParentProperty, propertyGetter, refreshHolder.value);
|
||||
InheritableProperty<Boolean> property = activeProperty.get();
|
||||
if (setting == null || property == null || updating.value) {
|
||||
return;
|
||||
@@ -2270,7 +2464,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
updating.value = true;
|
||||
try {
|
||||
boolean overridden = isPropertyOverridden(setting, property);
|
||||
button.setRawValue(overridden ? getDirectValue(property) : getEffectiveValue(setting, propertyGetter));
|
||||
button.setRawValue(overridden
|
||||
? getDirectValue(property)
|
||||
: getEffectiveValue(setting, propertyGetter));
|
||||
button.setOverridden(overridden);
|
||||
button.setEffectiveValue(getEffectiveValue(setting, propertyGetter));
|
||||
} finally {
|
||||
@@ -2278,6 +2474,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
}
|
||||
};
|
||||
InvalidationListener weakRefresh = holder.weak(refresh);
|
||||
refreshHolder.value = weakRefresh;
|
||||
activeParentSetting.addListener(weakRefresh);
|
||||
|
||||
button.rawValueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
InheritableProperty<Boolean> property = activeProperty.get();
|
||||
@@ -2328,9 +2526,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
refresh.invalidated(newValue);
|
||||
});
|
||||
|
||||
SettingsManager.getGameSettings().addListener(weakRefresh);
|
||||
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
|
||||
|
||||
S setting = currentSetting.get();
|
||||
if (setting != null) {
|
||||
activeProperty.set(propertyGetter.apply(setting));
|
||||
@@ -2399,7 +2594,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
/// Returns the explicitly configured parent preset for an instance, falling back to the default preset.
|
||||
private GameSettings.Preset getExplicitParentGameSettings(GameSettings.Instance instance) {
|
||||
GameSettingsPresetID parent = instance.parentProperty().getValue();
|
||||
GameSettings.Preset parentSetting = SettingsManager.getGameSettings(parent);
|
||||
GameSettings.Preset parentSetting = getAvailableParentPreset(parent);
|
||||
return parentSetting != null ? parentSetting : SettingsManager.getDefaultGameSettingsPresetOrCreate();
|
||||
}
|
||||
|
||||
@@ -2438,6 +2633,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
assert isPresetSetting == (instanceId == null);
|
||||
|
||||
if (instanceId != null) {
|
||||
this.currentGameVersionNumber.set(GameVersionNumber.asGameVersion(repository.getGameVersion(instanceId)));
|
||||
|
||||
@Nullable GameSettings.Instance setting = repository.getInstanceGameSettingsOrCreate(instanceId);
|
||||
this.currentSetting.set((S) setting);
|
||||
setSettingsReadOnly(
|
||||
@@ -2446,6 +2643,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
setting != null ? this::forceOverwriteInstanceGameSettings : null);
|
||||
loadIcon();
|
||||
} else {
|
||||
this.currentGameVersionNumber.set(GameVersionNumber.unknown());
|
||||
this.currentSetting.set((S) SettingsManager.getDefaultGameSettingsPresetOrCreate());
|
||||
setSettingsReadOnly(
|
||||
SettingsManager.isGameSettingsReadOnly(),
|
||||
@@ -2457,15 +2655,15 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
/// Updates the page read-only state used when settings cannot be saved safely.
|
||||
///
|
||||
/// @param readOnly whether the current settings should be displayed read-only
|
||||
/// @param message the warning message shown when the page is read-only
|
||||
/// @param message the warning message shown when the page is read-only
|
||||
private void setSettingsReadOnly(boolean readOnly, String message) {
|
||||
setSettingsReadOnly(readOnly, message, null);
|
||||
}
|
||||
|
||||
/// Updates the page read-only state used when settings cannot be saved safely.
|
||||
///
|
||||
/// @param readOnly whether the current settings should be displayed read-only
|
||||
/// @param message the warning message shown when the page is read-only
|
||||
/// @param readOnly whether the current settings should be displayed read-only
|
||||
/// @param message the warning message shown when the page is read-only
|
||||
/// @param forceOverwrite the action used to back up and overwrite the read-only file
|
||||
private void setSettingsReadOnly(boolean readOnly, String message, @Nullable Runnable forceOverwrite) {
|
||||
if (readOnly && forceOverwrite != null) {
|
||||
@@ -2600,13 +2798,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
}
|
||||
|
||||
if (JavaManager.isInitialized()) {
|
||||
GameVersionNumber gameVersionNumber;
|
||||
GameVersionNumber gameVersionNumber = this.currentGameVersionNumber.get();
|
||||
Version version;
|
||||
if (this.instanceId == null) {
|
||||
gameVersionNumber = GameVersionNumber.unknown();
|
||||
version = null;
|
||||
} else {
|
||||
gameVersionNumber = repository != null ? GameVersionNumber.asGameVersion(repository.getGameVersion(this.instanceId)) : GameVersionNumber.unknown();
|
||||
version = repository != null ? repository.getResolvedVersion(this.instanceId) : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXSlider;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
@@ -28,7 +29,6 @@ import javafx.scene.control.Label;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.setting.SettingsManager;
|
||||
import org.jackhuang.hmcl.setting.GameSettings;
|
||||
import org.jackhuang.hmcl.setting.property.SettingProperty;
|
||||
import org.jackhuang.hmcl.ui.MemoryStatusBar;
|
||||
@@ -48,7 +48,6 @@ import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
||||
import static org.jackhuang.hmcl.util.DataSizeUnit.GIGABYTES;
|
||||
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -69,7 +68,8 @@ final class IndependentSettingBinder {
|
||||
Supplier<JFXButton> inheritanceButtonFactory,
|
||||
BiConsumer<JFXButton, Boolean> inheritanceButtonUpdater,
|
||||
WeakListenerHolder listenerHolder,
|
||||
Function<GameSettings.Instance, GameSettings.Preset> parentGetter) {
|
||||
Function<GameSettings.Instance, GameSettings.Preset> parentGetter,
|
||||
Observable parentSettingsInvalidation) {
|
||||
ObjectProperty<@Nullable SettingProperty<String>> activeProperty = new SimpleObjectProperty<>();
|
||||
ObjectProperty<@Nullable SettingProperty<String>> activeParentProperty = new SimpleObjectProperty<>();
|
||||
final Holder<Boolean> updating = new Holder<>(false);
|
||||
@@ -84,7 +84,12 @@ final class IndependentSettingBinder {
|
||||
|
||||
InvalidationListener refresh = observable -> {
|
||||
GameSettings setting = currentSetting.get();
|
||||
updateParentPropertyListener(setting, activeParentProperty, propertyGetter, parentGetter, refreshHolder.value);
|
||||
updateParentPropertyListener(
|
||||
setting,
|
||||
activeParentProperty,
|
||||
propertyGetter,
|
||||
parentGetter,
|
||||
refreshHolder.value);
|
||||
SettingProperty<String> property = activeProperty.get();
|
||||
if (setting == null || property == null || updating.value) {
|
||||
return;
|
||||
@@ -104,6 +109,7 @@ final class IndependentSettingBinder {
|
||||
};
|
||||
InvalidationListener weakRefresh = listenerHolder.weak(refresh);
|
||||
refreshHolder.value = weakRefresh;
|
||||
parentSettingsInvalidation.addListener(weakRefresh);
|
||||
|
||||
textField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
GameSettings setting = currentSetting.get();
|
||||
@@ -161,9 +167,12 @@ final class IndependentSettingBinder {
|
||||
Supplier<JFXButton> inheritanceButtonFactory,
|
||||
BiConsumer<JFXButton, Boolean> inheritanceButtonUpdater,
|
||||
WeakListenerHolder listenerHolder,
|
||||
Function<GameSettings.Instance, GameSettings.Preset> parentGetter) {
|
||||
Function<GameSettings.Instance, GameSettings.Preset> parentGetter,
|
||||
Observable parentSettingsInvalidation) {
|
||||
ObjectProperty<@Nullable SettingProperty<Integer>> activeProperty = new SimpleObjectProperty<>();
|
||||
ObjectProperty<@Nullable SettingProperty<Integer>> activeParentProperty = new SimpleObjectProperty<>();
|
||||
final Holder<Boolean> updating = new Holder<>(false);
|
||||
final Holder<InvalidationListener> refreshHolder = new Holder<>();
|
||||
@Nullable JFXButton inheritButton;
|
||||
if (presetSetting) {
|
||||
inheritButton = null;
|
||||
@@ -174,6 +183,12 @@ final class IndependentSettingBinder {
|
||||
|
||||
InvalidationListener refresh = observable -> {
|
||||
GameSettings setting = currentSetting.get();
|
||||
updateParentPropertyListener(
|
||||
setting,
|
||||
activeParentProperty,
|
||||
propertyGetter,
|
||||
parentGetter,
|
||||
refreshHolder.value);
|
||||
SettingProperty<Integer> property = activeProperty.get();
|
||||
if (setting == null || property == null || updating.value) {
|
||||
return;
|
||||
@@ -193,6 +208,8 @@ final class IndependentSettingBinder {
|
||||
}
|
||||
};
|
||||
InvalidationListener weakRefresh = listenerHolder.weak(refresh);
|
||||
refreshHolder.value = weakRefresh;
|
||||
parentSettingsInvalidation.addListener(weakRefresh);
|
||||
|
||||
textField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
GameSettings setting = currentSetting.get();
|
||||
@@ -257,12 +274,16 @@ final class IndependentSettingBinder {
|
||||
@Nullable JFXButton maxMemoryButton,
|
||||
BiConsumer<JFXButton, Boolean> inheritanceButtonUpdater,
|
||||
WeakListenerHolder listenerHolder,
|
||||
Function<GameSettings.Instance, GameSettings.Preset> parentGetter) {
|
||||
Function<GameSettings.Instance, GameSettings.Preset> parentGetter,
|
||||
Observable parentSettingsInvalidation) {
|
||||
ObjectProperty<@Nullable SettingProperty<Boolean>> activeAutoMemoryProperty = new SimpleObjectProperty<>();
|
||||
ObjectProperty<@Nullable SettingProperty<Integer>> activeMaxMemoryProperty = new SimpleObjectProperty<>();
|
||||
ObjectProperty<@Nullable SettingProperty<Boolean>> activeParentAutoMemoryProperty = new SimpleObjectProperty<>();
|
||||
ObjectProperty<@Nullable SettingProperty<Integer>> activeParentMaxMemoryProperty = new SimpleObjectProperty<>();
|
||||
Label physicalMemoryLabel = (Label) memoryStatusLabels.getLeft();
|
||||
Label allocatedMemoryLabel = (Label) memoryStatusLabels.getRight();
|
||||
final Holder<Boolean> updating = new Holder<>(false);
|
||||
final Holder<InvalidationListener> refreshHolder = new Holder<>();
|
||||
|
||||
int totalMemoryMiB = Math.max(1, (int) MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize()));
|
||||
maxMemorySlider.setValueFactory(slider -> Bindings.createStringBinding(
|
||||
@@ -271,6 +292,18 @@ final class IndependentSettingBinder {
|
||||
|
||||
InvalidationListener refresh = observable -> {
|
||||
GameSettings setting = currentSetting.get();
|
||||
updateParentPropertyListener(
|
||||
setting,
|
||||
activeParentAutoMemoryProperty,
|
||||
GameSettings::autoMemoryProperty,
|
||||
parentGetter,
|
||||
refreshHolder.value);
|
||||
updateParentPropertyListener(
|
||||
setting,
|
||||
activeParentMaxMemoryProperty,
|
||||
GameSettings::maxMemoryProperty,
|
||||
parentGetter,
|
||||
refreshHolder.value);
|
||||
SettingProperty<Boolean> autoMemoryProperty = activeAutoMemoryProperty.get();
|
||||
SettingProperty<Integer> maxMemoryProperty = activeMaxMemoryProperty.get();
|
||||
if (setting == null || autoMemoryProperty == null || maxMemoryProperty == null || updating.value) {
|
||||
@@ -299,6 +332,8 @@ final class IndependentSettingBinder {
|
||||
}
|
||||
};
|
||||
InvalidationListener weakRefresh = listenerHolder.weak(refresh);
|
||||
refreshHolder.value = weakRefresh;
|
||||
parentSettingsInvalidation.addListener(weakRefresh);
|
||||
|
||||
choiceList.selectedValueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
GameSettings setting = currentSetting.get();
|
||||
@@ -433,8 +468,6 @@ final class IndependentSettingBinder {
|
||||
}
|
||||
refresh.invalidated(newValue);
|
||||
});
|
||||
SettingsManager.getGameSettings().addListener(weakRefresh);
|
||||
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
|
||||
memoryStatusBar.memoryStatusProperty().addListener(observable -> {
|
||||
GameSettings setting = currentSetting.get();
|
||||
if (setting == null) {
|
||||
@@ -468,12 +501,21 @@ final class IndependentSettingBinder {
|
||||
LineInheritableToggleButton button,
|
||||
Function<GameSettings, SettingProperty<Boolean>> propertyGetter,
|
||||
WeakListenerHolder listenerHolder,
|
||||
Function<GameSettings.Instance, GameSettings.Preset> parentGetter) {
|
||||
Function<GameSettings.Instance, GameSettings.Preset> parentGetter,
|
||||
Observable parentSettingsInvalidation) {
|
||||
ObjectProperty<@Nullable SettingProperty<Boolean>> activeProperty = new SimpleObjectProperty<>();
|
||||
ObjectProperty<@Nullable SettingProperty<Boolean>> activeParentProperty = new SimpleObjectProperty<>();
|
||||
final Holder<Boolean> updating = new Holder<>(false);
|
||||
final Holder<InvalidationListener> refreshHolder = new Holder<>();
|
||||
|
||||
InvalidationListener refresh = observable -> {
|
||||
GameSettings setting = currentSetting.get();
|
||||
updateParentPropertyListener(
|
||||
setting,
|
||||
activeParentProperty,
|
||||
propertyGetter,
|
||||
parentGetter,
|
||||
refreshHolder.value);
|
||||
SettingProperty<Boolean> property = activeProperty.get();
|
||||
if (setting == null || property == null || updating.value) {
|
||||
return;
|
||||
@@ -483,7 +525,9 @@ final class IndependentSettingBinder {
|
||||
try {
|
||||
boolean overridden = isOverridden(setting, property);
|
||||
Boolean effectiveValue = getEffectiveValue(setting, propertyGetter, parentGetter);
|
||||
button.setRawValue(overridden ? getDirectValue(property) : Boolean.TRUE.equals(effectiveValue));
|
||||
button.setRawValue(overridden
|
||||
? getDirectValue(property)
|
||||
: Boolean.TRUE.equals(effectiveValue));
|
||||
button.setOverridden(overridden);
|
||||
button.setEffectiveValue(Boolean.TRUE.equals(effectiveValue));
|
||||
} finally {
|
||||
@@ -491,6 +535,8 @@ final class IndependentSettingBinder {
|
||||
}
|
||||
};
|
||||
InvalidationListener weakRefresh = listenerHolder.weak(refresh);
|
||||
refreshHolder.value = weakRefresh;
|
||||
parentSettingsInvalidation.addListener(weakRefresh);
|
||||
|
||||
button.rawValueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
GameSettings setting = currentSetting.get();
|
||||
@@ -503,7 +549,8 @@ final class IndependentSettingBinder {
|
||||
try {
|
||||
setOverridden(setting, property, true);
|
||||
property.setValue(newValue);
|
||||
button.setEffectiveValue(Boolean.TRUE.equals(getEffectiveValue(setting, propertyGetter, parentGetter)));
|
||||
button.setEffectiveValue(Boolean.TRUE.equals(
|
||||
getEffectiveValue(setting, propertyGetter, parentGetter)));
|
||||
} finally {
|
||||
updating.value = false;
|
||||
}
|
||||
@@ -522,7 +569,8 @@ final class IndependentSettingBinder {
|
||||
if (newValue) {
|
||||
property.setValue(button.getRawValue());
|
||||
}
|
||||
button.setEffectiveValue(Boolean.TRUE.equals(getEffectiveValue(setting, propertyGetter, parentGetter)));
|
||||
button.setEffectiveValue(Boolean.TRUE.equals(
|
||||
getEffectiveValue(setting, propertyGetter, parentGetter)));
|
||||
} finally {
|
||||
updating.value = false;
|
||||
}
|
||||
@@ -569,47 +617,26 @@ final class IndependentSettingBinder {
|
||||
Label allocatedMemoryLabel,
|
||||
@Nullable Boolean autoMemory,
|
||||
@Nullable Integer maxMemory) {
|
||||
memoryStatusBar.memoryAllocatedProperty().set(calculateAllocatedMemory(memoryStatusBar, autoMemory, maxMemory));
|
||||
updateMemoryLabels(memoryStatusBar, physicalMemoryLabel, allocatedMemoryLabel, autoMemory, maxMemory);
|
||||
}
|
||||
|
||||
private static double calculateAllocatedMemory(
|
||||
MemoryStatusBar memoryStatusBar,
|
||||
@Nullable Boolean autoMemory,
|
||||
@Nullable Integer maxMemory) {
|
||||
long maxMemoryBytes = Math.max(0, maxMemory != null ? maxMemory : 0) * 1024L * 1024L;
|
||||
return HMCLGameRepository.getAllocatedMemory(
|
||||
maxMemoryBytes,
|
||||
memoryStatusBar.getMemoryStatus().getAvailable(),
|
||||
Boolean.TRUE.equals(autoMemory));
|
||||
}
|
||||
|
||||
private static void updateMemoryLabels(
|
||||
MemoryStatusBar memoryStatusBar,
|
||||
Label physicalMemoryLabel,
|
||||
Label allocatedMemoryLabel,
|
||||
@Nullable Boolean autoMemory,
|
||||
@Nullable Integer maxMemory) {
|
||||
PhysicalMemoryStatus memoryStatus = memoryStatusBar.getMemoryStatus();
|
||||
long maxMemoryBytes = Math.max(0, maxMemory != null ? maxMemory : 0) * 1024L * 1024L;
|
||||
boolean autoMemoryEnabled = Boolean.TRUE.equals(autoMemory);
|
||||
|
||||
long memoryAllocated;
|
||||
if (Boolean.TRUE.equals(autoMemory)) {
|
||||
memoryAllocated = HMCLGameRepository.getAutoAllocatedMemory(memoryStatus.available());
|
||||
} else if (maxMemory != null && maxMemory > 0) {
|
||||
memoryAllocated = maxMemory * 1024L * 1024L;
|
||||
} else {
|
||||
memoryAllocated = GameSettings.SUGGESTED_MEMORY * 1024L * 1024L;
|
||||
}
|
||||
|
||||
memoryStatusBar.memoryAllocatedProperty().set(memoryAllocated);
|
||||
|
||||
physicalMemoryLabel.setText(i18n("settings.memory.used_per_total",
|
||||
GIGABYTES.convertFromBytes(memoryStatus.getUsed()),
|
||||
GIGABYTES.convertFromBytes(memoryStatus.getTotal())));
|
||||
allocatedMemoryLabel.setText(i18n(
|
||||
memoryStatus.hasAvailable() && maxMemoryBytes > memoryStatus.getAvailable()
|
||||
? (autoMemoryEnabled
|
||||
? "settings.memory.allocate.auto.exceeded"
|
||||
: "settings.memory.allocate.manual.exceeded")
|
||||
: (autoMemoryEnabled
|
||||
? "settings.memory.allocate.auto"
|
||||
: "settings.memory.allocate.manual"),
|
||||
GIGABYTES.convertFromBytes(maxMemoryBytes),
|
||||
GIGABYTES.convertFromBytes(HMCLGameRepository.getAllocatedMemory(
|
||||
maxMemoryBytes,
|
||||
memoryStatus.getAvailable(),
|
||||
autoMemoryEnabled)),
|
||||
GIGABYTES.convertFromBytes(memoryStatus.getAvailable())));
|
||||
GIGABYTES.convertFromBytes(memoryStatus.total())));
|
||||
allocatedMemoryLabel.setText(
|
||||
memoryStatus.hasAvailable() && memoryAllocated > memoryStatus.available()
|
||||
? i18n("settings.memory.allocate.exceeded", GIGABYTES.convertFromBytes(memoryAllocated), GIGABYTES.convertFromBytes(memoryStatus.available()))
|
||||
: i18n("settings.memory.allocate", GIGABYTES.convertFromBytes(memoryAllocated)));
|
||||
}
|
||||
|
||||
private static <T> void toggleOverride(
|
||||
@@ -656,9 +683,6 @@ final class IndependentSettingBinder {
|
||||
}
|
||||
refresh.invalidated(newProperty);
|
||||
});
|
||||
SettingsManager.getGameSettings().addListener(weakRefresh);
|
||||
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
|
||||
|
||||
GameSettings setting = currentSetting.get();
|
||||
if (setting != null) {
|
||||
SettingProperty<T> property = propertyGetter.apply(setting);
|
||||
|
||||
@@ -76,7 +76,6 @@ final class PresetManagementPane extends ComponentSublist {
|
||||
this.holder = Objects.requireNonNull(holder);
|
||||
|
||||
setTitle(i18n("settings.type.global.preset.manage_all"));
|
||||
setHasSubtitle(true);
|
||||
|
||||
LineButton createButton = new LineButton();
|
||||
createButton.setTitle(i18n("settings.type.global.preset.create"));
|
||||
@@ -189,7 +188,7 @@ final class PresetManagementPane extends ComponentSublist {
|
||||
|
||||
/// Creates a new preset and selects it for editing.
|
||||
private void createPreset() {
|
||||
int number = createDefaultPresetNumber();
|
||||
int number = SettingsManager.gameSettingsPresets().newPresetAutoNameNumber();
|
||||
PromptDialogPane.Builder.StringQuestion nameQuestion =
|
||||
new PromptDialogPane.Builder.StringQuestion("", "")
|
||||
.setPromptText(i18n("settings.type.global.preset.auto_name", number));
|
||||
@@ -216,26 +215,6 @@ final class PresetManagementPane extends ComponentSublist {
|
||||
}).addQuestion(nameQuestion));
|
||||
}
|
||||
|
||||
/// Returns the first automatic preset number that is not used by existing presets.
|
||||
private int createDefaultPresetNumber() {
|
||||
for (int index = 1; ; index++) {
|
||||
String name = i18n("settings.type.global.preset.auto_name", index);
|
||||
boolean used = false;
|
||||
for (GameSettings.Preset setting : SettingsManager.getGameSettings()) {
|
||||
Integer autoNameNumber = setting.autoNameNumberProperty().getValue();
|
||||
if ((autoNameNumber != null && autoNameNumber == index)
|
||||
|| Objects.equals(name, getPresetDisplayName(setting))) {
|
||||
used = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!used) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Asks the user for a new preset name.
|
||||
private void renamePreset(GameSettings.Preset setting) {
|
||||
@Nullable String currentName = getPresetCustomName(setting);
|
||||
|
||||
@@ -45,7 +45,10 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -154,13 +157,13 @@ public final class ImageUtils {
|
||||
if (doScale) {
|
||||
targetWidth = requestedWidth;
|
||||
targetHeight = requestedHeight;
|
||||
pixels = scale(defaultImage.array,
|
||||
defaultImage.width, defaultImage.height,
|
||||
pixels = scale(defaultImage.array(),
|
||||
defaultImage.width(), defaultImage.height(),
|
||||
targetWidth, targetHeight);
|
||||
} else {
|
||||
targetWidth = defaultImage.width;
|
||||
targetHeight = defaultImage.height;
|
||||
pixels = defaultImage.array;
|
||||
targetWidth = defaultImage.width();
|
||||
targetHeight = defaultImage.height();
|
||||
pixels = defaultImage.array();
|
||||
}
|
||||
|
||||
WritableImage image = new WritableImage(targetWidth, targetHeight);
|
||||
@@ -219,18 +222,10 @@ public final class ImageUtils {
|
||||
(byte) 0x0d, (byte) 0x0a, (byte) 0x1a, (byte) 0x0a,
|
||||
};
|
||||
|
||||
private static final class PngChunkHeader {
|
||||
private record PngChunkHeader(int length, int chunkType) {
|
||||
private static final int IDAT_HEADER = 0x49444154;
|
||||
private static final int acTL_HEADER = 0x6163544c;
|
||||
|
||||
private final int length;
|
||||
private final int chunkType;
|
||||
|
||||
private PngChunkHeader(int length, int chunkType) {
|
||||
this.length = length;
|
||||
this.chunkType = chunkType;
|
||||
}
|
||||
|
||||
private static @Nullable PngChunkHeader readHeader(ByteBuffer headerBuffer) {
|
||||
if (headerBuffer.remaining() < 8)
|
||||
return null;
|
||||
@@ -327,7 +322,7 @@ public final class ImageUtils {
|
||||
int[] buffer = new int[Math.multiplyExact(width, height)];
|
||||
for (int frameIndex = 0; frameIndex < frames.size(); frameIndex++) {
|
||||
var frame = frames.get(frameIndex);
|
||||
PngFrameControl control = frame.control;
|
||||
PngFrameControl control = frame.control();
|
||||
|
||||
if (frameIndex == 0 && (
|
||||
control.xOffset != 0 || control.yOffset != 0
|
||||
@@ -347,7 +342,7 @@ public final class ImageUtils {
|
||||
int[] currentFrameBuffer = buffer.clone();
|
||||
if (control.blendOp == 0) {
|
||||
for (int row = 0; row < control.height; row++) {
|
||||
System.arraycopy(frame.bitmap.array,
|
||||
System.arraycopy(frame.bitmap().array(),
|
||||
row * control.width,
|
||||
currentFrameBuffer,
|
||||
(control.yOffset + row) * width + control.xOffset,
|
||||
@@ -360,7 +355,7 @@ public final class ImageUtils {
|
||||
int srcIndex = row * control.width + col;
|
||||
int dstIndex = (control.yOffset + row) * width + control.xOffset + col;
|
||||
|
||||
int srcPixel = frame.bitmap.array[srcIndex];
|
||||
int srcPixel = frame.bitmap().array()[srcIndex];
|
||||
int dstPixel = currentFrameBuffer[dstIndex];
|
||||
|
||||
int srcAlpha = (srcPixel >>> 24) & 0xFF;
|
||||
@@ -444,7 +439,7 @@ public final class ImageUtils {
|
||||
PngAnimationControl animationControl = sequence.getAnimationControl();
|
||||
int cycleCount;
|
||||
if (animationControl != null) {
|
||||
cycleCount = animationControl.numPlays;
|
||||
cycleCount = animationControl.numPlays();
|
||||
if (cycleCount == 0)
|
||||
cycleCount = Timeline.INDEFINITE;
|
||||
} else {
|
||||
|
||||
@@ -9,22 +9,12 @@ package org.jackhuang.hmcl.ui.image.apng.argb8888;
|
||||
* to be compatible with the input array used to build Android Bitmap objects, though of course
|
||||
* its use is not limited to that.
|
||||
*/
|
||||
public final class Argb8888Bitmap {
|
||||
public final int[] array;
|
||||
public final int width;
|
||||
public final int height;
|
||||
public record Argb8888Bitmap(int[] array, int width, int height) {
|
||||
|
||||
public Argb8888Bitmap(int width, int height) {
|
||||
this(new int[width * height], width, height);
|
||||
}
|
||||
|
||||
public Argb8888Bitmap(int[] array, int width, int height) {//}, int x, int y) {
|
||||
this.array = array;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
// this.x = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Bitmap that shares the byte array of this bitmap.
|
||||
* This is useful when rendering a number of animation frames from an APNG file
|
||||
@@ -37,7 +27,7 @@ public final class Argb8888Bitmap {
|
||||
* from the original bitmap).
|
||||
* @return new bitmap object sharing the same data array.
|
||||
*/
|
||||
public Argb8888Bitmap makeView(int width, int height) {//}, int x, int y) {
|
||||
public Argb8888Bitmap makeView(int width, int height) {
|
||||
if ((width * height) > (this.width * this.height)) {
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Requested width and height (%d x %d) exceeds maximum pixels allowed by host bitmap (%d x %d",
|
||||
|
||||
+3
-10
@@ -37,7 +37,7 @@ public final class Argb8888BitmapSequence {
|
||||
|
||||
public void receiveAnimationControl(PngAnimationControl animationControl) {
|
||||
this.animationControl = animationControl;
|
||||
this.animationFrames = new ArrayList<>(animationControl.numFrames);
|
||||
this.animationFrames = new ArrayList<>(animationControl.numFrames());
|
||||
}
|
||||
|
||||
public void receiveDefaultImage(Argb8888Bitmap bitmap) {
|
||||
@@ -49,7 +49,7 @@ public final class Argb8888BitmapSequence {
|
||||
}
|
||||
|
||||
public boolean isAnimated() {
|
||||
return null != animationControl && animationControl.numFrames > 0;
|
||||
return null != animationControl && animationControl.numFrames() > 0;
|
||||
}
|
||||
|
||||
public PngAnimationControl getAnimationControl() {
|
||||
@@ -60,13 +60,6 @@ public final class Argb8888BitmapSequence {
|
||||
return animationFrames;
|
||||
}
|
||||
|
||||
public static final class Frame {
|
||||
public final PngFrameControl control;
|
||||
public final Argb8888Bitmap bitmap;
|
||||
|
||||
public Frame(PngFrameControl control, Argb8888Bitmap bitmap) {
|
||||
this.control = control;
|
||||
this.bitmap = bitmap;
|
||||
}
|
||||
public record Frame(PngFrameControl control, Argb8888Bitmap bitmap) {
|
||||
}
|
||||
}
|
||||
@@ -12,28 +12,21 @@ import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
|
||||
* Note that the transparency of individual entries in the palette is modified in-place
|
||||
* when the trNS chunk is processed.
|
||||
*
|
||||
* @param argbArray The colour array is public and mutable by design.
|
||||
* @see BasicArgb8888Director
|
||||
*/
|
||||
public class Argb8888Palette {
|
||||
public record Argb8888Palette(int[] argbArray) {
|
||||
protected static Argb8888Palette monochromePalette = null;
|
||||
protected static Argb8888Palette greyPalette2 = null;
|
||||
protected static Argb8888Palette greyPalette4 = null;
|
||||
protected static Argb8888Palette greyPalette8 = null;
|
||||
|
||||
/**
|
||||
* The colour array is public and mutable by design.
|
||||
*
|
||||
* @see this.get()
|
||||
*/
|
||||
public final int[] argbArray;
|
||||
|
||||
/**
|
||||
* Construct a new palette using the specific array passed as the argument as-is (not copied).
|
||||
*
|
||||
* @param argbArray array of colours to use as-is (it is not copied).
|
||||
*/
|
||||
public Argb8888Palette(int[] argbArray) {
|
||||
this.argbArray = argbArray;
|
||||
public Argb8888Palette {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,29 +82,33 @@ public class Argb8888Palette {
|
||||
}
|
||||
|
||||
public static Argb8888Palette forGreyscale(int bitDepth) throws PngException {
|
||||
switch (bitDepth) {
|
||||
case 1:
|
||||
return switch (bitDepth) {
|
||||
case 1 -> {
|
||||
if (null == monochromePalette) {
|
||||
monochromePalette = forGreyscale(2, 0xff); // Worth it or not really?
|
||||
}
|
||||
return monochromePalette;
|
||||
case 2:
|
||||
yield monochromePalette;
|
||||
}
|
||||
case 2 -> {
|
||||
if (null == greyPalette2) {
|
||||
greyPalette2 = forGreyscale(4, 0x55);
|
||||
}
|
||||
return greyPalette2;
|
||||
case 4:
|
||||
yield greyPalette2;
|
||||
}
|
||||
case 4 -> {
|
||||
if (null == greyPalette4) {
|
||||
greyPalette4 = forGreyscale(16, 0x11);
|
||||
}
|
||||
return greyPalette4;
|
||||
case 8: // TODO: need??
|
||||
yield greyPalette4;
|
||||
}
|
||||
case 8 -> {
|
||||
if (null == greyPalette8) {
|
||||
greyPalette8 = forGreyscale(256, 0x01);
|
||||
}
|
||||
return greyPalette8;
|
||||
default:
|
||||
throw new PngIntegrityException(String.format("Valid greyscale bit depths are 1, 2, 4, 8, not %d", bitDepth));
|
||||
}
|
||||
yield greyPalette8;
|
||||
}
|
||||
default ->
|
||||
throw new PngIntegrityException(String.format("Valid greyscale bit depths are 1, 2, 4, 8, not %d", bitDepth));
|
||||
};
|
||||
}
|
||||
}
|
||||
+22
-22
@@ -122,8 +122,8 @@ public class Argb8888Processors {
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int[] destArray = this.bitmap.array();
|
||||
final int width = this.bitmap.width();
|
||||
int writePosition = this.y * width;
|
||||
int lastWritePosition = writePosition + width;
|
||||
int bit = highBit;
|
||||
@@ -136,7 +136,7 @@ public class Argb8888Processors {
|
||||
|
||||
int dest;
|
||||
if (palette != null) {
|
||||
dest = palette.argbArray[index];
|
||||
dest = palette.argbArray()[index];
|
||||
} else {
|
||||
dest = 0;
|
||||
}
|
||||
@@ -168,8 +168,8 @@ public class Argb8888Processors {
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int[] destArray = this.bitmap.array();
|
||||
final int width = this.bitmap.width();
|
||||
int writePosition = this.y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
|
||||
@@ -177,7 +177,7 @@ public class Argb8888Processors {
|
||||
|
||||
int dest;
|
||||
if (palette != null) {
|
||||
dest = palette.argbArray[index];
|
||||
dest = palette.argbArray()[index];
|
||||
} else {
|
||||
dest = 0;
|
||||
}
|
||||
@@ -203,8 +203,8 @@ public class Argb8888Processors {
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int[] destArray = this.bitmap.array();
|
||||
final int width = this.bitmap.width();
|
||||
int writePosition = this.y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
final int v = srcBytes[srcPosition++];
|
||||
@@ -236,8 +236,8 @@ public class Argb8888Processors {
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int[] destArray = this.bitmap.array();
|
||||
final int width = this.bitmap.width();
|
||||
final int alpha = 0xff000000; // No alpha in the image means every pixel must be fully opaque
|
||||
int writePosition = this.y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
@@ -274,8 +274,8 @@ public class Argb8888Processors {
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int[] destArray = this.bitmap.array();
|
||||
final int width = this.bitmap.width();
|
||||
int writePosition = this.y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
final int k = 0xff & srcBytes[srcPosition++];
|
||||
@@ -303,8 +303,8 @@ public class Argb8888Processors {
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int[] destArray = this.bitmap.array();
|
||||
final int width = this.bitmap.width();
|
||||
int writePosition = this.y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
final int k = 0xff & srcBytes[srcPosition++];
|
||||
@@ -333,8 +333,8 @@ public class Argb8888Processors {
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int[] destArray = this.bitmap.array();
|
||||
final int width = this.bitmap.width();
|
||||
final int alpha = 0xff000000; // No alpha in the image means every pixel must be fully opaque
|
||||
int writePosition = this.y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
@@ -364,8 +364,8 @@ public class Argb8888Processors {
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int[] destArray = this.bitmap.array();
|
||||
final int width = this.bitmap.width();
|
||||
int writePosition = this.y * width;
|
||||
//srcPosition++; // skip filter byte
|
||||
for (int x = 0; x < width; x++) {
|
||||
@@ -395,8 +395,8 @@ public class Argb8888Processors {
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int[] destArray = this.bitmap.array();
|
||||
final int width = this.bitmap.width();
|
||||
final int alpha = 0xff000000; // No alpha in the image means every pixel must be fully opaque
|
||||
int writePosition = this.y * width;
|
||||
//srcPosition++; // skip filter byte
|
||||
@@ -431,8 +431,8 @@ public class Argb8888Processors {
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int[] destArray = this.bitmap.array();
|
||||
final int width = this.bitmap.width();
|
||||
//final int alpha = 0xff000000; // No alpha in the image means every pixel must be fully opaque
|
||||
int writePosition = this.y * width;
|
||||
//srcPosition++; // skip filter byte
|
||||
|
||||
+2
-2
@@ -55,7 +55,7 @@ public abstract class Argb8888ScanlineProcessor extends BasicScanlineProcessor {
|
||||
public Argb8888ScanlineProcessor cloneWithNewBitmap(PngHeader header) {
|
||||
Argb8888ScanlineProcessor cloned = clone(header.bytesPerRow, new Argb8888Bitmap(header.width, header.height));
|
||||
if (this.palette != null) {
|
||||
cloned.setPalette(new Argb8888Palette(palette.argbArray));
|
||||
cloned.setPalette(new Argb8888Palette(palette.argbArray()));
|
||||
}
|
||||
return cloned;
|
||||
|
||||
@@ -71,7 +71,7 @@ public abstract class Argb8888ScanlineProcessor extends BasicScanlineProcessor {
|
||||
public Argb8888ScanlineProcessor cloneWithSharedBitmap(PngHeader header) {
|
||||
Argb8888ScanlineProcessor cloned = clone(header.bytesPerRow, bitmap.makeView(header.width, header.height));
|
||||
if (this.palette != null) {
|
||||
cloned.setPalette(new Argb8888Palette(palette.argbArray));
|
||||
cloned.setPalette(new Argb8888Palette(palette.argbArray()));
|
||||
}
|
||||
return cloned;
|
||||
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ public abstract class BasicArgb8888Director<ResultT> implements Argb8888Director
|
||||
}
|
||||
for (int i = 0; i < length; i++) {
|
||||
final int alpha = 0xff & bytes[position + i];
|
||||
palette.argbArray[i] = alpha << 24 | palette.argbArray[i] & 0x00FFFFFF;
|
||||
palette.argbArray()[i] = alpha << 24 | palette.argbArray()[i] & 0x00FFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,25 +3,20 @@
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng.chunks;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A PngAnimationControl object contains data parsed from the ``acTL``
|
||||
* animation control chunk of an animated PNG file.
|
||||
*/
|
||||
public class PngAnimationControl {
|
||||
public final int numFrames;
|
||||
public final int numPlays;
|
||||
|
||||
public PngAnimationControl(int numFrames, int numPlays) {
|
||||
this.numFrames = numFrames;
|
||||
this.numPlays = numPlays;
|
||||
}
|
||||
public record PngAnimationControl(int numFrames, int numPlays) {
|
||||
|
||||
public boolean loopForever() {
|
||||
return 0 == numPlays;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
public @NotNull String toString() {
|
||||
return "PngAnimationControl{" +
|
||||
"numFrames=" + numFrames +
|
||||
", numPlays=" + numPlays +
|
||||
|
||||
@@ -9,12 +9,7 @@ import java.io.IOException;
|
||||
/**
|
||||
* A PngGamma object represents data parsed from a ``gAMA`` chunk.
|
||||
*/
|
||||
public class PngGamma {
|
||||
public final int imageGamma;
|
||||
|
||||
public PngGamma(int imageGamma) {
|
||||
this.imageGamma = imageGamma;
|
||||
}
|
||||
public record PngGamma(int imageGamma) {
|
||||
|
||||
public static PngGamma from(DataInputStream dis) throws IOException {
|
||||
return new PngGamma(dis.readInt());
|
||||
|
||||
@@ -49,7 +49,7 @@ public abstract class PngContainerProcessor<ResultT> implements PngChunkProcesso
|
||||
@Override
|
||||
public void processAnimationControl(PngAnimationControl animationControl) {
|
||||
container.animationControl = animationControl;
|
||||
container.animationFrames = new ArrayList<PngFrameControl>(container.animationControl.numFrames);
|
||||
container.animationFrames = new ArrayList<PngFrameControl>(container.animationControl.numFrames());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.ImageContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.LineButton;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
@@ -136,14 +137,17 @@ public final class AboutPage extends SpinnerPane {
|
||||
if (obj.has("image")) {
|
||||
JsonElement image = obj.get("image");
|
||||
if (image.isJsonPrimitive()) {
|
||||
button.setLeading(loadImage(image.getAsString()));
|
||||
var imageView = new ImageContainer(32, 32);
|
||||
imageView.setImage(loadImage(image.getAsString()));
|
||||
imageView.setMouseTransparent(true);
|
||||
|
||||
|
||||
button.setLeading(imageView);
|
||||
} else if (image.isJsonObject()) {
|
||||
holder.add(FXUtils.onWeakChangeAndOperate(Themes.darkModeProperty(), darkMode -> {
|
||||
button.setLeading(darkMode
|
||||
? loadImage(image.getAsJsonObject().get("dark").getAsString())
|
||||
: loadImage(image.getAsJsonObject().get("light").getAsString())
|
||||
);
|
||||
}));
|
||||
holder.add(FXUtils.onWeakChangeAndOperate(Themes.darkModeProperty(), darkMode -> button.setLeading(darkMode
|
||||
? loadImage(image.getAsJsonObject().get("dark").getAsString())
|
||||
: loadImage(image.getAsJsonObject().get("light").getAsString())
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.jfoenix.controls.*;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
|
||||
import javafx.scene.control.ScrollPane;
|
||||
@@ -33,14 +34,15 @@ import org.jackhuang.hmcl.task.FetchTask;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
||||
@@ -64,10 +66,10 @@ public class DownloadSettingsPage extends StackPane {
|
||||
downloadSource.getStyleClass().add("card-non-transparent");
|
||||
{
|
||||
Function<DownloadSource, String> converter = source -> switch (source) {
|
||||
case DEFAULT -> i18n("settings.launcher.download_source.auto");
|
||||
case OFFICIAL -> i18n("download.provider.official");
|
||||
case MIRROR -> i18n("download.provider.mirror");
|
||||
};
|
||||
case DEFAULT -> i18n("settings.launcher.download_source.auto");
|
||||
case OFFICIAL -> i18n("download.provider.official");
|
||||
case MIRROR -> i18n("download.provider.mirror");
|
||||
};
|
||||
Function<DownloadSource, String> descriptionConverter = source -> {
|
||||
String bundleKey = switch (source) {
|
||||
case DEFAULT -> "download.provider.balanced.desc";
|
||||
@@ -106,91 +108,83 @@ public class DownloadSettingsPage extends StackPane {
|
||||
{
|
||||
var downloadList = new ComponentList();
|
||||
|
||||
VBox downloadThreads = new VBox(16);
|
||||
ComponentSublist fileCommonLocationSublist = new ComponentSublist(() -> {
|
||||
MultiFileItem<EnumCommonDirectory> fileCommonLocation = new MultiFileItem<>();
|
||||
fileCommonLocation.loadChildren(Arrays.asList(
|
||||
new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT),
|
||||
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM)
|
||||
.setChooserTitle(i18n("launcher.cache_directory.choose"))
|
||||
.setSelectionMode(FileSelector.SelectionMode.DIRECTORY)
|
||||
.bindBidirectional(settings().commonDirectoryProperty())
|
||||
));
|
||||
fileCommonLocation.selectedDataProperty().bindBidirectional(settings().commonDirectoryTypeProperty());
|
||||
return List.of(fileCommonLocation);
|
||||
});
|
||||
fileCommonLocationSublist.setTitle(i18n("launcher.cache_directory"));
|
||||
fileCommonLocationSublist.setHasSubtitle(true);
|
||||
fileCommonLocationSublist.descriptionProperty().bind(
|
||||
Bindings.createObjectBinding(() -> Optional.ofNullable(settings().getResolvedCommonDirectory())
|
||||
.orElse(i18n("launcher.cache_directory.disabled")),
|
||||
settings().commonDirectoryProperty(), settings().commonDirectoryTypeProperty()));
|
||||
|
||||
ComponentSublist fileCommonLocationSublist = new ComponentSublist();
|
||||
JFXButton cleanButton = FXUtils.newBorderButton(i18n("launcher.cache_directory.clean"));
|
||||
cleanButton.setOnAction(e -> clearCacheDirectory());
|
||||
fileCommonLocationSublist.setHeaderRight(cleanButton);
|
||||
|
||||
{
|
||||
{
|
||||
MultiFileItem<EnumCommonDirectory> fileCommonLocation = new MultiFileItem<>();
|
||||
fileCommonLocation.loadChildren(Arrays.asList(
|
||||
new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT),
|
||||
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM)
|
||||
.setChooserTitle(i18n("launcher.cache_directory.choose"))
|
||||
.setSelectionMode(FileSelector.SelectionMode.DIRECTORY)
|
||||
.bindBidirectional(settings().commonDirectoryProperty())
|
||||
));
|
||||
fileCommonLocation.selectedDataProperty().bindBidirectional(settings().commonDirectoryTypeProperty());
|
||||
ComponentSublist downloadThreadsSublist = new ComponentSublist(() -> {
|
||||
var downloadThreadsList = new RadioChoiceList<Boolean>();
|
||||
downloadThreadsList.setChoices(
|
||||
new RadioChoiceList.Choice<>(i18n("settings.launcher.download.threads.auto"), true),
|
||||
new RadioChoiceList.Choice<>(i18n("settings.launcher.download.threads.custom"), false) {
|
||||
@Override
|
||||
protected Node createRightNode() {
|
||||
HBox hbox = new HBox(8);
|
||||
hbox.setViewOrder(-1);
|
||||
hbox.setAlignment(Pos.CENTER);
|
||||
// hbox.setPadding(new Insets(0, 0, 0, 30));
|
||||
hbox.disableProperty().bind(settings().autoDownloadThreadsProperty());
|
||||
|
||||
fileCommonLocationSublist.getContent().add(fileCommonLocation);
|
||||
fileCommonLocationSublist.setTitle(i18n("launcher.cache_directory"));
|
||||
fileCommonLocationSublist.setHasSubtitle(true);
|
||||
fileCommonLocationSublist.descriptionProperty().bind(
|
||||
Bindings.createObjectBinding(() -> Optional.ofNullable(settings().getResolvedCommonDirectory())
|
||||
.orElse(i18n("launcher.cache_directory.disabled")),
|
||||
settings().commonDirectoryProperty(), settings().commonDirectoryTypeProperty()));
|
||||
JFXSlider slider = new JFXSlider(1, 256, 64);
|
||||
HBox.setHgrow(slider, Priority.ALWAYS);
|
||||
|
||||
JFXButton cleanButton = FXUtils.newBorderButton(i18n("launcher.cache_directory.clean"));
|
||||
cleanButton.setOnAction(e -> clearCacheDirectory());
|
||||
fileCommonLocationSublist.setHeaderRight(cleanButton);
|
||||
}
|
||||
JFXTextField threadsField = new JFXTextField();
|
||||
FXUtils.setLimitWidth(threadsField, 60);
|
||||
FXUtils.bind(threadsField, settings().downloadThreadsProperty(), SafeStringConverter.fromInteger()
|
||||
.restrict(it -> it > 0)
|
||||
.fallbackTo(FetchTask.DEFAULT_CONCURRENCY)
|
||||
.asPredicate(Validator.addTo(threadsField)));
|
||||
|
||||
{
|
||||
JFXCheckBox chkAutoDownloadThreads = new JFXCheckBox(i18n("settings.launcher.download.threads.auto"));
|
||||
VBox.setMargin(chkAutoDownloadThreads, new Insets(8, 0, 0, 0));
|
||||
chkAutoDownloadThreads.selectedProperty().bindBidirectional(settings().autoDownloadThreadsProperty());
|
||||
downloadThreads.getChildren().add(chkAutoDownloadThreads);
|
||||
var changedByTextField = new Holder<>(false);
|
||||
FXUtils.onChangeAndOperate(settings().downloadThreadsProperty(), value -> {
|
||||
changedByTextField.value = true;
|
||||
slider.setValue(value.intValue());
|
||||
changedByTextField.value = false;
|
||||
});
|
||||
slider.valueProperty().addListener((value, oldVal, newVal) -> {
|
||||
if (changedByTextField.value) return;
|
||||
settings().downloadThreadsProperty().set(value.getValue().intValue());
|
||||
});
|
||||
|
||||
chkAutoDownloadThreads.selectedProperty().addListener((a, b, newValue) -> {
|
||||
if (newValue) {
|
||||
settings().downloadThreadsProperty().set(FetchTask.DEFAULT_CONCURRENCY);
|
||||
hbox.getChildren().setAll(slider, threadsField);
|
||||
return hbox;
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
downloadThreadsList.selectedValueProperty().bindBidirectional(settings().autoDownloadThreadsProperty());
|
||||
|
||||
return List.of(downloadThreadsList);
|
||||
});
|
||||
downloadThreadsSublist.setTitle(i18n("settings.launcher.download.threads"));
|
||||
downloadThreadsSublist.descriptionProperty().bind(Bindings.createStringBinding(() -> {
|
||||
if (settings().autoDownloadThreadsProperty().get()) {
|
||||
return i18n("settings.launcher.download.threads.auto");
|
||||
} else {
|
||||
return Integer.toString(settings().downloadThreadsProperty().get());
|
||||
}
|
||||
}, settings().autoDownloadThreadsProperty(), settings().downloadThreadsProperty()));
|
||||
|
||||
{
|
||||
HBox hbox = new HBox(8);
|
||||
hbox.setStyle("-fx-view-order: -1;"); // prevent the indicator from being covered by the hint
|
||||
hbox.setAlignment(Pos.CENTER);
|
||||
hbox.setPadding(new Insets(0, 0, 0, 30));
|
||||
hbox.disableProperty().bind(settings().autoDownloadThreadsProperty());
|
||||
Label label = new Label(i18n("settings.launcher.download.threads"));
|
||||
|
||||
JFXSlider slider = new JFXSlider(1, 256, 64);
|
||||
HBox.setHgrow(slider, Priority.ALWAYS);
|
||||
|
||||
JFXTextField threadsField = new JFXTextField();
|
||||
FXUtils.setLimitWidth(threadsField, 60);
|
||||
FXUtils.bind(threadsField, settings().downloadThreadsProperty(), SafeStringConverter.fromInteger()
|
||||
.restrict(it -> it > 0)
|
||||
.fallbackTo(FetchTask.DEFAULT_CONCURRENCY)
|
||||
.asPredicate(Validator.addTo(threadsField)));
|
||||
|
||||
AtomicBoolean changedByTextField = new AtomicBoolean(false);
|
||||
FXUtils.onChangeAndOperate(settings().downloadThreadsProperty(), value -> {
|
||||
changedByTextField.set(true);
|
||||
slider.setValue(value.intValue());
|
||||
changedByTextField.set(false);
|
||||
});
|
||||
slider.valueProperty().addListener((value, oldVal, newVal) -> {
|
||||
if (changedByTextField.get()) return;
|
||||
settings().downloadThreadsProperty().set(value.getValue().intValue());
|
||||
});
|
||||
|
||||
hbox.getChildren().setAll(label, slider, threadsField);
|
||||
downloadThreads.getChildren().add(hbox);
|
||||
}
|
||||
|
||||
{
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
VBox.setMargin(hintPane, new Insets(0, 0, 0, 30));
|
||||
hintPane.disableProperty().bind(settings().autoDownloadThreadsProperty());
|
||||
hintPane.setText(i18n("settings.launcher.download.threads.hint"));
|
||||
downloadThreads.getChildren().add(hintPane);
|
||||
}
|
||||
}
|
||||
|
||||
downloadList.getContent().addAll(fileCommonLocationSublist, downloadThreads);
|
||||
downloadList.getContent().addAll(fileCommonLocationSublist, downloadThreadsSublist);
|
||||
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("download")), downloadList);
|
||||
}
|
||||
|
||||
|
||||
@@ -232,6 +232,7 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
this.remoteVersionBox.setConverter(FXUtils.stringConverter(JavaRemoteVersion::getDistributionVersion));
|
||||
|
||||
this.packageTypeBox = new JFXComboBox<>(FXCollections.observableArrayList());
|
||||
this.packageTypeBox.setConverter(FXUtils.stringConverter(JavaPackageType::getDisplayName));
|
||||
|
||||
this.downloadButton = new JFXButton(i18n("download"));
|
||||
downloadButton.setOnAction(e -> onDownload());
|
||||
@@ -371,7 +372,7 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
if (version == null)
|
||||
return;
|
||||
|
||||
Controllers.taskDialog(new GetTask(downloadProvider.injectURLWithCandidates(version.getLinks().getPkgInfoUri()))
|
||||
Controllers.taskDialog(new GetTask(downloadProvider.injectURLWithCandidates(version.getLinks().pkgInfoUri()))
|
||||
.setExecutor(Schedulers.io())
|
||||
.thenComposeAsync(json -> {
|
||||
DiscoResult<DiscoRemoteFileInfo> result = JsonUtils.fromNonNullJson(json, DiscoResult.typeOf(DiscoRemoteFileInfo.class));
|
||||
@@ -379,20 +380,20 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
throw new IOException("Illegal result: " + json);
|
||||
|
||||
DiscoRemoteFileInfo fileInfo = result.getResult().get(0);
|
||||
if (StringUtils.isNotBlank(fileInfo.getChecksumType())
|
||||
&& !fileInfo.getChecksumType().equals("sha1") && !fileInfo.getChecksumType().equals("sha256") && !fileInfo.getChecksumType().equals("md5"))
|
||||
throw new IOException("Unsupported checksum type: " + fileInfo.getChecksumType());
|
||||
if (StringUtils.isBlank(fileInfo.getDirectDownloadUri()))
|
||||
if (StringUtils.isNotBlank(fileInfo.checksumType())
|
||||
&& !fileInfo.checksumType().equals("sha1") && !fileInfo.checksumType().equals("sha256") && !fileInfo.checksumType().equals("md5"))
|
||||
throw new IOException("Unsupported checksum type: " + fileInfo.checksumType());
|
||||
if (StringUtils.isBlank(fileInfo.directDownloadUri()))
|
||||
throw new IOException("Missing download URI: " + json);
|
||||
|
||||
Path targetFile = Files.createTempFile("hmcl-java-", "." + version.getArchiveType());
|
||||
targetFile.toFile().deleteOnExit();
|
||||
|
||||
Task<FileDownloadTask.IntegrityCheck> getIntegrityCheck;
|
||||
if (StringUtils.isNotBlank(fileInfo.getChecksum()))
|
||||
getIntegrityCheck = Task.completed(new FileDownloadTask.IntegrityCheck(fileInfo.getChecksumType(), fileInfo.getChecksum()));
|
||||
else if (StringUtils.isNotBlank(fileInfo.getChecksumUri()))
|
||||
getIntegrityCheck = new GetTask(downloadProvider.injectURLWithCandidates(fileInfo.getChecksumUri()))
|
||||
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()))
|
||||
.thenApplyAsync(checksum -> {
|
||||
checksum = checksum.trim();
|
||||
|
||||
@@ -400,15 +401,15 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
if (idx > 0)
|
||||
checksum = checksum.substring(0, idx);
|
||||
|
||||
return new FileDownloadTask.IntegrityCheck(fileInfo.getChecksumType(), checksum);
|
||||
return new FileDownloadTask.IntegrityCheck(fileInfo.checksumType(), checksum);
|
||||
});
|
||||
else
|
||||
getIntegrityCheck = Task.completed(null);
|
||||
|
||||
return getIntegrityCheck
|
||||
.thenComposeAsync(integrityCheck ->
|
||||
new FileDownloadTask(downloadProvider.injectURLWithCandidates(fileInfo.getDirectDownloadUri()),
|
||||
targetFile, integrityCheck).setName(fileInfo.getFileName()))
|
||||
new FileDownloadTask(downloadProvider.injectURLWithCandidates(fileInfo.directDownloadUri()),
|
||||
targetFile, integrityCheck).setName(fileInfo.fileName()))
|
||||
.thenSupplyAsync(() -> targetFile);
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), ((result, exception) -> {
|
||||
|
||||
@@ -25,6 +25,7 @@ import javafx.collections.FXCollections;
|
||||
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;
|
||||
@@ -226,6 +227,7 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
|
||||
private static final class JavaItemCell extends ListCell<JavaRuntime> {
|
||||
private final Node graphic;
|
||||
private final Label label = new Label();
|
||||
private final TwoLineListItem content;
|
||||
|
||||
private SVG removeIcon;
|
||||
@@ -241,11 +243,17 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
center.setSpacing(8);
|
||||
center.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
label.setAlignment(Pos.CENTER);
|
||||
label.setMinSize(24, 24);
|
||||
label.setMaxSize(24, 24);
|
||||
label.setPrefSize(24, 24);
|
||||
label.setStyle("-fx-background-color: -monet-secondary-container; -fx-background-radius: 2; -fx-padding: 2; -fx-font-weight: normal; -fx-font-size: 12px;");
|
||||
|
||||
this.content = new TwoLineListItem();
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
|
||||
BorderPane.setAlignment(content, Pos.CENTER);
|
||||
center.getChildren().setAll(content);
|
||||
center.getChildren().setAll(label, content);
|
||||
root.setCenter(center);
|
||||
|
||||
HBox right = new HBox();
|
||||
@@ -293,6 +301,9 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
int parsedVersion = item.getParsedVersion();
|
||||
label.setText(parsedVersion >= 0 ? String.valueOf(parsedVersion) : "?");
|
||||
|
||||
content.setTitle((item.isJDK() ? "JDK" : "JRE") + " " + item.getVersion());
|
||||
content.setSubtitle(item.getBinary().toString());
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.RotateTransition;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -29,6 +30,7 @@ import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.ImageView;
|
||||
@@ -254,14 +256,37 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
|
||||
menuButton = new JFXButton();
|
||||
menuButton.getStyleClass().add("menu-button");
|
||||
menuButton.setOnAction(e -> GameListPopupMenu.show(
|
||||
menuButton.setOnAction(e -> {
|
||||
JFXPopup popup = GameListPopupMenu.showAndGetPopup(
|
||||
menuButton,
|
||||
JFXPopup.PopupVPosition.BOTTOM,
|
||||
JFXPopup.PopupHPosition.RIGHT,
|
||||
0,
|
||||
-menuButton.getHeight(),
|
||||
repository, versions
|
||||
));
|
||||
);
|
||||
|
||||
if (popup != null) {
|
||||
Node graphic = menuButton.getGraphic();
|
||||
if (graphic != null) {
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
Duration duration = Duration.millis(200);
|
||||
RotateTransition rotateOpen = new RotateTransition(duration, graphic);
|
||||
rotateOpen.setToAngle(-180);
|
||||
FXUtils.playAnimation(graphic, "arrow-rotation", rotateOpen);
|
||||
|
||||
popup.setOnHidden(windowEvent -> {
|
||||
RotateTransition rotateClose = new RotateTransition(duration, graphic);
|
||||
rotateClose.setToAngle(0);
|
||||
FXUtils.playAnimation(graphic, "arrow-rotation", rotateClose);
|
||||
});
|
||||
} else {
|
||||
graphic.setRotate(-180);
|
||||
popup.setOnHidden(windowEvent -> graphic.setRotate(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
FXUtils.installFastTooltip(menuButton, i18n("version.switch"));
|
||||
menuButton.setGraphic(SVG.ARROW_DROP_UP.createIcon(30));
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -69,6 +70,7 @@ import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -213,6 +215,60 @@ public class PersonalizationPage extends StackPane {
|
||||
refresh.invalidated(null);
|
||||
}
|
||||
|
||||
/// Binds a line toggle button to an inheritable theme appearance setting.
|
||||
private static void bindThemeAppearanceToggleButton(
|
||||
LineToggleButton button,
|
||||
String setting,
|
||||
BooleanProperty directProperty,
|
||||
Supplier<Boolean> effectiveValueSupplier) {
|
||||
JFXButton inheritButton = createThemeAppearanceOverrideButton();
|
||||
button.setTitleTrailing(inheritButton);
|
||||
|
||||
Holder<Boolean> updating = new Holder<>(false);
|
||||
InvalidationListener refresh = ignored -> {
|
||||
if (updating.value) {
|
||||
return;
|
||||
}
|
||||
updating.value = true;
|
||||
try {
|
||||
boolean overridden = settings().getThemeAppearanceOverrides().contains(setting);
|
||||
button.setSelected(overridden ? directProperty.get() : effectiveValueSupplier.get());
|
||||
updateThemeAppearanceOverrideButton(inheritButton, !overridden);
|
||||
} finally {
|
||||
updating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
button.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (updating.value) {
|
||||
return;
|
||||
}
|
||||
updating.value = true;
|
||||
try {
|
||||
directProperty.set(Boolean.TRUE.equals(newValue));
|
||||
settings().getThemeAppearanceOverrides().add(setting);
|
||||
updateThemeAppearanceOverrideButton(inheritButton, false);
|
||||
} finally {
|
||||
updating.value = false;
|
||||
}
|
||||
refresh.invalidated(null);
|
||||
});
|
||||
directProperty.addListener(refresh);
|
||||
addThemeAppearanceRefreshListener(refresh);
|
||||
|
||||
inheritButton.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
if (!settings().getThemeAppearanceOverrides().contains(setting)) {
|
||||
directProperty.set(button.isSelected());
|
||||
settings().getThemeAppearanceOverrides().add(setting);
|
||||
} else {
|
||||
settings().getThemeAppearanceOverrides().remove(setting);
|
||||
}
|
||||
refresh.invalidated(null);
|
||||
event.consume();
|
||||
});
|
||||
refresh.invalidated(null);
|
||||
}
|
||||
|
||||
/// Returns the display name for the currently selected launcher theme.
|
||||
private static String getSelectedThemeTitle() {
|
||||
ThemeReference reference = settings().getSelectedThemeOrDefault();
|
||||
@@ -294,36 +350,40 @@ public class PersonalizationPage extends StackPane {
|
||||
String userName = System.getProperty("user.name").trim();
|
||||
String defaultAuthorName = StringUtils.isBlank(userName) ? "Unknown" : userName;
|
||||
|
||||
PromptDialogPane.Builder.StringQuestion packIdQuestion = new PromptDialogPane.Builder.StringQuestion(
|
||||
i18n("theme_pack.export.id"),
|
||||
"")
|
||||
.setPromptText(defaultPackId);
|
||||
PromptDialogPane.Builder.StringQuestion packNameQuestion = new PromptDialogPane.Builder.StringQuestion(
|
||||
i18n("theme_pack.export.name"),
|
||||
"")
|
||||
.setPromptText(defaultPackName);
|
||||
PromptDialogPane.Builder.StringQuestion versionQuestion = new PromptDialogPane.Builder.StringQuestion(
|
||||
i18n("theme_pack.export.version"),
|
||||
"")
|
||||
.setPromptText(ThemePackManager.CURRENT_THEME_PACK_VERSION);
|
||||
PromptDialogPane.Builder.StringQuestion authorNameQuestion = new PromptDialogPane.Builder.StringQuestion(
|
||||
i18n("theme_pack.export.author"),
|
||||
"")
|
||||
.setPromptText(defaultAuthorName);
|
||||
|
||||
Controllers.prompt(new PromptDialogPane.Builder(i18n("theme_pack.export.title"), (questions, handler) -> handler.resolve())
|
||||
.addQuestion(packIdQuestion)
|
||||
.addQuestion(packNameQuestion)
|
||||
.addQuestion(versionQuestion)
|
||||
.addQuestion(authorNameQuestion)).thenAccept(questions -> exportCurrentThemePack(
|
||||
StringUtils.isBlank(packIdQuestion.getValue()) ? defaultPackId : packIdQuestion.getValue().trim(),
|
||||
defaultPackId,
|
||||
StringUtils.isBlank(versionQuestion.getValue())
|
||||
? ThemePackManager.CURRENT_THEME_PACK_VERSION
|
||||
: versionQuestion.getValue().trim(),
|
||||
StringUtils.isBlank(packNameQuestion.getValue()) ? defaultPackName : packNameQuestion.getValue().trim(),
|
||||
StringUtils.isBlank(authorNameQuestion.getValue()) ? defaultAuthorName : authorNameQuestion.getValue().trim()));
|
||||
}
|
||||
|
||||
/// Saves current launcher appearance as a theme-pack file with the given package metadata.
|
||||
private void exportCurrentThemePack(String packId, String packName, String authorName) {
|
||||
private void exportCurrentThemePack(String packId, String version, String packName, String authorName) {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("theme_pack.export.title"));
|
||||
String initialFileName = sanitizeThemePackFileName(packName) + ThemePackExporter.FILE_EXTENSION;
|
||||
chooser.setInitialFileName(FileUtils.isNameValid(initialFileName)
|
||||
? initialFileName
|
||||
: "theme-pack" + ThemePackExporter.FILE_EXTENSION);
|
||||
String initialFileName = sanitizeThemePackFileName(packName);
|
||||
chooser.setInitialFileName(
|
||||
(FileUtils.isNameValid(initialFileName) ? initialFileName : "theme-pack")
|
||||
+ (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS ? "" : ThemePackExporter.FILE_EXTENSION)
|
||||
);
|
||||
chooser.getExtensionFilters().setAll(
|
||||
new FileChooser.ExtensionFilter(i18n("theme_pack.file"), "*" + ThemePackExporter.FILE_EXTENSION));
|
||||
|
||||
@@ -340,6 +400,7 @@ public class PersonalizationPage extends StackPane {
|
||||
ThemePackManager.exportCurrent(
|
||||
output,
|
||||
packId,
|
||||
version,
|
||||
packName,
|
||||
authorName);
|
||||
Controllers.dialog(
|
||||
@@ -439,6 +500,10 @@ public class PersonalizationPage extends StackPane {
|
||||
i18n("settings.launcher.theme_color_type.default"),
|
||||
ThemeColorType.DEFAULT);
|
||||
|
||||
var systemColorChoice = new RadioChoiceList.Choice<ThemeColorType>(
|
||||
i18n("settings.launcher.theme_color_type.system"),
|
||||
ThemeColorType.SYSTEM);
|
||||
|
||||
var customColorChoice = new RadioChoiceList.Choice<ThemeColorType>(
|
||||
i18n("settings.launcher.theme_color_type.custom"),
|
||||
ThemeColorType.CUSTOM) {
|
||||
@@ -455,7 +520,11 @@ public class PersonalizationPage extends StackPane {
|
||||
|
||||
RadioChoiceList<ThemeColorType> themeColorChoiceList = new RadioChoiceList<>();
|
||||
themeColorChoiceList.setFallbackValue(ThemeColorType.DEFAULT);
|
||||
themeColorChoiceList.setChoices(Arrays.asList(defaultColorChoice, customColorChoice, backgroundColorChoice));
|
||||
themeColorChoiceList.setChoices(Arrays.asList(
|
||||
defaultColorChoice,
|
||||
systemColorChoice,
|
||||
customColorChoice,
|
||||
backgroundColorChoice));
|
||||
|
||||
JFXButton themeColorOverrideButton = createThemeAppearanceOverrideButton();
|
||||
themeColorSublist.setTitleRight(themeColorOverrideButton);
|
||||
@@ -1072,6 +1141,25 @@ public class PersonalizationPage extends StackPane {
|
||||
refresh.invalidated(null);
|
||||
themeAppearanceList.getContent().add(titleBarTransparentButton);
|
||||
}
|
||||
|
||||
LineToggleButton windowTransparentButton = new LineToggleButton();
|
||||
windowTransparentButton.setTitle(i18n("settings.launcher.window_transparent"));
|
||||
bindThemeAppearanceToggleButton(
|
||||
windowTransparentButton,
|
||||
LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT,
|
||||
settings().windowTransparentProperty(),
|
||||
() -> {
|
||||
try {
|
||||
return Objects.requireNonNullElse(
|
||||
ThemePackManager.resolveCurrentWindowTransparent(
|
||||
ThemePackManager.currentResolveContext()),
|
||||
false);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
themeAppearanceList.getContent().add(windowTransparentButton);
|
||||
|
||||
content.getChildren().addAll(
|
||||
ComponentList.createComponentListTitle(i18n("settings.launcher.appearance")),
|
||||
themeAppearanceList,
|
||||
|
||||
@@ -41,7 +41,6 @@ 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.util.AprilFools;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
@@ -195,7 +194,7 @@ public final class SettingsPage extends ScrollPane {
|
||||
{
|
||||
ComponentList miscPaneList = new ComponentList();
|
||||
|
||||
if (AprilFools.isShowAprilFoolsSettings()) {
|
||||
{
|
||||
LineToggleButton disableAprilFools = new LineToggleButton();
|
||||
disableAprilFools.setTitle(i18n("settings.launcher.disable_april_fools"));
|
||||
disableAprilFools.setSubtitle(i18n("settings.take_effect_after_restart"));
|
||||
|
||||
@@ -32,8 +32,7 @@ import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.TextFlow;
|
||||
@@ -550,17 +549,10 @@ public class TerracottaControllerPage extends StackPane {
|
||||
|
||||
private ComponentSublist getThirdPartyDownloadNodes() {
|
||||
ComponentSublist locals = new ComponentSublist();
|
||||
|
||||
var header = new LinePane();
|
||||
header.getStyleClass().add("no-padding");
|
||||
header.setLargeTitle(true);
|
||||
header.setMinHeight(LinePane.USE_COMPUTED_SIZE);
|
||||
HBox.setHgrow(header, Priority.ALWAYS);
|
||||
header.setMouseTransparent(true);
|
||||
header.setLeading(FXUtils.newBuiltinImage("/assets/img/terracotta.png"));
|
||||
header.setTitle(i18n("terracotta.from_local.title"));
|
||||
header.setSubtitle(i18n("terracotta.from_local.desc"));
|
||||
locals.setHeaderLeft(header);
|
||||
locals.setLargeTitle(true);
|
||||
locals.setLeading(new ImageView(FXUtils.newBuiltinImage("/assets/img/terracotta.png")));
|
||||
locals.setTitle(i18n("terracotta.from_local.title"));
|
||||
locals.setSubtitle(i18n("terracotta.from_local.desc"));
|
||||
|
||||
for (TerracottaMetadata.Link link : TerracottaMetadata.PACKAGE_LINKS) {
|
||||
LineButton item = new LineButton();
|
||||
|
||||
@@ -514,15 +514,23 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
pane.setCenter(spinnerPane);
|
||||
{
|
||||
spinnerPane.loadingProperty().bind(getSkinnable().loadingProperty());
|
||||
spinnerPane.failedReasonProperty().bind(Bindings.createStringBinding(() -> {
|
||||
if (getSkinnable().isFailed()) {
|
||||
return i18n("download.failed.refresh");
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, getSkinnable().failedProperty()));
|
||||
spinnerPane.failedReasonProperty().bind(
|
||||
Bindings.createStringBinding(() -> {
|
||||
if (getSkinnable().isFailed()) {
|
||||
return i18n("download.failed.refresh");
|
||||
} else if (!getSkinnable().isLoading() && getSkinnable().pageCount.get() >= 0 && getSkinnable().items.isEmpty()) {
|
||||
return i18n("download.failed.no_results_found");
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
getSkinnable().failedProperty(),
|
||||
getSkinnable().loadingProperty(),
|
||||
getSkinnable().pageCount,
|
||||
getSkinnable().items)
|
||||
);
|
||||
spinnerPane.setOnFailedAction(e -> {
|
||||
if (getSkinnable().retrySearch != null) {
|
||||
if (getSkinnable().isFailed() && getSkinnable().retrySearch != null) {
|
||||
getSkinnable().retrySearch.run();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -19,6 +19,8 @@ package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
@@ -35,25 +37,35 @@ public class GameAdvancedListItem extends AdvancedListItem {
|
||||
private final ImageContainer imageContainer;
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
private HMCLGameRepository repository;
|
||||
@SuppressWarnings("unused")
|
||||
@SuppressWarnings({"unused", "FieldCanBeLocal"})
|
||||
private Consumer<Event> onVersionIconChangedListener;
|
||||
|
||||
@SuppressWarnings({"unused", "FieldCanBeLocal"})
|
||||
private Consumer<RefreshedVersionsEvent> onRefreshedVersionsListener;
|
||||
|
||||
public GameAdvancedListItem() {
|
||||
this.imageContainer = new ImageContainer(LEFT_GRAPHIC_SIZE);
|
||||
imageContainer.setMouseTransparent(true);
|
||||
AdvancedListItem.setAlignment(imageContainer, Pos.CENTER);
|
||||
setLeftGraphic(imageContainer);
|
||||
|
||||
holder.add(FXUtils.onWeakChangeAndOperate(GameDirectoryManager.selectedInstanceProperty(), this::loadVersion));
|
||||
holder.add(FXUtils.onWeakChangeAndOperate(GameDirectoryManager.selectedInstanceProperty(), it -> this.loadVersion()));
|
||||
}
|
||||
|
||||
private void loadVersion(String version) {
|
||||
if (GameDirectoryManager.getSelectedRepository() != repository) {
|
||||
private void loadVersion() {
|
||||
String version = GameDirectoryManager.getSelectedInstance();
|
||||
|
||||
boolean repositoryChanged = GameDirectoryManager.getSelectedRepository() != repository;
|
||||
if (repositoryChanged) {
|
||||
repository = GameDirectoryManager.getSelectedRepository();
|
||||
if (repository != null) {
|
||||
onVersionIconChangedListener = repository.onVersionIconChanged.registerWeak(event -> {
|
||||
this.loadVersion(repository.getSelectedInstance());
|
||||
});
|
||||
onVersionIconChangedListener = repository.onVersionIconChanged.registerWeak(event -> {
|
||||
FXUtils.runInFX(this::loadVersion);
|
||||
});
|
||||
|
||||
if (!repository.isLoaded()) {
|
||||
onRefreshedVersionsListener = EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class)
|
||||
.registerWeak(event -> FXUtils.runInFX(this::loadVersion));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (version != null && repository != null && repository.hasVersion(version)) {
|
||||
|
||||
@@ -80,6 +80,7 @@ public final class GameListCell extends ListCell<GameListItem> {
|
||||
|
||||
{
|
||||
HBox center = new HBox();
|
||||
BorderPane.setMargin(center, new Insets(0, 0, 0, 8));
|
||||
center.setMouseTransparent(true);
|
||||
root.setCenter(center);
|
||||
center.setPrefWidth(Region.USE_PREF_SIZE);
|
||||
|
||||
@@ -51,6 +51,12 @@ public final class GameListPopupMenu extends StackPane {
|
||||
public static void show(Node owner, JFXPopup.PopupVPosition vAlign, JFXPopup.PopupHPosition hAlign,
|
||||
double initOffsetX, double initOffsetY,
|
||||
HMCLGameRepository repository, List<Version> versions) {
|
||||
showAndGetPopup(owner, vAlign, hAlign, initOffsetX, initOffsetY, repository, versions);
|
||||
}
|
||||
|
||||
public static JFXPopup showAndGetPopup(Node owner, JFXPopup.PopupVPosition vAlign, JFXPopup.PopupHPosition hAlign,
|
||||
double initOffsetX, double initOffsetY,
|
||||
HMCLGameRepository repository, List<Version> versions) {
|
||||
GameListPopupMenu menu = new GameListPopupMenu();
|
||||
menu.getItems().setAll(versions.stream()
|
||||
.filter(it -> repository.hasVersion(it.getId()))
|
||||
@@ -58,6 +64,8 @@ public final class GameListPopupMenu extends StackPane {
|
||||
.toList());
|
||||
JFXPopup popup = new JFXPopup(menu);
|
||||
popup.show(owner, vAlign, hAlign, initOffsetX, initOffsetY);
|
||||
|
||||
return popup;
|
||||
}
|
||||
|
||||
private final JFXListView<GameItem> listView = new JFXListView<>();
|
||||
|
||||
@@ -163,7 +163,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
}
|
||||
|
||||
// Sinytra Connector
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && modManager.hasMod("connectormod", ModLoaderType.NEO_FORGE)
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && (modManager.hasMod("connector", ModLoaderType.NEO_FORGE) || modManager.hasMod("connectormod", ModLoaderType.NEO_FORGE))
|
||||
|| "1.20.1".equals(gameVersion) && analyzer.has(LibraryAnalyzer.LibraryType.FORGE) && modManager.hasMod("connectormod", ModLoaderType.FORGE)) {
|
||||
supportedLoaders.add(ModLoaderType.FABRIC);
|
||||
}
|
||||
|
||||
@@ -328,46 +328,6 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
iconPaths.add(this.localModFile.getLogoPath());
|
||||
}
|
||||
|
||||
iconPaths.addAll(List.of(
|
||||
"icon.png",
|
||||
"logo.png",
|
||||
"mod_logo.png",
|
||||
"pack.png",
|
||||
"logoFile.png",
|
||||
"assets/icon.png",
|
||||
"assets/logo.png",
|
||||
"assets/mod_icon.png",
|
||||
"assets/mod_logo.png",
|
||||
"META-INF/icon.png",
|
||||
"META-INF/logo.png",
|
||||
"META-INF/mod_icon.png",
|
||||
"textures/icon.png",
|
||||
"textures/logo.png",
|
||||
"textures/mod_icon.png",
|
||||
"resources/icon.png",
|
||||
"resources/logo.png",
|
||||
"resources/mod_icon.png"
|
||||
));
|
||||
|
||||
String modId = this.localModFile.getId();
|
||||
if (StringUtils.isNotBlank(modId)) {
|
||||
iconPaths.addAll(List.of(
|
||||
"assets/" + modId + "/icon.png",
|
||||
"assets/" + modId + "/logo.png",
|
||||
"assets/" + modId.replace("-", "") + "/icon.png",
|
||||
"assets/" + modId.replace("-", "") + "/logo.png",
|
||||
modId + ".png",
|
||||
modId + "-logo.png",
|
||||
modId + "-icon.png",
|
||||
modId + "_logo.png",
|
||||
modId + "_icon.png",
|
||||
"textures/" + modId + "/icon.png",
|
||||
"textures/" + modId + "/logo.png",
|
||||
"resources/" + modId + "/icon.png",
|
||||
"resources/" + modId + "/logo.png"
|
||||
));
|
||||
}
|
||||
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(this.localModFile.getFile())) {
|
||||
for (String path : iconPaths) {
|
||||
Path iconPath = fs.getPath(path);
|
||||
@@ -562,7 +522,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
private static final PseudoClass WARNING = PseudoClass.getPseudoClass("warning");
|
||||
|
||||
JFXCheckBox checkBox = new JFXCheckBox();
|
||||
ImageContainer imageContainer = new ImageContainer(24);
|
||||
ImageContainer imageContainer = new ImageContainer(32);
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
JFXButton restoreButton = FXUtils.newToggleButton4(SVG.RESTORE);
|
||||
JFXButton infoButton = FXUtils.newToggleButton4(SVG.INFO);
|
||||
|
||||
@@ -471,7 +471,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
private final ResourcePackListPage page;
|
||||
|
||||
private final JFXCheckBox checkBox;
|
||||
private final ImageContainer imageContainer = new ImageContainer(24);
|
||||
private final ImageContainer imageContainer = new ImageContainer(32);
|
||||
private final TwoLineListItem content = new TwoLineListItem();
|
||||
private final JFXButton btnReveal = FXUtils.newToggleButton4(SVG.FOLDER);
|
||||
private final JFXButton btnInfo = FXUtils.newToggleButton4(SVG.INFO);
|
||||
|
||||
@@ -314,8 +314,13 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
|
||||
@Override
|
||||
protected void updateItem(World world, boolean empty) {
|
||||
World oldWorld = getItem();
|
||||
boolean oldEmpty = isEmpty();
|
||||
|
||||
super.updateItem(world, empty);
|
||||
|
||||
if (oldWorld == world && oldEmpty == empty) return;
|
||||
|
||||
this.content.getTags().clear();
|
||||
|
||||
if (empty || world == null) {
|
||||
|
||||
@@ -34,7 +34,6 @@ import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
||||
public final class AprilFools {
|
||||
|
||||
private static final boolean ENABLED;
|
||||
private static final boolean SHOW_APRIL_FOOLS_SETTINGS;
|
||||
|
||||
static {
|
||||
var date = LocalDate.now();
|
||||
@@ -58,14 +57,6 @@ public final class AprilFools {
|
||||
aprilFoolsMode = date.getMonth() == Month.APRIL && date.getDayOfMonth() == 1;
|
||||
|
||||
ENABLED = aprilFoolsMode && !settings().disableAprilFoolsProperty().get();
|
||||
SHOW_APRIL_FOOLS_SETTINGS = aprilFoolsMode || supportedRegion && date.getMonth() == Month.MARCH && date.getDayOfMonth() > 30;
|
||||
}
|
||||
|
||||
/// Whether April Fools settings should be shown.
|
||||
///
|
||||
/// This method returns true if April Fools settings should be shown.
|
||||
public static boolean isShowAprilFoolsSettings() {
|
||||
return SHOW_APRIL_FOOLS_SETTINGS;
|
||||
}
|
||||
|
||||
/// Whether April Fools is enabled.
|
||||
|
||||
@@ -78,7 +78,7 @@ public final class NativePatcher {
|
||||
JavaRuntime javaVersion,
|
||||
GameSettings.Effective settings,
|
||||
List<String> javaArguments) {
|
||||
if (settings.get(GameSettings::useCustomNativesProperty)) {
|
||||
if (settings.getInheritable(GameSettings::useCustomNativesProperty)) {
|
||||
if (gameVersion != null && GameVersionNumber.compare(gameVersion, "1.19") < 0)
|
||||
return version;
|
||||
|
||||
@@ -96,8 +96,8 @@ public final class NativePatcher {
|
||||
return version.setLibraries(newLibraries);
|
||||
}
|
||||
|
||||
final boolean useNativeGLFW = settings.get(GameSettings::useNativeGLFWProperty);
|
||||
final boolean useNativeOpenAL = settings.get(GameSettings::useNativeOpenALProperty);
|
||||
final boolean useNativeGLFW = settings.getInheritable(GameSettings::useNativeGLFWProperty);
|
||||
final boolean useNativeOpenAL = settings.getInheritable(GameSettings::useNativeOpenALProperty);
|
||||
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && (useNativeGLFW || useNativeOpenAL)
|
||||
&& gameVersion != null && GameVersionNumber.compare(gameVersion, "1.19") >= 0) {
|
||||
@@ -124,7 +124,7 @@ public final class NativePatcher {
|
||||
Architecture arch = javaVersion.getArchitecture();
|
||||
GameVersionNumber gameVersionNumber = gameVersion != null ? GameVersionNumber.asGameVersion(gameVersion) : null;
|
||||
|
||||
if (settings.get(GameSettings::notPatchNativesProperty))
|
||||
if (settings.getInheritable(GameSettings::notPatchNativesProperty))
|
||||
return version;
|
||||
|
||||
if (arch.isX86() && (os == OperatingSystem.WINDOWS || os == OperatingSystem.LINUX || os == OperatingSystem.MACOS))
|
||||
|
||||
@@ -7,14 +7,17 @@
|
||||
}
|
||||
|
||||
.log-window {
|
||||
-fixed-log-background: #282828;
|
||||
-fixed-log-toggle-selected: #DEE2E6;
|
||||
-fixed-log-toggle-unselected: #6C757D;
|
||||
-fixed-log-text-fill: #FFFFFF;
|
||||
-fixed-log-trace: #495057;
|
||||
-fixed-log-debug: #343A40;
|
||||
-fixed-log-info: #212529;
|
||||
-fixed-log-warn: #331904;
|
||||
-fixed-log-error: #58151C;
|
||||
-fixed-log-fatal: #842029;
|
||||
-fixed-log-selected: #6C757D;
|
||||
-fixed-log-text-fill: #e3e3e3;
|
||||
-fixed-log-trace-background: #495057;
|
||||
-fixed-log-debug-background: #343A40;
|
||||
-fixed-log-info-background: #212529;
|
||||
-fixed-log-warn-background: #413c26;
|
||||
-fixed-log-warn-text-fill: #fdf3aa;
|
||||
-fixed-log-error-background: #4e3534;
|
||||
-fixed-log-error-text-fill: #f9dedc;
|
||||
-fixed-log-fatal-background: #842029;
|
||||
-fixed-log-selected: #3e3e3e;
|
||||
}
|
||||
@@ -7,14 +7,17 @@
|
||||
}
|
||||
|
||||
.log-window {
|
||||
-fixed-log-background: #ffffff;
|
||||
-fixed-log-toggle-selected: #000000;
|
||||
-fixed-log-toggle-unselected: #6C757D;
|
||||
-fixed-log-text-fill: #000000;
|
||||
-fixed-log-trace: #EEE9E0;
|
||||
-fixed-log-debug: #EEE9E0;
|
||||
-fixed-log-info: #FFFFFF;
|
||||
-fixed-log-warn: #FFEECC;
|
||||
-fixed-log-error: #FFCCBB;
|
||||
-fixed-log-fatal: #F7A699;
|
||||
-fixed-log-selected: #C4C4C4;
|
||||
-fixed-log-text-fill: #1f1f1f;
|
||||
-fixed-log-trace-background: #EEE9E0;
|
||||
-fixed-log-debug-background: #EEE9E0;
|
||||
-fixed-log-info-background: #FFFFFF;
|
||||
-fixed-log-warn-background: #fef6d5;
|
||||
-fixed-log-warn-text-fill: #3e2f00;
|
||||
-fixed-log-error-background: #fcebeb;
|
||||
-fixed-log-error-text-fill: #410e0b;
|
||||
-fixed-log-fatal-background: #F7A699;
|
||||
-fixed-log-selected: #f2f2f2;
|
||||
}
|
||||
@@ -396,6 +396,10 @@
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
.installer-item-status:incompatible {
|
||||
-fx-opacity: 0.6;
|
||||
}
|
||||
|
||||
.installer-item-wrapper {
|
||||
-fx-background-color: -monet-surface;
|
||||
-fx-background-radius: 4;
|
||||
@@ -448,16 +452,24 @@
|
||||
}
|
||||
|
||||
.launch-pane > .jfx-button.launch-button {
|
||||
-fx-max-width: 207px;
|
||||
-fx-min-width: 207px;
|
||||
-fx-max-width: 200px;
|
||||
-fx-min-width: 200px;
|
||||
-fx-border-width: 0 3px 0 0;
|
||||
-fx-border-color: -monet-on-surface-variant;
|
||||
-fx-border-color: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
transparent 20%,
|
||||
-monet-on-surface-variant 20%,
|
||||
-monet-on-surface-variant 80%,
|
||||
transparent 80%,
|
||||
transparent 100%
|
||||
);
|
||||
-fx-background-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
.launch-pane > .jfx-button.menu-button {
|
||||
-fx-max-width: 20px;
|
||||
-fx-min-width: 20px;
|
||||
-fx-max-width: 27px;
|
||||
-fx-min-width: 27px;
|
||||
-fx-font-size: 15px;
|
||||
-fx-background-radius: 0 4px 4px 0;
|
||||
}
|
||||
@@ -1382,27 +1394,27 @@
|
||||
}
|
||||
|
||||
.log-toggle.fatal {
|
||||
-fx-background-color: -fixed-log-fatal;
|
||||
-fx-background-color: -fixed-log-fatal-background;
|
||||
}
|
||||
|
||||
.log-toggle.error {
|
||||
-fx-background-color: -fixed-log-error;
|
||||
-fx-background-color: -fixed-log-error-background;
|
||||
}
|
||||
|
||||
.log-toggle.warn {
|
||||
-fx-background-color: -fixed-log-warn;
|
||||
-fx-background-color: -fixed-log-warn-background;
|
||||
}
|
||||
|
||||
.log-toggle.info {
|
||||
-fx-background-color: -fixed-log-info;
|
||||
-fx-background-color: -fixed-log-info-background;
|
||||
}
|
||||
|
||||
.log-toggle.debug {
|
||||
-fx-background-color: -fixed-log-debug;
|
||||
-fx-background-color: -fixed-log-debug-background;
|
||||
}
|
||||
|
||||
.log-toggle.trace {
|
||||
-fx-background-color: -fixed-log-trace;
|
||||
-fx-background-color: -fixed-log-trace-background;
|
||||
}
|
||||
|
||||
.log-window-list-cell:selected {
|
||||
@@ -1410,7 +1422,7 @@
|
||||
}
|
||||
|
||||
.log-window {
|
||||
-fx-background-color: -monet-surface-container;
|
||||
-fx-background-color: -fixed-log-background;
|
||||
}
|
||||
|
||||
.log-window .scroll-bar .thumb {
|
||||
@@ -1433,36 +1445,36 @@
|
||||
|
||||
.log-window-list-cell:fatal {
|
||||
-fx-text-fill: -fixed-log-text-fill;
|
||||
-fx-background-color: -fixed-log-fatal;
|
||||
-fx-background-color: -fixed-log-fatal-background;
|
||||
}
|
||||
|
||||
.log-window-list-cell:error {
|
||||
-fx-text-fill: -fixed-log-text-fill;
|
||||
-fx-background-color: -fixed-log-error;
|
||||
-fx-text-fill: -fixed-log-error-text-fill;
|
||||
-fx-background-color: -fixed-log-error-background;
|
||||
}
|
||||
|
||||
.log-window-list-cell:warn {
|
||||
-fx-text-fill: -fixed-log-text-fill;
|
||||
-fx-background-color: -fixed-log-warn;
|
||||
-fx-text-fill: -fixed-log-warn-text-fill;
|
||||
-fx-background-color: -fixed-log-warn-background;
|
||||
}
|
||||
|
||||
.log-window-list-cell:info {
|
||||
-fx-text-fill: -fixed-log-text-fill;
|
||||
-fx-background-color: -fixed-log-info;
|
||||
-fx-background-color: -fixed-log-info-background;
|
||||
}
|
||||
|
||||
.log-window-list-cell {
|
||||
-fx-background-color: -fixed-log-info;
|
||||
-fx-background-color: -fixed-log-info-background;
|
||||
}
|
||||
|
||||
.log-window-list-cell:debug {
|
||||
-fx-text-fill: -fixed-log-text-fill;
|
||||
-fx-background-color: -fixed-log-debug;
|
||||
-fx-background-color: -fixed-log-debug-background;
|
||||
}
|
||||
|
||||
.log-window-list-cell:trace {
|
||||
-fx-text-fill: -fixed-log-text-fill;
|
||||
-fx-background-color: -fixed-log-trace;
|
||||
-fx-background-color: -fixed-log-trace-background;
|
||||
}
|
||||
|
||||
.log-window-list-cell:selected {
|
||||
@@ -1581,7 +1593,7 @@
|
||||
-fx-background-color: TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT;
|
||||
-fx-background-radius: 3px;
|
||||
-fx-background-insets: 0px;
|
||||
-fx-min-height: 25px;
|
||||
-fx-min-height: 36px;
|
||||
}
|
||||
|
||||
.color-palette-region {
|
||||
|
||||
@@ -399,6 +399,7 @@ download.external_link=Visit Download Website
|
||||
download.failed=Failed to download "%1$s", response code: %2$d.
|
||||
download.failed.empty=No versions are available. Please click here to go back.
|
||||
download.failed.no_code=Failed to download
|
||||
download.failed.no_results_found=No results found
|
||||
download.failed.refresh=Failed to fetch version list. Please click here to retry.
|
||||
download.game=New Game
|
||||
download.provider.bmclapi=BMCLAPI & MCIM
|
||||
@@ -487,7 +488,7 @@ fatal.samba=If you opened Hello Minecraft! Launcher from a Samba network drive,
|
||||
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" in "Global/Instance-specific Settings → Advanced Settings" to use CPU rendering for better compatibility.
|
||||
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\
|
||||
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\
|
||||
@@ -1374,6 +1375,7 @@ settings.advanced.game_dir.default=Default (".minecraft/")
|
||||
settings.advanced.game_dir.independent=Isolated (".minecraft/versions/<instance name>/", except for assets and libraries)
|
||||
settings.advanced.graphics=Graphics Settings
|
||||
settings.advanced.graphics_backend=Graphics API
|
||||
settings.advanced.graphics_backend.desc=Only effective for Minecraft 26.2+
|
||||
settings.advanced.graphics_backend.default=Default
|
||||
settings.advanced.graphics_backend.default.desc=Follow the game's settings
|
||||
settings.advanced.graphics_backend.opengl=OpenGL
|
||||
@@ -1534,7 +1536,7 @@ settings.game_directories.read_only=The game directory file was saved by a newer
|
||||
|
||||
settings.launcher=Launcher Settings
|
||||
settings.launcher.allow_auto_agent=Allow HMCL to modify the game
|
||||
settings.launcher.allow_auto_agent.subtitle=Allow HMCL to modify the game by attaching a Java Agent to improve the gaming experience.
|
||||
settings.launcher.allow_auto_agent.subtitle=Allow HMCL to modify the game by attaching a Java Agent to improve the gaming experience
|
||||
settings.launcher.animation=Animation
|
||||
settings.launcher.appearance=Appearance
|
||||
settings.launcher.brightness=Theme Mode
|
||||
@@ -1546,8 +1548,9 @@ settings.launcher.debug=Debug
|
||||
settings.launcher.disable_april_fools=Do not enable April Fools features
|
||||
settings.launcher.disable_auto_game_options=Do not switch game language
|
||||
settings.launcher.download=Download
|
||||
settings.launcher.download.threads=Threads
|
||||
settings.launcher.download.threads=Download Threads
|
||||
settings.launcher.download.threads.auto=Automatically Determine
|
||||
settings.launcher.download.threads.custom=Custom
|
||||
settings.launcher.download.threads.hint=Too many threads may cause your system to freeze, and your download speed may be affected by your ISP and download servers. It is not always the case that more threads increase your download speed.
|
||||
settings.launcher.download_source=Download Source
|
||||
settings.launcher.download_source.auto=Automatically Choose Download Sources
|
||||
@@ -1603,16 +1606,16 @@ settings.launcher.theme_color_type.background=Follow Background Wallpaper
|
||||
settings.launcher.theme_color_type.custom=Custom Color
|
||||
settings.launcher.theme_color_type.custom.description=Use the color selected above.
|
||||
settings.launcher.theme_color_type.default=Default
|
||||
settings.launcher.theme_color_type.system=Follow System
|
||||
settings.launcher.title_transparent=Transparent Titlebar
|
||||
settings.launcher.turn_off_animations=Disable Animation
|
||||
settings.launcher.version_list_source=Version List
|
||||
settings.launcher.window_transparent=Transparent Window
|
||||
settings.launcher.background.settings.opacity=Opacity
|
||||
|
||||
settings.memory=Memory
|
||||
settings.memory.allocate.auto=%1$.1f GiB Minimum / %2$.1f GiB Allocated
|
||||
settings.memory.allocate.auto.exceeded=%1$.1f GiB Minimum / %2$.1f GiB Allocated (%3$.1f GiB Available)
|
||||
settings.memory.allocate.manual=%1$.1f GiB Allocated
|
||||
settings.memory.allocate.manual.exceeded=%1$.1f GiB Allocated (%3$.1f GiB Available)
|
||||
settings.memory.allocate=%1$.1f GiB Allocated
|
||||
settings.memory.allocate.exceeded=%1$.1f GiB Allocated (%2$.1f GiB Available)
|
||||
settings.memory.auto_allocate=Automatically Allocate
|
||||
settings.memory.lower_bound=Minimum Memory
|
||||
settings.memory.manual_allocate=Manually Allocate Memory
|
||||
@@ -1631,11 +1634,14 @@ settings.type.global.preset=Preset
|
||||
settings.type.global.preset.create=Create Preset
|
||||
settings.type.global.preset.default=Use Default Preset
|
||||
settings.type.global.preset.duplicate_name=Preset name already exists
|
||||
settings.type.global.preset.manage_all=Default Preset
|
||||
settings.type.global.preset.help=A game settings preset manages a set of game settings that you can adjust on this page.\n\
|
||||
All instances inherit game settings from the current preset by default. You can override specific settings on the instance game settings page.
|
||||
settings.type.global.preset.manage_all=Current Preset
|
||||
settings.type.global.preset.name=Preset Name
|
||||
settings.type.global.preset.rename=Rename Preset
|
||||
settings.type.global.preset.remove=Delete Preset
|
||||
settings.type.global.preset.remove.confirm=Are you sure you want to delete preset "%s"? Instances using this preset will fall back to the default preset.
|
||||
settings.type.global.preset.subtitle=The current instance will inherit game settings from the specified preset
|
||||
settings.type.global.preset.auto_name=Preset %d
|
||||
|
||||
shaderpack.download.title=Download Shader - %1s
|
||||
@@ -1749,6 +1755,7 @@ theme_pack.export.id=Theme Pack ID
|
||||
theme_pack.export.name=Theme Name
|
||||
theme_pack.export.subtitle=Save the current launcher appearance as a theme-pack file.
|
||||
theme_pack.export.success=Theme pack has been saved to "%s".
|
||||
theme_pack.export.version=Theme Version
|
||||
theme_pack.export.title=Save theme pack
|
||||
theme_pack.file=HMCL Theme Pack
|
||||
theme_pack.import=Import Theme Pack
|
||||
|
||||
@@ -1428,10 +1428,8 @@ settings.launcher.version_list_source=قائمة الإصدارات
|
||||
settings.launcher.background.settings.opacity=الشفافية
|
||||
|
||||
settings.memory=الذاكرة
|
||||
settings.memory.allocate.auto=%1$.1f جيجابايت كحد أدنى / %2$.1f جيجابايت مخصص
|
||||
settings.memory.allocate.auto.exceeded=%1$.1f جيجابايت كحد أدنى / %2$.1f جيجابايت مخصص (%3$.1f جيجابايت متاح)
|
||||
settings.memory.allocate.manual=%1$.1f جيجابايت مخصص
|
||||
settings.memory.allocate.manual.exceeded=%1$.1f جيجابايت مخصص (%3$.1f جيجابايت متاح)
|
||||
settings.memory.allocate=%1$.1f جيجابايت مخصص
|
||||
settings.memory.allocate.exceeded=%1$.1f جيجابايت مخصص (%2$.1f جيجابايت متاح)
|
||||
settings.memory.auto_allocate=تخصيص تلقائي
|
||||
settings.memory.lower_bound=الحد الأدنى للذاكرة
|
||||
settings.memory.unit.mib=ميجابايت
|
||||
|
||||
@@ -1329,10 +1329,8 @@ settings.launcher.version_list_source=Lista de versiones
|
||||
settings.launcher.background.settings.opacity=Opacidad
|
||||
|
||||
settings.memory=Memoria
|
||||
settings.memory.allocate.auto=%1$.1f GiB Mínimo / %2$.1f GiB Asignado
|
||||
settings.memory.allocate.auto.exceeded=%1$.1f GiB Mínimo / %2$.1f GiB Asignado (%3$.1f GiB Disponible)
|
||||
settings.memory.allocate.manual=%1$.1f GiB Asignados
|
||||
settings.memory.allocate.manual.exceeded=%1$.1f GiB Asignados (%3$.1f GiB Disponibles)
|
||||
settings.memory.allocate=%1$.1f GiB Asignados
|
||||
settings.memory.allocate.exceeded=%1$.1f GiB Asignados (%2$.1f GiB Disponibles)
|
||||
settings.memory.auto_allocate=Asignar automáticamente
|
||||
settings.memory.lower_bound=Memoria mínima
|
||||
settings.memory.unit.mib=MiB
|
||||
|
||||
@@ -856,10 +856,8 @@ settings.launcher.title_transparent=タイトルの透明性
|
||||
settings.launcher.version_list_source=バージョンリストソース
|
||||
|
||||
settings.memory=ゲームメモリ
|
||||
settings.memory.allocate.auto=最小 %1$.1f GiB /実際の %2$.1f GiB
|
||||
settings.memory.allocate.auto.exceeded=最小 %1$.1f GiB /実際の %2$.1f GiB(%3$.1f GiB使用可能)
|
||||
settings.memory.allocate.manual=割り当て %1$.1f GiB
|
||||
settings.memory.allocate.manual.exceeded=割り当て %1$.1f GiB(%3$.1f GiB使用可能)
|
||||
settings.memory.allocate=割り当て %1$.1f GiB
|
||||
settings.memory.allocate.exceeded=割り当て %1$.1f GiB(%2$.1f GiB使用可能)
|
||||
settings.memory.auto_allocate=メモリサイズの自動割り当て
|
||||
settings.memory.lower_bound=最小メモリ
|
||||
settings.memory.unit.mib=MiB
|
||||
|
||||
@@ -1110,7 +1110,7 @@ settings.icon=戲徵
|
||||
|
||||
settings.launcher=啟者置設
|
||||
settings.launcher.allow_auto_agent=許改戲
|
||||
settings.launcher.allow_auto_agent.subtitle=許 HMCL 假附加爪哇 Agent 以改戲,而優其戲感。
|
||||
settings.launcher.allow_auto_agent.subtitle=許 HMCL 假附加爪哇 Agent 以改戲,而優其戲感
|
||||
settings.launcher.appearance=外觀
|
||||
settings.launcher.common_path.tooltip=啟者置戲資與府庫於是。倘戲有成案,則弗取公庫之案。
|
||||
settings.launcher.debug=勘誤
|
||||
@@ -1152,10 +1152,8 @@ settings.launcher.version_list_source=版列供者
|
||||
settings.launcher.background.settings.opacity=陰翳
|
||||
|
||||
settings.memory=戲憶
|
||||
settings.memory.allocate.auto=至低分之 %1$.1f GiB / 實分之 %2$.1f GiB
|
||||
settings.memory.allocate.auto.exceeded=至低分之 %1$.1f GiB / 實分之 %2$.1f GiB (械祇 %3$.1f GiB)
|
||||
settings.memory.allocate.manual=戲分之 %1$.1f GiB
|
||||
settings.memory.allocate.manual.exceeded=戲分之 %1$.1f GiB (械祇 %3$.1f GiB)
|
||||
settings.memory.allocate=戲分之 %1$.1f GiB
|
||||
settings.memory.allocate.exceeded=戲分之 %1$.1f GiB (械祇 %2$.1f GiB)
|
||||
settings.memory.auto_allocate=自調其憶
|
||||
settings.memory.lower_bound=至低憶界
|
||||
settings.memory.unit.mib=兆二進字節
|
||||
|
||||
@@ -1318,10 +1318,8 @@ settings.launcher.version_list_source=Список версий
|
||||
settings.launcher.background.settings.opacity=Непрозрачность
|
||||
|
||||
settings.memory=Оперативная память
|
||||
settings.memory.allocate.auto=Минимум %1$.1f ГиБ / Выделено %2$.1f ГиБ
|
||||
settings.memory.allocate.auto.exceeded=Минимум %1$.1f ГиБ / Выделено %2$.1f ГиБ (Доступно %3$.1f ГиБ)
|
||||
settings.memory.allocate.manual=Выделено %1$.1f ГиБ
|
||||
settings.memory.allocate.manual.exceeded=Выделено %1$.1f ГиБ (Доступно %3$.1f ГиБ)
|
||||
settings.memory.allocate=Выделено %1$.1f ГиБ
|
||||
settings.memory.allocate.exceeded=Выделено %1$.1f ГиБ (Доступно %2$.1f ГиБ)
|
||||
settings.memory.auto_allocate=Автовыделение
|
||||
settings.memory.lower_bound=Мин. объём памяти
|
||||
settings.memory.unit.mib=МиБ
|
||||
|
||||
@@ -1267,10 +1267,8 @@ settings.launcher.version_list_source=Список версій
|
||||
settings.launcher.background.settings.opacity=Непрозорість
|
||||
|
||||
settings.memory=Пам'ять
|
||||
settings.memory.allocate.auto=%1$.1f ГіБ Мінімум / %2$.1f ГіБ Виділено
|
||||
settings.memory.allocate.auto.exceeded=%1$.1f ГіБ Мінімум / %2$.1f ГіБ Виділено (%3$.1f ГіБ Доступно)
|
||||
settings.memory.allocate.manual=%1$.1f ГіБ Виділено
|
||||
settings.memory.allocate.manual.exceeded=%1$.1f ГіБ Виділено (%3$.1f ГіБ Доступно)
|
||||
settings.memory.allocate=%1$.1f ГіБ Виділено
|
||||
settings.memory.allocate.exceeded=%1$.1f ГіБ Виділено (%2$.1f ГіБ Доступно)
|
||||
settings.memory.auto_allocate=Автоматично виділяти
|
||||
settings.memory.lower_bound=Мінімальна пам'ять
|
||||
settings.memory.unit.mib=МіБ
|
||||
|
||||
@@ -397,6 +397,7 @@ download.external_link=開啟下載網站
|
||||
download.failed=下載失敗:%1$s\n錯誤碼:%2$d
|
||||
download.failed.empty=沒有可供安裝的版本,按一下此處返回。
|
||||
download.failed.no_code=下載失敗
|
||||
download.failed.no_results_found=搜尋無結果
|
||||
download.failed.refresh=載入版本清單失敗,按一下此處重試。
|
||||
download.game=新遊戲
|
||||
download.provider.bmclapi=BMCLAPI & MCIM
|
||||
@@ -457,7 +458,7 @@ fatal.deprecated_java_version.download_link=下載 Java %d
|
||||
fatal.samba=如果您正在透過 Samba 共亯的目錄中開啟 Hello Minecraft! Launcher,啟動器可能無法正常工作,請嘗試更新您的 Java 或在本機目錄內開啟 HMCL。
|
||||
fatal.illegal_char=由於您的執行路徑中存在無效字元『=』,您將無法使用外部登入帳戶以及離線登入更換外觀功能。
|
||||
fatal.unsupported_platform=Minecraft 尚未對您的平臺提供完善支援,所以可能影響遊戲體驗或無法啟動遊戲。\n\
|
||||
若無法啟動 Minecraft 1.17 及更高版本,可以嘗試在「(全域/實例特定) 遊戲設定 → 進階設定 → 除錯選項」中將「繪製器」切換為「Mesa LLVMpipe」,以獲得更好的相容性。
|
||||
若無法啟動 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 安裝相容包。
|
||||
# When translating the following string into non-Latin languages, please retain the English translation
|
||||
@@ -1167,6 +1168,7 @@ settings.advanced.game_dir.default=預設 (".minecraft/")
|
||||
settings.advanced.game_dir.independent=各實例獨立 (".minecraft/versions/<實例名>/",除 assets、libraries 外)
|
||||
settings.advanced.graphics=圖形設定
|
||||
settings.advanced.graphics_backend=圖形 API
|
||||
settings.advanced.graphics_backend.desc=僅對 Minecraft 26.2+ 生效
|
||||
settings.advanced.graphics_backend.default=預設
|
||||
settings.advanced.graphics_backend.default.desc=遵循遊戲設定
|
||||
settings.advanced.graphics_backend.opengl=OpenGL
|
||||
@@ -1292,7 +1294,7 @@ settings.game.inherit_global=目前繼承遊戲設定預設。點擊後使用實
|
||||
settings.game.instance_settings.unsupported=目前遊戲設定檔案是由更高版本的 HMCL 儲存的,目前版本的 HMCL 無法正常載入。
|
||||
settings.game.presets.unsupported=遊戲設定預設檔案是由更高版本的 HMCL 儲存的,目前版本的 HMCL 無法修改全域遊戲設定。
|
||||
settings.game.isolation=版本隔離
|
||||
settings.game.isolation.subtitle=啟用後,目前實例將使用獨立的遊戲執行路徑設定,而不是繼承全域設定。
|
||||
settings.game.isolation.subtitle=啟用後,目前實例將使用獨立的遊戲執行路徑設定,而不是繼承全域設定
|
||||
settings.game.java_directory=遊戲 Java
|
||||
settings.game.java_directory.auto=自動選取合適的 Java
|
||||
settings.game.java_directory.auto.not_found=沒有合適的 Java
|
||||
@@ -1325,7 +1327,7 @@ settings.game_directories.read_only=遊戲目錄設定檔是由更高版本的 H
|
||||
|
||||
settings.launcher=啟動器設定
|
||||
settings.launcher.allow_auto_agent=允許 HMCL 修改遊戲
|
||||
settings.launcher.allow_auto_agent.subtitle=允許 HMCL 透過附加 Java Agent 修改遊戲以改善遊戲體驗。
|
||||
settings.launcher.allow_auto_agent.subtitle=允許 HMCL 透過附加 Java Agent 修改遊戲以改善遊戲體驗
|
||||
settings.launcher.animation=动画
|
||||
settings.launcher.appearance=外觀
|
||||
settings.launcher.brightness=主題模式
|
||||
@@ -1337,12 +1339,13 @@ 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=下載執行緒數
|
||||
settings.launcher.download.threads.auto=自動選取執行緒數
|
||||
settings.launcher.download.threads.custom=自訂執行緒數
|
||||
settings.launcher.download.threads.hint=執行緒數過高可能導致系統卡頓。你的下載速度會受到網際網路運營商、下載來源伺服器等方面的影響。調高下載執行緒數不一定能大幅提升總下載速度。
|
||||
settings.launcher.download_source=下載來源
|
||||
settings.launcher.download_source.auto=自動選取下載來源
|
||||
settings.launcher.default_addon_source=遊戲內容預設下載來源
|
||||
settings.launcher.default_addon_source=附加內容預設下載來源
|
||||
settings.launcher.enable_game_list=在首頁內顯示遊戲清單
|
||||
settings.launcher.misc=雜項
|
||||
settings.launcher.font=字體
|
||||
@@ -1394,16 +1397,16 @@ 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=遊戲記憶體
|
||||
settings.memory.allocate.auto=最低分配 %1$.1f GiB / 實際分配 %2$.1f GiB
|
||||
settings.memory.allocate.auto.exceeded=最低分配 %1$.1f GiB / 實際分配 %2$.1f GiB (%3$.1f GiB 可用)
|
||||
settings.memory.allocate.manual=遊戲分配 %1$.1f GiB
|
||||
settings.memory.allocate.manual.exceeded=遊戲分配 %1$.1f GiB (%3$.1f GiB 可用)
|
||||
settings.memory.allocate=遊戲分配 %1$.1f GiB
|
||||
settings.memory.allocate.exceeded=遊戲分配 %1$.1f GiB (%2$.1f GiB 可用)
|
||||
settings.memory.auto_allocate=自動分配
|
||||
settings.memory.lower_bound=最低分配
|
||||
settings.memory.manual_allocate=手動選取記憶體
|
||||
@@ -1422,11 +1425,14 @@ settings.type.global.preset=預設
|
||||
settings.type.global.preset.create=新增預設
|
||||
settings.type.global.preset.default=使用預設項目
|
||||
settings.type.global.preset.duplicate_name=預設名稱已存在
|
||||
settings.type.global.preset.manage_all=預設項目
|
||||
settings.type.global.preset.help=遊戲設定預設管理著一組遊戲設定,你可以在下方調整這些設定。\n\
|
||||
所有實例預設繼承當前預設中的遊戲設定,你可以在實例遊戲設定頁面中覆蓋特定的設定。
|
||||
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=確定要刪除預設「%s」嗎?使用該預設的實例將改用預設項目。
|
||||
settings.type.global.preset.subtitle=當前實例將繼承指定預設中的遊戲設定
|
||||
settings.type.global.preset.auto_name=預設 %d
|
||||
|
||||
shaderpack.download.title=光影下載 - %1s
|
||||
@@ -1540,6 +1546,7 @@ theme_pack.export.id=主題包 ID
|
||||
theme_pack.export.name=主題名稱
|
||||
theme_pack.export.subtitle=將目前啟動器外觀儲存為主題包檔案
|
||||
theme_pack.export.success=主題包已儲存到「%s」
|
||||
theme_pack.export.version=主題版本
|
||||
theme_pack.export.title=儲存主題包
|
||||
theme_pack.file=HMCL 主題包
|
||||
theme_pack.import=匯入主題包
|
||||
|
||||
@@ -399,6 +399,7 @@ download.external_link=打开下载网站
|
||||
download.failed=下载失败: %1$s,\n错误码:%2$d\n你可以点击右上角帮助按钮进行求助。
|
||||
download.failed.empty=[没有可供安装的版本,点击此处返回]\n(你可以点击右上角帮助按钮进行求助)
|
||||
download.failed.no_code=下载失败
|
||||
download.failed.no_results_found=搜索无结果
|
||||
download.failed.refresh=[加载版本列表失败,点击此处重试]\n(你可以点击右上角帮助按钮进行求助)
|
||||
download.game=新游戏
|
||||
download.provider.bmclapi=BMCLAPI & MCIM
|
||||
@@ -460,7 +461,7 @@ fatal.deprecated_java_version.download_link=下载 Java %d
|
||||
fatal.samba=如果你正在通过 Samba 共享的文件夹中运行 Hello Minecraft! Launcher,启动器可能无法正常工作。请尝试更新你的 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”,以获得更好的兼容性。\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如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
@@ -1172,6 +1173,7 @@ settings.advanced.game_dir.default=默认 (".minecraft/")
|
||||
settings.advanced.game_dir.independent=各实例独立 (存放在 ".minecraft/versions/<实例名>/",除 assets、libraries 外)
|
||||
settings.advanced.graphics=图形设置
|
||||
settings.advanced.graphics_backend=图形 API
|
||||
settings.advanced.graphics_backend.desc=仅对 Minecraft 26.2+ 生效
|
||||
settings.advanced.graphics_backend.default=默认
|
||||
settings.advanced.graphics_backend.default.desc=遵循游戏设置
|
||||
settings.advanced.graphics_backend.opengl=OpenGL
|
||||
@@ -1297,7 +1299,7 @@ settings.game.inherit_global=当前继承游戏设置预设。点击后使用实
|
||||
settings.game.instance_settings.unsupported=当前游戏设置文件是由更高版本的 HMCL 保存的,当前版本的 HMCL 无法正常加载。
|
||||
settings.game.presets.unsupported=游戏设置预设文件是由更高版本的 HMCL 保存的,当前版本的 HMCL 无法修改全局游戏设置。
|
||||
settings.game.isolation=版本隔离
|
||||
settings.game.isolation.subtitle=启用后,当前实例将使用独立的游戏运行路径设置,而不是继承全局设置。
|
||||
settings.game.isolation.subtitle=启用后,当前实例将使用独立的游戏运行路径设置,而不是继承全局设置
|
||||
settings.game.java_directory=游戏 Java
|
||||
settings.game.java_directory.auto=自动选择合适的 Java
|
||||
settings.game.java_directory.auto.not_found=没有合适的 Java
|
||||
@@ -1330,7 +1332,7 @@ settings.game_directories.read_only=游戏文件夹配置文件是由更高版
|
||||
|
||||
settings.launcher=启动器设置
|
||||
settings.launcher.allow_auto_agent=允许 HMCL 修改游戏
|
||||
settings.launcher.allow_auto_agent.subtitle=允许 HMCL 通过附加 Java Agent 修改游戏以改善游戏体验。
|
||||
settings.launcher.allow_auto_agent.subtitle=允许 HMCL 通过附加 Java Agent 修改游戏以改善游戏体验
|
||||
settings.launcher.animation=动画
|
||||
settings.launcher.appearance=外观
|
||||
settings.launcher.brightness=主题模式
|
||||
@@ -1342,12 +1344,13 @@ 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=下载线程数
|
||||
settings.launcher.download.threads.auto=自动选择线程数
|
||||
settings.launcher.download.threads.custom=自定义线程数
|
||||
settings.launcher.download.threads.hint=线程数过高可能导致系统卡顿。你的下载速度会受到互联网运营商、下载源服务器等方面的影响。调高下载线程数不一定能大幅提升总下载速度。
|
||||
settings.launcher.download_source=下载源
|
||||
settings.launcher.download_source.auto=自动选择下载源
|
||||
settings.launcher.default_addon_source=游戏内容默认下载源
|
||||
settings.launcher.default_addon_source=附加内容默认下载源
|
||||
settings.launcher.enable_game_list=在主页内显示版本列表
|
||||
settings.launcher.misc=杂项
|
||||
settings.launcher.font=启动器字体
|
||||
@@ -1399,16 +1402,16 @@ 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=游戏内存
|
||||
settings.memory.allocate.auto=最低分配 %1$.1f GiB / 实际分配 %2$.1f GiB
|
||||
settings.memory.allocate.auto.exceeded=最低分配 %1$.1f GiB / 实际分配 %2$.1f GiB (%3$.1f GiB 可用)
|
||||
settings.memory.allocate.manual=游戏分配 %1$.1f GiB
|
||||
settings.memory.allocate.manual.exceeded=游戏分配 %1$.1f GiB (设备仅 %3$.1f GiB 可用)
|
||||
settings.memory.allocate=游戏分配 %1$.1f GiB
|
||||
settings.memory.allocate.exceeded=游戏分配 %1$.1f GiB (设备仅 %2$.1f GiB 可用)
|
||||
settings.memory.auto_allocate=自动分配内存
|
||||
settings.memory.lower_bound=最低内存分配
|
||||
settings.memory.manual_allocate=手动选择内存
|
||||
@@ -1427,11 +1430,14 @@ settings.type.global.preset=预设
|
||||
settings.type.global.preset.create=新建预设
|
||||
settings.type.global.preset.default=使用默认预设
|
||||
settings.type.global.preset.duplicate_name=预设名称已存在
|
||||
settings.type.global.preset.manage_all=默认预设
|
||||
settings.type.global.preset.help=游戏设置预设管理着一组游戏设置,你可以在下方调整这些设置。\n\
|
||||
所有实例默认继承当前预设中的游戏设置,你可以在实例游戏设置页面中覆盖特定的设置。
|
||||
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=确定要删除预设“%s”吗?使用该预设的实例将回退到默认预设。
|
||||
settings.type.global.preset.subtitle=当前实例将继承指定预设中的游戏设置
|
||||
settings.type.global.preset.auto_name=预设 %d
|
||||
|
||||
shaderpack.download.title=光影下载 - %1s
|
||||
@@ -1545,6 +1551,7 @@ theme_pack.export.id=主题包 ID
|
||||
theme_pack.export.name=主题名称
|
||||
theme_pack.export.subtitle=将当前启动器外观保存为主题包文件
|
||||
theme_pack.export.success=主题包已保存到“%s”
|
||||
theme_pack.export.version=主题版本
|
||||
theme_pack.export.title=保存主题包
|
||||
theme_pack.file=HMCL 主题包
|
||||
theme_pack.import=导入主题包
|
||||
|
||||
@@ -902,6 +902,149 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-sdl:3.4.1:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-sdl:3.4.1:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-sdl/3.4.1/lwjgl-sdl-3.4.1-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-sdl/3.4.1/lwjgl-sdl-3.4.1-natives-linux-arm64.jar",
|
||||
"sha1": "b5a62eb82113b0227077fb270128487a80a20299",
|
||||
"size": 1434288
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl:3.4.2:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl/3.4.2/lwjgl-3.4.2-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl/3.4.2/lwjgl-3.4.2-natives-linux-arm64.jar",
|
||||
"sha1": "a2229c542e410157bc79aead90243bd50dbcd79c",
|
||||
"size": 125841
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-jemalloc:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-jemalloc:3.4.2:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-jemalloc/3.4.2/lwjgl-jemalloc-3.4.2-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-jemalloc/3.4.2/lwjgl-jemalloc-3.4.2-natives-linux-arm64.jar",
|
||||
"sha1": "8e51830cf9077fb0a89a6f43fa09df55c8c0e8ef",
|
||||
"size": 235933
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-openal:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-openal:3.4.2:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-openal/3.4.2/lwjgl-openal-3.4.2-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-openal/3.4.2/lwjgl-openal-3.4.2-natives-linux-arm64.jar",
|
||||
"sha1": "a3c98570beeffd241263e9bc63d8650147b33c4c",
|
||||
"size": 866040
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-opengl:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-opengl:3.4.2:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-opengl/3.4.2/lwjgl-opengl-3.4.2-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-opengl/3.4.2/lwjgl-opengl-3.4.2-natives-linux-arm64.jar",
|
||||
"sha1": "52f6e847226e41f60773a62314cfada4f82b578c",
|
||||
"size": 80853
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-glfw:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-glfw:3.4.2:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-glfw/3.4.2/lwjgl-glfw-3.4.2-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-glfw/3.4.2/lwjgl-glfw-3.4.2-natives-linux-arm64.jar",
|
||||
"sha1": "943ec4651c874e9e0a59724976923c7bd8fceb9f",
|
||||
"size": 144523
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-stb:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-stb:3.4.2:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-stb/3.4.2/lwjgl-stb-3.4.2-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-stb/3.4.2/lwjgl-stb-3.4.2-natives-linux-arm64.jar",
|
||||
"sha1": "30a261559033e22e7eb7f9a4ac1f2ea96d76f4d8",
|
||||
"size": 258999
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-tinyfd:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-tinyfd:3.4.2:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-tinyfd/3.4.2/lwjgl-tinyfd-3.4.2-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-tinyfd/3.4.2/lwjgl-tinyfd-3.4.2-natives-linux-arm64.jar",
|
||||
"sha1": "03b72a19851c1e4fa0ad51a03f78d08f664afc91",
|
||||
"size": 45348
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-freetype:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-freetype:3.4.2:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-freetype/3.4.2/lwjgl-freetype-3.4.2-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-freetype/3.4.2/lwjgl-freetype-3.4.2-natives-linux-arm64.jar",
|
||||
"sha1": "b5fb3db06d1ecb15f54fabda6a4914ae933525ee",
|
||||
"size": 1319451
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-shaderc:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-shaderc:3.4.2:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-shaderc/3.4.2/lwjgl-shaderc-3.4.2-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.2/lwjgl-shaderc-3.4.2-natives-linux-arm64.jar",
|
||||
"sha1": "23403f5c295d946d1d2f7785c551f48be2b2fe9b",
|
||||
"size": 3463790
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-spvc:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-spvc:3.4.2:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-spvc/3.4.2/lwjgl-spvc-3.4.2-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.2/lwjgl-spvc-3.4.2-natives-linux-arm64.jar",
|
||||
"sha1": "8d7b46d68788d217143eee402126ea286d100683",
|
||||
"size": 1166742
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-vma:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-vma:3.4.2:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-vma/3.4.2/lwjgl-vma-3.4.2-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.2/lwjgl-vma-3.4.2-natives-linux-arm64.jar",
|
||||
"sha1": "c6fb35dd852aedf2266d462c9b7142c41a860298",
|
||||
"size": 60868
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-sdl:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-sdl:3.4.2:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-sdl/3.4.2/lwjgl-sdl-3.4.2-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-sdl/3.4.2/lwjgl-sdl-3.4.2-natives-linux-arm64.jar",
|
||||
"sha1": "e723c33b467c5ff2af02942a1459db4e3686ce57",
|
||||
"size": 1455063
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl.lwjgl:lwjgl-platform:2.9.0:natives": {
|
||||
"name": "org.glavo.hmcl:lwjgl2-natives:2.9.3",
|
||||
"downloads": {
|
||||
@@ -2637,6 +2780,182 @@
|
||||
"org.lwjgl:lwjgl-shaderc:3.4.1:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-spvc:3.4.1:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-vma:3.4.1:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-sdl:3.4.1:natives-linux": null,
|
||||
"org.lwjgl:lwjgl:3.4.2": {
|
||||
"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.2: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.2": {
|
||||
"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.2:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-openal:3.4.2": {
|
||||
"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.2:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-opengl:3.4.2": {
|
||||
"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.2:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-glfw:3.4.2": {
|
||||
"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.2:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-stb:3.4.2": {
|
||||
"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.2:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-tinyfd:3.4.2": {
|
||||
"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.2:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-freetype:3.4.2": {
|
||||
"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.2:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-shaderc:3.4.2": {
|
||||
"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.2:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-spvc:3.4.2": {
|
||||
"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.2:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-vma:3.4.2": {
|
||||
"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.2:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-vulkan:3.4.2": {
|
||||
"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.2": {
|
||||
"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.2:natives-linux": null,
|
||||
"net.java.dev.jna:jna:5.8.0": {
|
||||
"name": "net.java.dev.jna:jna:5.13.0",
|
||||
"downloads": {
|
||||
@@ -4384,6 +4703,160 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-sdl:3.4.1": {
|
||||
"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.1:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-sdl:3.4.1:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-sdl/3.4.1/lwjgl-sdl-3.4.1-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-sdl/3.4.1/lwjgl-sdl-3.4.1-natives-linux-riscv64.jar",
|
||||
"sha1": "d443cd6e73467f437d0be30a3ece7b6fe162a10a",
|
||||
"size": 1207531
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl:3.4.2:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl/3.4.2/lwjgl-3.4.2-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl/3.4.2/lwjgl-3.4.2-natives-linux-riscv64.jar",
|
||||
"sha1": "402730ca3ff248480161a9ed3a9fdf8d4fde91c9",
|
||||
"size": 125934
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-jemalloc:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-jemalloc:3.4.2:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-jemalloc/3.4.2/lwjgl-jemalloc-3.4.2-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-jemalloc/3.4.2/lwjgl-jemalloc-3.4.2-natives-linux-riscv64.jar",
|
||||
"sha1": "a77b3d7ff67746d6e564e71d394794060470f8af",
|
||||
"size": 248366
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-openal:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-openal:3.4.2:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-openal/3.4.2/lwjgl-openal-3.4.2-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-openal/3.4.2/lwjgl-openal-3.4.2-natives-linux-riscv64.jar",
|
||||
"sha1": "e2ca54d5de15665bfea2b0565605b7833a1f432d",
|
||||
"size": 938689
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-opengl:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-opengl:3.4.2:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-opengl/3.4.2/lwjgl-opengl-3.4.2-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-opengl/3.4.2/lwjgl-opengl-3.4.2-natives-linux-riscv64.jar",
|
||||
"sha1": "f3daf496bf2a69399dc4e09fe2e2d201893c540f",
|
||||
"size": 79432
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-glfw:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-glfw:3.4.2:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-glfw/3.4.2/lwjgl-glfw-3.4.2-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-glfw/3.4.2/lwjgl-glfw-3.4.2-natives-linux-riscv64.jar",
|
||||
"sha1": "d74e0dcc6378f3113178ca2813901e837f1d1cd5",
|
||||
"size": 141084
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-stb:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-stb:3.4.2:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-stb/3.4.2/lwjgl-stb-3.4.2-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-stb/3.4.2/lwjgl-stb-3.4.2-natives-linux-riscv64.jar",
|
||||
"sha1": "855b70e32183c611e1299af340e415800fd7e1a8",
|
||||
"size": 268104
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-tinyfd:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-tinyfd:3.4.2:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-tinyfd/3.4.2/lwjgl-tinyfd-3.4.2-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-tinyfd/3.4.2/lwjgl-tinyfd-3.4.2-natives-linux-riscv64.jar",
|
||||
"sha1": "7efeb9e482978a26dfbb1997f75dba16111a3479",
|
||||
"size": 56944
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-freetype:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-freetype:3.4.2:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-freetype/3.4.2/lwjgl-freetype-3.4.2-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-freetype/3.4.2/lwjgl-freetype-3.4.2-natives-linux-riscv64.jar",
|
||||
"sha1": "9ff54d6bc88c303ba8b159e70a3ecdb4b0d2264b",
|
||||
"size": 1420614
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-shaderc:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-shaderc:3.4.2:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-shaderc/3.4.2/lwjgl-shaderc-3.4.2-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.2/lwjgl-shaderc-3.4.2-natives-linux-riscv64.jar",
|
||||
"sha1": "fba9ad10d282fd27c5818bb8eeafb6f9310bea6d",
|
||||
"size": 3617389
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-spvc:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-spvc:3.4.2:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-spvc/3.4.2/lwjgl-spvc-3.4.2-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.2/lwjgl-spvc-3.4.2-natives-linux-riscv64.jar",
|
||||
"sha1": "a2f363009b51e8be9b81c0eb003f6b3f49b7e0c9",
|
||||
"size": 1221335
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-vma:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-vma:3.4.2:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-vma/3.4.2/lwjgl-vma-3.4.2-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.2/lwjgl-vma-3.4.2-natives-linux-riscv64.jar",
|
||||
"sha1": "367ae2f56dac78acf1fe634f06247c183e657717",
|
||||
"size": 66535
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-sdl:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-sdl:3.4.2:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-sdl/3.4.2/lwjgl-sdl-3.4.2-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-sdl/3.4.2/lwjgl-sdl-3.4.2-natives-linux-riscv64.jar",
|
||||
"sha1": "89f145ab3fc5a049ea23dcbd045b3a7782744f76",
|
||||
"size": 1494992
|
||||
}
|
||||
}
|
||||
},
|
||||
"com.github.oshi:oshi-core:6.6.5": {
|
||||
"name": "com.github.oshi:oshi-core:6.8.0",
|
||||
"downloads": {
|
||||
@@ -6427,6 +6900,160 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-sdl:3.4.1": {
|
||||
"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.1:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-sdl:3.4.1:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-sdl/3.4.1/lwjgl-sdl-3.4.1-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-sdl/3.4.1/lwjgl-sdl-3.4.1-natives-freebsd.jar",
|
||||
"sha1": "1616d4bd1f5ae03faa9c815c728f74d186526eee",
|
||||
"size": 1241210
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl:3.4.2:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl/3.4.2/lwjgl-3.4.2-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl/3.4.2/lwjgl-3.4.2-natives-freebsd.jar",
|
||||
"sha1": "21ddad3dd00753f11618283192c1bc5736049d33",
|
||||
"size": 110201
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-jemalloc:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-jemalloc:3.4.2:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-jemalloc/3.4.2/lwjgl-jemalloc-3.4.2-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-jemalloc/3.4.2/lwjgl-jemalloc-3.4.2-natives-freebsd.jar",
|
||||
"sha1": "4428d2e872a98a9d176955932be42300d62289a5",
|
||||
"size": 160301
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-openal:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-openal:3.4.2:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-openal/3.4.2/lwjgl-openal-3.4.2-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-openal/3.4.2/lwjgl-openal-3.4.2-natives-freebsd.jar",
|
||||
"sha1": "8634fe3955f934ade0d6e532a05f2d032d236e2b",
|
||||
"size": 851114
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-opengl:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-opengl:3.4.2:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-opengl/3.4.2/lwjgl-opengl-3.4.2-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-opengl/3.4.2/lwjgl-opengl-3.4.2-natives-freebsd.jar",
|
||||
"sha1": "525c36e210f5d1377a5a43340bbfe4410cb92ad4",
|
||||
"size": 83073
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-glfw:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-glfw:3.4.2:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-glfw/3.4.2/lwjgl-glfw-3.4.2-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-glfw/3.4.2/lwjgl-glfw-3.4.2-natives-freebsd.jar",
|
||||
"sha1": "57e4f91a563b1c6922493d464b3959e6d0729650",
|
||||
"size": 108294
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-stb:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-stb:3.4.2:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-stb/3.4.2/lwjgl-stb-3.4.2-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-stb/3.4.2/lwjgl-stb-3.4.2-natives-freebsd.jar",
|
||||
"sha1": "613223c342f73b9bacb021427b7b9fe4c884da5b",
|
||||
"size": 225722
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-tinyfd:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-tinyfd:3.4.2:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-tinyfd/3.4.2/lwjgl-tinyfd-3.4.2-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-tinyfd/3.4.2/lwjgl-tinyfd-3.4.2-natives-freebsd.jar",
|
||||
"sha1": "2fecbb68cf59babd6feccf0e46dcf101e8f1bacd",
|
||||
"size": 40587
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-freetype:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-freetype:3.4.2:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-freetype/3.4.2/lwjgl-freetype-3.4.2-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-freetype/3.4.2/lwjgl-freetype-3.4.2-natives-freebsd.jar",
|
||||
"sha1": "c76950cce5113badc2e2efc4239cb53cf3aeb10b",
|
||||
"size": 1217460
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-shaderc:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-shaderc:3.4.2:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-shaderc/3.4.2/lwjgl-shaderc-3.4.2-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.2/lwjgl-shaderc-3.4.2-natives-freebsd.jar",
|
||||
"sha1": "69342b19493df3bdefeb2be70109c281b83b1a46",
|
||||
"size": 3007116
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-spvc:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-spvc:3.4.2:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-spvc/3.4.2/lwjgl-spvc-3.4.2-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.2/lwjgl-spvc-3.4.2-natives-freebsd.jar",
|
||||
"sha1": "4232b25322e980a736e6a41a3fa81955e9eb3a77",
|
||||
"size": 1122080
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-vma:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-vma:3.4.2:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-vma/3.4.2/lwjgl-vma-3.4.2-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.2/lwjgl-vma-3.4.2-natives-freebsd.jar",
|
||||
"sha1": "f01da65a11d2ef6a88d87969e22c8df367eeb6e6",
|
||||
"size": 63341
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-sdl:3.4.2:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-sdl:3.4.2:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-sdl/3.4.2/lwjgl-sdl-3.4.2-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-sdl/3.4.2/lwjgl-sdl-3.4.2-natives-freebsd.jar",
|
||||
"sha1": "cf0638454457e5977f1b5dc2db8911e430942322",
|
||||
"size": 1240342
|
||||
}
|
||||
}
|
||||
},
|
||||
"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,
|
||||
|
||||
@@ -1,82 +1,119 @@
|
||||
{
|
||||
"$schema" : "https://schemas.glavo.site/hmcl/theme-pack/1.0.0",
|
||||
"id" : "hmcl.classic",
|
||||
"version" : "1.0.0",
|
||||
"name" : {
|
||||
"default" : "Classic",
|
||||
"zh-Hans" : "经典主题",
|
||||
"zh-Hant" : "經典主題"
|
||||
"$schema": "https://schemas.glavo.site/hmcl/theme-pack/1.0.0",
|
||||
"id": "hmcl.classic",
|
||||
"version": "1.0.0",
|
||||
"name": {
|
||||
"default": "Classic",
|
||||
"es": "Clásico",
|
||||
"ja": "クラシック",
|
||||
"ru": "Классическая",
|
||||
"uk": "Класична",
|
||||
"zh-Hans": "经典主题",
|
||||
"zh-Hant": "經典主題",
|
||||
"lzh": "舊觀"
|
||||
},
|
||||
"description" : {
|
||||
"default" : "Classic HMCL launcher themes",
|
||||
"zh-Hans" : "HMCL 启动器经典主题",
|
||||
"zh-Hant" : "HMCL 啟動器經典主題"
|
||||
"description": {
|
||||
"default": "Classic HMCL launcher themes",
|
||||
"es": "Temas clásicos del launcher HMCL",
|
||||
"ja": "HMCL ランチャーのクラシックテーマ",
|
||||
"ru": "Классические темы лаунчера HMCL",
|
||||
"uk": "Класичні теми лаунчера HMCL",
|
||||
"zh-Hans": "HMCL 启动器经典主题",
|
||||
"zh-Hant": "HMCL 啟動器經典主題",
|
||||
"lzh": "HMCL 啟動器之舊觀"
|
||||
},
|
||||
"authors" : [
|
||||
"authors": [
|
||||
{
|
||||
"name" : "HMCL"
|
||||
"name": {
|
||||
"default": "HMCL",
|
||||
"en-Qabs": "ꞀƆWH"
|
||||
}
|
||||
}
|
||||
],
|
||||
"themes" : [
|
||||
"themes": [
|
||||
{
|
||||
"id" : "2021-08-26",
|
||||
"name" : "HMCL 3.15",
|
||||
"description" : {
|
||||
"default" : "HMCL 3.15 classic appearance",
|
||||
"zh-Hans" : "HMCL 3.15 经典外观",
|
||||
"zh-Hant" : "HMCL 3.15 經典外觀"
|
||||
"id": "2021-08-26",
|
||||
"name": {
|
||||
"default": "Current Theme",
|
||||
"es": "Tema actual",
|
||||
"ja": "現在のテーマ",
|
||||
"ru": "Текущая тема",
|
||||
"uk": "Поточна тема",
|
||||
"zh-Hans": "当前主题",
|
||||
"zh-Hant": "當前主題",
|
||||
"lzh": "今觀"
|
||||
},
|
||||
"color" : "#5C6BC0",
|
||||
"brightness" : "light",
|
||||
"colorStyle" : "fidelity",
|
||||
"background" : {
|
||||
"type" : "builtin",
|
||||
"id" : "2021-08-26",
|
||||
"opacity" : 1.0
|
||||
"description": {
|
||||
"default": "HMCL current theme",
|
||||
"es": "Tema actual de HMCL",
|
||||
"ja": "HMCL の現在のテーマ",
|
||||
"ru": "Текущая тема HMCL",
|
||||
"uk": "Поточна тема HMCL",
|
||||
"zh-Hans": "HMCL 当前主题",
|
||||
"zh-Hant": "HMCL 當前主題",
|
||||
"lzh": "HMCL 之今觀"
|
||||
},
|
||||
"titleBar" : {
|
||||
"transparent" : false
|
||||
"color": "#5C6BC0",
|
||||
"brightness": "light",
|
||||
"colorStyle": "fidelity",
|
||||
"background": {
|
||||
"type": "builtin",
|
||||
"id": "2021-08-26",
|
||||
"opacity": 1.0
|
||||
},
|
||||
"titleBar": {
|
||||
"transparent": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id" : "2016-02-25",
|
||||
"name" : "HMCL 3.3",
|
||||
"description" : {
|
||||
"default" : "HMCL 3.3 classic appearance",
|
||||
"zh-Hans" : "HMCL 3.3 经典外观",
|
||||
"zh-Hant" : "HMCL 3.3 經典外觀"
|
||||
"id": "2016-02-25",
|
||||
"name": "HMCL 3.3",
|
||||
"description": {
|
||||
"default": "HMCL 3.3 classic appearance",
|
||||
"es": "Apariencia clásica de HMCL 3.3",
|
||||
"ja": "HMCL 3.3 のクラシックな外観",
|
||||
"ru": "Классический вид HMCL 3.3",
|
||||
"uk": "Класичний вигляд HMCL 3.3",
|
||||
"zh-Hans": "HMCL 3.3 经典外观",
|
||||
"zh-Hant": "HMCL 3.3 經典外觀",
|
||||
"lzh": "HMCL 3.3 之舊觀"
|
||||
},
|
||||
"color" : "#5C6BC0",
|
||||
"brightness" : "light",
|
||||
"colorStyle" : "fidelity",
|
||||
"background" : {
|
||||
"type" : "builtin",
|
||||
"id" : "2016-02-25",
|
||||
"opacity" : 1.0
|
||||
"color": "#5C6BC0",
|
||||
"brightness": "light",
|
||||
"colorStyle": "fidelity",
|
||||
"background": {
|
||||
"type": "builtin",
|
||||
"id": "2016-02-25",
|
||||
"opacity": 1.0
|
||||
},
|
||||
"titleBar" : {
|
||||
"transparent" : true
|
||||
"titleBar": {
|
||||
"transparent": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id" : "2015-06-22",
|
||||
"name" : {
|
||||
"default" : "HMCL 2.3"
|
||||
"id": "2015-06-22",
|
||||
"name": {
|
||||
"default": "HMCL 2.3"
|
||||
},
|
||||
"description" : {
|
||||
"default" : "HMCL 2.3 classic appearance",
|
||||
"zh-Hans" : "HMCL 2.3 经典外观",
|
||||
"zh-Hant" : "HMCL 2.3 經典外觀"
|
||||
"description": {
|
||||
"default": "HMCL 2.3 classic appearance",
|
||||
"es": "Apariencia clásica de HMCL 2.3",
|
||||
"ja": "HMCL 2.3 のクラシックな外観",
|
||||
"ru": "Классический вид HMCL 2.3",
|
||||
"uk": "Класичний вигляд HMCL 2.3",
|
||||
"zh-Hans": "HMCL 2.3 经典外观",
|
||||
"zh-Hant": "HMCL 2.3 經典外觀",
|
||||
"lzh": "HMCL 2.3 之舊觀"
|
||||
},
|
||||
"color" : "#E67E22",
|
||||
"brightness" : "light",
|
||||
"colorStyle" : "fidelity",
|
||||
"background" : {
|
||||
"type" : "builtin",
|
||||
"id" : "2015-06-22"
|
||||
"color": "#E67E22",
|
||||
"brightness": "light",
|
||||
"colorStyle": "fidelity",
|
||||
"background": {
|
||||
"type": "builtin",
|
||||
"id": "2015-06-22"
|
||||
},
|
||||
"titleBar" : {
|
||||
"transparent" : false
|
||||
"titleBar": {
|
||||
"transparent": false
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -4,17 +4,31 @@
|
||||
"version": "1.0.0",
|
||||
"name": {
|
||||
"default": "Default",
|
||||
"en-Qabs": "ʇꞁnɐɟǝᗡ",
|
||||
"es": "Predeterminado",
|
||||
"ja": "デフォルト",
|
||||
"ru": "По умолчанию",
|
||||
"uk": "За замовчуванням",
|
||||
"zh-Hans": "默认主题",
|
||||
"zh-Hant": "預設主題"
|
||||
"zh-Hant": "預設主題",
|
||||
"lzh": "預設主題"
|
||||
},
|
||||
"description": {
|
||||
"default": "HMCL Default",
|
||||
"es": "HMCL Predeterminado",
|
||||
"ja": "HMCL デフォルト",
|
||||
"ru": "HMCL По умолчанию",
|
||||
"uk": "HMCL За замовчуванням",
|
||||
"zh-Hans": "HMCL 默认主题",
|
||||
"zh-Hant": "HMCL 預設主題"
|
||||
"zh-Hant": "HMCL 預設主題",
|
||||
"lzh": "HMCL 預設主題"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "HMCL"
|
||||
"name": {
|
||||
"default": "HMCL",
|
||||
"en-Qabs": "ꞀƆWH"
|
||||
}
|
||||
}
|
||||
],
|
||||
"theme": {}
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.LocalizedText;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
@@ -95,8 +96,8 @@ public final class GameDirectoriesTest {
|
||||
|
||||
GameDirectories gameDirectories = Objects.requireNonNull(LegacyConfigMigrator.extractGameDirectoriesFromConfigJson(settings));
|
||||
|
||||
GameDirectoryID defaultGameDirectoryId = LegacyConfigMigrator.getLegacyProfileID("Default");
|
||||
GameDirectoryID homeGameDirectoryId = LegacyConfigMigrator.getLegacyProfileID("Home");
|
||||
GameDirectoryID defaultGameDirectoryId = GameDirectoryManager.DEFAULT_GAME_DIRECTORY_ID;
|
||||
GameDirectoryID homeGameDirectoryId = GameDirectoryManager.HOME_GAME_DIRECTORY_ID;
|
||||
GameDirectory defaultGameDirectory = gameDirectories.getGameDirectories().stream()
|
||||
.filter(gameDirectory -> defaultGameDirectoryId.equals(gameDirectory.getId()))
|
||||
.findFirst()
|
||||
@@ -356,12 +357,36 @@ public final class GameDirectoriesTest {
|
||||
GameDirectory userGameDirectory = assertSingleDefaultGameDirectory(
|
||||
userDirectories,
|
||||
PortablePath.fromPath(Metadata.MINECRAFT_DIRECTORY));
|
||||
assertEquals(GameDirectoryManager.DEFAULT_GAME_DIRECTORY_ID, localGameDirectory.getId());
|
||||
assertEquals(GameDirectoryManager.HOME_GAME_DIRECTORY_ID, userGameDirectory.getId());
|
||||
assertEquals(2, GameDirectoryManager.getGameDirectories().size());
|
||||
assertTrue(GameDirectoryManager.getGameDirectories().contains(localGameDirectory));
|
||||
assertTrue(GameDirectoryManager.getGameDirectories().contains(userGameDirectory));
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests that newly created stores use the stable IDs for their built-in game directories.
|
||||
@Test
|
||||
public void createsBuiltInGameDirectoriesWithStableIdsForNewStores() throws ReflectiveOperationException {
|
||||
GameDirectories userDirectories = new GameDirectories();
|
||||
userDirectories.setUserFile(true);
|
||||
userDirectories.setNewlyCreated(true);
|
||||
GameDirectories localDirectories = new GameDirectories();
|
||||
localDirectories.setUserFile(false);
|
||||
localDirectories.setNewlyCreated(true);
|
||||
|
||||
try (GameDirectoryEnvironment ignored = new GameDirectoryEnvironment(localDirectories, userDirectories)) {
|
||||
GameDirectoryManager.init();
|
||||
|
||||
GameDirectory localGameDirectory = assertSingleDefaultGameDirectory(localDirectories, PortablePath.of(".minecraft"));
|
||||
GameDirectory userGameDirectory = assertSingleDefaultGameDirectory(
|
||||
userDirectories,
|
||||
PortablePath.fromPath(Metadata.MINECRAFT_DIRECTORY));
|
||||
assertEquals(GameDirectoryManager.DEFAULT_GAME_DIRECTORY_ID, localGameDirectory.getId());
|
||||
assertEquals(GameDirectoryManager.HOME_GAME_DIRECTORY_ID, userGameDirectory.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests that the selected game directory getter restores a valid selection instead of returning null.
|
||||
@Test
|
||||
public void selectedGameDirectoryGetterNeverReturnsNullWhenGameDirectoriesAreLoaded() throws ReflectiveOperationException {
|
||||
@@ -407,6 +432,231 @@ public final class GameDirectoriesTest {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests that new isolated installing instances resolve content directories under the version root before metadata is saved.
|
||||
@Test
|
||||
public void newIsolatedInstallingInstanceUsesVersionRootBeforeVersionExists(@TempDir Path tempDirectory)
|
||||
throws ReflectiveOperationException {
|
||||
GameSettingsPresetID defaultPresetId =
|
||||
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174002");
|
||||
GameSettings.Preset defaultPreset = new GameSettings.Preset(defaultPresetId);
|
||||
defaultPreset.defaultIsolationTypeProperty().setValue(DefaultIsolationType.MODDED);
|
||||
GameSettingsPresets presets = new GameSettingsPresets();
|
||||
presets.getPresets().setAll(defaultPreset);
|
||||
|
||||
GameDirectory gameDirectory = new GameDirectory(
|
||||
GameDirectoryID.generate(),
|
||||
LocalizedText.plain("Dev"),
|
||||
PortablePath.of(tempDirectory.toString()));
|
||||
GameDirectories localDirectories = new GameDirectories();
|
||||
localDirectories.getGameDirectories().add(gameDirectory);
|
||||
GameDirectories userDirectories = new GameDirectories();
|
||||
|
||||
try (GameDirectoryEnvironment ignored =
|
||||
new GameDirectoryEnvironment(localDirectories, userDirectories, presets)) {
|
||||
settings().defaultGameSettingsPresetProperty().set(defaultPresetId);
|
||||
HMCLGameRepository repository = new HMCLGameRepository(gameDirectory);
|
||||
String id = "1.21.11-fabric";
|
||||
|
||||
assertFalse(repository.hasVersion(id));
|
||||
assertEquals(repository.getBaseDirectory(), repository.getRunDirectory(id));
|
||||
|
||||
repository.applyDefaultIsolationSettingForNewInstance(id, true);
|
||||
|
||||
assertEquals(repository.getVersionRoot(id), repository.getRunDirectory(id));
|
||||
assertEquals(repository.getVersionRoot(id).resolve("mods"), repository.getModsDirectory(id));
|
||||
|
||||
assertTrue(repository.removeVersionFromDisk(id));
|
||||
assertEquals(repository.getBaseDirectory(), repository.getRunDirectory(id));
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests that instance settings without an explicit parent use the default preset.
|
||||
@Test
|
||||
public void nullInstanceParentUsesDefaultPresetInsteadOfLegacyGameDirectoryPreset()
|
||||
throws ReflectiveOperationException {
|
||||
GameSettingsPresetID defaultPresetId =
|
||||
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174000");
|
||||
GameSettingsPresetID legacyPresetId =
|
||||
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174001");
|
||||
GameSettings.Preset defaultPreset = new GameSettings.Preset(defaultPresetId);
|
||||
GameSettings.Preset legacyPreset = new GameSettings.Preset(legacyPresetId);
|
||||
GameSettingsPresets presets = new GameSettingsPresets();
|
||||
presets.getPresets().setAll(defaultPreset, legacyPreset);
|
||||
|
||||
GameDirectory gameDirectory = new GameDirectory(
|
||||
GameDirectoryID.generate(),
|
||||
LocalizedText.plain("Dev"),
|
||||
PortablePath.of("local/Dev"),
|
||||
legacyPresetId);
|
||||
GameDirectories localDirectories = new GameDirectories();
|
||||
localDirectories.getGameDirectories().add(gameDirectory);
|
||||
GameDirectories userDirectories = new GameDirectories();
|
||||
|
||||
try (GameDirectoryEnvironment ignored =
|
||||
new GameDirectoryEnvironment(localDirectories, userDirectories, presets)) {
|
||||
settings().defaultGameSettingsPresetProperty().set(defaultPresetId);
|
||||
HMCLGameRepository repository = new HMCLGameRepository(gameDirectory);
|
||||
|
||||
assertSame(defaultPreset, repository.getParentGameSettings(new GameSettings.Instance()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests that legacy per-version settings migration stores the game directory preset as an explicit parent.
|
||||
@Test
|
||||
public void legacyInstanceSettingsMigrationStoresLegacyGameDirectoryPresetAsParent(@TempDir Path tempDirectory)
|
||||
throws IOException, ReflectiveOperationException {
|
||||
GameSettingsPresetID defaultPresetId =
|
||||
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174000");
|
||||
GameSettingsPresetID legacyPresetId =
|
||||
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174001");
|
||||
GameSettingsPresets presets = new GameSettingsPresets();
|
||||
presets.getPresets().setAll(
|
||||
new GameSettings.Preset(defaultPresetId),
|
||||
new GameSettings.Preset(legacyPresetId));
|
||||
|
||||
GameDirectory gameDirectory = new GameDirectory(
|
||||
GameDirectoryID.generate(),
|
||||
LocalizedText.plain("Dev"),
|
||||
PortablePath.of(tempDirectory.toString()),
|
||||
legacyPresetId);
|
||||
GameDirectories localDirectories = new GameDirectories();
|
||||
localDirectories.getGameDirectories().add(gameDirectory);
|
||||
GameDirectories userDirectories = new GameDirectories();
|
||||
|
||||
try (GameDirectoryEnvironment ignored =
|
||||
new GameDirectoryEnvironment(localDirectories, userDirectories, presets)) {
|
||||
settings().defaultGameSettingsPresetProperty().set(defaultPresetId);
|
||||
HMCLGameRepository repository = new HMCLGameRepository(gameDirectory);
|
||||
Path versionRoot = repository.getVersionRoot("1.20.1");
|
||||
Files.createDirectories(versionRoot);
|
||||
Files.writeString(versionRoot.resolve("hmclversion.cfg"), """
|
||||
{
|
||||
"usesGlobal": true
|
||||
}
|
||||
""");
|
||||
|
||||
GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettings("1.20.1"));
|
||||
|
||||
assertEquals(legacyPresetId, setting.parentProperty().getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests that startup migration leaves legacy per-version settings for lazy migration.
|
||||
@Test
|
||||
public void startupMigrationSkipsLegacyInstanceSettingsFile(@TempDir Path tempDirectory)
|
||||
throws IOException, ReflectiveOperationException {
|
||||
GameSettingsPresetID defaultPresetId =
|
||||
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174000");
|
||||
GameSettingsPresetID legacyPresetId =
|
||||
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174001");
|
||||
GameSettingsPresets presets = new GameSettingsPresets();
|
||||
presets.getPresets().setAll(
|
||||
new GameSettings.Preset(defaultPresetId),
|
||||
new GameSettings.Preset(legacyPresetId));
|
||||
|
||||
GameDirectory gameDirectory = new GameDirectory(
|
||||
GameDirectoryID.generate(),
|
||||
LocalizedText.plain("Dev"),
|
||||
PortablePath.of(tempDirectory.toString()),
|
||||
legacyPresetId);
|
||||
GameDirectories localDirectories = new GameDirectories();
|
||||
localDirectories.getGameDirectories().add(gameDirectory);
|
||||
GameDirectories userDirectories = new GameDirectories();
|
||||
|
||||
try (GameDirectoryEnvironment ignored =
|
||||
new GameDirectoryEnvironment(localDirectories, userDirectories, presets)) {
|
||||
settings().defaultGameSettingsPresetProperty().set(defaultPresetId);
|
||||
HMCLGameRepository repository = new HMCLGameRepository(gameDirectory);
|
||||
writeVersionJson(repository, "1.20.1");
|
||||
Path versionRoot = repository.getVersionRoot("1.20.1");
|
||||
Files.writeString(versionRoot.resolve(LegacyGameSettingsMigrator.LEGACY_INSTANCE_SETTINGS_FILENAME), """
|
||||
{
|
||||
"usesGlobal": true
|
||||
}
|
||||
""");
|
||||
|
||||
LegacyConfigMigrator.migrateLegacyInstanceGameSettings(localDirectories, presets);
|
||||
|
||||
assertFalse(Files.exists(repository.getInstanceConfigDirectory("1.20.1")
|
||||
.resolve(LegacyGameSettingsMigrator.INSTANCE_GAME_SETTINGS_FILENAME)));
|
||||
GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettings("1.20.1"));
|
||||
assertEquals(legacyPresetId, setting.parentProperty().getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests that existing versions without instance settings store the legacy game directory parent.
|
||||
@Test
|
||||
public void absentInstanceSettingsStoreLegacyGameDirectoryPresetAsParent(@TempDir Path tempDirectory)
|
||||
throws IOException, ReflectiveOperationException {
|
||||
GameSettingsPresetID defaultPresetId =
|
||||
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174000");
|
||||
GameSettingsPresetID legacyPresetId =
|
||||
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174001");
|
||||
GameSettings.Preset legacyPreset = new GameSettings.Preset(legacyPresetId);
|
||||
legacyPreset.defaultIsolationTypeProperty().setValue(DefaultIsolationType.ALWAYS);
|
||||
GameSettingsPresets presets = new GameSettingsPresets();
|
||||
presets.getPresets().setAll(
|
||||
new GameSettings.Preset(defaultPresetId),
|
||||
legacyPreset);
|
||||
|
||||
GameDirectory gameDirectory = new GameDirectory(
|
||||
GameDirectoryID.generate(),
|
||||
LocalizedText.plain("Dev"),
|
||||
PortablePath.of(tempDirectory.toString()),
|
||||
legacyPresetId);
|
||||
GameDirectories localDirectories = new GameDirectories();
|
||||
localDirectories.getGameDirectories().add(gameDirectory);
|
||||
GameDirectories userDirectories = new GameDirectories();
|
||||
|
||||
try (GameDirectoryEnvironment ignored =
|
||||
new GameDirectoryEnvironment(localDirectories, userDirectories, presets)) {
|
||||
settings().defaultGameSettingsPresetProperty().set(defaultPresetId);
|
||||
HMCLGameRepository repository = new HMCLGameRepository(gameDirectory);
|
||||
writeVersionJson(repository, "1.20.1");
|
||||
LegacyConfigMigrator.migrateLegacyInstanceGameSettings(localDirectories, presets);
|
||||
|
||||
GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettings("1.20.1"));
|
||||
|
||||
assertEquals(legacyPresetId, setting.parentProperty().getValue());
|
||||
assertTrue(setting.getOverrideProperties().contains(GameSettings.PROPERTY_RUNNING_DIRECTORY));
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests that versions created after migration do not inherit the legacy game directory parent.
|
||||
@Test
|
||||
public void newInstanceAfterMigrationDoesNotUseLegacyGameDirectoryParent(@TempDir Path tempDirectory)
|
||||
throws IOException, ReflectiveOperationException {
|
||||
GameSettingsPresetID defaultPresetId =
|
||||
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174000");
|
||||
GameSettingsPresetID legacyPresetId =
|
||||
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174001");
|
||||
GameSettings.Preset defaultPreset = new GameSettings.Preset(defaultPresetId);
|
||||
defaultPreset.defaultIsolationTypeProperty().setValue(DefaultIsolationType.NEVER);
|
||||
GameSettings.Preset legacyPreset = new GameSettings.Preset(legacyPresetId);
|
||||
legacyPreset.defaultIsolationTypeProperty().setValue(DefaultIsolationType.ALWAYS);
|
||||
GameSettingsPresets presets = new GameSettingsPresets();
|
||||
presets.getPresets().setAll(defaultPreset, legacyPreset);
|
||||
|
||||
GameDirectory gameDirectory = new GameDirectory(
|
||||
GameDirectoryID.generate(),
|
||||
LocalizedText.plain("Dev"),
|
||||
PortablePath.of(tempDirectory.toString()),
|
||||
legacyPresetId);
|
||||
GameDirectories localDirectories = new GameDirectories();
|
||||
localDirectories.getGameDirectories().add(gameDirectory);
|
||||
GameDirectories userDirectories = new GameDirectories();
|
||||
|
||||
try (GameDirectoryEnvironment ignored =
|
||||
new GameDirectoryEnvironment(localDirectories, userDirectories, presets)) {
|
||||
settings().defaultGameSettingsPresetProperty().set(defaultPresetId);
|
||||
HMCLGameRepository repository = new HMCLGameRepository(gameDirectory);
|
||||
LegacyConfigMigrator.migrateLegacyInstanceGameSettings(localDirectories, presets);
|
||||
writeVersionJson(repository, "1.20.1");
|
||||
|
||||
assertNull(repository.getInstanceGameSettings("1.20.1"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Temporary static state override for game directory tests.
|
||||
private static final class GameDirectoryEnvironment implements AutoCloseable {
|
||||
/// The reflected SettingsManager local game directories field.
|
||||
@@ -418,6 +668,9 @@ public final class GameDirectoriesTest {
|
||||
/// The reflected SettingsManager launcher settings field.
|
||||
private final Field launcherSettingsField;
|
||||
|
||||
/// The reflected SettingsManager game settings presets field.
|
||||
private final Field gameSettingsPresetsField;
|
||||
|
||||
/// The reflected SettingsManager local game directories access field.
|
||||
private final Field localGameDirectoriesAccessField;
|
||||
|
||||
@@ -448,6 +701,9 @@ public final class GameDirectoriesTest {
|
||||
/// The previous launcher settings instance.
|
||||
private final Object previousLauncherSettings;
|
||||
|
||||
/// The previous game settings presets instance.
|
||||
private final Object previousGameSettingsPresets;
|
||||
|
||||
/// The previous local game directories access.
|
||||
private final SettingFileAccess previousLocalGameDirectoriesAccess;
|
||||
|
||||
@@ -469,12 +725,22 @@ public final class GameDirectoriesTest {
|
||||
/// The previous repository map entries.
|
||||
private final Map<GameDirectory, HMCLGameRepository> previousRepositories;
|
||||
|
||||
/// Replaces game-directory-related static state with the given stores.
|
||||
/// Replaces game-directory-related static state with the given stores and an empty preset store.
|
||||
private GameDirectoryEnvironment(GameDirectories localDirectories, GameDirectories userDirectories)
|
||||
throws ReflectiveOperationException {
|
||||
this(localDirectories, userDirectories, new GameSettingsPresets());
|
||||
}
|
||||
|
||||
/// Replaces game-directory-related static state with the given stores.
|
||||
private GameDirectoryEnvironment(
|
||||
GameDirectories localDirectories,
|
||||
GameDirectories userDirectories,
|
||||
GameSettingsPresets gameSettingsPresets)
|
||||
throws ReflectiveOperationException {
|
||||
localGameDirectoriesField = SettingsManager.class.getDeclaredField("localGameDirectories");
|
||||
userGameDirectoriesField = SettingsManager.class.getDeclaredField("userGameDirectories");
|
||||
launcherSettingsField = SettingsManager.class.getDeclaredField("launcherSettings");
|
||||
gameSettingsPresetsField = SettingsManager.class.getDeclaredField("gameSettingsPresets");
|
||||
localGameDirectoriesAccessField = SettingsManager.class.getDeclaredField("localGameDirectoriesAccess");
|
||||
userGameDirectoriesAccessField = SettingsManager.class.getDeclaredField("userGameDirectoriesAccess");
|
||||
initializedField = GameDirectoryManager.class.getDeclaredField("initialized");
|
||||
@@ -485,6 +751,7 @@ public final class GameDirectoriesTest {
|
||||
localGameDirectoriesField.setAccessible(true);
|
||||
userGameDirectoriesField.setAccessible(true);
|
||||
launcherSettingsField.setAccessible(true);
|
||||
gameSettingsPresetsField.setAccessible(true);
|
||||
localGameDirectoriesAccessField.setAccessible(true);
|
||||
userGameDirectoriesAccessField.setAccessible(true);
|
||||
initializedField.setAccessible(true);
|
||||
@@ -496,6 +763,7 @@ public final class GameDirectoriesTest {
|
||||
previousLocalGameDirectories = localGameDirectoriesField.get(null);
|
||||
previousUserGameDirectories = userGameDirectoriesField.get(null);
|
||||
previousLauncherSettings = launcherSettingsField.get(null);
|
||||
previousGameSettingsPresets = gameSettingsPresetsField.get(null);
|
||||
previousLocalGameDirectoriesAccess = (SettingFileAccess) localGameDirectoriesAccessField.get(null);
|
||||
previousUserGameDirectoriesAccess = (SettingFileAccess) userGameDirectoriesAccessField.get(null);
|
||||
previousInitialized = initializedField.getBoolean(null);
|
||||
@@ -527,6 +795,7 @@ public final class GameDirectoriesTest {
|
||||
localGameDirectoriesField.set(null, localDirectories);
|
||||
userGameDirectoriesField.set(null, userDirectories);
|
||||
launcherSettingsField.set(null, new LauncherSettings());
|
||||
gameSettingsPresetsField.set(null, gameSettingsPresets);
|
||||
localGameDirectoriesAccessField.set(null, SettingFileAccess.READ_WRITE);
|
||||
userGameDirectoriesAccessField.set(null, SettingFileAccess.READ_WRITE);
|
||||
initializedField.setBoolean(null, false);
|
||||
@@ -558,12 +827,24 @@ public final class GameDirectoriesTest {
|
||||
localGameDirectoriesField.set(null, previousLocalGameDirectories);
|
||||
userGameDirectoriesField.set(null, previousUserGameDirectories);
|
||||
launcherSettingsField.set(null, previousLauncherSettings);
|
||||
gameSettingsPresetsField.set(null, previousGameSettingsPresets);
|
||||
localGameDirectoriesAccessField.set(null, previousLocalGameDirectoriesAccess);
|
||||
userGameDirectoriesAccessField.set(null, previousUserGameDirectoriesAccess);
|
||||
initializedField.setBoolean(null, previousInitialized);
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes a minimal valid version json for repository refresh tests.
|
||||
private static void writeVersionJson(HMCLGameRepository repository, String id) throws IOException {
|
||||
Path versionRoot = repository.getVersionRoot(id);
|
||||
Files.createDirectories(versionRoot);
|
||||
Files.writeString(versionRoot.resolve(id + ".json"), """
|
||||
{
|
||||
"id": "%s"
|
||||
}
|
||||
""".formatted(id));
|
||||
}
|
||||
|
||||
/// Returns the only default game directory in the given store, asserting its path.
|
||||
private static GameDirectory assertSingleDefaultGameDirectory(GameDirectories gameDirectories, PortablePath path) {
|
||||
assertEquals(1, gameDirectories.getGameDirectories().size());
|
||||
|
||||
@@ -137,6 +137,7 @@ public final class LauncherSettingsMigrationTest {
|
||||
assertEquals(ThemeColor.of("#336699"), launcherSettings.customThemeColorProperty().get());
|
||||
assertEquals(ThemeColorType.CUSTOM, launcherSettings.themeColorTypeProperty().get());
|
||||
assertTrue(launcherSettings.titleBarTransparentProperty().get());
|
||||
assertTrue(launcherSettings.windowTransparentProperty().get());
|
||||
assertEquals("/pictures/background.png", launcherSettings.customBackgroundImagePathProperty().get());
|
||||
assertEquals("https://example.com/background.png",
|
||||
launcherSettings.networkBackgroundImageUrlProperty().get());
|
||||
@@ -148,6 +149,8 @@ public final class LauncherSettingsMigrationTest {
|
||||
assertTrue(launcherSettings.getThemeAppearanceOverrides().contains(LauncherSettings.THEME_APPEARANCE_COLOR));
|
||||
assertTrue(launcherSettings.getThemeAppearanceOverrides().contains(
|
||||
LauncherSettings.THEME_APPEARANCE_TITLE_BAR_TRANSPARENT));
|
||||
assertTrue(launcherSettings.getThemeAppearanceOverrides().contains(
|
||||
LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT));
|
||||
assertFalse(launcherSettings.getThemeAppearanceOverrides().contains("networkBackgroundImageCachePolicy"));
|
||||
assertEquals("/pictures/background.png", serialized.get("customBackgroundImagePath").getAsString());
|
||||
assertEquals("https://example.com/background.png",
|
||||
@@ -158,6 +161,7 @@ public final class LauncherSettingsMigrationTest {
|
||||
assertEquals(ThemeColorType.CUSTOM.name(), serialized.get("themeColorType").getAsString());
|
||||
assertEquals("dark", serialized.get("themeBrightnessMode").getAsString());
|
||||
assertTrue(serialized.get("titleBarTransparent").getAsBoolean());
|
||||
assertTrue(serialized.get("windowTransparent").getAsBoolean());
|
||||
assertEquals("#336699", serialized.get("customBackgroundPaint").getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.theme;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
/// Tests launcher background opacity storage.
|
||||
@NotNullByDefault
|
||||
public final class LauncherBackgroundTest {
|
||||
/// Verifies opacity changes reuse the unmodified JavaFX background.
|
||||
@Test
|
||||
public void changesOpacityWithoutChangingBackground() {
|
||||
Background background = new Background(
|
||||
new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY));
|
||||
LauncherBackground changed = new LauncherBackground(background, 0.4);
|
||||
|
||||
assertSame(background, changed.background());
|
||||
assertEquals(0.4, changed.opacity());
|
||||
assertEquals(1.0, ((Color) changed.background().getFills().get(0).getFill()).getOpacity());
|
||||
}
|
||||
|
||||
/// Verifies invalid node opacity values are rejected.
|
||||
@Test
|
||||
public void rejectsInvalidOpacity() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new LauncherBackground(Background.EMPTY, -0.1));
|
||||
assertThrows(IllegalArgumentException.class, () -> new LauncherBackground(Background.EMPTY, 1.1));
|
||||
assertThrows(IllegalArgumentException.class, () -> new LauncherBackground(Background.EMPTY, Double.NaN));
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ public final class ThemePackManagerTest {
|
||||
launcherSettingsField.set(null, launcherSettings);
|
||||
|
||||
ThemePackManager.ExportedThemePack exported =
|
||||
ThemePackManager.createCurrent("example.export", "Example", "Tester");
|
||||
ThemePackManager.createCurrent("example.export", "1.0", "Example", "Tester");
|
||||
Theme theme = exported.manifest().themes().get(0);
|
||||
ThemeBackgroundSettings background = Objects.requireNonNull(theme.appearance().background());
|
||||
ThemeBackground.Image image = assertInstanceOf(ThemeBackground.Image.class, background.source());
|
||||
@@ -146,7 +146,7 @@ public final class ThemePackManagerTest {
|
||||
launcherSettingsField.set(null, launcherSettings);
|
||||
|
||||
ThemePackManager.ExportedThemePack exported =
|
||||
ThemePackManager.createCurrent("example.export", "Example", "Tester");
|
||||
ThemePackManager.createCurrent("example.export", "1.0", "Example", "Tester");
|
||||
try {
|
||||
Theme theme = exported.manifest().themes().get(0);
|
||||
ThemeBackgroundSettings background = Objects.requireNonNull(theme.appearance().background());
|
||||
|
||||
@@ -86,6 +86,7 @@ public final class ThemePackManifestTest {
|
||||
|
||||
assertEquals(BackgroundType.DEFAULT, background.type());
|
||||
assertEquals(1.0, background.opacity(), 0.0);
|
||||
assertNull(theme.appearance().windowTransparent());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.addon.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.addon.mod.ModManager;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
@@ -103,12 +104,13 @@ public final class ForgeNewModMetadata {
|
||||
@JsonAdapter(AuthorDeserializer.class)
|
||||
private final String authors;
|
||||
private final String description;
|
||||
private final String logoFile;
|
||||
|
||||
public Mod() {
|
||||
this("", "", "", "", "", "", "");
|
||||
this("", "", "", "", "", "", "", "");
|
||||
}
|
||||
|
||||
public Mod(String modId, String version, String displayName, String side, String displayURL, String authors, String description) {
|
||||
public Mod(String modId, String version, String displayName, String side, String displayURL, String authors, String description, String logoFile) {
|
||||
this.modId = modId;
|
||||
this.version = version;
|
||||
this.displayName = displayName;
|
||||
@@ -116,6 +118,7 @@ public final class ForgeNewModMetadata {
|
||||
this.displayURL = displayURL;
|
||||
this.authors = authors;
|
||||
this.description = description;
|
||||
this.logoFile = logoFile;
|
||||
}
|
||||
|
||||
public String getModId() {
|
||||
@@ -146,6 +149,10 @@ public final class ForgeNewModMetadata {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String getLogoFile() {
|
||||
return logoFile;
|
||||
}
|
||||
|
||||
static final class AuthorDeserializer implements JsonDeserializer<String> {
|
||||
@Override
|
||||
public String deserialize(JsonElement authors, Type type, JsonDeserializationContext context) throws JsonParseException {
|
||||
@@ -235,10 +242,12 @@ public final class ForgeNewModMetadata {
|
||||
|
||||
ModLoaderType type = analyzeLoader(tomlParseResult, mod.getModId(), modLoaderType);
|
||||
|
||||
String logoPath = StringUtils.isNotBlank(mod.getLogoFile()) ? mod.getLogoFile() : metadata.getLogoFile();
|
||||
|
||||
return new LocalModFile(modManager, modManager.getLocalMod(mod.getModId(), type), modFile, mod.getDisplayName(), new LocalAddonFile.Description(mod.getDescription()),
|
||||
mod.getAuthors(), jarVersion == null ? mod.getVersion() : mod.getVersion().replace("${file.jarVersion}", jarVersion), "",
|
||||
mod.getDisplayURL(),
|
||||
metadata.getLogoFile());
|
||||
logoPath);
|
||||
}
|
||||
|
||||
private static LocalModFile fromEmbeddedMod(ModManager modManager, Path modFile, ZipFileTree tree, ModLoaderType modLoaderType) throws IOException {
|
||||
|
||||
@@ -21,7 +21,8 @@ import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.download.game.LibraryDownloadTask;
|
||||
import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.game.LibraryDownloadInfo;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.CacheRepository;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
@@ -37,7 +38,6 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -90,7 +90,7 @@ public class DefaultCacheRepository extends CacheRepository {
|
||||
public void tryCacheLibrary(Library library, Path jar) {
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
if (index.getLibraries().stream().anyMatch(it -> library.getName().equals(it.getName())))
|
||||
if (index.libraries().stream().anyMatch(it -> library.getName().equals(it.name())))
|
||||
return;
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
@@ -132,13 +132,13 @@ public class DefaultCacheRepository extends CacheRepository {
|
||||
|
||||
try {
|
||||
// check if this library is from Forge
|
||||
List<LibraryIndex> libraries = index.getLibraries().stream()
|
||||
.filter(it -> it.getName().equals(library.getName()))
|
||||
.collect(Collectors.toList());
|
||||
List<LibraryIndex> libraries = index.libraries().stream()
|
||||
.filter(it -> it.name().equals(library.getName()))
|
||||
.toList();
|
||||
for (LibraryIndex libIndex : libraries) {
|
||||
if (fileExists(SHA1, libIndex.getHash())) {
|
||||
Path file = getFile(SHA1, libIndex.getHash());
|
||||
if (libIndex.getType().equalsIgnoreCase(LibraryIndex.TYPE_FORGE)) {
|
||||
if (fileExists(SHA1, libIndex.hash())) {
|
||||
Path file = getFile(SHA1, libIndex.hash());
|
||||
if (libIndex.type().equalsIgnoreCase(LibraryIndex.TYPE_FORGE)) {
|
||||
if (LibraryDownloadTask.checksumValid(file, library.getChecksums()))
|
||||
return Optional.of(file);
|
||||
}
|
||||
@@ -191,7 +191,7 @@ public class DefaultCacheRepository extends CacheRepository {
|
||||
writeLock.lock();
|
||||
try {
|
||||
LibraryIndex libIndex = new LibraryIndex(library.getName(), hash, forge ? LibraryIndex.TYPE_FORGE : LibraryIndex.TYPE_JAR);
|
||||
index.getLibraries().add(libIndex);
|
||||
index.libraries().add(libIndex);
|
||||
saveIndex();
|
||||
} finally {
|
||||
writeLock.unlock();
|
||||
@@ -225,24 +225,17 @@ public class DefaultCacheRepository extends CacheRepository {
|
||||
/// ]
|
||||
/// }
|
||||
/// }
|
||||
///```
|
||||
/// ```
|
||||
/// assets and versions will not be included in index.
|
||||
private static final class Index implements Validation {
|
||||
private final Set<LibraryIndex> libraries;
|
||||
|
||||
private record Index(@NotNull Set<LibraryIndex> libraries) implements Validation {
|
||||
public Index() {
|
||||
this(new HashSet<>());
|
||||
}
|
||||
|
||||
public Index(Set<LibraryIndex> libraries) {
|
||||
private Index(Set<LibraryIndex> libraries) {
|
||||
this.libraries = Objects.requireNonNull(libraries);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Set<LibraryIndex> getLibraries() {
|
||||
return libraries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
if (libraries == null)
|
||||
@@ -250,57 +243,17 @@ public class DefaultCacheRepository extends CacheRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class LibraryIndex implements Validation {
|
||||
private final String name;
|
||||
private final String hash;
|
||||
private final String type;
|
||||
|
||||
private record LibraryIndex(@NotNull String name, @NotNull String hash, @NotNull String type) implements Validation {
|
||||
public LibraryIndex() {
|
||||
this("", "", "");
|
||||
}
|
||||
|
||||
public LibraryIndex(String name, String hash, String type) {
|
||||
this.name = name;
|
||||
this.hash = hash;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getHash() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
if (name == null || hash == null || type == null)
|
||||
throw new JsonParseException("Index.LibraryIndex.* cannot be null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
LibraryIndex that = (LibraryIndex) o;
|
||||
return Objects.equals(name, that.name) &&
|
||||
Objects.equals(hash, that.hash) &&
|
||||
Objects.equals(type, that.type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, hash, type);
|
||||
}
|
||||
|
||||
public static final String TYPE_FORGE = "forge";
|
||||
public static final String TYPE_JAR = "jar";
|
||||
}
|
||||
|
||||
@@ -272,7 +272,7 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||
if (!optArgument.isPresent()) {
|
||||
return null;
|
||||
}
|
||||
List<Argument> gameArguments = optArgument.get().getGame();
|
||||
List<Argument> gameArguments = optArgument.get().game();
|
||||
if (gameArguments == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -280,11 +280,11 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||
for (int i = 0; i < gameArguments.size() - 1; i++) {
|
||||
Argument argument = gameArguments.get(i);
|
||||
if (argument instanceof StringArgument) {
|
||||
String argumentValue = ((StringArgument) argument).getArgument();
|
||||
String argumentValue = ((StringArgument) argument).argument();
|
||||
if ("--fml.neoForgeVersion".equals(argumentValue) || "--fml.forgeVersion".equals(argumentValue)) {
|
||||
Argument next = gameArguments.get(i + 1);
|
||||
if (next instanceof StringArgument) {
|
||||
return ((StringArgument) next).getArgument();
|
||||
return ((StringArgument) next).argument();
|
||||
}
|
||||
return null; // Normally, there should not be two --fml.neoForgeVersion argument.
|
||||
}
|
||||
|
||||
@@ -21,15 +21,17 @@ package org.jackhuang.hmcl.download.java;
|
||||
* @author Glavo
|
||||
*/
|
||||
public enum JavaPackageType {
|
||||
JRE(false, false),
|
||||
JDK(true, false),
|
||||
JREFX(false, true),
|
||||
JDKFX(true, true);
|
||||
JRE("JRE", false, false),
|
||||
JDK("JDK", true, false),
|
||||
JREFX("JRE FX", false, true),
|
||||
JDKFX("JDK FX", true, true);
|
||||
|
||||
private final String displayName;
|
||||
private final boolean jdk;
|
||||
private final boolean javafx;
|
||||
|
||||
JavaPackageType(boolean jdk, boolean javafx) {
|
||||
JavaPackageType(String displayName, boolean jdk, boolean javafx) {
|
||||
this.displayName = displayName;
|
||||
this.jdk = jdk;
|
||||
this.javafx = javafx;
|
||||
}
|
||||
@@ -41,6 +43,10 @@ public enum JavaPackageType {
|
||||
return javafx ? JREFX : JRE;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public boolean isJDK() {
|
||||
return jdk;
|
||||
}
|
||||
|
||||
+2
-19
@@ -240,24 +240,7 @@ public final class DiscoJavaRemoteVersion implements JavaRemoteVersion {
|
||||
return "DiscoJavaRemoteVersion " + JsonUtils.GSON.toJson(this);
|
||||
}
|
||||
|
||||
public static final class Links {
|
||||
@SerializedName("pkg_info_uri")
|
||||
private final String pkgInfoUri;
|
||||
|
||||
@SerializedName("pkg_download_redirect")
|
||||
private final String pkgDownloadRedirect;
|
||||
|
||||
public Links(String pkgInfoUri, String pkgDownloadRedirect) {
|
||||
this.pkgInfoUri = pkgInfoUri;
|
||||
this.pkgDownloadRedirect = pkgDownloadRedirect;
|
||||
}
|
||||
|
||||
public String getPkgInfoUri() {
|
||||
return pkgInfoUri;
|
||||
}
|
||||
|
||||
public String getPkgDownloadRedirect() {
|
||||
return pkgDownloadRedirect;
|
||||
}
|
||||
public record Links(@SerializedName("pkg_info_uri") String pkgInfoUri,
|
||||
@SerializedName("pkg_download_redirect") String pkgDownloadRedirect) {
|
||||
}
|
||||
}
|
||||
Loaded 100 of 145 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user