Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1232b5dc8 | ||
|
|
f8f047d67e | ||
|
|
86529c6f15 | ||
|
|
d63829c344 | ||
|
|
34359a7e86 | ||
|
|
aa335d8365 | ||
|
|
a565d3b7ce | ||
|
|
3bff3c7f23 | ||
|
|
4e6e832e9c | ||
|
|
d6648a840e | ||
|
|
f8c2af716f | ||
|
|
bd812ccb67 | ||
|
|
dad190f7e6 | ||
|
|
1b1ba64a38 | ||
|
|
e31268c39d | ||
|
|
cb9d32dc91 | ||
|
|
36568df5b1 |
No files matched your search
@@ -48,3 +48,9 @@ jobs:
|
||||
name: HMCL-${{ env.SHORT_SHA }}-sh
|
||||
path: HMCL/build/libs/HMCL-*.sh
|
||||
archive: false
|
||||
- name: Upload DEB
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}-deb
|
||||
path: HMCL/build/libs/HMCL-*.deb
|
||||
archive: false
|
||||
@@ -46,16 +46,20 @@ jobs:
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar.sha256"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh.sha256"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.deb"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.deb.sha256"
|
||||
- name: Verify artifacts
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
export JAR_SHA256=$(cat HMCL-$HMCL_VERSION.jar.sha256 | tr -d '\n')
|
||||
export EXE_SHA256=$(cat HMCL-$HMCL_VERSION.exe.sha256 | tr -d '\n')
|
||||
export SH_SHA256=$(cat HMCL-$HMCL_VERSION.sh.sha256 | tr -d '\n')
|
||||
export DEB_SHA256=$(cat HMCL-$HMCL_VERSION.deb.sha256 | tr -d '\n')
|
||||
|
||||
echo "$JAR_SHA256 HMCL-$HMCL_VERSION.jar" | sha256sum -c
|
||||
echo "$EXE_SHA256 HMCL-$HMCL_VERSION.exe" | sha256sum -c
|
||||
echo "$SH_SHA256 HMCL-$HMCL_VERSION.sh" | sha256sum -c
|
||||
echo "$DEB_SHA256 HMCL-$HMCL_VERSION.deb" | sha256sum -c
|
||||
- name: Generate release note
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
@@ -67,6 +71,7 @@ jobs:
|
||||
echo "| [HMCL-$HMCL_VERSION.exe]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.deb]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.deb) | \`$(cat HMCL-$HMCL_VERSION.deb.sha256)\` |" >> RELEASE_NOTE
|
||||
|
||||
# CNB Release Note
|
||||
echo "[更新日志](https://docs.hmcl.net/changelog/${{ matrix.channel }}.html#HMCL-$HMCL_VERSION)" >> CNB_RELEASE_NOTE
|
||||
@@ -76,6 +81,8 @@ jobs:
|
||||
echo "| [HMCL-$HMCL_VERSION.exe]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> CNB_RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.jar]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> CNB_RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.sh]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> CNB_RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.deb]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.deb) | \`$(cat HMCL-$HMCL_VERSION.deb.sha256)\` |" >> CNB_RELEASE_NOTE
|
||||
|
||||
env:
|
||||
GH_DOWNLOAD_BASE_URL: https://github.com/${{ github.repository }}/releases/download
|
||||
CNB_DOWNLOAD_BASE_URL: https://cnb.cool/HMCL-dev/HMCL/-/releases/download
|
||||
@@ -86,6 +93,7 @@ jobs:
|
||||
"HMCL-${{ env.HMCL_VERSION }}.exe" \
|
||||
"HMCL-${{ env.HMCL_VERSION }}.jar" \
|
||||
"HMCL-${{ env.HMCL_VERSION }}.sh" \
|
||||
"HMCL-${{ env.HMCL_VERSION }}.deb" \
|
||||
--target "${{ env.HMCL_COMMIT_SHA }}" \
|
||||
--title "${{ env.HMCL_TAG_NAME }}" \
|
||||
--notes-file RELEASE_NOTE \
|
||||
@@ -120,6 +128,9 @@ jobs:
|
||||
|
||||
echo "Uploading HMCL-$HMCL_VERSION.sh"
|
||||
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "$HMCL_TAG_NAME" --file-name "HMCL-$HMCL_VERSION.sh"
|
||||
|
||||
echo "Uploading HMCL-$HMCL_VERSION.deb"
|
||||
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "$HMCL_TAG_NAME" --file-name "HMCL-$HMCL_VERSION.deb"
|
||||
env:
|
||||
CNB_TOKEN: ${{ secrets.CNB_SYNC_TOKEN }}
|
||||
CNB_REPO: HMCL-dev/HMCL
|
||||
+37
-8
@@ -6,6 +6,8 @@ import org.jackhuang.hmcl.gradle.l10n.CreateLanguageList
|
||||
import org.jackhuang.hmcl.gradle.l10n.CreateLocaleNamesResourceBundle
|
||||
import org.jackhuang.hmcl.gradle.l10n.UpsideDownTranslate
|
||||
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
|
||||
import org.jackhuang.hmcl.gradle.pack.CreateDeb
|
||||
import org.jackhuang.hmcl.gradle.pack.ReleaseType
|
||||
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
|
||||
import java.net.URI
|
||||
import java.nio.file.FileSystems
|
||||
@@ -251,20 +253,22 @@ tasks.processResources {
|
||||
}
|
||||
}
|
||||
|
||||
fun artifactFile(ext: String) = jarPath.resolveSibling(jarPath.nameWithoutExtension + '.' + ext)
|
||||
|
||||
val makeExecutables by tasks.registering {
|
||||
val extensions = listOf("exe", "sh")
|
||||
|
||||
dependsOn(tasks.jar)
|
||||
|
||||
inputs.file(jarPath)
|
||||
outputs.files(extensions.map { File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + it) })
|
||||
outputs.files(extensions.map { artifactFile(it) })
|
||||
|
||||
doLast {
|
||||
val jarContent = jarPath.readBytes()
|
||||
|
||||
ZipFile(jarPath).use { zipFile ->
|
||||
for (extension in extensions) {
|
||||
val output = File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + extension)
|
||||
val output = artifactFile(extension)
|
||||
val entry = zipFile.getEntry("assets/HMCLauncher.$extension")
|
||||
?: throw GradleException("HMCLauncher.$extension not found")
|
||||
|
||||
@@ -279,8 +283,31 @@ val makeExecutables by tasks.registering {
|
||||
}
|
||||
}
|
||||
|
||||
val makeDeb by tasks.registering(CreateDeb::class) {
|
||||
dependsOn(makeExecutables)
|
||||
|
||||
val debFile = layout.file(provider { artifactFile("deb") })
|
||||
|
||||
val debChannel = when (versionType) {
|
||||
"stable" -> ReleaseType.STABLE
|
||||
"dev" -> ReleaseType.DEVELOPMENT
|
||||
else -> ReleaseType.NIGHTLY
|
||||
}
|
||||
|
||||
version.set(project.version.toString())
|
||||
releaseType.set(debChannel)
|
||||
appShFile.set(layout.file(provider { artifactFile("sh") }))
|
||||
iconFile.set(layout.projectDirectory.file("image/hmcl.png"))
|
||||
outputFile.set(debFile)
|
||||
|
||||
doLast {
|
||||
createChecksum(debFile.get().asFile)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.build {
|
||||
dependsOn(makeExecutables)
|
||||
dependsOn(makeDeb)
|
||||
}
|
||||
|
||||
fun parseToolOptions(options: String?): MutableList<String> {
|
||||
@@ -377,12 +404,14 @@ val upgradeTerracottaConfig = tasks.register<TerracottaConfigUpgradeTask>("upgra
|
||||
val destination = layout.projectDirectory.file("src/main/resources/assets/terracotta.json")
|
||||
val source = layout.projectDirectory.file("terracotta-template.json");
|
||||
|
||||
classifiers.set(listOf(
|
||||
"windows-x86_64", "windows-arm64",
|
||||
"macos-x86_64", "macos-arm64",
|
||||
"linux-x86_64", "linux-arm64", "linux-loongarch64", "linux-riscv64",
|
||||
"freebsd-x86_64"
|
||||
))
|
||||
classifiers.set(
|
||||
listOf(
|
||||
"windows-x86_64", "windows-arm64",
|
||||
"macos-x86_64", "macos-arm64",
|
||||
"linux-x86_64", "linux-arm64", "linux-loongarch64", "linux-riscv64",
|
||||
"freebsd-x86_64"
|
||||
)
|
||||
)
|
||||
|
||||
version.set(libs.versions.terracotta)
|
||||
downloadURL.set($$"https://github.com/burningtnt/Terracotta/releases/download/v${version}/terracotta-${version}-${classifier}-pkg.tar.gz")
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -45,7 +45,6 @@ public final class Metadata {
|
||||
public static final int RECOMMENDED_JAVA_VERSION = 21;
|
||||
|
||||
public static final String PUBLISH_URL = "https://hmcl.huangyuhui.net";
|
||||
public static final String ABOUT_URL = PUBLISH_URL + "/about";
|
||||
public static final String DOWNLOAD_URL = PUBLISH_URL + "/download";
|
||||
public static final String HMCL_UPDATE_URL = System.getProperty("hmcl.update_source.override", PUBLISH_URL + "/api/update_link");
|
||||
public static final String MANUAL_UPDATE_URL = "https://github.com/HMCL-dev/HMCL/releases";
|
||||
|
||||
@@ -468,7 +468,9 @@ public final class Accounts {
|
||||
}
|
||||
} else if (exception instanceof MicrosoftService.XBox400Exception) {
|
||||
return i18n("account.methods.microsoft.error.wrong_verify_method");
|
||||
} else if (exception instanceof MicrosoftService.NoMinecraftJavaEditionProfileException) {
|
||||
} else if (exception instanceof MicrosoftService.MinecraftJavaEditionLicenseNotFoundException) {
|
||||
return i18n("account.methods.microsoft.error.no_license");
|
||||
} else if (exception instanceof MicrosoftService.MinecraftJavaEditionProfileNotFoundException) {
|
||||
return i18n("account.methods.microsoft.error.no_character");
|
||||
} else if (exception instanceof MicrosoftService.NoXuiException) {
|
||||
return i18n("account.methods.microsoft.error.add_family");
|
||||
|
||||
@@ -45,7 +45,7 @@ public enum VersionIconType {
|
||||
public static VersionIconType getIconType(ModLoaderType modLoaderType) {
|
||||
return switch (modLoaderType) {
|
||||
case FORGE -> VersionIconType.FORGE;
|
||||
case NEO_FORGED -> VersionIconType.NEO_FORGE;
|
||||
case NEO_FORGE -> VersionIconType.NEO_FORGE;
|
||||
case FABRIC -> VersionIconType.FABRIC;
|
||||
case QUILT -> VersionIconType.QUILT;
|
||||
case LITE_LOADER -> VersionIconType.CHICKEN;
|
||||
|
||||
@@ -70,6 +70,7 @@ import org.jackhuang.hmcl.util.i18n.SupportedLocale;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -594,7 +595,7 @@ public final class Controllers {
|
||||
return pane.getCompletableFuture();
|
||||
}
|
||||
|
||||
public static TaskExecutorDialogPane taskDialog(TaskExecutor executor, String title, TaskCancellationAction onCancel) {
|
||||
public static TaskExecutorDialogPane taskDialog(TaskExecutor executor, String title, @NotNull TaskCancellationAction onCancel) {
|
||||
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(onCancel);
|
||||
pane.setTitle(title);
|
||||
pane.setExecutor(executor);
|
||||
@@ -602,7 +603,7 @@ public final class Controllers {
|
||||
return pane;
|
||||
}
|
||||
|
||||
public static TaskExecutorDialogPane taskDialog(Task<?> task, String title, TaskCancellationAction onCancel) {
|
||||
public static TaskExecutorDialogPane taskDialog(Task<?> task, String title, @NotNull TaskCancellationAction onCancel) {
|
||||
TaskExecutor executor = task.executor();
|
||||
TaskExecutorDialogPane pane = taskDialog(executor, title, onCancel);
|
||||
executor.start();
|
||||
|
||||
@@ -62,6 +62,7 @@ public enum SVG {
|
||||
EXTENSION_FILL("M8.8 21H5q-.825 0-1.4125-.5875T3 19V15.2q1.2 0 2.1-.7625T6 12.5q0-1.175-.9-1.9375T3 9.8V6q0-.825.5875-1.4125T5 4H9q0-1.05.725-1.775T11.5 1.5q1.05 0 1.775.725T14 4h4q.825 0 1.4125.5875T20 6v4q1.05 0 1.775.725T22.5 12.5q0 1.05-.725 1.775T20 15v4q0 .825-.5875 1.4125T18 21H14.2q0-1.25-.7875-2.125T11.5 18q-1.125 0-1.9125.875T8.8 21Z"),
|
||||
FEEDBACK("M12 15Q12.425 15 12.7125 14.7125T13 14Q13 13.575 12.7125 13.2875T12 13Q11.575 13 11.2875 13.2875T11 14Q11 14.425 11.2875 14.7125T12 15ZM11 11H13V5H11V11ZM2 22V4Q2 3.175 2.5875 2.5875T4 2H20Q20.825 2 21.4125 2.5875T22 4V16Q22 16.825 21.4125 17.4125T20 18H6L2 22ZM5.15 16H20V4H4V17.125L5.15 16ZM4 16V4 16Z"),
|
||||
FEEDBACK_FILL("M2 22V4q0-.825.5875-1.4125T4 2H20q.825 0 1.4125.5875T22 4V16q0 .825-.5875 1.4125T20 18H6L2 22Zm10-7q.425 0 .7125-.2875T13 14t-.2875-.7125T12 13t-.7125.2875T11 14t.2875.7125T12 15Zm-1-4h2V5H11v6Z"),
|
||||
FILE_OPEN("M6 22q-0.825 0-1.4125-0.5875T4 20v-16q0-0.825 0.5875-1.4125T6 2h8l6 6v6h-2v-5H13v-5H6v16h9v2H6Zm15.95 0.375L19 19.425v2.225h-2v-5.65h5.65v2h-2.25l2.95 2.95-1.4 1.425Zm-15.95-2.375v-16 16Z"),
|
||||
FOLDER("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8V18Q22 18.825 21.4125 19.4125T20 20H4ZM4 18H20V8H11.175L9.175 6H4V18ZM4 18V6 18Z"),
|
||||
FOLDER_COPY("M3 21Q2.175 21 1.5875 20.4125T1 19V6H3V19H20V21H3ZM7 17Q6.175 17 5.5875 16.4125T5 15V4Q5 3.175 5.5875 2.5875T7 2H12L14 4H21Q21.825 4 22.4125 4.5875T23 6V15Q23 15.825 22.4125 16.4125T21 17H7ZM7 15H21V6H13.175L11.175 4H7V15ZM7 15V4 15Z"),
|
||||
FOLDER_OPEN("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8H11.175L9.175 6H4V18L6.4 10H23.5L20.925 18.575Q20.725 19.225 20.1875 19.6125T19 20H4ZM6.1 18H19L20.8 12H7.9L6.1 18ZM6.1 18 7.9 12 6.1 18ZM4 8V6 8Z"),
|
||||
|
||||
@@ -50,10 +50,10 @@ public final class UpgradeDialog extends JFXDialogLayout {
|
||||
setHeading(new Label(i18n("update.changelog")));
|
||||
setBody(new JFXSpinner());
|
||||
|
||||
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
|
||||
String url = CHANGELOG_URL + remoteVersion.channel().channelName + ".html";
|
||||
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
VersionNumber targetVersion = VersionNumber.asVersion(remoteVersion.getVersion());
|
||||
VersionNumber targetVersion = VersionNumber.asVersion(remoteVersion.version());
|
||||
VersionNumber currentVersion = VersionNumber.asVersion(Metadata.VERSION);
|
||||
if (targetVersion.compareTo(currentVersion) <= 0)
|
||||
// Downgrade update, no need to display changelog
|
||||
|
||||
@@ -303,7 +303,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
for (String key : ALLOWED_LINKS) {
|
||||
String value = links.get(key);
|
||||
if (value != null) {
|
||||
Hyperlink link = new Hyperlink(i18n("account.injector.link." + key));
|
||||
JFXHyperlink link = new JFXHyperlink(i18n("account.injector.link." + key));
|
||||
FXUtils.installSlowTooltip(link, value);
|
||||
link.setOnAction(e -> FXUtils.openLink(value));
|
||||
result.add(link);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
@@ -30,6 +31,7 @@ import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.nio.file.Path;
|
||||
@@ -38,10 +40,18 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class FileSelector extends HBox {
|
||||
private final StringProperty value = new SimpleStringProperty();
|
||||
private String chooserTitle = i18n("selector.choose_file");
|
||||
private boolean directory = false;
|
||||
private String chooserTitle = "";
|
||||
private SelectionMode selectionMode = SelectionMode.FILE;
|
||||
private final ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
|
||||
|
||||
JFXButton selectButton = FXUtils.newToggleButton4(SVG.FOLDER_OPEN, 15);
|
||||
|
||||
public enum SelectionMode {
|
||||
FILE,
|
||||
DIRECTORY,
|
||||
FILE_OR_DIRECTORY
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value.get();
|
||||
}
|
||||
@@ -63,12 +73,12 @@ public class FileSelector extends HBox {
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isDirectory() {
|
||||
return directory;
|
||||
public SelectionMode getSelectionMode() {
|
||||
return selectionMode;
|
||||
}
|
||||
|
||||
public FileSelector setDirectory(boolean directory) {
|
||||
this.directory = directory;
|
||||
public FileSelector setSelectionMode(SelectionMode selectionMode) {
|
||||
this.selectionMode = selectionMode;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -80,26 +90,20 @@ public class FileSelector extends HBox {
|
||||
JFXTextField customField = new JFXTextField();
|
||||
FXUtils.bindString(customField, valueProperty());
|
||||
|
||||
JFXButton selectButton = FXUtils.newToggleButton4(SVG.FOLDER_OPEN, 15);
|
||||
selectButton.setOnAction(e -> {
|
||||
if (directory) {
|
||||
DirectoryChooser chooser = new DirectoryChooser();
|
||||
chooser.setTitle(chooserTitle);
|
||||
Path dir = FileUtils.toPath(chooser.showDialog(Controllers.getStage()));
|
||||
if (dir != null) {
|
||||
String path = FileUtils.getAbsolutePath(dir);
|
||||
customField.setText(path);
|
||||
value.setValue(path);
|
||||
}
|
||||
} else {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.getExtensionFilters().addAll(getExtensionFilters());
|
||||
chooser.setTitle(chooserTitle);
|
||||
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
if (file != null) {
|
||||
String path = FileUtils.getAbsolutePath(file);
|
||||
customField.setText(path);
|
||||
value.setValue(path);
|
||||
switch (selectionMode) {
|
||||
case FILE -> openFileChooser(customField);
|
||||
case DIRECTORY -> openDirectoryChooser(customField);
|
||||
case FILE_OR_DIRECTORY -> {
|
||||
PopupMenu selectPopupMenu = new PopupMenu();
|
||||
JFXPopup selectModePopup = new JFXPopup(selectPopupMenu);
|
||||
|
||||
selectPopupMenu.getContent().addAll(
|
||||
new IconedMenuItem(SVG.FILE_OPEN, i18n("selector.choose_file"), () -> openFileChooser(customField), selectModePopup),
|
||||
new IconedMenuItem(SVG.FOLDER_OPEN, i18n("selector.choose_directory"), () -> openDirectoryChooser(customField), selectModePopup)
|
||||
);
|
||||
|
||||
selectModePopup.show(selectButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, -selectButton.getWidth(), 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -108,4 +112,27 @@ public class FileSelector extends HBox {
|
||||
setSpacing(3);
|
||||
getChildren().addAll(customField, selectButton);
|
||||
}
|
||||
|
||||
private void openFileChooser(JFXTextField customField) {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.getExtensionFilters().addAll(getExtensionFilters());
|
||||
chooser.setTitle(StringUtils.isBlank(chooserTitle) ? i18n("selector.choose_file") : chooserTitle);
|
||||
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
if (file != null) {
|
||||
String path = FileUtils.getAbsolutePath(file);
|
||||
customField.setText(path);
|
||||
value.setValue(path);
|
||||
}
|
||||
}
|
||||
|
||||
private void openDirectoryChooser(JFXTextField customField) {
|
||||
DirectoryChooser chooser = new DirectoryChooser();
|
||||
chooser.setTitle(StringUtils.isBlank(chooserTitle) ? i18n("selector.choose_directory") : chooserTitle);
|
||||
Path dir = FileUtils.toPath(chooser.showDialog(Controllers.getStage()));
|
||||
if (dir != null) {
|
||||
String path = FileUtils.getAbsolutePath(dir);
|
||||
customField.setText(path);
|
||||
value.setValue(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
@@ -56,6 +57,7 @@ public class InputDialogPane extends JFXDialogLayout implements DialogAware {
|
||||
this.setBody(new VBox(textField));
|
||||
|
||||
lblCreationWarning = new Label();
|
||||
lblCreationWarning.setPadding(new Insets(0, 5, 0, 0));
|
||||
|
||||
acceptPane = new SpinnerPane();
|
||||
acceptPane.getStyleClass().add("small-spinner-pane");
|
||||
|
||||
@@ -267,8 +267,8 @@ public final class MultiFileItem<T> extends VBox {
|
||||
selector.setValue(value);
|
||||
}
|
||||
|
||||
public FileOption<T> setDirectory(boolean directory) {
|
||||
selector.setDirectory(directory);
|
||||
public FileOption<T> setSelectionMode(FileSelector.SelectionMode selectionMode) {
|
||||
selector.setSelectionMode(selectionMode);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ public final class AboutPage extends SpinnerPane {
|
||||
|
||||
ComponentList legal = new ComponentList();
|
||||
{
|
||||
var copyright = LineButton.createExternalLinkButton(Metadata.ABOUT_URL);
|
||||
var copyright = new LineButton();
|
||||
copyright.setLargeTitle(true);
|
||||
copyright.setTitle(i18n("about.copyright"));
|
||||
copyright.setSubtitle(i18n("about.copyright.statement"));
|
||||
|
||||
@@ -28,6 +28,7 @@ import javafx.collections.ObservableList;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.ImageView;
|
||||
@@ -170,6 +171,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
FXUtils.setLimitHeight(updatePane, 55);
|
||||
StackPane.setAlignment(updatePane, Pos.TOP_RIGHT);
|
||||
FXUtils.onClicked(updatePane, this::onUpgrade);
|
||||
updatePane.setCursor(Cursor.HAND);
|
||||
FXUtils.onChange(showUpdateProperty(), this::showUpdate);
|
||||
|
||||
{
|
||||
@@ -183,7 +185,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
prompt.setSubtitle(i18n("update.bubble.subtitle"));
|
||||
prompt.setPickOnBounds(false);
|
||||
prompt.titleProperty().bind(BindingMapping.of(latestVersionProperty()).map(latestVersion ->
|
||||
latestVersion == null ? "" : i18n("update.bubble.title", latestVersion.getVersion())));
|
||||
latestVersion == null ? "" : i18n("update.bubble.title", latestVersion.version())));
|
||||
|
||||
hBox.getChildren().setAll(SVG.UPDATE.createIcon(20), prompt);
|
||||
}
|
||||
@@ -279,10 +281,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
|
||||
if (show && !config().isDisableAutoShowUpdateDialog()
|
||||
&& getLatestVersion() != null
|
||||
&& !Objects.equals(config().getPromptedVersion(), getLatestVersion().getVersion())) {
|
||||
Controllers.dialog(new MessageDialogPane.Builder("", i18n("update.bubble.title", getLatestVersion().getVersion()), MessageDialogPane.MessageType.INFO)
|
||||
&& !Objects.equals(config().getPromptedVersion(), getLatestVersion().version())) {
|
||||
Controllers.dialog(new MessageDialogPane.Builder("", i18n("update.bubble.title", getLatestVersion().version()), MessageDialogPane.MessageType.INFO)
|
||||
.addAction(i18n("button.view"), () -> {
|
||||
config().setPromptedVersion(getLatestVersion().getVersion());
|
||||
config().setPromptedVersion(getLatestVersion().version());
|
||||
onUpgrade();
|
||||
})
|
||||
.addCancel(null)
|
||||
|
||||
@@ -135,6 +135,7 @@ public class PersonalizationPage extends StackPane {
|
||||
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumBackgroundImage.CUSTOM)
|
||||
.setChooserTitle(i18n("launcher.background.choose"))
|
||||
.addExtensionFilter(FXUtils.getImageExtensionFilter())
|
||||
.setSelectionMode(FileSelector.SelectionMode.FILE_OR_DIRECTORY)
|
||||
.bindBidirectional(config().backgroundImageProperty()),
|
||||
new MultiFileItem.StringOption<>(i18n("launcher.background.network"), EnumBackgroundImage.NETWORK)
|
||||
.setValidators(new URLValidator(true))
|
||||
|
||||
@@ -121,8 +121,7 @@ public final class SettingsPage extends ScrollPane {
|
||||
ObjectProperty<UpdateChannel> updateChannel;
|
||||
{
|
||||
|
||||
JFXButton updateButton = new JFXButton();
|
||||
updateButton.setGraphic(SVG.UPDATE.createIcon(20));
|
||||
JFXButton updateButton = FXUtils.newToggleButton4(SVG.UPDATE, 20);
|
||||
updateButton.setOnAction(e -> onUpdate());
|
||||
updateButton.setPadding(Insets.EMPTY);
|
||||
FXUtils.installFastTooltip(updateButton, i18n("update.tooltip"));
|
||||
@@ -158,7 +157,7 @@ public final class SettingsPage extends ScrollPane {
|
||||
updatePane.pseudoClassStateChanged(PseudoClass.getPseudoClass("active"), outdated);
|
||||
|
||||
if (UpdateChecker.isOutdated()) {
|
||||
lblUpdateSubProperty.set(i18n("update.newest_version", UpdateChecker.getLatestVersion().getVersion()));
|
||||
lblUpdateSubProperty.set(i18n("update.newest_version", UpdateChecker.getLatestVersion().version()));
|
||||
} else if (UpdateChecker.isCheckingUpdate()) {
|
||||
lblUpdateSubProperty.set(i18n("update.checking"));
|
||||
} else {
|
||||
@@ -211,7 +210,7 @@ public final class SettingsPage extends ScrollPane {
|
||||
new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT),
|
||||
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM)
|
||||
.setChooserTitle(i18n("launcher.cache_directory.choose"))
|
||||
.setDirectory(true)
|
||||
.setSelectionMode(FileSelector.SelectionMode.DIRECTORY)
|
||||
.bindBidirectional(config().commonDirectoryProperty())
|
||||
));
|
||||
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());
|
||||
|
||||
@@ -246,7 +246,7 @@ public class TerracottaControllerPage extends StackPane {
|
||||
} else {
|
||||
handler.resolve();
|
||||
}
|
||||
});
|
||||
}, "", new RequiredValidator(i18n("input.not_empty")));
|
||||
});
|
||||
|
||||
if (ThreadLocalRandom.current().nextDouble() < 0.02D) {
|
||||
|
||||
@@ -183,7 +183,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
nativesDirSublist.setHasSubtitle(true);
|
||||
nativesDirCustomOption = new MultiFileItem.FileOption<>(i18n("settings.advanced.natives_directory.custom"), NativesDirectoryType.CUSTOM)
|
||||
.setChooserTitle(i18n("settings.advanced.natives_directory.choose"))
|
||||
.setDirectory(true);
|
||||
.setSelectionMode(FileSelector.SelectionMode.DIRECTORY);
|
||||
nativesDirItem.loadChildren(Arrays.asList(
|
||||
new MultiFileItem.Option<>(i18n("settings.advanced.natives_directory.default"), NativesDirectoryType.VERSION_FOLDER),
|
||||
nativesDirCustomOption
|
||||
|
||||
@@ -441,7 +441,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
case CLEANROOM:
|
||||
content.addTag(i18n("install.installer.cleanroom"));
|
||||
break;
|
||||
case NEO_FORGED:
|
||||
case NEO_FORGE:
|
||||
content.addTag(i18n("install.installer.neoforge"));
|
||||
break;
|
||||
case FABRIC:
|
||||
|
||||
@@ -160,11 +160,11 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.FABRIC) && modManager.hasMod("kilt", ModLoaderType.FABRIC)) {
|
||||
supportedLoaders.add(ModLoaderType.FORGE);
|
||||
supportedLoaders.add(ModLoaderType.NEO_FORGED);
|
||||
supportedLoaders.add(ModLoaderType.NEO_FORGE);
|
||||
}
|
||||
|
||||
// Sinytra Connector
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && modManager.hasMod("connectormod", ModLoaderType.NEO_FORGED)
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && modManager.hasMod("connectormod", ModLoaderType.NEO_FORGE)
|
||||
|| "1.20.1".equals(gameVersion) && analyzer.has(LibraryAnalyzer.LibraryType.FORGE) && modManager.hasMod("connectormod", ModLoaderType.FORGE)) {
|
||||
supportedLoaders.add(ModLoaderType.FABRIC);
|
||||
}
|
||||
|
||||
@@ -487,7 +487,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
case FORGE -> i18n("install.installer.forge");
|
||||
case CLEANROOM -> i18n("install.installer.cleanroom");
|
||||
case LEGACY_FABRIC -> i18n("install.installer.legacyfabric");
|
||||
case NEO_FORGED -> i18n("install.installer.neoforge");
|
||||
case NEO_FORGE -> i18n("install.installer.neoforge");
|
||||
case FABRIC -> i18n("install.installer.fabric");
|
||||
case LITE_LOADER -> i18n("install.installer.liteloader");
|
||||
case QUILT -> i18n("install.installer.quilt");
|
||||
@@ -657,7 +657,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
case FORGE -> content.addTagWarning(i18n("install.installer.forge"));
|
||||
case LEGACY_FABRIC -> content.addTagWarning(i18n("install.installer.legacyfabric"));
|
||||
case CLEANROOM -> content.addTagWarning(i18n("install.installer.cleanroom"));
|
||||
case NEO_FORGED -> content.addTagWarning(i18n("install.installer.neoforge"));
|
||||
case NEO_FORGE -> content.addTagWarning(i18n("install.installer.neoforge"));
|
||||
case FABRIC -> content.addTagWarning(i18n("install.installer.fabric"));
|
||||
case LITE_LOADER -> content.addTagWarning(i18n("install.installer.liteloader"));
|
||||
case QUILT -> content.addTagWarning(i18n("install.installer.quilt"));
|
||||
|
||||
@@ -239,7 +239,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
gameDirItem.disableProperty().bind(modpack);
|
||||
gameDirCustomOption = new MultiFileItem.FileOption<>(i18n("settings.custom"), GameDirectoryType.CUSTOM)
|
||||
.setChooserTitle(i18n("settings.game.working_directory.choose"))
|
||||
.setDirectory(true);
|
||||
.setSelectionMode(FileSelector.SelectionMode.DIRECTORY);
|
||||
|
||||
gameDirItem.loadChildren(Arrays.asList(
|
||||
new MultiFileItem.Option<>(i18n("settings.advanced.game_dir.default"), GameDirectoryType.ROOT_FOLDER),
|
||||
|
||||
@@ -40,10 +40,12 @@ import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
@@ -153,7 +155,7 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
|
||||
LOG.warning("Failed to create backup", exception);
|
||||
Controllers.dialog(i18n("world.backup.create.failed", StringUtils.getStackTrace(exception)), null, MessageDialogPane.MessageType.WARNING);
|
||||
}
|
||||
}), i18n("world.backup"), null);
|
||||
}), i18n("world.backup"), TaskCancellationAction.NO_CANCEL);
|
||||
}
|
||||
|
||||
private final class WorldBackupsPageSkin extends ToolbarListPageSkin<BackupInfo, WorldBackupsPage> {
|
||||
|
||||
@@ -27,8 +27,8 @@ final class HMCLDownloadTask extends FileDownloadTask {
|
||||
private final RemoteVersion.Type archiveFormat;
|
||||
|
||||
public HMCLDownloadTask(RemoteVersion version, Path target) {
|
||||
super(version.getUrl(), target, version.getIntegrityCheck());
|
||||
archiveFormat = version.getType();
|
||||
super(version.url(), target, version.integrityCheck());
|
||||
archiveFormat = version.type();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -23,11 +23,13 @@ import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class RemoteVersion {
|
||||
public record RemoteVersion(UpdateChannel channel, String version, String url, Type type, IntegrityCheck integrityCheck,
|
||||
boolean preview, boolean force) {
|
||||
|
||||
public static RemoteVersion fetch(UpdateChannel channel, boolean preview, String url) throws IOException {
|
||||
try {
|
||||
@@ -46,54 +48,8 @@ public final class RemoteVersion {
|
||||
}
|
||||
}
|
||||
|
||||
private final UpdateChannel channel;
|
||||
private final String version;
|
||||
private final String url;
|
||||
private final Type type;
|
||||
private final IntegrityCheck integrityCheck;
|
||||
private final boolean preview;
|
||||
private final boolean force;
|
||||
|
||||
public RemoteVersion(UpdateChannel channel, String version, String url, Type type, IntegrityCheck integrityCheck, boolean preview, boolean force) {
|
||||
this.channel = channel;
|
||||
this.version = version;
|
||||
this.url = url;
|
||||
this.type = type;
|
||||
this.integrityCheck = integrityCheck;
|
||||
this.preview = preview;
|
||||
this.force = force;
|
||||
}
|
||||
|
||||
public UpdateChannel getChannel() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public IntegrityCheck getIntegrityCheck() {
|
||||
return integrityCheck;
|
||||
}
|
||||
|
||||
public boolean isPreview() {
|
||||
return preview;
|
||||
}
|
||||
|
||||
public boolean isForce() {
|
||||
return force;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
public @NotNull String toString() {
|
||||
return "[" + version + " from " + url + "]";
|
||||
}
|
||||
|
||||
|
||||
@@ -43,13 +43,13 @@ public final class UpdateChecker {
|
||||
RemoteVersion latest = latestVersion.get();
|
||||
if (latest == null || isDevelopmentVersion(Metadata.VERSION)) {
|
||||
return false;
|
||||
} else if (latest.isForce()
|
||||
} else if (latest.force()
|
||||
|| Metadata.isNightly()
|
||||
|| latest.getChannel() == UpdateChannel.NIGHTLY
|
||||
|| latest.getChannel() != UpdateChannel.getChannel()) {
|
||||
return !latest.getVersion().equals(Metadata.VERSION);
|
||||
|| latest.channel() == UpdateChannel.NIGHTLY
|
||||
|| latest.channel() != UpdateChannel.getChannel()) {
|
||||
return !latest.version().equals(Metadata.VERSION);
|
||||
} else {
|
||||
return VersionNumber.compare(Metadata.VERSION, latest.getVersion()) < 0;
|
||||
return VersionNumber.compare(Metadata.VERSION, latest.version()) < 0;
|
||||
}
|
||||
},
|
||||
latestVersion);
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
// Copy from javafx.swing
|
||||
/*
|
||||
* Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code 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
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import javafx.scene.image.*;
|
||||
import javafx.scene.image.Image;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.awt.image.SampleModel;
|
||||
import java.awt.image.SinglePixelPackedSampleModel;
|
||||
import java.nio.IntBuffer;
|
||||
|
||||
/**
|
||||
* This class provides utility methods for converting data types between
|
||||
* Swing/AWT and JavaFX formats.
|
||||
*
|
||||
* @since JavaFX 2.2
|
||||
*/
|
||||
public final class SwingFXUtils {
|
||||
private SwingFXUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshots the specified {@link BufferedImage} and stores a copy of
|
||||
* its pixels into a JavaFX {@link Image} object, creating a new
|
||||
* object if needed.
|
||||
* The returned {@code Image} will be a static snapshot of the state
|
||||
* of the pixels in the {@code BufferedImage} at the time the method
|
||||
* completes. Further changes to the {@code BufferedImage} will not
|
||||
* be reflected in the {@code Image}.
|
||||
* <p>
|
||||
* The optional JavaFX {@link WritableImage} parameter may be reused
|
||||
* to store the copy of the pixels.
|
||||
* A new {@code Image} will be created if the supplied object is null,
|
||||
* is too small or of a type which the image pixels cannot be easily
|
||||
* converted into.
|
||||
*
|
||||
* @param bimg the {@code BufferedImage} object to be converted
|
||||
* @param wimg an optional {@code WritableImage} object that can be
|
||||
* used to store the returned pixel data
|
||||
* @return an {@code Image} object representing a snapshot of the
|
||||
* current pixels in the {@code BufferedImage}.
|
||||
* @since JavaFX 2.2
|
||||
*/
|
||||
public static WritableImage toFXImage(BufferedImage bimg, WritableImage wimg) {
|
||||
int bw = bimg.getWidth();
|
||||
int bh = bimg.getHeight();
|
||||
switch (bimg.getType()) {
|
||||
case BufferedImage.TYPE_INT_ARGB:
|
||||
case BufferedImage.TYPE_INT_ARGB_PRE:
|
||||
break;
|
||||
default:
|
||||
BufferedImage converted =
|
||||
new BufferedImage(bw, bh, BufferedImage.TYPE_INT_ARGB_PRE);
|
||||
Graphics2D g2d = converted.createGraphics();
|
||||
g2d.drawImage(bimg, 0, 0, null);
|
||||
g2d.dispose();
|
||||
bimg = converted;
|
||||
break;
|
||||
}
|
||||
// assert(bimg.getType == TYPE_INT_ARGB[_PRE]);
|
||||
if (wimg != null) {
|
||||
int iw = (int) wimg.getWidth();
|
||||
int ih = (int) wimg.getHeight();
|
||||
if (iw < bw || ih < bh) {
|
||||
wimg = null;
|
||||
} else if (bw < iw || bh < ih) {
|
||||
int[] empty = new int[iw];
|
||||
PixelWriter pw = wimg.getPixelWriter();
|
||||
PixelFormat<IntBuffer> pf = PixelFormat.getIntArgbPreInstance();
|
||||
if (bw < iw) {
|
||||
pw.setPixels(bw, 0, iw - bw, bh, pf, empty, 0, 0);
|
||||
}
|
||||
if (bh < ih) {
|
||||
pw.setPixels(0, bh, iw, ih - bh, pf, empty, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wimg == null) {
|
||||
wimg = new WritableImage(bw, bh);
|
||||
}
|
||||
PixelWriter pw = wimg.getPixelWriter();
|
||||
DataBufferInt db = (DataBufferInt) bimg.getRaster().getDataBuffer();
|
||||
int[] data = db.getData();
|
||||
int offset = bimg.getRaster().getDataBuffer().getOffset();
|
||||
int scan = 0;
|
||||
SampleModel sm = bimg.getRaster().getSampleModel();
|
||||
if (sm instanceof SinglePixelPackedSampleModel) {
|
||||
scan = ((SinglePixelPackedSampleModel) sm).getScanlineStride();
|
||||
}
|
||||
|
||||
PixelFormat<IntBuffer> pf = (bimg.isAlphaPremultiplied() ?
|
||||
PixelFormat.getIntArgbPreInstance() :
|
||||
PixelFormat.getIntArgbInstance());
|
||||
pw.setPixels(0, 0, bw, bh, pf, data, offset, scan);
|
||||
return wimg;
|
||||
}
|
||||
|
||||
public static WritableImage toFXImage(BufferedImage bimg, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) {
|
||||
if (requestedWidth <= 0. || requestedHeight <= 0.) {
|
||||
return toFXImage(bimg, null);
|
||||
}
|
||||
|
||||
int width = (int) requestedWidth;
|
||||
int height = (int) requestedHeight;
|
||||
|
||||
// Calculate actual dimensions if preserveRatio is true
|
||||
if (preserveRatio) {
|
||||
double originalWidth = bimg.getWidth();
|
||||
double originalHeight = bimg.getHeight();
|
||||
double scaleX = requestedWidth / originalWidth;
|
||||
double scaleY = requestedHeight / originalHeight;
|
||||
double scale = Math.min(scaleX, scaleY);
|
||||
|
||||
width = (int) (originalWidth * scale);
|
||||
height = (int) (originalHeight * scale);
|
||||
}
|
||||
|
||||
// Create scaled BufferedImage
|
||||
BufferedImage scaledImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
|
||||
Graphics2D g2d = scaledImage.createGraphics();
|
||||
try {
|
||||
if (smooth) {
|
||||
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
}
|
||||
|
||||
g2d.drawImage(bimg, 0, 0, width, height, null);
|
||||
} finally {
|
||||
g2d.dispose();
|
||||
}
|
||||
|
||||
return toFXImage(scaledImage, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,12 @@
|
||||
"subtitleLocalized" : "about.thanks_to.mcim.statement",
|
||||
"externalLink" : "https://github.com/mcmod-info-mirror"
|
||||
},
|
||||
{
|
||||
"image" : "/assets/img/8mi-tech.png",
|
||||
"titleLocalized" : "about.thanks_to.8mi-tech",
|
||||
"subtitleLocalized" : "about.thanks_to.8mi-tech.statement",
|
||||
"externalLink" : "https://8mi.tech"
|
||||
},
|
||||
{
|
||||
"image" : {
|
||||
"light" : "/assets/img/github.png",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
@@ -48,6 +48,8 @@ about.thanks_to.shulkersakura.statement=For providing the logo for HMCL.
|
||||
about.thanks_to.users=HMCL User Group Members
|
||||
about.thanks_to.users.statement=Thanks for donations, bug reports, and so on.
|
||||
about.thanks_to.yushijinhun.statement=For providing the authlib-injector related support.
|
||||
about.thanks_to.8mi-tech=8Mi-Tech (Quanzhou) Co., Ltd.
|
||||
about.thanks_to.8mi-tech.statement=Provides an accelerated download service for some of the game's static resources.
|
||||
about.open_source=Open Source
|
||||
about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL)
|
||||
|
||||
@@ -108,9 +110,9 @@ account.methods.microsoft.close_page=Microsoft account authorization is now comp
|
||||
account.methods.microsoft.deauthorize=Deauthorize
|
||||
account.methods.microsoft.error.add_family=Please click <a href="https://support.microsoft.com/account-billing/837badbc-999e-54d2-2617-d19206b9540a">here</a> to change your account birth date to be over 18 years old, or add your account to a family.
|
||||
account.methods.microsoft.error.country_unavailable=Xbox Live is not available in your current country/region.
|
||||
account.methods.microsoft.error.missing_xbox_account=Your Microsoft account does not have a linked Xbox account yet. Please click <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">here</a> to link one.
|
||||
account.methods.microsoft.error.no_character=Please confirm that you have purchased Minecraft: Java Edition.\n\
|
||||
If you have already purchased it, a game profile may not have been created. Please click <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">here</a> to create a game profile.
|
||||
account.methods.microsoft.error.missing_xbox_account=Please confirm that you have purchased Minecraft: Java Edition. If you have, please click <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">here</a> to create your game profile.
|
||||
account.methods.microsoft.error.no_license=No Minecraft purchase record found. Please click <a href="https://www.xbox.com/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj">here</a> to get the game.\nIf you are using Game Pass, please create your game profile <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">here</a>. If you have already purchased the game, please check if you are logged into the correct account.
|
||||
account.methods.microsoft.error.no_character=You have not created a game profile yet. Please click <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">here</a> to create one.
|
||||
account.methods.microsoft.error.banned=Your account may have been banned by Xbox Live.\n\
|
||||
You can click <a href="https://enforcement.xbox.com/enforcement/showenforcementhistory">here</a> to check the ban status of your account.
|
||||
account.methods.microsoft.error.unknown=Failed to log in, error code: %d.
|
||||
@@ -1307,6 +1309,7 @@ search.page_n=%1$d / %2$s
|
||||
|
||||
selector.choose=Choose
|
||||
selector.choose_file=Choose file
|
||||
selector.choose_directory=Choose directory
|
||||
selector.custom=Custom
|
||||
|
||||
settings=Settings
|
||||
|
||||
@@ -46,6 +46,8 @@ about.thanks_to.shulkersakura.statement=鑄其徽
|
||||
about.thanks_to.users=HMCL 群組諸員
|
||||
about.thanks_to.users.statement=敬謝諸君之貲勉、報謬、謀案
|
||||
about.thanks_to.yushijinhun.statement=有助於 authlib-injector
|
||||
about.thanks_to.8mi-tech=八藍米資訊科技 (泉州) 有限公所
|
||||
about.thanks_to.8mi-tech.statement=供遊戲部分靜態資源之加速下載役
|
||||
about.open_source=開源
|
||||
about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL)
|
||||
|
||||
|
||||
@@ -47,6 +47,8 @@ about.thanks_to.shulkersakura.statement=提供 HMCL 的標誌
|
||||
about.thanks_to.users=HMCL 使用者群組成員
|
||||
about.thanks_to.users.statement=感謝使用者群組成員贊助充電、積極催更、回報問題、出謀劃策
|
||||
about.thanks_to.yushijinhun.statement=提供 authlib-injector 相關支援
|
||||
about.thanks_to.8mi-tech=八藍米信息科技 (泉州) 有限公司
|
||||
about.thanks_to.8mi-tech.statement=提供了遊戲部分靜態資源的加速下載服務
|
||||
about.open_source=開放原始碼
|
||||
about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL/)
|
||||
|
||||
@@ -109,8 +111,9 @@ account.methods.microsoft.close_page=已完成 Microsoft 帳戶授權。啟動
|
||||
account.methods.microsoft.deauthorize=移除應用存取權
|
||||
account.methods.microsoft.error.add_family=請點擊 <a href="https://support.microsoft.com/account-billing/837badbc-999e-54d2-2617-d19206b9540a">此處</a> 更改你的帳戶出生日期,使年齡滿 18 歲以上,或將帳戶加入到家庭中。
|
||||
account.methods.microsoft.error.country_unavailable=你所在的國家或地區不受 Xbox Live 的支援。
|
||||
account.methods.microsoft.error.missing_xbox_account=請點擊下方 <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">此處</a> 關聯 Xbox 帳戶。
|
||||
account.methods.microsoft.error.no_character=請確認你已經購買了 Minecraft: Java 版。\n若已購買,則可能未建立遊戲檔案。請點擊 <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">此處</a> 建立遊戲檔案。
|
||||
account.methods.microsoft.error.missing_xbox_account=請確認你已經購買了 Minecraft: Java 版。若已購買,請點擊 <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">此處</a> 建立遊戲檔案。
|
||||
account.methods.microsoft.error.no_license=未找到 Minecraft 購買記錄。請點擊 <a href="https://www.xbox.com/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj">此處</a> 購買遊戲。\n若你使用 Game Pass,請在 <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">此處</a> 建立遊戲檔案。若已購買,請檢查登入的帳戶是否正確。
|
||||
account.methods.microsoft.error.no_character=你可能未建立遊戲檔案。請點擊 <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">此處</a> 建立遊戲檔案。
|
||||
account.methods.microsoft.error.banned=你的帳戶可能被 Xbox Live 封禁。\n你可以點擊 <a href="https://enforcement.xbox.com/enforcement/showenforcementhistory">此處</a> 按鈕查詢帳戶封禁狀態。
|
||||
account.methods.microsoft.error.unknown=登入失敗。錯誤碼:%d。
|
||||
account.methods.microsoft.error.wrong_verify_method=登入失敗。請在 Microsoft 帳戶登入頁面嘗試使用密碼登入,不要使用其他登入方式。
|
||||
@@ -1098,6 +1101,7 @@ search.page_n=%d / %s
|
||||
|
||||
selector.choose=選取
|
||||
selector.choose_file=選取檔案
|
||||
selector.choose_directory=選取目錄
|
||||
selector.custom=自訂
|
||||
|
||||
settings=設定
|
||||
|
||||
@@ -47,6 +47,8 @@ about.thanks_to.shulkersakura.statement=提供 HMCL 的徽标
|
||||
about.thanks_to.users=HMCL 用户群成员
|
||||
about.thanks_to.users.statement=感谢用户群成员赞助充电、积极催更、反馈问题、出谋划策
|
||||
about.thanks_to.yushijinhun.statement=提供 authlib-injector 相关支持
|
||||
about.thanks_to.8mi-tech=八蓝米信息科技 (泉州) 有限公司
|
||||
about.thanks_to.8mi-tech.statement=提供了游戏部分静态资源的加速下载服务
|
||||
about.open_source=开源
|
||||
about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL)
|
||||
|
||||
@@ -110,8 +112,9 @@ account.methods.microsoft.close_page=已完成微软账户授权。其余登录
|
||||
account.methods.microsoft.deauthorize=解除账户授权
|
||||
account.methods.microsoft.error.add_family=请点击 <a href="https://support.microsoft.com/account-billing/837badbc-999e-54d2-2617-d19206b9540a">此处</a> 更改你的账户出生日期,使年龄满 18 岁以上,或将账户加入到家庭中。\n你可以点击右上角帮助按钮进行求助。
|
||||
account.methods.microsoft.error.country_unavailable=你所在的国家或地区不受 Xbox Live 的支持。
|
||||
account.methods.microsoft.error.missing_xbox_account=请点击 <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">此处</a> 关联 Xbox 账户。\n你可以点击右上角帮助按钮进行求助。
|
||||
account.methods.microsoft.error.no_character=请确认你已经购买了 Minecraft: Java 版。\n若已购买,则可能未创建游戏档案。请点击 <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">此处</a> 创建游戏档案。\n你可以点击右上角帮助按钮进行求助。
|
||||
account.methods.microsoft.error.missing_xbox_account=请确认你已经购买了 Minecraft: Java 版。若已购买,请点击 <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">此处</a> 创建游戏档案。\n你可以点击右上角帮助按钮进行求助。
|
||||
account.methods.microsoft.error.no_license=未找到 Minecraft 购买记录。请点击 <a href="https://www.xbox.com/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj">此处</a> 购买游戏。\n若你使用 Game Pass,请在 <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">此处</a> 创建游戏档案。若已购买,请检查登录的账户是否正确。\n你可以点击右上角帮助按钮进行求助。
|
||||
account.methods.microsoft.error.no_character=你尚未创建游戏档案。请点击 <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">此处</a> 创建游戏档案。\n你可以点击右上角帮助按钮进行求助。
|
||||
account.methods.microsoft.error.banned=你的账户可能被 Xbox Live 封禁。\n你可以点击 <a href="https://enforcement.xbox.com/enforcement/showenforcementhistory">此处</a> 按钮查询账户封禁状态。
|
||||
account.methods.microsoft.error.unknown=未知问题。错误码:%d。\n你可以点击右上角帮助按钮进行求助。
|
||||
account.methods.microsoft.error.wrong_verify_method=登录失败。请在微软账户登录页面使用密码登录,不要使用其他登录方式。\n你可以点击右上角帮助按钮进行求助。
|
||||
@@ -1103,6 +1106,7 @@ search.page_n=%d / %s
|
||||
|
||||
selector.choose=选择
|
||||
selector.choose_file=选择文件
|
||||
selector.choose_directory=选择文件夹
|
||||
selector.custom=自定义
|
||||
|
||||
settings=设置
|
||||
|
||||
@@ -44,6 +44,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
}
|
||||
|
||||
.dialog-card {
|
||||
border-radius: 4px;
|
||||
background-color: var(--monet-surface-container-high);
|
||||
padding: 24px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.14), 0 0 0 1px rgba(0, 0, 0, 0.02);
|
||||
|
||||
@@ -25,7 +25,10 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.OAuth;
|
||||
import org.jackhuang.hmcl.auth.ServerDisconnectException;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.*;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.*;
|
||||
import org.jackhuang.hmcl.util.io.*;
|
||||
@@ -245,7 +248,16 @@ public class MicrosoftService {
|
||||
.createConnection();
|
||||
int responseCode = conn.getResponseCode();
|
||||
if (responseCode == HTTP_NOT_FOUND) {
|
||||
throw new NoMinecraftJavaEditionProfileException();
|
||||
MinecraftLicense license = HttpRequest.GET("https://api.minecraftservices.com/entitlements/license")
|
||||
.authorization(tokenType, accessToken)
|
||||
.getJson(MinecraftLicense.class);
|
||||
boolean hasMinecraftLicense = license != null && license.items() != null && license.items().stream()
|
||||
.anyMatch(item -> "game_minecraft".equals(item.name()));
|
||||
if (!hasMinecraftLicense) {
|
||||
throw new MinecraftJavaEditionLicenseNotFoundException();
|
||||
} else {
|
||||
throw new MinecraftJavaEditionProfileNotFoundException();
|
||||
}
|
||||
} else if (responseCode != 200) {
|
||||
throw new ResponseCodeException("https://api.minecraftservices.com/minecraft/profile", responseCode);
|
||||
}
|
||||
@@ -324,7 +336,10 @@ public class MicrosoftService {
|
||||
public final static class XBox400Exception extends AuthenticationException {
|
||||
}
|
||||
|
||||
public final static class NoMinecraftJavaEditionProfileException extends AuthenticationException {
|
||||
public final static class MinecraftJavaEditionProfileNotFoundException extends AuthenticationException {
|
||||
}
|
||||
|
||||
public final static class MinecraftJavaEditionLicenseNotFoundException extends AuthenticationException {
|
||||
}
|
||||
|
||||
public final static class NoXuiException extends AuthenticationException {
|
||||
@@ -425,6 +440,21 @@ public class MicrosoftService {
|
||||
|
||||
}
|
||||
|
||||
@JsonSerializable
|
||||
public record MinecraftLicense(
|
||||
@SerializedName("items") List<MinecraftLicenseItem> items,
|
||||
@SerializedName("signature") String signature,
|
||||
@SerializedName("keyId") String keyId
|
||||
) {
|
||||
}
|
||||
|
||||
@JsonSerializable
|
||||
public record MinecraftLicenseItem(
|
||||
@SerializedName("name") String name,
|
||||
@SerializedName("signature") String signature
|
||||
) {
|
||||
}
|
||||
|
||||
public static class MinecraftProfileResponse extends MinecraftErrorResponse implements Validation {
|
||||
@SerializedName("id")
|
||||
UUID id;
|
||||
|
||||
@@ -242,7 +242,7 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||
}
|
||||
},
|
||||
CLEANROOM(true, "cleanroom", "com\\.cleanroommc", "cleanroom", ModLoaderType.CLEANROOM),
|
||||
NEO_FORGE(true, "neoforge", "net\\.neoforged\\.fancymodloader", "(core|loader)", ModLoaderType.NEO_FORGED) {
|
||||
NEO_FORGE(true, "neoforge", "net\\.neoforged\\.fancymodloader", "(core|loader)", ModLoaderType.NEO_FORGE) {
|
||||
private final Pattern NEO_FORGE_VERSION_MATCHER = Pattern.compile("^([0-9.]+)-(?<forge>[0-9.]+)(-([0-9.]+))?$");
|
||||
|
||||
@Override
|
||||
@@ -390,7 +390,7 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||
public static final String MOD_LAUNCHER_MAIN = "cpw.mods.modlauncher.Launcher";
|
||||
public static final String BOOTSTRAP_LAUNCHER_MAIN = "cpw.mods.bootstraplauncher.BootstrapLauncher";
|
||||
public static final String FORGE_BOOTSTRAP_MAIN = "net.minecraftforge.bootstrap.ForgeBootstrap";
|
||||
public static final String NEO_FORGED_BOOTSTRAP_MAIN = "net.neoforged.fml.startup.Client";
|
||||
public static final String NEO_FORGE_BOOTSTRAP_MAIN = "net.neoforged.fml.startup.Client";
|
||||
|
||||
public static final Set<String> FORGE_OPTIFINE_MAIN = Set.of(
|
||||
LibraryAnalyzer.VANILLA_MAIN,
|
||||
@@ -398,7 +398,7 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||
LibraryAnalyzer.MOD_LAUNCHER_MAIN,
|
||||
LibraryAnalyzer.BOOTSTRAP_LAUNCHER_MAIN,
|
||||
LibraryAnalyzer.FORGE_BOOTSTRAP_MAIN,
|
||||
LibraryAnalyzer.NEO_FORGED_BOOTSTRAP_MAIN
|
||||
LibraryAnalyzer.NEO_FORGE_BOOTSTRAP_MAIN
|
||||
);
|
||||
|
||||
public static final VersionRange<VersionNumber> FORGE_OPTIFINE_BROKEN_RANGE = VersionNumber.between("48.0.0", "49.0.50");
|
||||
|
||||
+12
-3
@@ -52,10 +52,13 @@ public final class LegacyFabricVersionList extends VersionList<LegacyFabricRemot
|
||||
lock.writeLock().lock();
|
||||
|
||||
try {
|
||||
for (String gameVersion : gameVersions)
|
||||
for (String loaderVersion : loaderVersions)
|
||||
for (String metaGameVersion : gameVersions) {
|
||||
String gameVersion = normalizeVersion(metaGameVersion);
|
||||
for (String loaderVersion : loaderVersions) {
|
||||
versions.put(gameVersion, new LegacyFabricRemoteVersion(gameVersion, loaderVersion,
|
||||
Collections.singletonList(getLaunchMetaUrl(gameVersion, loaderVersion))));
|
||||
Collections.singletonList(getLaunchMetaUrl(metaGameVersion, loaderVersion))));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
@@ -71,6 +74,12 @@ public final class LegacyFabricVersionList extends VersionList<LegacyFabricRemot
|
||||
.stream().map(GameVersion::getVersion).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static String normalizeVersion(String version) {
|
||||
return version.startsWith("2point0_")
|
||||
? "2.0_" + version.substring("2point0_".length())
|
||||
: version;
|
||||
}
|
||||
|
||||
private static String getLaunchMetaUrl(String gameVersion, String loaderVersion) {
|
||||
return String.format("https://meta.legacyfabric.net/v2/versions/loader/%s/%s", gameVersion, loaderVersion);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public enum ModLoaderType {
|
||||
UNKNOWN,
|
||||
FORGE,
|
||||
CLEANROOM,
|
||||
NEO_FORGED,
|
||||
NEO_FORGE,
|
||||
FABRIC,
|
||||
QUILT,
|
||||
LITE_LOADER,
|
||||
|
||||
@@ -47,7 +47,7 @@ public final class ModManager {
|
||||
var map = new HashMap<String, List<Pair<ModMetadataReader, ModLoaderType>>>();
|
||||
var zipReaders = List.<Pair<ModMetadataReader, ModLoaderType>>of(
|
||||
pair(ForgeNewModMetadata::fromForgeFile, ModLoaderType.FORGE),
|
||||
pair(ForgeNewModMetadata::fromNeoForgeFile, ModLoaderType.NEO_FORGED),
|
||||
pair(ForgeNewModMetadata::fromNeoForgeFile, ModLoaderType.NEO_FORGE),
|
||||
pair(ForgeOldModMetadata::fromFile, ModLoaderType.FORGE),
|
||||
pair(FabricModMetadata::fromFile, ModLoaderType.FABRIC),
|
||||
pair(QuiltModMetadata::fromFile, ModLoaderType.QUILT),
|
||||
|
||||
@@ -598,7 +598,7 @@ public class CurseAddon implements RemoteMod.IMod {
|
||||
if ("fabric".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FABRIC);
|
||||
else if ("forge".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FORGE);
|
||||
else if ("quilt".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.QUILT);
|
||||
else if ("neoforge".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.NEO_FORGED);
|
||||
else if ("neoforge".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.NEO_FORGE);
|
||||
else return Stream.empty();
|
||||
}).collect(Collectors.toList())
|
||||
);
|
||||
|
||||
@@ -174,15 +174,15 @@ public final class ForgeNewModMetadata {
|
||||
}
|
||||
|
||||
public static LocalModFile fromNeoForgeFile(ModManager modManager, Path modFile, ZipFileTree tree) throws IOException {
|
||||
return fromFile(modManager, modFile, tree, ModLoaderType.NEO_FORGED);
|
||||
return fromFile(modManager, modFile, tree, ModLoaderType.NEO_FORGE);
|
||||
}
|
||||
|
||||
private static LocalModFile fromFile(ModManager modManager, Path modFile, ZipFileTree tree, ModLoaderType modLoaderType) throws IOException {
|
||||
if (modLoaderType != ModLoaderType.FORGE && modLoaderType != ModLoaderType.NEO_FORGED) {
|
||||
if (modLoaderType != ModLoaderType.FORGE && modLoaderType != ModLoaderType.NEO_FORGE) {
|
||||
throw new IOException("Invalid mod loader: " + modLoaderType);
|
||||
}
|
||||
|
||||
if (modLoaderType == ModLoaderType.NEO_FORGED) {
|
||||
if (modLoaderType == ModLoaderType.NEO_FORGE) {
|
||||
try {
|
||||
return fromFile0("META-INF/neoforge.mods.toml", modLoaderType, modManager, modFile, tree);
|
||||
} catch (Exception ignored) {
|
||||
@@ -344,7 +344,7 @@ public final class ForgeNewModMetadata {
|
||||
result = ModLoaderType.FORGE;
|
||||
break loop;
|
||||
case "neoforge":
|
||||
result = ModLoaderType.NEO_FORGED;
|
||||
result = ModLoaderType.NEO_FORGE;
|
||||
break loop;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -668,7 +668,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
loaders.stream().flatMap(loader -> {
|
||||
if ("fabric".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FABRIC);
|
||||
else if ("forge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FORGE);
|
||||
else if ("neoforge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.NEO_FORGED);
|
||||
else if ("neoforge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.NEO_FORGE);
|
||||
else if ("quilt".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.QUILT);
|
||||
else if ("liteloader".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.LITE_LOADER);
|
||||
else return Stream.empty();
|
||||
|
||||
@@ -11,6 +11,7 @@ dependencies {
|
||||
implementation(libs.gson)
|
||||
implementation(libs.jna)
|
||||
implementation(libs.kala.compress.tar)
|
||||
implementation(libs.kala.compress.ar)
|
||||
}
|
||||
|
||||
java {
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.gradle.pack;
|
||||
|
||||
import kala.compress.archivers.ar.ArArchiveEntry;
|
||||
import kala.compress.archivers.ar.ArArchiveOutputStream;
|
||||
import kala.compress.archivers.tar.TarArchiveEntry;
|
||||
import kala.compress.archivers.tar.TarArchiveOutputStream;
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
import org.gradle.api.logging.Logger;
|
||||
import org.gradle.api.logging.Logging;
|
||||
import org.gradle.api.provider.Property;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.api.tasks.InputFile;
|
||||
import org.gradle.api.tasks.OutputFile;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
/// Creates a Debian package for the current HMCL channel.
|
||||
///
|
||||
/// ## Package layout
|
||||
///
|
||||
/// The generated `data.tar.gz` contains four installed artifacts:
|
||||
///
|
||||
/// - the bundled HMCL shell launcher under `/usr/share/java/hmcl/`
|
||||
/// - a channel-specific command under `/usr/bin/`
|
||||
/// - a desktop entry under `/usr/share/applications/`
|
||||
/// - the HMCL icon under `/usr/share/icons/hicolor/256x256/apps/`
|
||||
///
|
||||
/// ## Channel commands and aliases
|
||||
///
|
||||
/// Every package installs a channel-specific executable such as `hmcl-stable`
|
||||
/// or `hmcl-beta`. The generic `hmcl` command is intentionally not shipped as a
|
||||
/// plain file. Instead, maintainer scripts register the channel command into
|
||||
/// the shared `hmcl` alternatives group so multiple channel packages can
|
||||
/// coexist without file conflicts.
|
||||
///
|
||||
/// @author Glavo
|
||||
public abstract class CreateDeb extends DefaultTask {
|
||||
public static final Logger LOGGER = Logging.getLogger(CreateDeb.class);
|
||||
|
||||
private static final int DIRECTORY_MODE = 0755;
|
||||
private static final int EXECUTABLE_MODE = 0755;
|
||||
private static final int REGULAR_FILE_MODE = 0644;
|
||||
|
||||
/// Debian version written into the `control` file and output filename.
|
||||
@Input
|
||||
public abstract Property<String> getVersion();
|
||||
|
||||
/// Release type metadata that controls package name, launcher name, and alias priority.
|
||||
@Input
|
||||
public abstract Property<ReleaseType> getReleaseType();
|
||||
|
||||
/// Executable `.sh` artifact produced by `makeExecutables`.
|
||||
@InputFile
|
||||
public abstract RegularFileProperty getAppShFile();
|
||||
|
||||
/// Desktop icon installed into the hicolor icon theme.
|
||||
@InputFile
|
||||
public abstract RegularFileProperty getIconFile();
|
||||
|
||||
/// Final `.deb` archive written by this task.
|
||||
@OutputFile
|
||||
public abstract RegularFileProperty getOutputFile();
|
||||
|
||||
private ReleaseType getCurrentType() {
|
||||
return getReleaseType().get();
|
||||
}
|
||||
|
||||
private String getCurrentTypeName() {
|
||||
return getCurrentType().getName();
|
||||
}
|
||||
|
||||
private String getLauncherPath() {
|
||||
return "/usr/bin/hmcl-" + getCurrentTypeName();
|
||||
}
|
||||
|
||||
private String getTargetPath() {
|
||||
return "/usr/share/java/hmcl/" + getAppShFile().getAsFile().get().getName();
|
||||
}
|
||||
|
||||
private String getDesktopFilePath() {
|
||||
return "/usr/share/applications/hmcl-%s.desktop".formatted(getCurrentTypeName());
|
||||
}
|
||||
|
||||
private String getIconTargetPath() {
|
||||
return "/usr/share/icons/hicolor/256x256/apps/hmcl-%s.png".formatted(getCurrentTypeName());
|
||||
}
|
||||
|
||||
/// Ensures parent directories exist in the tar stream before child entries are written.
|
||||
private static void makeDirectories(Set<String> directories, TarArchiveOutputStream output, String dirName) throws IOException {
|
||||
if (dirName.isEmpty() || ".".equals(dirName) || directories.contains(dirName))
|
||||
return;
|
||||
|
||||
int idx = dirName.lastIndexOf('/');
|
||||
if (idx > 0) {
|
||||
makeDirectories(directories, output, dirName.substring(0, idx));
|
||||
}
|
||||
|
||||
TarArchiveEntry entry = new TarArchiveEntry(dirName + "/", true);
|
||||
entry.setMode(DIRECTORY_MODE);
|
||||
output.putArchiveEntry(entry);
|
||||
output.closeArchiveEntry();
|
||||
directories.add(dirName);
|
||||
}
|
||||
|
||||
/// Writes UTF-8 text content as a tar entry with the requested file mode.
|
||||
private static void putEntry(Set<String> directories, TarArchiveOutputStream output, String name, String content, int mode) throws IOException {
|
||||
putEntry(directories, output, name, content.getBytes(StandardCharsets.UTF_8), mode);
|
||||
}
|
||||
|
||||
/// Writes binary content as a tar entry and creates parent directories on demand.
|
||||
private static void putEntry(Set<String> directories, TarArchiveOutputStream output, String name, byte[] content, int mode) throws IOException {
|
||||
int idx = name.lastIndexOf('/');
|
||||
if (idx > 0) {
|
||||
makeDirectories(directories, output, name.substring(0, idx));
|
||||
}
|
||||
|
||||
TarArchiveEntry entry = new TarArchiveEntry(name);
|
||||
entry.setMode(mode);
|
||||
entry.setSize(content.length);
|
||||
output.putArchiveEntry(entry);
|
||||
output.write(content);
|
||||
output.closeArchiveEntry();
|
||||
}
|
||||
|
||||
/// Builds a valid `.deb` archive with `debian-binary`, `control.tar.gz`, and `data.tar.gz`.
|
||||
@TaskAction
|
||||
public void run() throws IOException {
|
||||
Path appShFile = getAppShFile().getAsFile().get().toPath();
|
||||
if (!Files.isRegularFile(appShFile))
|
||||
throw new IOException("Invalid app script file: " + appShFile);
|
||||
|
||||
Path iconFile = getIconFile().getAsFile().get().toPath();
|
||||
if (!Files.isRegularFile(iconFile))
|
||||
throw new IOException("Invalid icon file: " + iconFile);
|
||||
|
||||
byte[] appShBytes = Files.readAllBytes(appShFile);
|
||||
if (appShBytes.length == 0)
|
||||
throw new IOException("Empty app script file: " + appShFile);
|
||||
|
||||
byte[] iconBytes = Files.readAllBytes(iconFile);
|
||||
if (iconBytes.length == 0)
|
||||
throw new IOException("Empty icon file: " + iconFile);
|
||||
|
||||
byte[] launcherScriptBytes = getLauncherScript().getBytes(StandardCharsets.UTF_8);
|
||||
byte[] desktopInfoBytes = getDesktopInfo().getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
LOGGER.lifecycle("Creating control.tar.gz");
|
||||
var controlData = new ByteArrayOutputStream();
|
||||
try (var output = new TarArchiveOutputStream(new GZIPOutputStream(controlData))) {
|
||||
output.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
|
||||
|
||||
Set<String> directories = new HashSet<>();
|
||||
putEntry(directories, output, "./control", getControl(appShBytes.length, launcherScriptBytes.length, desktopInfoBytes.length, iconBytes.length), REGULAR_FILE_MODE);
|
||||
putEntry(directories, output, "./postinst", getPostinst(), EXECUTABLE_MODE);
|
||||
putEntry(directories, output, "./prerm", getPrerm(), EXECUTABLE_MODE);
|
||||
}
|
||||
|
||||
Path outputFile = getOutputFile().get().getAsFile().toPath();
|
||||
Files.createDirectories(outputFile.getParent());
|
||||
|
||||
LOGGER.lifecycle("Creating data.tar.gz");
|
||||
var dataTarBuffer = new ByteArrayOutputStream(12 * 1024 * 1024);
|
||||
try (var output = new TarArchiveOutputStream(new GZIPOutputStream(dataTarBuffer))) {
|
||||
output.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
|
||||
|
||||
Set<String> directories = new HashSet<>();
|
||||
putEntry(directories, output, "." + getTargetPath(), appShBytes, EXECUTABLE_MODE);
|
||||
putEntry(directories, output, "." + getLauncherPath(), launcherScriptBytes, EXECUTABLE_MODE);
|
||||
putEntry(directories, output, "." + getDesktopFilePath(), desktopInfoBytes, REGULAR_FILE_MODE);
|
||||
putEntry(directories, output, "." + getIconTargetPath(), iconBytes, REGULAR_FILE_MODE);
|
||||
}
|
||||
|
||||
LOGGER.lifecycle("Creating deb file");
|
||||
try (var output = new ArArchiveOutputStream(Files.newOutputStream(outputFile,
|
||||
StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE))) {
|
||||
putArEntry(output, "debian-binary", "2.0\n".getBytes(StandardCharsets.UTF_8));
|
||||
putArEntry(output, "control.tar.gz", controlData.toByteArray());
|
||||
putArEntry(output, "data.tar.gz", dataTarBuffer.toByteArray());
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds one member to the outer ar container used by Debian packages.
|
||||
private static void putArEntry(ArArchiveOutputStream output, String name, byte[] content) throws IOException {
|
||||
ArArchiveEntry entry = new ArArchiveEntry(name, content.length);
|
||||
output.putArchiveEntry(entry);
|
||||
output.write(content);
|
||||
output.closeArchiveEntry();
|
||||
}
|
||||
|
||||
/// Generates the package metadata file.
|
||||
///
|
||||
/// `Depends` is deliberately omitted so the package stays policy-light and
|
||||
/// leaves runtime dependency resolution to the target environment.
|
||||
private String getControl(long appSize, long launcherScriptSize, long desktopInfoSize, long iconSize) {
|
||||
long installedSize = (appSize + launcherScriptSize + desktopInfoSize + iconSize + 1023) / 1024;
|
||||
|
||||
return """
|
||||
Package: %s
|
||||
Version: %s
|
||||
Section: games
|
||||
Priority: optional
|
||||
Architecture: all
|
||||
Installed-Size: %d
|
||||
Maintainer: Glavo <zjx001202@gmail.com>
|
||||
Description: Hello Minecraft! Launcher
|
||||
Homepage: https://github.com/HMCL-dev/HMCL
|
||||
""".formatted(getCurrentType().getPackageName(), getVersion().get(), Math.max(installedSize, 1)) + "\n";
|
||||
}
|
||||
|
||||
private static final String COMMON_LAUNCHER_PATH = "/usr/bin/hmcl";
|
||||
|
||||
/// Registers the channel command into the shared `hmcl` alternatives group.
|
||||
private String getPostinst() {
|
||||
return """
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
if [ "$1" = configure ]; then
|
||||
update-alternatives --install %s hmcl %s %d
|
||||
fi
|
||||
""".formatted(COMMON_LAUNCHER_PATH, getLauncherPath(), getCurrentType().getAlternativesPriority());
|
||||
}
|
||||
|
||||
/// Removes the channel command from the shared `hmcl` alternatives group.
|
||||
private String getPrerm() {
|
||||
return """
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
if [ "$1" = remove ] || [ "$1" = deconfigure ]; then
|
||||
update-alternatives --remove hmcl %s
|
||||
fi
|
||||
""".formatted(getLauncherPath());
|
||||
}
|
||||
|
||||
/// Creates a tiny wrapper that launches the bundled shell script from the user's home directory.
|
||||
private String getLauncherScript() {
|
||||
return """
|
||||
#!/usr/bin/env bash
|
||||
cd "$HOME"
|
||||
if [ -z "${HMCL_USER_HOME:-}" ]; then
|
||||
if [ -z "${XDG_DATA_HOME:-}" ]; then
|
||||
export HMCL_USER_HOME="$HOME/.local/share/hmcl"
|
||||
else
|
||||
export HMCL_USER_HOME="$XDG_DATA_HOME/hmcl"
|
||||
fi
|
||||
fi
|
||||
if [ -z "${HMCL_LOCAL_HOME:-}" ]; then
|
||||
export HMCL_LOCAL_HOME="$HMCL_USER_HOME/local-%s"
|
||||
fi
|
||||
if [ -z "${HMCL_DEPENDENCIES_DIR:-}" ]; then
|
||||
export HMCL_DEPENDENCIES_DIR="$HMCL_USER_HOME/dependencies"
|
||||
fi
|
||||
exec %s "$@"
|
||||
""".formatted(getCurrentTypeName(), getTargetPath());
|
||||
}
|
||||
|
||||
/// Generates the desktop entry that points to the channel-specific launcher command.
|
||||
private String getDesktopInfo() {
|
||||
return """
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=%s
|
||||
Comment=Hello Minecraft! Launcher
|
||||
Exec=%s
|
||||
Icon=%s
|
||||
Terminal=false
|
||||
StartupNotify=false
|
||||
Categories=Game;
|
||||
Keywords=mc;minecraft;
|
||||
""".formatted(getCurrentType().getDisplayName(), getLauncherPath(), getIconTargetPath());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.gradle.pack;
|
||||
|
||||
/// Debian packaging metadata for one HMCL release type.
|
||||
///
|
||||
/// The package name, installed command, desktop file, and alternatives
|
||||
/// priority are intentionally centralized here so `CreateDeb` can stay focused
|
||||
/// on archive layout instead of duplicating channel-specific branching.
|
||||
public enum ReleaseType {
|
||||
STABLE("stable", "hmcl", "HMCL", 100),
|
||||
DEVELOPMENT("beta", "hmcl-beta", "HMCL (Beta)", 200),
|
||||
NIGHTLY("nightly", "hmcl-nightly", "HMCL (Nightly)", 300);
|
||||
|
||||
private final String name;
|
||||
private final String packageName;
|
||||
private final String displayName;
|
||||
private final int alternativesPriority;
|
||||
|
||||
ReleaseType(String name, String packageName, String displayName, int alternativesPriority) {
|
||||
this.name = name;
|
||||
this.packageName = packageName;
|
||||
this.displayName = displayName;
|
||||
this.alternativesPriority = alternativesPriority;
|
||||
}
|
||||
|
||||
///
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/// Debian package name written into `control` and used in the output filename.
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
/// Priority used when registering the generic `hmcl` alias.
|
||||
public int getAlternativesPriority() {
|
||||
return alternativesPriority;
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,7 @@
|
||||
|
||||
<module name="SuppressionSingleFilter">
|
||||
<property name="checks" value="RegexpHeader"/>
|
||||
<property name="files" value=".*[\\/](SwingFXUtils|ScrollUtils|TypeUtils).java"/>
|
||||
<property name="files" value=".*[\\/](ScrollUtils|TypeUtils).java"/>
|
||||
</module>
|
||||
|
||||
<module name="RegexpHeader">
|
||||
|
||||
Vendored
+1
-1
@@ -14,7 +14,7 @@ pipeline {
|
||||
stage('Build') {
|
||||
steps {
|
||||
sh 'bash ./config/jenkins/config-jenkins.sh'
|
||||
sh './gradlew clean makeExecutables --stacktrace --no-daemon'
|
||||
sh './gradlew clean makeExecutables makeDeb --stacktrace --no-daemon'
|
||||
archiveArtifacts artifacts: 'HMCL/build/libs/*'
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -14,7 +14,7 @@ pipeline {
|
||||
stage('Build') {
|
||||
steps {
|
||||
sh 'bash ./config/jenkins/config-jenkins.sh'
|
||||
sh './gradlew clean makeExecutables --stacktrace --no-daemon'
|
||||
sh './gradlew clean makeExecutables makeDeb --stacktrace --no-daemon'
|
||||
archiveArtifacts artifacts: 'HMCL/build/libs/*'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ hmclauncher = { module = "org.glavo.hmcl:HMCLauncher", version.ref = "hmclaunche
|
||||
jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" }
|
||||
kala-compress-zip = { module = "org.glavo.kala:kala-compress-archivers-zip", version.ref = "kala-compress" }
|
||||
kala-compress-tar = { module = "org.glavo.kala:kala-compress-archivers-tar", version.ref = "kala-compress" }
|
||||
kala-compress-ar = { module = "org.glavo.kala:kala-compress-archivers-ar", version.ref = "kala-compress" }
|
||||
simple-png-javafx = { module = "org.glavo:simple-png-javafx", version.ref = "simple-png-javafx" }
|
||||
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
|
||||
tomlj = { module = "org.tomlj:tomlj", version.ref = "tomlj" }
|
||||
|
||||
Reference in new issue
Block a user