Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7035767828 | ||
|
|
13a2777c87 | ||
|
|
a4f9438d9e | ||
|
|
4445021d66 | ||
|
|
f6a3b76a0b | ||
|
|
42bb602ff5 | ||
|
|
4547448718 | ||
|
|
10bda7e6f4 | ||
|
|
f239140055 | ||
|
|
6f4b557f63 | ||
|
|
5118ce4e62 | ||
|
|
a705b7cf95 | ||
|
|
0d5d1ae4e6 | ||
|
|
b57547415b | ||
|
|
b60d889bf7 | ||
|
|
2f463c1eda | ||
|
|
d1e006ed29 | ||
|
|
ba58905c9c | ||
|
|
de221b9dae | ||
|
|
0913c5e710 | ||
|
|
41fd38d8d1 | ||
|
|
7ec880852f | ||
|
|
9213b7fb2e | ||
|
|
80f1367d9a | ||
|
|
f521a041a9 | ||
|
|
be9efbe6ee | ||
|
|
8f3c716ca7 |
No files matched your search
@@ -13,6 +13,7 @@ hs_err_pid*
|
||||
/externalgames
|
||||
NVIDIA
|
||||
minecraft-exported-crash-info*
|
||||
hmcl-exported-logs-*
|
||||
|
||||
# gradle build
|
||||
/build/
|
||||
|
||||
@@ -37,6 +37,8 @@ val launcherExe = System.getenv("HMCL_LAUNCHER_EXE")
|
||||
|
||||
version = "$versionRoot.$buildNumber"
|
||||
|
||||
val embedResources by configurations.registering
|
||||
|
||||
dependencies {
|
||||
implementation(project(":HMCLCore"))
|
||||
implementation(project(":HMCLBoot"))
|
||||
@@ -47,6 +49,8 @@ dependencies {
|
||||
if (launcherExe == null) {
|
||||
implementation(libs.hmclauncher)
|
||||
}
|
||||
|
||||
embedResources(libs.authlib.injector)
|
||||
}
|
||||
|
||||
fun digest(algorithm: String, bytes: ByteArray): ByteArray = MessageDigest.getInstance(algorithm).digest(bytes)
|
||||
@@ -132,6 +136,10 @@ tasks.shadowJar {
|
||||
exclude(project(":HMCLBoot"))
|
||||
}
|
||||
|
||||
into("assets/") {
|
||||
from(embedResources)
|
||||
}
|
||||
|
||||
manifest {
|
||||
attributes(
|
||||
"Created-By" to "Copyright(c) 2013-2025 huangyuhui.",
|
||||
@@ -141,8 +149,8 @@ tasks.shadowJar {
|
||||
"Microsoft-Auth-Id" to microsoftAuthId,
|
||||
"Microsoft-Auth-Secret" to microsoftAuthSecret,
|
||||
"CurseForge-Api-Key" to curseForgeApiKey,
|
||||
"Authlib-Injector-Version" to libs.authlib.injector.get().version!!,
|
||||
"Build-Channel" to versionType,
|
||||
"Class-Path" to "pack200.jar",
|
||||
"Add-Opens" to listOf(
|
||||
"java.base/java.lang",
|
||||
"java.base/java.lang.reflect",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
@@ -160,8 +160,10 @@ public final class LauncherHelper {
|
||||
}),
|
||||
Task.composeAsync(() -> {
|
||||
Renderer renderer = setting.getRenderer();
|
||||
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
Library lib = NativePatcher.getMesaLoader(java);
|
||||
if (renderer != Renderer.DEFAULT
|
||||
&& OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
|
||||
&& OperatingSystem.WINDOWS_VERSION != null) {
|
||||
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.WINDOWS_VERSION);
|
||||
if (lib == null)
|
||||
return null;
|
||||
File file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
@@ -41,9 +42,9 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
|
||||
protected abstract SortType getBackedRemoteModRepositorySortOrder();
|
||||
|
||||
@Override
|
||||
public SearchResult search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||
if (!StringUtils.containsChinese(searchFilter)) {
|
||||
return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
|
||||
return getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
|
||||
}
|
||||
|
||||
Set<String> englishSearchFiltersSet = new HashSet<>(INITIAL_CAPACITY);
|
||||
@@ -65,7 +66,7 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
|
||||
RemoteMod[] searchResultArray = new RemoteMod[pageSize];
|
||||
int totalPages, chineseIndex = 0, englishIndex = pageSize - 1;
|
||||
{
|
||||
SearchResult searchResult = getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder);
|
||||
SearchResult searchResult = getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder);
|
||||
for (Iterator<RemoteMod> iterator = searchResult.getUnsortedResults().iterator(); iterator.hasNext(); ) {
|
||||
if (chineseIndex > englishIndex) {
|
||||
LOG.warning("Too many search results! Are the backed remote mod repository broken? Or are the API broken?");
|
||||
|
||||
@@ -56,6 +56,7 @@ public final class JavaManager {
|
||||
}
|
||||
|
||||
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_GLOBAL_DIRECTORY.resolve("java"));
|
||||
public static final HMCLJavaRepository LOCAL_REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_CURRENT_DIRECTORY.resolve("java"));
|
||||
|
||||
public static String getMojangJavaPlatform(Platform platform) {
|
||||
if (platform.getOperatingSystem() == OperatingSystem.WINDOWS) {
|
||||
@@ -571,6 +572,10 @@ public final class JavaManager {
|
||||
for (JavaRuntime java : REPOSITORY.getAllJava(platform)) {
|
||||
javaRuntimes.put(java.getBinary(), java);
|
||||
}
|
||||
|
||||
for (JavaRuntime java : LOCAL_REPOSITORY.getAllJava(platform)) {
|
||||
javaRuntimes.put(java.getBinary(), java);
|
||||
}
|
||||
}
|
||||
|
||||
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, boolean verify) {
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.util.FileSaver;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
@@ -68,18 +69,6 @@ public final class Accounts {
|
||||
|
||||
private static final AuthlibInjectorArtifactProvider AUTHLIB_INJECTOR_DOWNLOADER = createAuthlibInjectorArtifactProvider();
|
||||
|
||||
private static void triggerAuthlibInjectorUpdateCheck() {
|
||||
if (AUTHLIB_INJECTOR_DOWNLOADER instanceof AuthlibInjectorDownloader) {
|
||||
Schedulers.io().execute(() -> {
|
||||
try {
|
||||
((AuthlibInjectorDownloader) AUTHLIB_INJECTOR_DOWNLOADER).checkUpdate();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to check update for authlib-injector", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static final OAuthServer.Factory OAUTH_CALLBACK = new OAuthServer.Factory();
|
||||
|
||||
public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
|
||||
@@ -337,8 +326,6 @@ public final class Accounts {
|
||||
});
|
||||
}
|
||||
|
||||
triggerAuthlibInjectorUpdateCheck();
|
||||
|
||||
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
|
||||
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
|
||||
continue;
|
||||
@@ -371,24 +358,18 @@ public final class Accounts {
|
||||
// ==== authlib-injector ====
|
||||
private static AuthlibInjectorArtifactProvider createAuthlibInjectorArtifactProvider() {
|
||||
String authlibinjectorLocation = System.getProperty("hmcl.authlibinjector.location");
|
||||
if (authlibinjectorLocation == null) {
|
||||
return new AuthlibInjectorDownloader(
|
||||
Metadata.DEPENDENCIES_DIRECTORY.resolve("universal").resolve("authlib-injector.jar"),
|
||||
DownloadProviders::getDownloadProvider) {
|
||||
@Override
|
||||
public Optional<AuthlibInjectorArtifactInfo> getArtifactInfoImmediately() {
|
||||
Optional<AuthlibInjectorArtifactInfo> local = super.getArtifactInfoImmediately();
|
||||
if (local.isPresent()) {
|
||||
return local;
|
||||
}
|
||||
// search authlib-injector.jar in current directory, it's used as a fallback
|
||||
return parseArtifact(Metadata.CURRENT_DIRECTORY.resolve("authlib-injector.jar"));
|
||||
}
|
||||
};
|
||||
} else {
|
||||
if (authlibinjectorLocation != null) {
|
||||
LOG.info("Using specified authlib-injector: " + authlibinjectorLocation);
|
||||
return new SimpleAuthlibInjectorArtifactProvider(Paths.get(authlibinjectorLocation));
|
||||
}
|
||||
|
||||
String authlibInjectorVersion = JarUtils.getManifestAttribute("Authlib-Injector-Version", null);
|
||||
if (authlibInjectorVersion == null)
|
||||
throw new AssertionError("Missing Authlib-Injector-Version");
|
||||
|
||||
String authlibInjectorFileName = "authlib-injector-" + authlibInjectorVersion + ".jar";
|
||||
return new AuthlibInjectorExtractor(Accounts.class.getResource("/assets/" + authlibInjectorFileName),
|
||||
Metadata.DEPENDENCIES_DIRECTORY.resolve("universal").resolve(authlibInjectorFileName));
|
||||
}
|
||||
|
||||
private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) {
|
||||
|
||||
@@ -29,6 +29,7 @@ public enum VersionIconType {
|
||||
COMMAND("/assets/img/command.png"),
|
||||
OPTIFINE("/assets/img/optifine.png"),
|
||||
CRAFT_TABLE("/assets/img/craft_table.png"),
|
||||
APRIL_FOOLS("/assets/img/april_fools.png"),
|
||||
FABRIC("/assets/img/fabric.png"),
|
||||
FORGE("/assets/img/forge.png"),
|
||||
NEO_FORGE("/assets/img/neoforge.png"),
|
||||
|
||||
@@ -216,7 +216,9 @@ public final class Controllers {
|
||||
|
||||
if (targetProperty != null
|
||||
&& Controllers.stage != null
|
||||
&& !Controllers.stage.isIconified()) {
|
||||
&& !Controllers.stage.isIconified()
|
||||
&& !Controllers.stage.isFullScreen()
|
||||
&& !Controllers.stage.isMaximized()) {
|
||||
targetProperty.set(sourceProperty.get());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,10 +25,7 @@ import javafx.beans.Observable;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.WeakListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.*;
|
||||
import javafx.collections.ObservableMap;
|
||||
import javafx.event.Event;
|
||||
@@ -832,10 +829,144 @@ public final class FXUtils {
|
||||
property.addListener(binding);
|
||||
}
|
||||
|
||||
public static void unbindColorPicker(ColorPicker colorPicker, Property<Paint> property) {
|
||||
PaintBidirectionalBinding binding = new PaintBidirectionalBinding(colorPicker, property);
|
||||
colorPicker.valueProperty().removeListener(binding);
|
||||
property.removeListener(binding);
|
||||
private static final class WindowsSizeBidirectionalBinding implements InvalidationListener, WeakListener {
|
||||
private final WeakReference<JFXComboBox<String>> comboBoxRef;
|
||||
private final WeakReference<IntegerProperty> widthPropertyRef;
|
||||
private final WeakReference<IntegerProperty> heightPropertyRef;
|
||||
|
||||
private final int hashCode;
|
||||
|
||||
private boolean updating = false;
|
||||
|
||||
private WindowsSizeBidirectionalBinding(JFXComboBox<String> comboBox,
|
||||
IntegerProperty widthProperty,
|
||||
IntegerProperty heightProperty) {
|
||||
this.comboBoxRef = new WeakReference<>(comboBox);
|
||||
this.widthPropertyRef = new WeakReference<>(widthProperty);
|
||||
this.heightPropertyRef = new WeakReference<>(heightProperty);
|
||||
this.hashCode = System.identityHashCode(comboBox)
|
||||
^ System.identityHashCode(widthProperty)
|
||||
^ System.identityHashCode(heightProperty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidated(Observable observable) {
|
||||
if (!updating) {
|
||||
var comboBox = this.comboBoxRef.get();
|
||||
var widthProperty = this.widthPropertyRef.get();
|
||||
var heightProperty = this.heightPropertyRef.get();
|
||||
|
||||
if (comboBox == null || widthProperty == null || heightProperty == null) {
|
||||
if (comboBox != null) {
|
||||
comboBox.focusedProperty().removeListener(this);
|
||||
comboBox.sceneProperty().removeListener(this);
|
||||
}
|
||||
if (widthProperty != null)
|
||||
widthProperty.removeListener(this);
|
||||
if (heightProperty != null)
|
||||
heightProperty.removeListener(this);
|
||||
} else {
|
||||
updating = true;
|
||||
try {
|
||||
int width = widthProperty.get();
|
||||
int height = heightProperty.get();
|
||||
|
||||
if (observable instanceof ReadOnlyProperty<?>
|
||||
&& ((ReadOnlyProperty<?>) observable).getBean() == comboBox) {
|
||||
String value = comboBox.valueProperty().get();
|
||||
if (value == null)
|
||||
value = "";
|
||||
int idx = value.indexOf('x');
|
||||
if (idx < 0)
|
||||
idx = value.indexOf('*');
|
||||
|
||||
if (idx < 0) {
|
||||
LOG.warning("Bad window size: " + value);
|
||||
comboBox.setValue(width + "x" + height);
|
||||
return;
|
||||
}
|
||||
|
||||
String widthStr = value.substring(0, idx).trim();
|
||||
String heightStr = value.substring(idx + 1).trim();
|
||||
|
||||
int newWidth;
|
||||
int newHeight;
|
||||
try {
|
||||
newWidth = Integer.parseInt(widthStr);
|
||||
newHeight = Integer.parseInt(heightStr);
|
||||
} catch (NumberFormatException e) {
|
||||
LOG.warning("Bad window size: " + value);
|
||||
comboBox.setValue(width + "x" + height);
|
||||
return;
|
||||
}
|
||||
|
||||
widthProperty.set(newWidth);
|
||||
heightProperty.set(newHeight);
|
||||
} else {
|
||||
comboBox.setValue(width + "x" + height);
|
||||
}
|
||||
} finally {
|
||||
updating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wasGarbageCollected() {
|
||||
return this.comboBoxRef.get() == null
|
||||
|| this.widthPropertyRef.get() == null
|
||||
|| this.heightPropertyRef.get() == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (!(obj instanceof WindowsSizeBidirectionalBinding))
|
||||
return false;
|
||||
|
||||
var that = (WindowsSizeBidirectionalBinding) obj;
|
||||
|
||||
var comboBox = this.comboBoxRef.get();
|
||||
var widthProperty = this.widthPropertyRef.get();
|
||||
var heightProperty = this.heightPropertyRef.get();
|
||||
|
||||
var thatComboBox = that.comboBoxRef.get();
|
||||
var thatWidthProperty = that.widthPropertyRef.get();
|
||||
var thatHeightProperty = that.heightPropertyRef.get();
|
||||
|
||||
if (comboBox == null || widthProperty == null || heightProperty == null
|
||||
|| thatComboBox == null || thatWidthProperty == null || thatHeightProperty == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return comboBox == thatComboBox
|
||||
&& widthProperty == thatWidthProperty
|
||||
&& heightProperty == thatHeightProperty;
|
||||
}
|
||||
}
|
||||
|
||||
public static void bindWindowsSize(JFXComboBox<String> comboBox, IntegerProperty widthProperty, IntegerProperty heightProperty) {
|
||||
comboBox.setValue(widthProperty.get() + "x" + heightProperty.get());
|
||||
var binding = new WindowsSizeBidirectionalBinding(comboBox, widthProperty, heightProperty);
|
||||
comboBox.focusedProperty().addListener(binding);
|
||||
comboBox.sceneProperty().addListener(binding);
|
||||
widthProperty.addListener(binding);
|
||||
heightProperty.addListener(binding);
|
||||
}
|
||||
|
||||
public static void unbindWindowsSize(JFXComboBox<String> comboBox, IntegerProperty widthProperty, IntegerProperty heightProperty) {
|
||||
var binding = new WindowsSizeBidirectionalBinding(comboBox, widthProperty, heightProperty);
|
||||
comboBox.focusedProperty().removeListener(binding);
|
||||
comboBox.sceneProperty().removeListener(binding);
|
||||
widthProperty.removeListener(binding);
|
||||
heightProperty.removeListener(binding);
|
||||
}
|
||||
|
||||
public static void bindAllEnabled(BooleanProperty allEnabled, BooleanProperty... children) {
|
||||
|
||||
@@ -34,10 +34,7 @@ import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelReader;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.input.DragEvent;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
@@ -167,6 +164,14 @@ public class DecoratorController {
|
||||
// press ESC to go back
|
||||
onEscPressed(navigator, this::back);
|
||||
|
||||
// press F11 to toggle full screen
|
||||
navigator.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
|
||||
if (e.getCode() == KeyCode.F11) {
|
||||
stage.setFullScreen(!stage.isFullScreen());
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// For JavaFX 12+
|
||||
MouseButton button = MouseButton.valueOf("BACK");
|
||||
|
||||
@@ -18,8 +18,11 @@
|
||||
package org.jackhuang.hmcl.ui.decorator;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -29,6 +32,7 @@ import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.effect.BlurType;
|
||||
import javafx.scene.effect.DropShadow;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
@@ -50,6 +54,8 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
private final Stage primaryStage;
|
||||
private final TransitionPane navBarPane;
|
||||
|
||||
private final InvalidationListener onWindowsStatusChange;
|
||||
|
||||
private double mouseInitX, mouseInitY, stageInitX, stageInitY, stageInitWidth, stageInitHeight;
|
||||
|
||||
/**
|
||||
@@ -80,9 +86,25 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
|
||||
skinnable.getSnackbar().registerSnackbarContainer(parent);
|
||||
|
||||
root.addEventFilter(MouseEvent.MOUSE_RELEASED, this::onMouseReleased);
|
||||
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, this::onMouseDragged);
|
||||
root.addEventFilter(MouseEvent.MOUSE_MOVED, this::onMouseMoved);
|
||||
EventHandler<MouseEvent> onMouseReleased = this::onMouseReleased;
|
||||
EventHandler<MouseEvent> onMouseDragged = this::onMouseDragged;
|
||||
EventHandler<MouseEvent> onMouseMoved = this::onMouseMoved;
|
||||
onWindowsStatusChange = observable -> {
|
||||
if (primaryStage.isIconified() || primaryStage.isFullScreen() || primaryStage.isMaximized()) {
|
||||
root.removeEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
|
||||
root.removeEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
|
||||
root.removeEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
|
||||
} else {
|
||||
root.addEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
|
||||
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
|
||||
root.addEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
|
||||
}
|
||||
};
|
||||
WeakInvalidationListener weakOnWindowsStatusChange = new WeakInvalidationListener(onWindowsStatusChange);
|
||||
primaryStage.iconifiedProperty().addListener(weakOnWindowsStatusChange);
|
||||
primaryStage.maximizedProperty().addListener(weakOnWindowsStatusChange);
|
||||
primaryStage.fullScreenProperty().addListener(weakOnWindowsStatusChange);
|
||||
onWindowsStatusChange.invalidated(null);
|
||||
|
||||
shadowContainer.getChildren().setAll(parent);
|
||||
root.getChildren().setAll(shadowContainer);
|
||||
@@ -155,6 +177,12 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
|
||||
BorderPane titleBar = new BorderPane();
|
||||
titleContainer.getChildren().add(titleBar);
|
||||
titleBar.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
|
||||
primaryStage.setMaximized(!primaryStage.isMaximized());
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
|
||||
Rectangle buttonsContainerPlaceHolder = new Rectangle();
|
||||
{
|
||||
|
||||
@@ -17,24 +17,17 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXSpinner;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.application.Platform;
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
@@ -55,291 +48,67 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.HintPane;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.ui.wizard.Refreshable;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.wrap;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.*;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class VersionsPage extends BorderPane implements WizardPage, Refreshable {
|
||||
public final class VersionsPage extends Control implements WizardPage, Refreshable {
|
||||
private final String gameVersion;
|
||||
private final String libraryId;
|
||||
private final String title;
|
||||
private final Navigation navigation;
|
||||
|
||||
private final JFXListView<RemoteVersion> list;
|
||||
private final JFXSpinner spinner;
|
||||
private final StackPane failedPane;
|
||||
private final StackPane emptyPane;
|
||||
private final TransitionPane root;
|
||||
private final JFXCheckBox chkRelease;
|
||||
private final JFXCheckBox chkSnapshot;
|
||||
private final JFXCheckBox chkOld;
|
||||
private final ComponentList centrePane;
|
||||
private final StackPane center;
|
||||
|
||||
private final VersionList<?> versionList;
|
||||
private Task<?> executor;
|
||||
private final Runnable callback;
|
||||
|
||||
private final HBox searchBar;
|
||||
private final StringProperty queryString = new SimpleStringProperty();
|
||||
private final ObservableList<RemoteVersion> versions = FXCollections.observableArrayList();
|
||||
private final ObjectProperty<Status> status = new SimpleObjectProperty<>(Status.LOADING);
|
||||
|
||||
public VersionsPage(Navigation navigation, String title, String gameVersion, DownloadProvider downloadProvider, String libraryId, Runnable callback) {
|
||||
this.title = title;
|
||||
this.gameVersion = gameVersion;
|
||||
this.libraryId = libraryId;
|
||||
this.navigation = navigation;
|
||||
|
||||
HintPane hintPane = new HintPane();
|
||||
hintPane.setText(i18n("sponsor.bmclapi"));
|
||||
hintPane.getStyleClass().add("sponsor-pane");
|
||||
FXUtils.onClicked(hintPane, this::onSponsor);
|
||||
BorderPane.setMargin(hintPane, new Insets(10, 10, 0, 10));
|
||||
this.setTop(hintPane);
|
||||
|
||||
root = new TransitionPane();
|
||||
BorderPane toolbarPane = new BorderPane();
|
||||
JFXButton btnRefresh;
|
||||
{
|
||||
spinner = new JFXSpinner();
|
||||
|
||||
center = new StackPane();
|
||||
center.setStyle("-fx-padding: 10;");
|
||||
{
|
||||
centrePane = new ComponentList();
|
||||
centrePane.getStyleClass().add("no-padding");
|
||||
{
|
||||
HBox checkPane = new HBox();
|
||||
checkPane.setSpacing(10);
|
||||
{
|
||||
chkRelease = new JFXCheckBox(i18n("version.game.releases"));
|
||||
chkRelease.setSelected(true);
|
||||
HBox.setMargin(chkRelease, new Insets(10, 0, 10, 0));
|
||||
|
||||
chkSnapshot = new JFXCheckBox(i18n("version.game.snapshots"));
|
||||
HBox.setMargin(chkSnapshot, new Insets(10, 0, 10, 0));
|
||||
|
||||
chkOld = new JFXCheckBox(i18n("version.game.old"));
|
||||
HBox.setMargin(chkOld, new Insets(10, 0, 10, 0));
|
||||
|
||||
checkPane.getChildren().setAll(chkRelease, chkSnapshot, chkOld);
|
||||
}
|
||||
|
||||
list = new JFXListView<>();
|
||||
list.getStyleClass().add("jfx-list-view-float");
|
||||
VBox.setVgrow(list, Priority.ALWAYS);
|
||||
|
||||
TransitionPane rightToolbarPane = new TransitionPane();
|
||||
{
|
||||
HBox refreshPane = new HBox();
|
||||
refreshPane.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
btnRefresh = new JFXButton(i18n("button.refresh"));
|
||||
btnRefresh.getStyleClass().add("jfx-tool-bar-button");
|
||||
btnRefresh.setOnAction(e -> onRefresh());
|
||||
|
||||
JFXButton btnSearch = new JFXButton(i18n("search"));
|
||||
btnSearch.getStyleClass().add("jfx-tool-bar-button");
|
||||
btnSearch.setGraphic(wrap(SVG.SEARCH.createIcon(Theme.blackFill(), -1)));
|
||||
|
||||
searchBar = new HBox();
|
||||
{
|
||||
searchBar.setAlignment(Pos.CENTER);
|
||||
searchBar.setPadding(new Insets(0, 5, 0, 0));
|
||||
|
||||
JFXTextField searchField = new JFXTextField();
|
||||
searchField.setPromptText(i18n("search"));
|
||||
HBox.setHgrow(searchField, Priority.ALWAYS);
|
||||
|
||||
JFXButton closeSearchBar = new JFXButton();
|
||||
closeSearchBar.getStyleClass().add("jfx-tool-bar-button");
|
||||
closeSearchBar.setGraphic(wrap(SVG.CLOSE.createIcon(Theme.blackFill(), -1)));
|
||||
closeSearchBar.setOnAction(e -> {
|
||||
searchField.clear();
|
||||
rightToolbarPane.setContent(refreshPane, ContainerAnimations.FADE);
|
||||
});
|
||||
onEscPressed(searchField, closeSearchBar::fire);
|
||||
PauseTransition pause = new PauseTransition(Duration.millis(100));
|
||||
pause.setOnFinished(e -> queryString.set(searchField.getText()));
|
||||
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
pause.setRate(1);
|
||||
pause.playFromStart();
|
||||
});
|
||||
|
||||
searchBar.getChildren().setAll(searchField, closeSearchBar);
|
||||
|
||||
btnSearch.setOnAction(e -> {
|
||||
rightToolbarPane.setContent(searchBar, ContainerAnimations.FADE);
|
||||
searchField.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
refreshPane.getChildren().setAll(new HBox(btnSearch, btnRefresh));
|
||||
rightToolbarPane.setContent(refreshPane, ContainerAnimations.NONE);
|
||||
}
|
||||
|
||||
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
|
||||
ignoreEvent(list, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
|
||||
toolbarPane.setLeft(checkPane);
|
||||
toolbarPane.setRight(rightToolbarPane);
|
||||
|
||||
centrePane.getContent().setAll(toolbarPane, list);
|
||||
}
|
||||
|
||||
center.getChildren().setAll(centrePane);
|
||||
}
|
||||
|
||||
failedPane = new StackPane();
|
||||
failedPane.getStyleClass().add("notice-pane");
|
||||
{
|
||||
Label label = new Label(i18n("download.failed.refresh"));
|
||||
FXUtils.onClicked(label, this::onRefresh);
|
||||
|
||||
failedPane.getChildren().setAll(label);
|
||||
}
|
||||
|
||||
emptyPane = new StackPane();
|
||||
emptyPane.getStyleClass().add("notice-pane");
|
||||
{
|
||||
Label label = new Label(i18n("download.failed.empty"));
|
||||
FXUtils.onClicked(label, this::onBack);
|
||||
|
||||
emptyPane.getChildren().setAll(label);
|
||||
}
|
||||
}
|
||||
this.setCenter(root);
|
||||
|
||||
versionList = downloadProvider.getVersionListById(libraryId);
|
||||
boolean hasType = versionList.hasType();
|
||||
chkRelease.setManaged(hasType);
|
||||
chkRelease.setVisible(hasType);
|
||||
chkSnapshot.setManaged(hasType);
|
||||
chkSnapshot.setVisible(hasType);
|
||||
chkOld.setManaged(hasType);
|
||||
chkOld.setVisible(hasType);
|
||||
|
||||
if (hasType) {
|
||||
centrePane.getContent().setAll(toolbarPane, list);
|
||||
} else {
|
||||
centrePane.getContent().setAll(list);
|
||||
}
|
||||
ComponentList.setVgrow(list, Priority.ALWAYS);
|
||||
|
||||
InvalidationListener listener = o -> {
|
||||
List<RemoteVersion> versions = loadVersions();
|
||||
String query = queryString.get();
|
||||
if (!StringUtils.isBlank(query)) {
|
||||
Predicate<RemoteVersion> predicate;
|
||||
if (query.startsWith("regex:")) {
|
||||
try {
|
||||
Pattern pattern = Pattern.compile(query.substring("regex:".length()));
|
||||
predicate = it -> pattern.matcher(it.getSelfVersion()).find();
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Illegal regular expression", e);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
String lowerQueryString = query.toLowerCase(Locale.ROOT);
|
||||
predicate = it -> it.getSelfVersion().toLowerCase(Locale.ROOT).contains(lowerQueryString);
|
||||
}
|
||||
|
||||
versions = versions.stream().filter(predicate).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
list.getItems().setAll(versions);
|
||||
};
|
||||
chkRelease.selectedProperty().addListener(listener);
|
||||
chkSnapshot.selectedProperty().addListener(listener);
|
||||
chkOld.selectedProperty().addListener(listener);
|
||||
queryString.addListener(listener);
|
||||
|
||||
btnRefresh.setGraphic(wrap(SVG.REFRESH.createIcon(Theme.blackFill(), -1)));
|
||||
|
||||
Holder<RemoteVersionListCell> lastCell = new Holder<>();
|
||||
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, libraryId));
|
||||
|
||||
FXUtils.onClicked(list, () -> {
|
||||
if (list.getSelectionModel().getSelectedIndex() < 0)
|
||||
return;
|
||||
navigation.getSettings().put(libraryId, list.getSelectionModel().getSelectedItem());
|
||||
callback.run();
|
||||
});
|
||||
this.versionList = downloadProvider.getVersionListById(libraryId);
|
||||
this.callback = callback;
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
private List<RemoteVersion> loadVersions() {
|
||||
return versionList.getVersions(gameVersion).stream()
|
||||
.filter(it -> {
|
||||
switch (it.getVersionType()) {
|
||||
case RELEASE:
|
||||
return chkRelease.isSelected();
|
||||
case PENDING:
|
||||
case SNAPSHOT:
|
||||
return chkSnapshot.isSelected();
|
||||
case OLD:
|
||||
return chkOld.isSelected();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.sorted().collect(Collectors.toList());
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new VersionsPageSkin(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh() {
|
||||
VersionList<?> currentVersionList = versionList;
|
||||
root.setContent(spinner, ContainerAnimations.FADE);
|
||||
executor = currentVersionList.refreshAsync(gameVersion).whenComplete(Schedulers.defaultScheduler(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
List<RemoteVersion> items = loadVersions();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
if (versionList != currentVersionList) return;
|
||||
if (currentVersionList.getVersions(gameVersion).isEmpty()) {
|
||||
root.setContent(emptyPane, ContainerAnimations.FADE);
|
||||
status.set(Status.LOADING);
|
||||
Task<?> task = versionList.refreshAsync(gameVersion)
|
||||
.thenSupplyAsync(() -> versionList.getVersions(gameVersion).stream().sorted().collect(Collectors.toList()))
|
||||
.whenComplete(Schedulers.javafx(), (items, exception) -> {
|
||||
if (exception == null) {
|
||||
versions.setAll(items);
|
||||
status.set(Status.SUCCESS);
|
||||
} else {
|
||||
if (items.isEmpty()) {
|
||||
chkRelease.setSelected(true);
|
||||
chkSnapshot.setSelected(true);
|
||||
chkOld.setSelected(true);
|
||||
} else {
|
||||
list.getItems().setAll(items);
|
||||
}
|
||||
root.setContent(center, ContainerAnimations.FADE);
|
||||
LOG.warning("Failed to fetch versions list", exception);
|
||||
status.set(Status.FAILED);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
LOG.warning("Failed to fetch versions list", exception);
|
||||
Platform.runLater(() -> {
|
||||
if (versionList != currentVersionList) return;
|
||||
root.setContent(failedPane, ContainerAnimations.FADE);
|
||||
});
|
||||
}
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/938
|
||||
System.gc();
|
||||
});
|
||||
executor.start();
|
||||
task.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -350,9 +119,6 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
@Override
|
||||
public void cleanup(Map<String, Object> settings) {
|
||||
settings.remove(libraryId);
|
||||
// fixme
|
||||
// if (executor != null)
|
||||
// executor.cancel(true);
|
||||
}
|
||||
|
||||
private void onRefresh() {
|
||||
@@ -367,10 +133,24 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
FXUtils.openLink("https://bmclapidoc.bangbang93.com");
|
||||
}
|
||||
|
||||
private enum Status {
|
||||
LOADING,
|
||||
FAILED,
|
||||
SUCCESS,
|
||||
}
|
||||
|
||||
private enum VersionTypeFilter {
|
||||
ALL,
|
||||
RELEASE,
|
||||
SNAPSHOTS,
|
||||
APRIL_FOOLS,
|
||||
OLD
|
||||
}
|
||||
|
||||
private static class RemoteVersionListCell extends ListCell<RemoteVersion> {
|
||||
final IconedTwoLineListItem content = new IconedTwoLineListItem();
|
||||
final RipplerContainer ripplerContainer = new RipplerContainer(content);
|
||||
final StackPane pane = new StackPane();
|
||||
private final IconedTwoLineListItem content = new IconedTwoLineListItem();
|
||||
private final RipplerContainer ripplerContainer = new RipplerContainer(content);
|
||||
private final StackPane pane = new StackPane();
|
||||
|
||||
private final Holder<RemoteVersionListCell> lastCell;
|
||||
|
||||
@@ -409,22 +189,30 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
}
|
||||
|
||||
if (remoteVersion instanceof GameRemoteVersion) {
|
||||
switch (remoteVersion.getVersionType()) {
|
||||
RemoteVersion.Type versionType = remoteVersion.getVersionType();
|
||||
String wikiSuffix = getWikiUrlSuffix(remoteVersion.getGameVersion());
|
||||
switch (versionType) {
|
||||
case RELEASE:
|
||||
content.getTags().setAll(i18n("version.game.release"));
|
||||
content.setImage(VersionIconType.GRASS.getIcon());
|
||||
content.setExternalLink(i18n("wiki.version.game.release", remoteVersion.getGameVersion()));
|
||||
content.setExternalLink(i18n("wiki.version.game", wikiSuffix));
|
||||
break;
|
||||
case PENDING:
|
||||
case SNAPSHOT:
|
||||
content.getTags().setAll(i18n("version.game.snapshot"));
|
||||
content.setImage(VersionIconType.COMMAND.getIcon());
|
||||
content.setExternalLink(i18n("wiki.version.game.snapshot", remoteVersion.getGameVersion()));
|
||||
if (versionType == RemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isSpecial()) {
|
||||
content.getTags().setAll(i18n("version.game.april_fools"));
|
||||
content.setImage(VersionIconType.APRIL_FOOLS.getIcon());
|
||||
} else {
|
||||
content.getTags().setAll(i18n("version.game.snapshot"));
|
||||
content.setImage(VersionIconType.COMMAND.getIcon());
|
||||
}
|
||||
content.setExternalLink(i18n("wiki.version.game", wikiSuffix));
|
||||
break;
|
||||
default:
|
||||
content.getTags().setAll(i18n("version.game.old"));
|
||||
content.setImage(VersionIconType.CRAFT_TABLE.getIcon());
|
||||
content.setExternalLink(null);
|
||||
content.setExternalLink(i18n("wiki.version.game", wikiSuffix));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -452,5 +240,285 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
content.setExternalLink(null);
|
||||
}
|
||||
}
|
||||
|
||||
private String getWikiUrlSuffix(String gameVersion) {
|
||||
String id = gameVersion.toLowerCase(Locale.ROOT);
|
||||
|
||||
switch (id) {
|
||||
case "0.30-1":
|
||||
case "0.30-2":
|
||||
case "c0.30_01c":
|
||||
return i18n("wiki.version.game.search", "Classic_0.30");
|
||||
case "in-20100206-2103":
|
||||
return i18n("wiki.version.game.search", "Indev_20100206");
|
||||
case "inf-20100630-1":
|
||||
return i18n("wiki.version.game.search", "Infdev_20100630");
|
||||
case "inf-20100630-2":
|
||||
return i18n("wiki.version.game.search", "Alpha_v1.0.0");
|
||||
case "1.19_deep_dark_experimental_snapshot-1":
|
||||
return "1.19-exp1";
|
||||
case "in-20100130":
|
||||
return i18n("wiki.version.game.search", "Indev_0.31_20100130");
|
||||
case "b1.6-tb3":
|
||||
return i18n("wiki.version.game.search", "Beta_1.6_Test_Build_3");
|
||||
case "1.14_combat-212796":
|
||||
return i18n("wiki.version.game.search", "1.14.3_-_Combat_Test");
|
||||
case "1.14_combat-0":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_2");
|
||||
case "1.14_combat-3":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_3");
|
||||
case "1_15_combat-1":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_4");
|
||||
case "1_15_combat-6":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_5");
|
||||
case "1_16_combat-0":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_6");
|
||||
case "1_16_combat-1":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_7");
|
||||
case "1_16_combat-2":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_7b");
|
||||
case "1_16_combat-3":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_7c");
|
||||
case "1_16_combat-4":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_8");
|
||||
case "1_16_combat-5":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_8b");
|
||||
case "1_16_combat-6":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_8c");
|
||||
}
|
||||
|
||||
if (id.startsWith("1.0.0-rc2")) return "RC2";
|
||||
if (id.startsWith("2.0")) return i18n("wiki.version.game.search", "2.0");
|
||||
if (id.startsWith("b1.8-pre1")) return "Beta_1.8-pre1";
|
||||
if (id.startsWith("b1.1-")) return i18n("wiki.version.game.search", "Beta_1.1");
|
||||
if (id.startsWith("a1.1.0")) return "Alpha_v1.1.0";
|
||||
if (id.startsWith("a1.0.14")) return "Alpha_v1.0.14";
|
||||
if (id.startsWith("a1.0.13_01")) return "Alpha_v1.0.13_01";
|
||||
if (id.startsWith("in-20100214")) return i18n("wiki.version.game.search", "Indev_20100214");
|
||||
|
||||
if (id.contains("experimental-snapshot")) {
|
||||
return id.replace("_experimental-snapshot-", "-exp");
|
||||
}
|
||||
|
||||
if (id.startsWith("inf-")) return id.replace("inf-", "Infdev_");
|
||||
if (id.startsWith("in-")) return id.replace("in-", "Indev_");
|
||||
if (id.startsWith("rd-")) return "pre-Classic_" + id;
|
||||
if (id.startsWith("b")) return id.replace("b", "Beta_");
|
||||
if (id.startsWith("a")) return id.replace("a", "Alpha_v");
|
||||
if (id.startsWith("c")) return id.replace("c", "Classic_").replace("st", "SURVIVAL_TEST");
|
||||
|
||||
return i18n("wiki.version.game.search", id);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class VersionsPageSkin extends SkinBase<VersionsPage> {
|
||||
private final JFXListView<RemoteVersion> list;
|
||||
|
||||
private final TransitionPane transitionPane;
|
||||
private final JFXSpinner spinner;
|
||||
|
||||
private final JFXTextField nameField;
|
||||
private final JFXComboBox<VersionTypeFilter> categoryField = new JFXComboBox<>();
|
||||
|
||||
VersionsPageSkin(VersionsPage control) {
|
||||
super(control);
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
|
||||
GridPane searchPane = new GridPane();
|
||||
root.setTop(searchPane);
|
||||
searchPane.getStyleClass().addAll("card");
|
||||
BorderPane.setMargin(searchPane, new Insets(10, 10, 0, 10));
|
||||
|
||||
ColumnConstraints nameColumn = new ColumnConstraints();
|
||||
nameColumn.setMinWidth(USE_PREF_SIZE);
|
||||
ColumnConstraints column1 = new ColumnConstraints();
|
||||
column1.setHgrow(Priority.ALWAYS);
|
||||
ColumnConstraints column2 = new ColumnConstraints();
|
||||
column2.setMaxWidth(150);
|
||||
ColumnConstraints column3 = new ColumnConstraints();
|
||||
|
||||
if (control.versionList.hasType())
|
||||
searchPane.getColumnConstraints().setAll(nameColumn, column1, nameColumn, column2, column3);
|
||||
else
|
||||
searchPane.getColumnConstraints().setAll(nameColumn, column1, column3);
|
||||
|
||||
searchPane.setHgap(16);
|
||||
searchPane.setVgap(10);
|
||||
|
||||
{
|
||||
int rowIndex = 0;
|
||||
|
||||
{
|
||||
nameField = new JFXTextField();
|
||||
nameField.setPromptText(i18n("version.search.prompt"));
|
||||
nameField.textProperty().addListener(o -> updateList());
|
||||
|
||||
if ("game".equals(control.libraryId)) {
|
||||
categoryField.getItems().setAll(
|
||||
VersionTypeFilter.ALL,
|
||||
VersionTypeFilter.RELEASE,
|
||||
VersionTypeFilter.SNAPSHOTS,
|
||||
VersionTypeFilter.APRIL_FOOLS,
|
||||
VersionTypeFilter.OLD
|
||||
);
|
||||
categoryField.getSelectionModel().select(VersionTypeFilter.RELEASE);
|
||||
} else {
|
||||
categoryField.getItems().setAll(
|
||||
VersionTypeFilter.ALL,
|
||||
VersionTypeFilter.RELEASE,
|
||||
VersionTypeFilter.SNAPSHOTS
|
||||
);
|
||||
categoryField.getSelectionModel().select(VersionTypeFilter.ALL);
|
||||
}
|
||||
categoryField.setConverter(stringConverter(type -> i18n("version.game." + type.name().toLowerCase(Locale.ROOT))));
|
||||
categoryField.getSelectionModel().selectedItemProperty().addListener(o -> updateList());
|
||||
|
||||
JFXButton refreshButton = FXUtils.newRaisedButton(i18n("button.refresh"));
|
||||
refreshButton.setOnAction(event -> control.onRefresh());
|
||||
|
||||
if (control.versionList.hasType()) {
|
||||
searchPane.addRow(rowIndex++,
|
||||
new Label(i18n("version.search")), nameField,
|
||||
new Label(i18n("version.game.type")), categoryField,
|
||||
refreshButton
|
||||
);
|
||||
} else {
|
||||
searchPane.addRow(rowIndex++,
|
||||
new Label(i18n("version.search")), nameField,
|
||||
refreshButton
|
||||
);
|
||||
}
|
||||
}
|
||||
// {
|
||||
// HBox actionsBox = new HBox(8);
|
||||
// GridPane.setColumnSpan(actionsBox, 4);
|
||||
// actionsBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
//
|
||||
// JFXButton refreshButton = FXUtils.newRaisedButton(i18n("button.refresh"));
|
||||
// refreshButton.setOnAction(event -> control.onRefresh());
|
||||
//
|
||||
// actionsBox.getChildren().setAll(refreshButton);
|
||||
//
|
||||
// searchPane.addRow(rowIndex++, actionsBox);
|
||||
// }
|
||||
}
|
||||
|
||||
{
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
root.setCenter(spinnerPane);
|
||||
|
||||
transitionPane = new TransitionPane();
|
||||
spinner = new JFXSpinner();
|
||||
|
||||
StackPane centerWrapper = new StackPane();
|
||||
centerWrapper.setStyle("-fx-padding: 10;");
|
||||
{
|
||||
ComponentList centrePane = new ComponentList();
|
||||
centrePane.getStyleClass().add("no-padding");
|
||||
{
|
||||
list = new JFXListView<>();
|
||||
list.getStyleClass().add("jfx-list-view-float");
|
||||
VBox.setVgrow(list, Priority.ALWAYS);
|
||||
|
||||
control.versions.addListener((InvalidationListener) o -> updateList());
|
||||
|
||||
Holder<RemoteVersionListCell> lastCell = new Holder<>();
|
||||
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, control.libraryId));
|
||||
|
||||
FXUtils.onClicked(list, () -> {
|
||||
if (list.getSelectionModel().getSelectedIndex() < 0)
|
||||
return;
|
||||
control.navigation.getSettings().put(control.libraryId, list.getSelectionModel().getSelectedItem());
|
||||
control.callback.run();
|
||||
});
|
||||
|
||||
ComponentList.setVgrow(list, Priority.ALWAYS);
|
||||
|
||||
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
|
||||
ignoreEvent(list, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
|
||||
centrePane.getContent().setAll(list);
|
||||
}
|
||||
|
||||
centerWrapper.getChildren().setAll(centrePane);
|
||||
}
|
||||
|
||||
StackPane failedPane = new StackPane();
|
||||
failedPane.getStyleClass().add("notice-pane");
|
||||
{
|
||||
Label label = new Label(i18n("download.failed.refresh"));
|
||||
FXUtils.onClicked(label, control::onRefresh);
|
||||
|
||||
failedPane.getChildren().setAll(label);
|
||||
}
|
||||
|
||||
StackPane emptyPane = new StackPane();
|
||||
emptyPane.getStyleClass().add("notice-pane");
|
||||
{
|
||||
Label label = new Label(i18n("download.failed.empty"));
|
||||
FXUtils.onClicked(label, control::onBack);
|
||||
|
||||
emptyPane.getChildren().setAll(label);
|
||||
}
|
||||
|
||||
FXUtils.onChangeAndOperate(control.status, status -> {
|
||||
if (status == Status.LOADING)
|
||||
transitionPane.setContent(spinner, ContainerAnimations.FADE);
|
||||
else if (status == Status.SUCCESS)
|
||||
transitionPane.setContent(centerWrapper, ContainerAnimations.FADE);
|
||||
else // if (status == Status.FAILED)
|
||||
transitionPane.setContent(failedPane, ContainerAnimations.FADE);
|
||||
});
|
||||
|
||||
root.setCenter(transitionPane);
|
||||
}
|
||||
|
||||
this.getChildren().setAll(root);
|
||||
}
|
||||
|
||||
private void updateList() {
|
||||
Stream<RemoteVersion> versions = getSkinnable().versions.stream();
|
||||
|
||||
VersionTypeFilter filter = categoryField.getSelectionModel().getSelectedItem();
|
||||
if (filter != null)
|
||||
versions = versions.filter(it -> {
|
||||
RemoteVersion.Type versionType = it.getVersionType();
|
||||
switch (filter) {
|
||||
case RELEASE:
|
||||
return versionType == RemoteVersion.Type.RELEASE;
|
||||
case SNAPSHOTS:
|
||||
return versionType == RemoteVersion.Type.SNAPSHOT
|
||||
|| versionType == RemoteVersion.Type.PENDING;
|
||||
case APRIL_FOOLS:
|
||||
return versionType == RemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(it.getGameVersion()).isSpecial();
|
||||
case OLD:
|
||||
return versionType == RemoteVersion.Type.OLD;
|
||||
case ALL:
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
String nameQuery = nameField.getText();
|
||||
if (!StringUtils.isBlank(nameQuery)) {
|
||||
if (nameQuery.startsWith("regex:")) {
|
||||
try {
|
||||
Pattern pattern = Pattern.compile(nameQuery.substring("regex:".length()));
|
||||
versions = versions.filter(it -> pattern.matcher(it.getSelfVersion()).find());
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Illegal regular expression: " + nameQuery, e);
|
||||
}
|
||||
} else {
|
||||
String lowerQueryString = nameQuery.toLowerCase(Locale.ROOT);
|
||||
versions = versions.filter(it -> it.getSelfVersion().toLowerCase(Locale.ROOT).contains(lowerQueryString));
|
||||
}
|
||||
}
|
||||
|
||||
//noinspection DataFlowIssue
|
||||
list.getItems().setAll(versions.collect(Collectors.toList()));
|
||||
list.scrollTo(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
@@ -42,10 +43,16 @@ import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
@@ -61,16 +68,24 @@ import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static org.jackhuang.hmcl.download.RemoteVersion.Type.RELEASE;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.SINE;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class MainPage extends StackPane implements DecoratorPage {
|
||||
private static final String ANNOUNCEMENT = "announcement";
|
||||
@@ -211,7 +226,6 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
launchButton.setPrefWidth(230);
|
||||
launchButton.setPrefHeight(55);
|
||||
//launchButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
launchButton.setOnAction(e -> launch());
|
||||
launchButton.setDefaultButton(true);
|
||||
launchButton.setClip(new Rectangle(-100, -100, 310, 200));
|
||||
{
|
||||
@@ -219,18 +233,34 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
graphic.setAlignment(Pos.CENTER);
|
||||
graphic.setTranslateX(-7);
|
||||
graphic.setMaxWidth(200);
|
||||
Label launchLabel = new Label(i18n("version.launch"));
|
||||
Label launchLabel = new Label();
|
||||
launchLabel.setStyle("-fx-font-size: 16px;");
|
||||
Label currentLabel = new Label();
|
||||
currentLabel.setStyle("-fx-font-size: 12px;");
|
||||
currentLabel.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
if (getCurrentGame() == null) {
|
||||
return i18n("version.empty");
|
||||
} else {
|
||||
return getCurrentGame();
|
||||
|
||||
FXUtils.onChangeAndOperate(currentGameProperty(), new Consumer<>() {
|
||||
private Tooltip tooltip;
|
||||
|
||||
@Override
|
||||
public void accept(String currentGame) {
|
||||
if (currentGame == null) {
|
||||
launchLabel.setText(i18n("version.launch.empty"));
|
||||
currentLabel.setText(null);
|
||||
graphic.getChildren().setAll(launchLabel);
|
||||
launchButton.setOnAction(e -> MainPage.this.launchNoGame());
|
||||
if (tooltip == null)
|
||||
tooltip = new Tooltip(i18n("version.launch.empty.tooltip"));
|
||||
FXUtils.installFastTooltip(launchButton, tooltip);
|
||||
} else {
|
||||
launchLabel.setText(i18n("version.launch"));
|
||||
currentLabel.setText(currentGame);
|
||||
graphic.getChildren().setAll(launchLabel, currentLabel);
|
||||
launchButton.setOnAction(e -> MainPage.this.launch());
|
||||
if (tooltip != null)
|
||||
Tooltip.uninstall(launchButton, tooltip);
|
||||
}
|
||||
}
|
||||
}, currentGameProperty()));
|
||||
graphic.getChildren().setAll(launchLabel, currentLabel);
|
||||
});
|
||||
|
||||
launchButton.setGraphic(graphic);
|
||||
}
|
||||
@@ -319,7 +349,46 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
}
|
||||
|
||||
private void launch() {
|
||||
Versions.launch(Profiles.getSelectedProfile());
|
||||
Profile profile = Profiles.getSelectedProfile();
|
||||
Versions.launch(profile, profile.getSelectedVersion(), null);
|
||||
}
|
||||
|
||||
private void launchNoGame() {
|
||||
DownloadProvider downloadProvider = DownloadProviders.getDownloadProvider();
|
||||
VersionList<?> versionList = downloadProvider.getVersionListById("game");
|
||||
|
||||
Holder<String> gameVersionHolder = new Holder<>();
|
||||
Task<?> task = versionList.refreshAsync("")
|
||||
.thenSupplyAsync(() -> versionList.getVersions("").stream()
|
||||
.filter(it -> it.getVersionType() == RELEASE)
|
||||
.sorted()
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IOException("No versions found")))
|
||||
.thenComposeAsync(version -> {
|
||||
Profile profile = Profiles.getSelectedProfile();
|
||||
DefaultDependencyManager dependency = profile.getDependency();
|
||||
String gameVersion = gameVersionHolder.value = version.getGameVersion();
|
||||
|
||||
return dependency.gameBuilder()
|
||||
.name(gameVersion)
|
||||
.gameVersion(gameVersion)
|
||||
.buildAsync();
|
||||
})
|
||||
.whenComplete(any -> profile.getRepository().refreshVersions())
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
profile.setSelectedVersion(gameVersionHolder.value);
|
||||
launch();
|
||||
} else if (exception instanceof CancellationException) {
|
||||
Controllers.showToast(i18n("message.cancelled"));
|
||||
} else {
|
||||
LOG.warning("Failed to install game", exception);
|
||||
Controllers.dialog(StringUtils.getStackTrace(exception),
|
||||
i18n("install.failed"),
|
||||
MessageDialogPane.MessageType.WARNING);
|
||||
}
|
||||
});
|
||||
Controllers.taskDialog(task, i18n("version.launch.empty.installing"), TaskCancellationAction.NORMAL);
|
||||
}
|
||||
|
||||
private void onMenu() {
|
||||
|
||||
@@ -23,6 +23,7 @@ import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -40,10 +41,12 @@ import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
@@ -123,22 +126,46 @@ public final class SettingsPage extends SettingsView {
|
||||
|
||||
@Override
|
||||
protected void onExportLogs() {
|
||||
// We cannot determine which file is JUL using.
|
||||
// So we write all the logs to a new file.
|
||||
thread(() -> {
|
||||
Path logFile = Paths.get("hmcl-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
|
||||
String nameBase = "hmcl-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss"));
|
||||
List<Path> recentLogFiles = LOG.findRecentLogFiles(5);
|
||||
|
||||
LOG.info("Exporting logs to " + logFile);
|
||||
try (OutputStream output = Files.newOutputStream(logFile)) {
|
||||
LOG.exportLogs(output);
|
||||
Path outputFile;
|
||||
try {
|
||||
if (recentLogFiles.isEmpty()) {
|
||||
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".log");
|
||||
|
||||
LOG.info("Exporting latest logs to " + outputFile);
|
||||
try (OutputStream output = Files.newOutputStream(outputFile)) {
|
||||
LOG.exportLogs(output);
|
||||
}
|
||||
} else {
|
||||
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".zip");
|
||||
|
||||
LOG.info("Exporting latest logs to " + outputFile);
|
||||
try (var os = Files.newOutputStream(outputFile);
|
||||
var zos = new ZipOutputStream(os)) {
|
||||
|
||||
for (Path path : recentLogFiles) {
|
||||
String zipEntryName = path.getFileName().toString();
|
||||
zos.putNextEntry(new ZipEntry(zipEntryName));
|
||||
Files.copy(path, zos);
|
||||
zos.closeEntry();
|
||||
}
|
||||
|
||||
zos.putNextEntry(new ZipEntry("latest.log"));
|
||||
LOG.exportLogs(zos);
|
||||
zos.closeEntry();
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(e), null, MessageType.ERROR));
|
||||
LOG.warning("Failed to export logs", e);
|
||||
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(e), null, MessageType.ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", logFile)));
|
||||
FXUtils.showFileInExplorer(logFile);
|
||||
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", outputFile)));
|
||||
FXUtils.showFileInExplorer(outputFile);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -39,10 +39,12 @@ import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -90,6 +92,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
|
||||
private int searchID = 0;
|
||||
protected RemoteModRepository repository;
|
||||
private final DownloadProvider downloadProvider;
|
||||
|
||||
private Runnable retrySearch;
|
||||
|
||||
@@ -101,6 +104,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
this.repository = repository;
|
||||
this.callback = callback;
|
||||
this.versionSelection = versionSelection;
|
||||
this.downloadProvider = DownloadProviders.getDownloadProvider();
|
||||
}
|
||||
|
||||
public ObservableList<Node> getActions() {
|
||||
@@ -171,7 +175,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
: "";
|
||||
}
|
||||
}).thenApplyAsync(
|
||||
gameVersion -> repository.search(gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteModRepository.SortOrder.DESC)
|
||||
gameVersion -> repository.search(downloadProvider, gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteModRepository.SortOrder.DESC)
|
||||
).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (searchID != currentSearchID) {
|
||||
return;
|
||||
@@ -231,6 +235,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
|
||||
private static class ModDownloadListPageSkin extends SkinBase<DownloadListPage> {
|
||||
private final JFXListView<RemoteMod> listView = new JFXListView<>();
|
||||
|
||||
protected ModDownloadListPageSkin(DownloadListPage control) {
|
||||
super(control);
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ public class VersionIconDialog extends DialogPane {
|
||||
createIcon(VersionIconType.CHEST),
|
||||
createIcon(VersionIconType.CHICKEN),
|
||||
createIcon(VersionIconType.COMMAND),
|
||||
createIcon(VersionIconType.APRIL_FOOLS),
|
||||
createIcon(VersionIconType.OPTIFINE),
|
||||
createIcon(VersionIconType.CRAFT_TABLE),
|
||||
createIcon(VersionIconType.FABRIC),
|
||||
|
||||
@@ -93,8 +93,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
private String versionId;
|
||||
|
||||
private final VBox rootPane;
|
||||
private final JFXTextField txtWidth;
|
||||
private final JFXTextField txtHeight;
|
||||
private final JFXComboBox<String> cboWindowsSize;
|
||||
private final JFXTextField txtServerIP;
|
||||
private final ComponentList componentList;
|
||||
private final JFXComboBox<LauncherVisibility> cboLauncherVisibility;
|
||||
@@ -389,29 +388,20 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
BorderPane right = new BorderPane();
|
||||
dimensionPane.setRight(right);
|
||||
{
|
||||
HBox hbox = new HBox();
|
||||
right.setLeft(hbox);
|
||||
hbox.setPrefWidth(210);
|
||||
hbox.setSpacing(3);
|
||||
hbox.setAlignment(Pos.CENTER);
|
||||
BorderPane.setAlignment(hbox, Pos.CENTER);
|
||||
{
|
||||
txtWidth = new JFXTextField();
|
||||
txtWidth.setPromptText("800");
|
||||
txtWidth.setPrefWidth(100);
|
||||
FXUtils.setValidateWhileTextChanged(txtWidth, true);
|
||||
txtWidth.getValidators().setAll(new NumberValidator(i18n("input.number"), false));
|
||||
|
||||
Label x = new Label("x");
|
||||
|
||||
txtHeight = new JFXTextField();
|
||||
txtHeight.setPromptText("480");
|
||||
txtHeight.setPrefWidth(100);
|
||||
FXUtils.setValidateWhileTextChanged(txtHeight, true);
|
||||
txtHeight.getValidators().setAll(new NumberValidator(i18n("input.number"), false));
|
||||
|
||||
hbox.getChildren().setAll(txtWidth, x, txtHeight);
|
||||
}
|
||||
cboWindowsSize = new JFXComboBox<>();
|
||||
cboWindowsSize.setPrefWidth(150);
|
||||
right.setLeft(cboWindowsSize);
|
||||
cboWindowsSize.setEditable(true);
|
||||
cboWindowsSize.setStyle("-fx-padding: 4 4 4 16");
|
||||
cboWindowsSize.setPromptText("854x480");
|
||||
cboWindowsSize.getItems().addAll(
|
||||
"854x480",
|
||||
"1280x720",
|
||||
"1600x900",
|
||||
"1920x1080",
|
||||
"2560x1440",
|
||||
"3840x2160"
|
||||
);
|
||||
|
||||
chkFullscreen = new JFXCheckBox();
|
||||
right.setRight(chkFullscreen);
|
||||
@@ -419,6 +409,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
chkFullscreen.setAlignment(Pos.CENTER);
|
||||
BorderPane.setAlignment(chkFullscreen, Pos.CENTER);
|
||||
BorderPane.setMargin(chkFullscreen, new Insets(0, 0, 0, 7));
|
||||
|
||||
cboWindowsSize.disableProperty().bind(chkFullscreen.selectedProperty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,8 +551,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
|
||||
// unbind data fields
|
||||
if (lastVersionSetting != null) {
|
||||
FXUtils.unbind(txtWidth, lastVersionSetting.widthProperty());
|
||||
FXUtils.unbind(txtHeight, lastVersionSetting.heightProperty());
|
||||
FXUtils.unbindWindowsSize(cboWindowsSize, lastVersionSetting.widthProperty(), lastVersionSetting.heightProperty());
|
||||
maxMemory.unbindBidirectional(lastVersionSetting.maxMemoryProperty());
|
||||
javaCustomOption.valueProperty().unbindBidirectional(lastVersionSetting.javaDirProperty());
|
||||
gameDirCustomOption.valueProperty().unbindBidirectional(lastVersionSetting.gameDirProperty());
|
||||
@@ -593,8 +584,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
javaVersionOption.valueProperty().unbind();
|
||||
|
||||
// bind new data fields
|
||||
FXUtils.bindInt(txtWidth, versionSetting.widthProperty());
|
||||
FXUtils.bindInt(txtHeight, versionSetting.heightProperty());
|
||||
FXUtils.bindWindowsSize(cboWindowsSize, versionSetting.widthProperty(), versionSetting.heightProperty());
|
||||
maxMemory.bindBidirectional(versionSetting.maxMemoryProperty());
|
||||
|
||||
javaCustomOption.bindBidirectional(versionSetting.javaDirProperty());
|
||||
|
||||
@@ -205,14 +205,6 @@ public final class Versions {
|
||||
});
|
||||
}
|
||||
|
||||
public static void launch(Profile profile) {
|
||||
launch(profile, profile.getSelectedVersion());
|
||||
}
|
||||
|
||||
public static void launch(Profile profile, String id) {
|
||||
launch(profile, id, null);
|
||||
}
|
||||
|
||||
public static void launch(Profile profile, String id, Consumer<LauncherHelper> injecter) {
|
||||
if (!checkVersionForLaunching(profile, id))
|
||||
return;
|
||||
|
||||
@@ -18,14 +18,9 @@
|
||||
package org.jackhuang.hmcl.upgrade;
|
||||
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.util.Pack200Utils;
|
||||
import org.tukaani.xz.XZInputStream;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.jar.JarOutputStream;
|
||||
|
||||
final class HMCLDownloadTask extends FileDownloadTask {
|
||||
|
||||
@@ -45,15 +40,6 @@ final class HMCLDownloadTask extends FileDownloadTask {
|
||||
switch (archiveFormat) {
|
||||
case JAR:
|
||||
break;
|
||||
|
||||
case PACK_XZ:
|
||||
byte[] raw = Files.readAllBytes(target);
|
||||
try (InputStream in = new XZInputStream(new ByteArrayInputStream(raw));
|
||||
JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) {
|
||||
Pack200Utils.unpack(in, out);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown format: " + archiveFormat);
|
||||
}
|
||||
|
||||
@@ -21,14 +21,13 @@ import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
|
||||
import org.jackhuang.hmcl.util.Pack200Utils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
public class RemoteVersion {
|
||||
public final class RemoteVersion {
|
||||
|
||||
public static RemoteVersion fetch(UpdateChannel channel, String url) throws IOException {
|
||||
try {
|
||||
@@ -36,12 +35,8 @@ public class RemoteVersion {
|
||||
String version = Optional.ofNullable(response.get("version")).map(JsonElement::getAsString).orElseThrow(() -> new IOException("version is missing"));
|
||||
String jarUrl = Optional.ofNullable(response.get("jar")).map(JsonElement::getAsString).orElse(null);
|
||||
String jarHash = Optional.ofNullable(response.get("jarsha1")).map(JsonElement::getAsString).orElse(null);
|
||||
String packXZUrl = Optional.ofNullable(response.get("packxz")).map(JsonElement::getAsString).orElse(null);
|
||||
String packXZHash = Optional.ofNullable(response.get("packxzsha1")).map(JsonElement::getAsString).orElse(null);
|
||||
boolean force = Optional.ofNullable(response.get("force")).map(JsonElement::getAsBoolean).orElse(false);
|
||||
if (Pack200Utils.isSupported() && packXZUrl != null && packXZHash != null) {
|
||||
return new RemoteVersion(channel, version, packXZUrl, Type.PACK_XZ, new IntegrityCheck("SHA-1", packXZHash), force);
|
||||
} else if (jarUrl != null && jarHash != null) {
|
||||
if (jarUrl != null && jarHash != null) {
|
||||
return new RemoteVersion(channel, version, jarUrl, Type.JAR, new IntegrityCheck("SHA-1", jarHash), force);
|
||||
} else {
|
||||
throw new IOException("No download url is available");
|
||||
@@ -97,7 +92,6 @@ public class RemoteVersion {
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
PACK_XZ,
|
||||
JAR
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,10 @@ import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WindowsVersion;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
@@ -178,7 +181,19 @@ public final class NativePatcher {
|
||||
return version.setLibraries(newLibraries);
|
||||
}
|
||||
|
||||
public static Library getMesaLoader(JavaRuntime javaVersion) {
|
||||
return getNatives(javaVersion.getPlatform()).get("mesa-loader");
|
||||
public static @Nullable Library getWindowsMesaLoader(@NotNull JavaRuntime javaVersion, @NotNull Renderer renderer, @NotNull WindowsVersion windowsVersion) {
|
||||
if (renderer == Renderer.DEFAULT)
|
||||
return null;
|
||||
|
||||
if (windowsVersion.compareTo(WindowsVersion.WINDOWS_10) >= 0) {
|
||||
return getNatives(javaVersion.getPlatform()).get("mesa-loader");
|
||||
} else if (windowsVersion.compareTo(WindowsVersion.WINDOWS_7) >= 0) {
|
||||
if (renderer == Renderer.LLVMPIPE)
|
||||
return getNatives(javaVersion.getPlatform()).get("software-renderer-loader");
|
||||
else
|
||||
return null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,12 @@
|
||||
"titleLocalized" : "about.thanks_to.mcbbs",
|
||||
"subtitleLocalized" : "about.thanks_to.mcbbs.statement"
|
||||
},
|
||||
{
|
||||
"image" : "/assets/img/mcim.png",
|
||||
"titleLocalized" : "about.thanks_to.mcim",
|
||||
"subtitleLocalized" : "about.thanks_to.mcim.statement",
|
||||
"externalLink" : "https://github.com/mcmod-info-mirror"
|
||||
},
|
||||
{
|
||||
"image" : "/assets/img/github.png",
|
||||
"titleLocalized" : "about.thanks_to.contributors",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 503 B |
@@ -39,6 +39,8 @@ about.thanks_to.zekerzhayard.statement=Contribute a lot of technical support to
|
||||
about.thanks_to.zkitefly.statement=Responsible for maintaining the documentation of HMCL.
|
||||
about.thanks_to.mcbbs=MCBBS (Minecraft Chinese Forum)
|
||||
about.thanks_to.mcbbs.statement=For providing the mcbbs.net download mirror for Chinese Mainland users. (No longer available)
|
||||
about.thanks_to.mcim=mcmod-info-mirror
|
||||
about.thanks_to.mcim.statement=Provides mod information cache acceleration service
|
||||
about.thanks_to.mcmod=MCMod (mcmod.cn)
|
||||
about.thanks_to.mcmod.statement=For providing the Simplified Chinese translations and wiki for various mods.
|
||||
about.thanks_to.red_lnn.statement=For providing the default background image.
|
||||
@@ -859,7 +861,7 @@ modpack.choose.local.detail=You can drag the modpack file here.
|
||||
modpack.choose.remote=Download from URL
|
||||
modpack.choose.remote.detail=A direct download link to the remote modpack file is required.
|
||||
modpack.choose.repository=Download Modpack from CurseForge or Modrinth
|
||||
modpack.choose.repository.detail=Remember to go back to this page and drop the modpack file here after the modpack is downloaded.
|
||||
modpack.choose.repository.detail=You can choose the desired modpack on the next page.
|
||||
modpack.choose.remote.tooltip=Please enter your modpack URL
|
||||
modpack.completion=Downloading dependencies
|
||||
modpack.desc=Describe your modpack, including an introduction and probably some changelog. Markdown and images from URL are currently supported.
|
||||
@@ -1426,12 +1428,18 @@ version.empty=No Instances
|
||||
version.empty.add=Add new instance
|
||||
version.empty.launch=No available instances. Clicking "OK" will take you to the "Download" page.\n\nYou can also download the game or switch game directories via the "Download" or "All Instances" buttons on the HMCL homepage.
|
||||
version.empty.hint=There are no Minecraft instances here.\nYou can try switching to another game directory or clicking here to download one.
|
||||
version.game.all=All
|
||||
version.game.april_fools=April Fools
|
||||
version.game.old=Historical
|
||||
version.game.release=Release
|
||||
version.game.releases=Releases
|
||||
version.game.snapshot=Snapshot
|
||||
version.game.snapshots=Snapshots
|
||||
version.game.type=Type
|
||||
version.launch=Launch Game
|
||||
version.launch.empty=Start Game
|
||||
version.launch.empty.installing=Installing Game
|
||||
version.launch.empty.tooltip=Install and launch the latest official release
|
||||
version.launch.test=Test Launch
|
||||
version.switch=Switch Instance
|
||||
version.launch_script=Export Launch Script
|
||||
@@ -1456,12 +1464,14 @@ version.manage.remove_libraries=Delete All Libraries
|
||||
version.manage.rename=Rename Instance
|
||||
version.manage.rename.message=Enter New Instance Name
|
||||
version.manage.rename.fail=Failed to rename the instance. Some files might be in use, or the name contains an invalid character.
|
||||
version.search=Name
|
||||
version.search.prompt=Enter the version name to search
|
||||
version.settings=Settings
|
||||
version.update=Update Modpack
|
||||
|
||||
wiki.tooltip=Minecraft Wiki Page
|
||||
wiki.version.game.release=https://minecraft.wiki/w/Java_Edition_%s
|
||||
wiki.version.game.snapshot=https://minecraft.wiki/w/Java_Edition_%s
|
||||
wiki.version.game=https://minecraft.wiki/w/Special:Search?search=%s
|
||||
wiki.version.game.search=Java_Edition_%s
|
||||
|
||||
wizard.prev=< Prev
|
||||
wizard.failed=Failed
|
||||
|
||||
@@ -865,7 +865,7 @@ modpack.choose.remote=Descargar desde Internet
|
||||
modpack.choose.remote.detail=Se requiere un enlace de descarga directa al archivo modpack remoto
|
||||
modpack.choose.remote.tooltip=Por favor, introduzca la URL de su modpack
|
||||
modpack.choose.repository=Descargar Modpack de CurseForge o Modrinth
|
||||
modpack.choose.repository.detail=Recuerda volver a esta página y soltar aquí el archivo del modpack una vez descargado.
|
||||
modpack.choose.repository.detail=Puedes elegir el modpack que desees en la página siguiente.
|
||||
modpack.completion=Descargando dependencias
|
||||
modpack.desc=Describa su modpack, incluyendo una introducción y probablemente algún registro de cambios. Actualmente se admiten Markdown e imágenes desde URL.
|
||||
modpack.description=Descripción
|
||||
@@ -1460,8 +1460,8 @@ version.settings=Configuración
|
||||
version.update=Actualizar Modpack
|
||||
|
||||
wiki.tooltip=Página de Minecraft Wiki
|
||||
wiki.version.game.release=https://es.minecraft.wiki/w/Java_Edition_%s
|
||||
wiki.version.game.snapshot=https://es.minecraft.wiki/w/Java_Edition_%s
|
||||
wiki.version.game=https://es.minecraft.wiki/w/Special:Search?search=%s
|
||||
wiki.version.game.search=Java_Edition_%s
|
||||
|
||||
wizard.prev=< Previo
|
||||
wizard.failed=Falló
|
||||
|
||||
@@ -963,6 +963,9 @@ version.manage.rename.fail=バージョンの名前を変更できませんで
|
||||
version.settings=設定
|
||||
version.update=modpackを更新する
|
||||
|
||||
wiki.version.game=https://ja.minecraft.wiki/w/Special:Search?search=%s
|
||||
wiki.version.game.search=Java_Edition_%s
|
||||
|
||||
wizard.prev=< 前へ
|
||||
wizard.failed=失敗
|
||||
wizard.finish=終了
|
||||
|
||||
@@ -864,7 +864,7 @@ modpack.choose.remote=Скачать
|
||||
modpack.choose.remote.detail=Необходима прямая ссылка на скачивание модпака
|
||||
modpack.choose.remote.tooltip=Введите URL-адрес модпака
|
||||
modpack.choose.repository=Скачать модпак с CurseForge или Modrinth
|
||||
modpack.choose.repository.detail=Не забудьте вернуться на эту страницу и перетащить файл модпака сюда после того, как модпак будет скачан.
|
||||
modpack.choose.repository.detail=Вы сможете выбрать нужный вам модпак на следующей странице.
|
||||
modpack.completion=Скачивание зависимостей
|
||||
modpack.desc=Опишите модпак, включая введение и журнал изменений. Поддерживаются Markdown и изображения по URL-адресу.
|
||||
modpack.description=Описание
|
||||
@@ -1460,8 +1460,8 @@ version.settings=Настройки
|
||||
version.update=Обновить модпак
|
||||
|
||||
wiki.tooltip=Страница Minecraft Wiki
|
||||
wiki.version.game.release=https://ru.minecraft.wiki/w/%s_(Java_Edition)
|
||||
wiki.version.game.snapshot=https://ru.minecraft.wiki/w/%s_(Java_Edition)
|
||||
wiki.version.game=https://ru.minecraft.wiki/w/Special:Search?search=%s
|
||||
wiki.version.game.search=%s_(Java_Edition)
|
||||
|
||||
wizard.prev=< Пред.
|
||||
wizard.failed=Не удалось
|
||||
|
||||
@@ -38,6 +38,8 @@ about.thanks_to.zekerzhayard.statement=為 HMCL 貢獻許多技術支援
|
||||
about.thanks_to.zkitefly.statement=負責維護 HMCL 的文件
|
||||
about.thanks_to.mcbbs=MCBBS (我的世界中文論壇)
|
||||
about.thanks_to.mcbbs.statement=提供 MCBBS 下載源 (現已停止服務)
|
||||
about.thanks_to.mcim=mcmod-info-mirror
|
||||
about.thanks_to.mcim.statement=提供 Mod 信息緩存加速服務
|
||||
about.thanks_to.mcmod=MC 百科 (mcmod.cn)
|
||||
about.thanks_to.mcmod.statement=提供模組簡體中文名映射表與模組百科
|
||||
about.thanks_to.red_lnn.statement=提供預設背景圖
|
||||
@@ -675,7 +677,7 @@ modpack.choose.local.detail=你可以直接將模組包檔案拖入本頁面以
|
||||
modpack.choose.remote=從網路下載模組包
|
||||
modpack.choose.remote.detail=需要提供模組包的下載連結
|
||||
modpack.choose.repository=從 CurseForge/Modrinth 下載模組包
|
||||
modpack.choose.repository.detail=下載後記得回到這個介面,把模組包拖進來哦
|
||||
modpack.choose.repository.detail=可直接在跳轉到的頁面安裝模組包
|
||||
modpack.choose.remote.tooltip=要下載的模組包的連結
|
||||
modpack.completion=下載模組包相關檔案
|
||||
modpack.desc=描述你要製作的模組包,比如模組包注意事項和更新紀錄。支援 Markdown (圖片請上傳至網路)。
|
||||
@@ -1219,12 +1221,18 @@ version.empty=沒有遊戲實例
|
||||
version.empty.add=進入下載頁安裝遊戲
|
||||
version.empty.launch=沒有可啟動的遊戲。點擊「確定」將進入「下載」頁面。\n你也可以點擊 HMCL 主介面左側的「下載」按鈕安裝遊戲,或在「實例清單」切換遊戲目錄。
|
||||
version.empty.hint=沒有已安裝的遊戲。\n你可以切換其他遊戲目錄,或者點擊此處進入遊戲下載頁面。
|
||||
version.game.all=全部
|
||||
version.game.april_fools=愚人節
|
||||
version.game.old=遠古版
|
||||
version.game.release=正式版
|
||||
version.game.releases=正式版
|
||||
version.game.snapshot=快照
|
||||
version.game.snapshots=快照
|
||||
version.game.type=版本類型
|
||||
version.launch=啟動遊戲
|
||||
version.launch.empty=開始遊戲
|
||||
version.launch.empty.installing=安裝遊戲
|
||||
version.launch.empty.tooltip=安裝並啟動最新正式版遊戲
|
||||
version.launch.test=測試遊戲
|
||||
version.switch=切換實例
|
||||
version.launch_script=生成啟動指令碼
|
||||
@@ -1249,12 +1257,14 @@ version.manage.remove_libraries=刪除所有支援庫檔案
|
||||
version.manage.rename=重新命名該實例
|
||||
version.manage.rename.message=請輸入新名稱
|
||||
version.manage.rename.fail=重新命名實例失敗,可能檔案被佔用或者名稱有特殊字元。
|
||||
version.search=名稱
|
||||
version.search.prompt=輸入版本名稱進行搜尋
|
||||
version.settings=遊戲設定
|
||||
version.update=更新模組包
|
||||
|
||||
wiki.tooltip=Minecraft Wiki 頁面
|
||||
wiki.version.game.release=https://zh.minecraft.wiki/w/Java%%E7%%89%%88%s?variant=zh-tw
|
||||
wiki.version.game.snapshot=https://zh.minecraft.wiki/w/%s?variant=zh-tw
|
||||
wiki.version.game=https://zh.minecraft.wiki/w/Special:Search?search=%s&variant=zh-tw
|
||||
wiki.version.game.search=Java版%s
|
||||
|
||||
wizard.prev=< 上一步
|
||||
wizard.failed=失敗
|
||||
|
||||
@@ -38,6 +38,8 @@ about.thanks_to.zekerzhayard.statement=为 HMCL 贡献许多技术支持
|
||||
about.thanks_to.zkitefly.statement=负责维护 HMCL 的文档
|
||||
about.thanks_to.mcbbs=MCBBS (我的世界中文论坛)
|
||||
about.thanks_to.mcbbs.statement=提供 MCBBS 下载源 (现已停止服务)
|
||||
about.thanks_to.mcim=mcmod-info-mirror
|
||||
about.thanks_to.mcim.statement=提供 Mod 信息缓存加速服务
|
||||
about.thanks_to.mcmod=MC 百科 (mcmod.cn)
|
||||
about.thanks_to.mcmod.statement=提供模组简体中文名映射表与模组百科
|
||||
about.thanks_to.red_lnn.statement=提供默认背景图
|
||||
@@ -685,7 +687,7 @@ modpack.choose.local.detail=你可以直接将整合包文件拖入本页面以
|
||||
modpack.choose.remote=从互联网下载整合包
|
||||
modpack.choose.remote.detail=需要提供整合包的下载链接
|
||||
modpack.choose.repository=从 CurseForge/Modrinth 下载整合包
|
||||
modpack.choose.repository.detail=下载后记得回到这个界面,把整合包拖进来哦
|
||||
modpack.choose.repository.detail=可直接在跳转到的页面安装整合包
|
||||
modpack.choose.remote.tooltip=要下载的整合包的链接
|
||||
modpack.completion=下载整合包相关文件
|
||||
modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录。支持 Markdown (图片请用网络链接)。
|
||||
@@ -1229,12 +1231,18 @@ version.empty=没有游戏版本
|
||||
version.empty.add=进入下载页安装游戏
|
||||
version.empty.launch=没有可启动的游戏。点击“确定”将进入“下载”页面。\n你也可以点击 HMCL 主界面左侧的“下载”按钮安装游戏,或在“版本列表”切换游戏文件夹。
|
||||
version.empty.hint=没有已安装的游戏。\n你可以切换其他游戏文件夹,或者点击此处进入游戏下载页面。
|
||||
version.game.all=全部
|
||||
version.game.april_fools=愚人节
|
||||
version.game.old=远古版
|
||||
version.game.release=正式版
|
||||
version.game.releases=正式版
|
||||
version.game.snapshot=快照
|
||||
version.game.snapshots=快照
|
||||
version.game.type=版本类型
|
||||
version.launch=启动游戏
|
||||
version.launch.empty=开始游戏
|
||||
version.launch.empty.installing=安装游戏
|
||||
version.launch.empty.tooltip=安装并启动最新正式版游戏
|
||||
version.launch.test=测试游戏
|
||||
version.switch=切换版本
|
||||
version.launch_script=生成启动脚本
|
||||
@@ -1259,12 +1267,14 @@ version.manage.remove_libraries=删除所有库文件
|
||||
version.manage.rename=重命名该版本
|
||||
version.manage.rename.message=请输入要修改的名称
|
||||
version.manage.rename.fail=重命名版本失败,可能文件被占用或者名字有特殊字符。
|
||||
version.search=名称
|
||||
version.search.prompt=输入版本名称进行搜索
|
||||
version.settings=游戏设置
|
||||
version.update=更新整合包
|
||||
|
||||
wiki.tooltip=Minecraft Wiki 页面
|
||||
wiki.version.game.release=https://zh.minecraft.wiki/w/Java%%E7%%89%%88%s?variant=zh-cn
|
||||
wiki.version.game.snapshot=https://zh.minecraft.wiki/w/%s?variant=zh-cn
|
||||
wiki.version.game=https://zh.minecraft.wiki/w/Special:Search?search=%s&variant=zh-cn
|
||||
wiki.version.game.search=Java版%s
|
||||
|
||||
wizard.prev=< 上一步
|
||||
wizard.failed=失败
|
||||
|
||||
@@ -4007,6 +4007,17 @@
|
||||
"size": 27971214
|
||||
}
|
||||
}
|
||||
},
|
||||
"software-renderer-loader": {
|
||||
"name": "org.glavo:llvmpipe-loader:1.0",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/glavo/llvmpipe-loader/1.0/llvmpipe-loader-1.0.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/glavo/llvmpipe-loader/1.0/llvmpipe-loader-1.0.jar",
|
||||
"sha1": "ff255415e5c4b2a18970da0a8e552b557ca013ae",
|
||||
"size": 12964773
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"windows-x86": {
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 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.auth.authlibinjector;
|
||||
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class AuthlibInjectorExtractor implements AuthlibInjectorArtifactProvider {
|
||||
private final URL source;
|
||||
private final Path artifactLocation;
|
||||
|
||||
public AuthlibInjectorExtractor(URL source, Path artifactLocation) {
|
||||
if (source == null)
|
||||
throw new IllegalArgumentException("Missing authlib injector");
|
||||
this.source = source;
|
||||
this.artifactLocation = artifactLocation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthlibInjectorArtifactInfo getArtifactInfo() throws IOException {
|
||||
Optional<AuthlibInjectorArtifactInfo> cached = getArtifactInfoImmediately();
|
||||
if (cached.isPresent())
|
||||
return cached.get();
|
||||
|
||||
synchronized (this) {
|
||||
cached = getArtifactInfoImmediately();
|
||||
if (cached.isPresent())
|
||||
return cached.get();
|
||||
|
||||
LOG.info("No local authlib-injector found, extracting");
|
||||
Files.createDirectories(artifactLocation.getParent());
|
||||
try (InputStream inputStream = source.openStream()) {
|
||||
FileUtils.saveSafely(artifactLocation, inputStream::transferTo);
|
||||
}
|
||||
return getArtifactInfoImmediately().orElseThrow(() ->
|
||||
new IOException("Failed to extract authlib-injector artifact"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AuthlibInjectorArtifactInfo> getArtifactInfoImmediately() {
|
||||
if (!Files.isRegularFile(artifactLocation))
|
||||
return Optional.empty();
|
||||
|
||||
try {
|
||||
return Optional.of(AuthlibInjectorArtifactInfo.from(artifactLocation));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Bad authlib-injector artifact", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,12 @@ public final class BMCLAPIDownloadProvider implements DownloadProvider {
|
||||
pair("https://authlib-injector.yushi.moe", apiRoot + "/mirrors/authlib-injector"),
|
||||
pair("https://repo1.maven.org/maven2", "https://mirrors.cloud.tencent.com/nexus/repository/maven-public"),
|
||||
pair("https://zkitefly.github.io/unlisted-versions-of-minecraft", "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto")
|
||||
// // https://github.com/mcmod-info-mirror/mcim-rust-api
|
||||
// pair("https://api.modrinth.com", "https://mod.mcimirror.top/modrinth"),
|
||||
// pair("https://cdn.modrinth.com", "https://mod.mcimirror.top"),
|
||||
// pair("https://api.curseforge.com", "https://mod.mcimirror.top/curseforge"),
|
||||
// pair("https://edge.forgecdn.net", "https://mod.mcimirror.top"),
|
||||
// pair("https://mediafilez.forgecdn.net", "https://mod.mcimirror.top")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ public class DefaultCacheRepository extends CacheRepository {
|
||||
} else {
|
||||
index = new Index();
|
||||
}
|
||||
} catch (IOException | JsonParseException e) {
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Unable to read index file", e);
|
||||
index = new Index();
|
||||
} finally {
|
||||
|
||||
@@ -54,15 +54,16 @@ public final class GameRemoteVersion extends RemoteVersion {
|
||||
|
||||
@Override
|
||||
public int compareTo(RemoteVersion o) {
|
||||
if (!(o instanceof GameRemoteVersion))
|
||||
if (!(o instanceof GameRemoteVersion)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dateCompare = o.getReleaseDate().compareTo(getReleaseDate());
|
||||
if (dateCompare != 0) {
|
||||
return dateCompare;
|
||||
}
|
||||
|
||||
return GameVersionNumber.compare(getSelfVersion(), o.getSelfVersion());
|
||||
return GameVersionNumber.compare(o.getSelfVersion(), getSelfVersion());
|
||||
}
|
||||
|
||||
private static Type getReleaseType(ReleaseType type) {
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ public final class NeoForgeBMCLVersionList extends VersionList<NeoForgeRemoteVer
|
||||
|
||||
@Override
|
||||
public boolean hasType() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemot
|
||||
|
||||
@Override
|
||||
public boolean hasType() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static final String OLD_URL = "https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/forge";
|
||||
|
||||
+5
-1
@@ -10,7 +10,7 @@ import java.util.List;
|
||||
|
||||
public class NeoForgeRemoteVersion extends RemoteVersion {
|
||||
public NeoForgeRemoteVersion(String gameVersion, String selfVersion, List<String> urls) {
|
||||
super(LibraryAnalyzer.LibraryType.NEO_FORGE.getPatchId(), gameVersion, selfVersion, null, urls);
|
||||
super(LibraryAnalyzer.LibraryType.NEO_FORGE.getPatchId(), gameVersion, selfVersion, null, getType(selfVersion), urls);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -18,6 +18,10 @@ public class NeoForgeRemoteVersion extends RemoteVersion {
|
||||
return new NeoForgeInstallTask(dependencyManager, baseVersion, this);
|
||||
}
|
||||
|
||||
private static Type getType(String version) {
|
||||
return version.contains("beta") ? Type.SNAPSHOT : Type.RELEASE;
|
||||
}
|
||||
|
||||
public static String normalize(String version) {
|
||||
if (version.startsWith("1.20.1-")) {
|
||||
if (version.startsWith("forge-", "1.20.1-".length())) {
|
||||
|
||||
@@ -168,7 +168,7 @@ public class DefaultLauncher extends Launcher {
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS)
|
||||
res.addDefault("-Duser.home=", options.getGameDir().getParent());
|
||||
res.addDefault("-Duser.home=", options.getGameDir().getAbsoluteFile().getParent());
|
||||
|
||||
Proxy.Type proxyType = options.getProxyType();
|
||||
if (proxyType == null) {
|
||||
|
||||
@@ -300,11 +300,13 @@ public final class RemoteMod {
|
||||
|
||||
public FileDownloadTask.IntegrityCheck getIntegrityCheck() {
|
||||
if (hashes.containsKey("md5")) {
|
||||
return new FileDownloadTask.IntegrityCheck("MD5", hashes.get("sha1"));
|
||||
return new FileDownloadTask.IntegrityCheck("MD5", hashes.get("md5"));
|
||||
} else if (hashes.containsKey("sha1")) {
|
||||
return new FileDownloadTask.IntegrityCheck("SHA-1", hashes.get("sha1"));
|
||||
} else if (hashes.containsKey("sha256")) {
|
||||
return new FileDownloadTask.IntegrityCheck("SHA-256", hashes.get("sha256"));
|
||||
} else if (hashes.containsKey("sha512")) {
|
||||
return new FileDownloadTask.IntegrityCheck("SHA-256", hashes.get("sha1"));
|
||||
return new FileDownloadTask.IntegrityCheck("SHA-512", hashes.get("sha512"));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.mod;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -83,7 +84,7 @@ public interface RemoteModRepository {
|
||||
}
|
||||
}
|
||||
|
||||
SearchResult search(String gameVersion, @Nullable Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder)
|
||||
SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder)
|
||||
throws IOException;
|
||||
|
||||
Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException;
|
||||
|
||||
+27
-24
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.mod.curse;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
@@ -26,6 +27,7 @@ import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@@ -47,6 +49,13 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
|
||||
private static final int WORD_PERFECT_MATCH_WEIGHT = 5;
|
||||
|
||||
private static <R extends HttpRequest> R withApiKey(R request) {
|
||||
if (request.getUrl().startsWith(PREFIX) && !apiKey.isEmpty()) {
|
||||
request.header("X-API-KEY", apiKey);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
public static boolean isAvailable() {
|
||||
return !apiKey.isEmpty();
|
||||
}
|
||||
@@ -100,29 +109,28 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchResult search(String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException {
|
||||
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException {
|
||||
int categoryId = 0;
|
||||
if (category != null && category.getSelf() instanceof CurseAddon.Category) {
|
||||
categoryId = ((CurseAddon.Category) category.getSelf()).getId();
|
||||
}
|
||||
Response<List<CurseAddon>> response = HttpRequest.GET(PREFIX + "/v1/mods/search",
|
||||
pair("gameId", "432"),
|
||||
pair("classId", Integer.toString(section)),
|
||||
pair("categoryId", Integer.toString(categoryId)),
|
||||
pair("gameVersion", gameVersion),
|
||||
pair("searchFilter", searchFilter),
|
||||
pair("sortField", Integer.toString(toModsSearchSortField(sortType))),
|
||||
pair("sortOrder", toSortOrder(sortOrder)),
|
||||
pair("index", Integer.toString(pageOffset * pageSize)),
|
||||
pair("pageSize", Integer.toString(pageSize)))
|
||||
.header("X-API-KEY", apiKey)
|
||||
Response<List<CurseAddon>> response = withApiKey(HttpRequest.GET(downloadProvider.injectURL(NetworkUtils.withQuery(PREFIX + "/v1/mods/search", mapOf(
|
||||
pair("gameId", "432"),
|
||||
pair("classId", Integer.toString(section)),
|
||||
pair("categoryId", Integer.toString(categoryId)),
|
||||
pair("gameVersion", gameVersion),
|
||||
pair("searchFilter", searchFilter),
|
||||
pair("sortField", Integer.toString(toModsSearchSortField(sortType))),
|
||||
pair("sortOrder", toSortOrder(sortOrder)),
|
||||
pair("index", Integer.toString(pageOffset * pageSize)),
|
||||
pair("pageSize", Integer.toString(pageSize)))))))
|
||||
.getJson(Response.typeOf(listTypeOf(CurseAddon.class)));
|
||||
if (searchFilter.isEmpty()) {
|
||||
return new SearchResult(response.getData().stream().map(CurseAddon::toMod), calculateTotalPages(response, pageSize));
|
||||
}
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/1549
|
||||
String lowerCaseSearchFilter = searchFilter.toLowerCase();
|
||||
String lowerCaseSearchFilter = searchFilter.toLowerCase(Locale.ROOT);
|
||||
Map<String, Integer> searchFilterWords = new HashMap<>();
|
||||
for (String s : StringUtils.tokenize(lowerCaseSearchFilter)) {
|
||||
searchFilterWords.put(s, searchFilterWords.getOrDefault(s, 0) + 1);
|
||||
@@ -162,9 +170,8 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
|
||||
long hash = Integer.toUnsignedLong(MurmurHash2.hash32(baos.toByteArray(), baos.size(), 1));
|
||||
|
||||
Response<FingerprintMatchesResult> response = HttpRequest.POST(PREFIX + "/v1/fingerprints/432")
|
||||
Response<FingerprintMatchesResult> response = withApiKey(HttpRequest.POST(PREFIX + "/v1/fingerprints/432"))
|
||||
.json(mapOf(pair("fingerprints", Collections.singletonList(hash))))
|
||||
.header("X-API-KEY", apiKey)
|
||||
.getJson(Response.typeOf(FingerprintMatchesResult.class));
|
||||
|
||||
if (response.getData().getExactMatches() == null || response.getData().getExactMatches().isEmpty()) {
|
||||
@@ -176,32 +183,28 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
|
||||
@Override
|
||||
public RemoteMod getModById(String id) throws IOException {
|
||||
Response<CurseAddon> response = HttpRequest.GET(PREFIX + "/v1/mods/" + id)
|
||||
.header("X-API-KEY", apiKey)
|
||||
Response<CurseAddon> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id))
|
||||
.getJson(Response.typeOf(CurseAddon.class));
|
||||
return response.data.toMod();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteMod.File getModFile(String modId, String fileId) throws IOException {
|
||||
Response<CurseAddon.LatestFile> response = HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s", PREFIX, modId, fileId))
|
||||
.header("X-API-KEY", apiKey)
|
||||
Response<CurseAddon.LatestFile> response = withApiKey(HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s", PREFIX, modId, fileId)))
|
||||
.getJson(Response.typeOf(CurseAddon.LatestFile.class));
|
||||
return response.getData().toVersion().getFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
|
||||
Response<List<CurseAddon.LatestFile>> response = HttpRequest.GET(PREFIX + "/v1/mods/" + id + "/files",
|
||||
pair("pageSize", "10000"))
|
||||
.header("X-API-KEY", apiKey)
|
||||
Response<List<CurseAddon.LatestFile>> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id + "/files",
|
||||
pair("pageSize", "10000")))
|
||||
.getJson(Response.typeOf(listTypeOf(CurseAddon.LatestFile.class)));
|
||||
return response.getData().stream().map(CurseAddon.LatestFile::toVersion);
|
||||
}
|
||||
|
||||
public List<CurseAddon.Category> getCategoriesImpl() throws IOException {
|
||||
Response<List<CurseAddon.Category>> categories = HttpRequest.GET(PREFIX + "/v1/categories", pair("gameId", "432"))
|
||||
.header("X-API-KEY", apiKey)
|
||||
Response<List<CurseAddon.Category>> categories = withApiKey(HttpRequest.GET(PREFIX + "/v1/categories", pair("gameId", "432")))
|
||||
.getJson(Response.typeOf(listTypeOf(CurseAddon.Category.class)));
|
||||
return reorganizeCategories(categories.getData(), section);
|
||||
}
|
||||
|
||||
+3
-2
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.mod.modrinth;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
@@ -79,7 +80,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchResult search(String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||
List<List<String>> facets = new ArrayList<>();
|
||||
facets.add(Collections.singletonList("project_type:" + projectType));
|
||||
if (StringUtils.isNotBlank(gameVersion)) {
|
||||
@@ -95,7 +96,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
pair("limit", Integer.toString(pageSize)),
|
||||
pair("index", convertSortType(sort))
|
||||
);
|
||||
Response<ProjectSearchResult> response = HttpRequest.GET(NetworkUtils.withQuery(PREFIX + "/v2/search", query))
|
||||
Response<ProjectSearchResult> response = HttpRequest.GET(downloadProvider.injectURL(NetworkUtils.withQuery(PREFIX + "/v2/search", query)))
|
||||
.getJson(Response.typeOf(ProjectSearchResult.class));
|
||||
return new SearchResult(response.getHits().stream().map(ProjectSearchResult::toMod), (int) Math.ceil((double) response.totalHits / pageSize));
|
||||
}
|
||||
|
||||
+16
-3
@@ -21,6 +21,7 @@ import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.GameBuilder;
|
||||
import org.jackhuang.hmcl.game.DefaultGameRepository;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
@@ -120,7 +121,7 @@ public class ServerModpackCompletionTask extends Task<Void> {
|
||||
dependencies.add(builder.buildAsync());
|
||||
}
|
||||
|
||||
Path rootPath = repository.getVersionRoot(version).toPath();
|
||||
Path rootPath = repository.getVersionRoot(version).toPath().toAbsolutePath().normalize();
|
||||
Map<String, ModpackConfiguration.FileInformation> files = manifest.getManifest().getFiles().stream()
|
||||
.collect(Collectors.toMap(ModpackConfiguration.FileInformation::getPath,
|
||||
Function.identity()));
|
||||
@@ -128,12 +129,24 @@ public class ServerModpackCompletionTask extends Task<Void> {
|
||||
Set<String> remoteFiles = remoteManifest.getFiles().stream().map(ModpackConfiguration.FileInformation::getPath)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Path runDirectory = repository.getRunDirectory(version).toPath().toAbsolutePath().normalize();
|
||||
Path modsDirectory = runDirectory.resolve("mods");
|
||||
|
||||
int total = 0;
|
||||
// for files in new modpack
|
||||
for (ModpackConfiguration.FileInformation file : remoteManifest.getFiles()) {
|
||||
Path actualPath = rootPath.resolve(file.getPath());
|
||||
Path actualPath = rootPath.resolve(file.getPath()).toAbsolutePath().normalize();
|
||||
String fileName = actualPath.getFileName().toString();
|
||||
|
||||
if (!actualPath.startsWith(rootPath)) {
|
||||
throw new IOException("Unsecure path: " + file.getPath());
|
||||
}
|
||||
|
||||
boolean download;
|
||||
if (!files.containsKey(file.getPath())) {
|
||||
if (!files.containsKey(file.getPath()) ||
|
||||
modsDirectory.equals(actualPath.getParent())
|
||||
&& Files.notExists(actualPath.resolveSibling(fileName + ModManager.DISABLED_EXTENSION))
|
||||
&& Files.notExists(actualPath.resolveSibling(fileName + ModManager.OLD_EXTENSION))) {
|
||||
// If old modpack does not have this entry, download it
|
||||
download = true;
|
||||
} else if (!Files.exists(actualPath)) {
|
||||
|
||||
@@ -38,11 +38,12 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
public final class CacheFileTask extends FetchTask<Path> {
|
||||
|
||||
public CacheFileTask(@NotNull String uri) {
|
||||
super(List.of(NetworkUtils.toURI(uri)));
|
||||
this(NetworkUtils.toURI(uri));
|
||||
}
|
||||
|
||||
public CacheFileTask(@NotNull URI uri) {
|
||||
super(List.of(uri));
|
||||
setName(uri.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
|
||||
@@ -75,7 +74,7 @@ public class CacheRepository {
|
||||
index = new LinkedHashMap<>();
|
||||
indexFileLastModified = null;
|
||||
}
|
||||
} catch (IOException | JsonParseException e) {
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Unable to read index file", e);
|
||||
index = new LinkedHashMap<>();
|
||||
indexFileLastModified = null;
|
||||
@@ -354,7 +353,7 @@ public class CacheRepository {
|
||||
}
|
||||
|
||||
public int compareTo(ETagItem other) {
|
||||
if (!url.equals(other.url))
|
||||
if (!url.equals(other.url) && !NetworkUtils.toURI(url).equals(NetworkUtils.toURI(other.url)))
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
ZonedDateTime thisTime = Lang.ignoringException(() -> ZonedDateTime.parse(remoteLastModified, DateTimeFormatter.RFC_1123_DATE_TIME), null);
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.util.jar.JarOutputStream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class Pack200Utils {
|
||||
private Pack200Utils() {
|
||||
}
|
||||
|
||||
private static final String[] IMPL_NAMES = {
|
||||
"java.util.jar.Pack200",
|
||||
"org.glavo.pack200.Pack200",
|
||||
"io.pack200.Pack200"
|
||||
};
|
||||
|
||||
private static final MethodHandle newUnpackerHandle;
|
||||
private static final MethodHandle unpackHandle;
|
||||
private static final MethodHandle unpackFileHandle;
|
||||
|
||||
static {
|
||||
Class<?> pack200Class = null;
|
||||
Class<?> unpackerClass = null;
|
||||
|
||||
for (String implName : IMPL_NAMES) {
|
||||
try {
|
||||
pack200Class = Class.forName(implName);
|
||||
unpackerClass = Class.forName(implName + "$Unpacker");
|
||||
break;
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
if (pack200Class == null) {
|
||||
LOG.warning("Pack200 not found");
|
||||
newUnpackerHandle = null;
|
||||
unpackHandle = null;
|
||||
unpackFileHandle = null;
|
||||
} else {
|
||||
final MethodHandles.Lookup lookup = MethodHandles.publicLookup();
|
||||
MethodHandle newUnpacker = null;
|
||||
MethodHandle unpack = null;
|
||||
MethodHandle unpackFile = null;
|
||||
try {
|
||||
newUnpacker = lookup.findStatic(pack200Class, "newUnpacker", MethodType.methodType(unpackerClass));
|
||||
unpack = lookup.findVirtual(unpackerClass, "unpack", MethodType.methodType(void.class, InputStream.class, JarOutputStream.class));
|
||||
unpackFile = lookup.findVirtual(unpackerClass, "unpack", MethodType.methodType(void.class, File.class, JarOutputStream.class));
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to find pack200 methods", e);
|
||||
}
|
||||
|
||||
if (newUnpacker != null) {
|
||||
newUnpackerHandle = newUnpacker;
|
||||
unpackHandle = unpack;
|
||||
unpackFileHandle = unpackFile;
|
||||
} else {
|
||||
newUnpackerHandle = null;
|
||||
unpackHandle = null;
|
||||
unpackFileHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static boolean isSupported() {
|
||||
return newUnpackerHandle != null;
|
||||
}
|
||||
|
||||
public static void unpack(InputStream in, JarOutputStream out) throws IOException {
|
||||
if (newUnpackerHandle == null) {
|
||||
throw new UnsupportedOperationException("Pack200");
|
||||
}
|
||||
|
||||
try {
|
||||
unpackHandle.invoke(newUnpackerHandle.invoke(), in, out);
|
||||
} catch (IOException | RuntimeException | Error e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void unpack(File in, JarOutputStream out) throws IOException {
|
||||
if (newUnpackerHandle == null) {
|
||||
throw new UnsupportedOperationException("Pack200");
|
||||
}
|
||||
|
||||
try {
|
||||
unpackFileHandle.invoke(newUnpackerHandle.invoke(), in, out);
|
||||
} catch (IOException | RuntimeException | Error e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,10 @@ public abstract class HttpRequest {
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public HttpRequest accept(String contentType) {
|
||||
return header("Accept", contentType);
|
||||
}
|
||||
|
||||
@@ -146,7 +146,10 @@ public final class NetworkUtils {
|
||||
boolean left = true;
|
||||
while (i < location.length()) {
|
||||
char ch = location.charAt(i);
|
||||
if (ch == ' ' || ch >= 0x80)
|
||||
if (ch == ' '
|
||||
|| ch == '[' || ch == ']'
|
||||
|| ch == '{' || ch == '}'
|
||||
|| ch >= 0x80)
|
||||
break;
|
||||
else if (ch == '?')
|
||||
left = false;
|
||||
@@ -163,22 +166,6 @@ public final class NetworkUtils {
|
||||
|
||||
for (; i < location.length(); i++) {
|
||||
char ch = location.charAt(i);
|
||||
if (Character.isSurrogate(ch)) {
|
||||
if (Character.isHighSurrogate(ch) && i < location.length() - 1) {
|
||||
char ch2 = location.charAt(i + 1);
|
||||
if (Character.isLowSurrogate(ch2)) {
|
||||
int codePoint = Character.toCodePoint(ch, ch2);
|
||||
encodeCodePoint(builder, codePoint);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid surrogate pair, encode as '?'
|
||||
builder.append("%3F");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == ' ') {
|
||||
if (left)
|
||||
builder.append("%20");
|
||||
@@ -187,7 +174,23 @@ public final class NetworkUtils {
|
||||
} else if (ch == '?') {
|
||||
left = false;
|
||||
builder.append('?');
|
||||
} else if (ch >= 0x80) {
|
||||
} else if (ch >= 0x80 || (left && (ch == '[' || ch == ']' || ch == '{' || ch == '}'))) {
|
||||
if (Character.isSurrogate(ch)) {
|
||||
if (Character.isHighSurrogate(ch) && i < location.length() - 1) {
|
||||
char ch2 = location.charAt(i + 1);
|
||||
if (Character.isLowSurrogate(ch2)) {
|
||||
int codePoint = Character.toCodePoint(ch, ch2);
|
||||
encodeCodePoint(builder, codePoint);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid surrogate pair, encode as '?'
|
||||
builder.append("%3F");
|
||||
continue;
|
||||
}
|
||||
|
||||
encodeCodePoint(builder, ch);
|
||||
} else {
|
||||
builder.append(ch);
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.logging;
|
||||
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.tukaani.xz.LZMA2Options;
|
||||
import org.tukaani.xz.XZOutputStream;
|
||||
|
||||
@@ -127,47 +145,10 @@ public final class Logger {
|
||||
String caller = CLASS_NAME + ".onExit";
|
||||
|
||||
if (logRetention > 0 && logFile != null) {
|
||||
List<Pair<Path, int[]>> list = new ArrayList<>();
|
||||
Pattern fileNamePattern = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})T(?<hour>\\d{2})-(?<minute>\\d{2})-(?<second>\\d{2})(\\.(?<n>\\d+))?\\.log(\\.(gz|xz))?");
|
||||
Path dir = logFile.getParent();
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
|
||||
for (Path path : stream) {
|
||||
Matcher matcher = fileNamePattern.matcher(path.getFileName().toString());
|
||||
if (matcher.matches() && Files.isRegularFile(path)) {
|
||||
int year = Integer.parseInt(matcher.group("year"));
|
||||
int month = Integer.parseInt(matcher.group("month"));
|
||||
int day = Integer.parseInt(matcher.group("day"));
|
||||
int hour = Integer.parseInt(matcher.group("hour"));
|
||||
int minute = Integer.parseInt(matcher.group("minute"));
|
||||
int second = Integer.parseInt(matcher.group("second"));
|
||||
int n = Optional.ofNullable(matcher.group("n")).map(Integer::parseInt).orElse(0);
|
||||
|
||||
list.add(Pair.pair(path, new int[]{year, month, day, hour, minute, second, n}));
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log(Level.WARNING, caller, "Failed to list log files in " + dir, e);
|
||||
}
|
||||
|
||||
var list = findRecentLogFiles(Integer.MAX_VALUE);
|
||||
if (list.size() > logRetention) {
|
||||
list.sort((a, b) -> {
|
||||
int[] v1 = a.getValue();
|
||||
int[] v2 = b.getValue();
|
||||
|
||||
assert v1.length == v2.length;
|
||||
|
||||
for (int i = 0; i < v1.length; i++) {
|
||||
int c = Integer.compare(v1[i], v2[i]);
|
||||
if (c != 0)
|
||||
return c;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
for (int i = 0, end = list.size() - logRetention; i < end; i++) {
|
||||
Path file = list.get(i).getKey();
|
||||
|
||||
Path file = list.get(i);
|
||||
try {
|
||||
if (!Files.isSameFile(file, logFile)) {
|
||||
log(Level.INFO, caller, "Delete old log file " + file, null);
|
||||
@@ -276,6 +257,40 @@ public final class Logger {
|
||||
return logFile;
|
||||
}
|
||||
|
||||
public @NotNull List<Path> findRecentLogFiles(int n) {
|
||||
if (n <= 0 || logFile == null)
|
||||
return List.of();
|
||||
|
||||
var currentLogFile = LogFile.ofFile(logFile);
|
||||
|
||||
Path logDir = logFile.getParent();
|
||||
if (logDir == null || !Files.isDirectory(logDir))
|
||||
return List.of();
|
||||
|
||||
var logFiles = new ArrayList<LogFile>();
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(logDir)) {
|
||||
for (Path path : stream) {
|
||||
LogFile item = LogFile.ofFile(path);
|
||||
if (item != null && (currentLogFile == null || item.compareTo(currentLogFile) < 0)) {
|
||||
logFiles.add(item);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log(Level.WARNING, CLASS_NAME + ".findRecentLogFiles", "Failed to list log files in " + logDir, e);
|
||||
return List.of();
|
||||
}
|
||||
logFiles.sort(Comparator.naturalOrder());
|
||||
|
||||
final int resultLength = Math.min(n, logFiles.size());
|
||||
final int offset = logFiles.size() - resultLength;
|
||||
|
||||
var result = new Path[resultLength];
|
||||
for (int i = 0; i < resultLength; i++) {
|
||||
result[i] = logFiles.get(i + offset).file;
|
||||
}
|
||||
return List.of(result);
|
||||
}
|
||||
|
||||
public void exportLogs(OutputStream output) throws IOException {
|
||||
Objects.requireNonNull(output);
|
||||
LogEvent.ExportLog event = new LogEvent.ExportLog(output);
|
||||
@@ -352,4 +367,69 @@ public final class Logger {
|
||||
public void trace(String msg, Throwable exception) {
|
||||
log(Level.TRACE, CallerFinder.getCaller(), msg, exception);
|
||||
}
|
||||
|
||||
private static final class LogFile implements Comparable<LogFile> {
|
||||
private static final Pattern FILE_NAME_PATTERN = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})T(?<hour>\\d{2})-(?<minute>\\d{2})-(?<second>\\d{2})(\\.(?<n>\\d+))?\\.log(\\.(gz|xz))?");
|
||||
|
||||
private static @Nullable LogFile ofFile(Path file) {
|
||||
if (!Files.isRegularFile(file))
|
||||
return null;
|
||||
|
||||
Matcher matcher = FILE_NAME_PATTERN.matcher(file.getFileName().toString());
|
||||
if (!matcher.matches())
|
||||
return null;
|
||||
|
||||
int year = Integer.parseInt(matcher.group("year"));
|
||||
int month = Integer.parseInt(matcher.group("month"));
|
||||
int day = Integer.parseInt(matcher.group("day"));
|
||||
int hour = Integer.parseInt(matcher.group("hour"));
|
||||
int minute = Integer.parseInt(matcher.group("minute"));
|
||||
int second = Integer.parseInt(matcher.group("second"));
|
||||
int n = Optional.ofNullable(matcher.group("n")).map(Integer::parseInt).orElse(0);
|
||||
|
||||
return new LogFile(file, year, month, day, hour, minute, second, n);
|
||||
}
|
||||
|
||||
private final Path file;
|
||||
private final int year;
|
||||
private final int month;
|
||||
private final int day;
|
||||
private final int hour;
|
||||
private final int minute;
|
||||
private final int second;
|
||||
private final int n;
|
||||
|
||||
private LogFile(Path file, int year, int month, int day, int hour, int minute, int second, int n) {
|
||||
this.file = file;
|
||||
this.year = year;
|
||||
this.month = month;
|
||||
this.day = day;
|
||||
this.hour = hour;
|
||||
this.minute = minute;
|
||||
this.second = second;
|
||||
this.n = n;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull Logger.LogFile that) {
|
||||
if (this.year != that.year) return Integer.compare(this.year, that.year);
|
||||
if (this.month != that.month) return Integer.compare(this.month, that.month);
|
||||
if (this.day != that.day) return Integer.compare(this.day, that.day);
|
||||
if (this.hour != that.hour) return Integer.compare(this.hour, that.hour);
|
||||
if (this.minute != that.minute) return Integer.compare(this.minute, that.minute);
|
||||
if (this.second != that.second) return Integer.compare(this.second, that.second);
|
||||
if (this.n != that.n) return Integer.compare(this.n, that.n);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(year, month, day, hour, minute, second, n);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return obj instanceof LogFile && compareTo((LogFile) obj) == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,10 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public boolean isSpecial() {
|
||||
return this instanceof Special;
|
||||
}
|
||||
|
||||
enum Type {
|
||||
PRE_CLASSIC, CLASSIC, INFDEV, ALPHA, BETA, NEW
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# https://gitlab.freedesktop.org/mesa/drm/-/blob/b65d6ede3ebe64dbde5805007ddef22f390f8050/data/amdgpu.ids
|
||||
# https://gitlab.freedesktop.org/mesa/libdrm/-/blob/7d43d9b6fe5df4b234937fce9e51adb1b327ded1/data/amdgpu.ids
|
||||
#
|
||||
# List of AMDGPU IDs
|
||||
#
|
||||
@@ -591,7 +591,9 @@
|
||||
74B6, 00, AMD Instinct MI308X
|
||||
74BD, 00, AMD Instinct MI300X HF
|
||||
7550, C0, AMD Radeon RX 9070 XT
|
||||
7550, C2, AMD Radeon RX 9070 GRE
|
||||
7550, C3, AMD Radeon RX 9070
|
||||
7551, C0, AMD Radeon AI PRO R9700
|
||||
9830, 00, AMD Radeon HD 8400 / R3 Series
|
||||
9831, 00, AMD Radeon HD 8400E
|
||||
9832, 00, AMD Radeon HD 8330
|
||||
|
||||
@@ -36,9 +36,12 @@ public class NetworkUtilsTest {
|
||||
assertEquals("https://github.com/HMCL-dev/HMCL/commits?author=Glavo", encodeLocation("https://github.com/HMCL-dev/HMCL/commits?author=Glavo"));
|
||||
assertEquals("https://www.example.com/file%20with%20space", encodeLocation("https://www.example.com/file with space"));
|
||||
assertEquals("https://www.example.com/file%20with%20space", encodeLocation("https://www.example.com/file%20with%20space"));
|
||||
assertEquals("https://www.example.com/%5Bfile%5D", encodeLocation("https://www.example.com/[file]"));
|
||||
assertEquals("https://www.example.com/%7Bfile%7D", encodeLocation("https://www.example.com/{file}"));
|
||||
assertEquals("https://www.example.com/%E6%B5%8B%E8%AF%95", encodeLocation("https://www.example.com/测试"));
|
||||
assertEquals("https://www.example.com/%F0%9F%98%87", encodeLocation("https://www.example.com/\uD83D\uDE07"));
|
||||
assertEquals("https://www.example.com/test?a=10+20", encodeLocation("https://www.example.com/test?a=10 20"));
|
||||
assertEquals("https://www.example.com/test?a=10+20&b=[30]&c={40}", encodeLocation("https://www.example.com/test?a=10 20&b=[30]&c={40}"));
|
||||
assertEquals("https://www.example.com/%E6%B5%8B%E8%AF%95?a=10+20", encodeLocation("https://www.example.com/测试?a=10 20"));
|
||||
|
||||
// Invalid surrogate pair
|
||||
|
||||
@@ -16,12 +16,13 @@ twelvemonkeys = "3.12.0"
|
||||
jna = "5.17.0"
|
||||
pci-ids = "0.4.0"
|
||||
java-info = "1.0"
|
||||
authlib-injector = "1.2.5"
|
||||
|
||||
# testing
|
||||
junit = "5.13.4"
|
||||
|
||||
# plugins
|
||||
shadow = "8.3.6"
|
||||
shadow = "9.0.1"
|
||||
|
||||
[libraries]
|
||||
hmclauncher = { module = "org.glavo.hmcl:HMCLauncher", version.ref = "hmclauncher" }
|
||||
@@ -43,6 +44,7 @@ jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
|
||||
jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }
|
||||
pci-ids = { module = "org.glavo:pci-ids", version.ref = "pci-ids" }
|
||||
java-info = { module = "org.glavo:java-info", version.ref = "java-info" }
|
||||
authlib-injector = { module = "org.glavo.hmcl:authlib-injector", version.ref = "authlib-injector" }
|
||||
|
||||
# testing
|
||||
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
|
||||
|
||||
Reference in new issue
Block a user