Compare commits

...
79 Commits
Author SHA1 Message Date
huanghongxun 1fb9fa0af9 fix: icon of selected item should be highlighted. 2021-08-04 00:09:22 +08:00
huanghongxun d35fb9a486 fix: (modpack download): add default category. 2021-08-04 00:08:23 +08:00
Yuhui Huang 37fb9a0d65 feat: (modpack download): category & sort. 2021-08-03 22:47:53 +08:00
Yuhui Huang af7cf393dc feat: download mods and modpacks from CurseForge. 2021-08-03 22:07:19 +08:00
Yuhui Huang 7a20af462b feat: (WIP) Curse mod/modpack download 2021-08-02 23:11:30 +08:00
Yuhui Huang 0939ed819e feat: title bar swipe animation. 2021-08-02 16:46:45 +08:00
Yuhui Huang 5da4ad7a62 feat: refine GameListPage UI. 2021-08-02 16:16:21 +08:00
Yuhui Huang 123c863e3d feat: refine profile list UI. 2021-08-02 16:05:37 +08:00
Yuhui Huang 6fc0b13dc7 feat: refine UI icons. 2021-08-02 15:28:57 +08:00
Yuhui Huang 59bf8d3ebc style: fix checkstyle. 2021-08-02 15:09:34 +08:00
Yuhui Huang 805393b184 fix: GameListItem launch icon -> launchOutline icon. 2021-08-02 15:07:31 +08:00
Yuhui Huang 5d0d77aff4 style: fix checkstyle. 2021-08-02 15:03:39 +08:00
Yuhui Huang d2e4ddc876 fix: inconsistent svg icon size. 2021-08-02 15:02:54 +08:00
huanghongxun 5b7ca8c751 feat: redesign UI of game management. 2021-08-02 13:36:36 +08:00
huanghongxun 16c7558497 fix: active tab indicator animation aligned to Material Design standard. 2021-07-31 23:23:07 +08:00
huanghongxun 9b6b3f8938 fix: no longer validate Forge dependencies since provided checksums are wrong. Closes #913. 2021-07-31 02:10:04 +08:00
huanghongxun 331d8d1bb6 fix: does not remove the old library when updating the library 2021-07-31 02:02:06 +08:00
huanghongxun e5cca24740 fix: Version.jar becomes wrong value after resolvePreservingPatches 2021-07-31 02:01:17 +08:00
huanghongxun 6f40335322 feat: print more effective logs when OptiFine Patcher failed to patch game. 2021-07-31 02:00:13 +08:00
huanghongxun b6de9d4ea2 fix: failed to duplicate version when no jar 2021-07-31 00:56:26 +08:00
Yuhui Huang 5f8f01e0ab Revert "fix: Version.jar should be resolved to id of self if null instead of the id of ancestor.". Closes #934.
This reverts commit 5de150b06e.
2021-07-28 16:15:11 +08:00
xtex 0d69e944f6 fix: #930(Search new Java registry location first) 2021-07-25 17:29:44 +08:00
huanghongxun 1dc4e3f730 fix: 1.17 Forge auto installation 2021-07-25 17:29:00 +08:00
huanghongxun 14d1bccd14 fix: update condition check for whether the game is modded. 2021-07-25 15:58:22 +08:00
huanghongxun 5de150b06e fix: Version.jar should be resolved to id of self if null instead of the id of ancestor. 2021-07-25 01:21:08 +08:00
huanghongxun e34dd39b9a fix: malformed locale name. Closes #925. 2021-07-24 14:56:18 +08:00
Haowei Wen e7d5faeeac fix: DirectoryStream not closed
https://github.com/huanghongxun/HMCL/commit/dc3167472175d023878f9ae32230b93964588bc1#r53347194
2021-07-12 13:19:16 +08:00
huanghongxun 46a7269087 chore: ignore hmcl.json 2021-07-10 23:25:26 +08:00
huanghongxun dc31674721 fix: Do not throw exception when path to cached jvm installations does not exist. 2021-07-10 23:24:31 +08:00
Glavo afe6bc7456 用户 JVM 参数优先于默认参数 (#903)
* close #885: use the GC selected by user by default

* fix: default JVM args conflict with user args

* Options take precedence over JVM args

* compile huge methods by default

* update settings.advanced.jvm_args text

* Using JVM args suppression options

* update settings.advanced.jvm_args text
2021-07-10 23:10:04 +08:00
huanghongxun 378fd04463 feat: placeholders in user customized game arguments. Closes #920 2021-07-10 23:09:11 +08:00
huanghongxun 3e751ba5c6 chore: some comments on "libraries_directory" 2021-07-10 23:08:39 +08:00
Glavo 8fc1ec7bb8 替换对 JDK 内置 Pack200 的依赖,允许在 JDK14 及更高版本上构建项目 (#904)
* fix: Pack200 has been removed

* Automatically add pack200 to class path

* add license header to Pack200Utils.java
2021-07-10 22:44:13 +08:00
huanghongxun e5326a3c4d fix: switch to "library_directory" defined by official launcher 2021-07-10 21:22:57 +08:00
huanghongxun fe279234d8 feat: Support "invalid" hash code in download info 2021-07-10 21:16:34 +08:00
huanghongxun ef9b041310 feat: Support placeholders in version.json. Closes #923 2021-07-10 21:16:33 +08:00
Ghost_chu 9db576d4bc 修复在manifest中的因拼写问题造成的与文档不符的问题 (#921) 2021-07-09 13:02:17 +08:00
Haowei Wen a84971abfc update english translation 2021-06-24 21:05:30 +08:00
Haowei Wen a7a79db842 update copyright year in settings page 2021-06-24 21:00:38 +08:00
Haowei Wen d5da14fb1e remove welcome page (reduce ~300k size) 2021-06-24 20:43:29 +08:00
Haowei Wen 0d2e287f67 load welcome.png only when necessary
This saves ~200ms startup time.
2021-06-23 06:08:11 +08:00
Haowei Wen c329f1cafb log error when background can't be loaded 2021-06-23 04:57:01 +08:00
Haowei Wen ab1a2f6fcb fix: NPE in ModpackInfoPage 2021-06-15 00:34:50 +08:00
Haowei Wen 463dc56702 delete license-header.txt since it's not used 2021-06-14 19:12:38 +08:00
Haowei Wen 88e23cd0d2 feat: speed up openjfx dependencies checking
SHA-1 of dependencies are defined in openjfx-dependencies.json
to avoid network requests during startup.

openjfx-dependencies.json can be generated using
tools/generate-openjfx-dependencies.sh
2021-06-14 14:22:56 +08:00
Haowei Wen 9776aafab6 fix missing license in some files 2021-06-14 02:45:17 +08:00
Haowei Wen fbb5a03e5d feat: select authlib-injector server after added
* The most recently added server will be at the top.
* In Add Account dialog, after adding an authlib-injector server,
  it will be automatically selected.
2021-06-13 22:58:05 +08:00
huanghongxun 7c11ce53f3 fix: NPE. Closes #896. 2021-06-12 18:43:54 +08:00
huanghongxun 66f4c022d5 fix: ask user whether to download java16. 2021-06-12 18:43:53 +08:00
yaoxi-std 4b36450f9a changed language files, fixed #898 2021-06-12 16:53:24 +08:00
huanghongxun 36b58bd1bb feat: Support CurseForge modpack with Fabric as modloader. Closes #897. 2021-06-12 01:25:10 +08:00
Glavo 0296b6f73d fix: export modpack 2021-06-08 19:06:09 +08:00
huanghongxun 447afaa76f fix: typo 2021-05-30 03:39:29 +08:00
huanghongxun a0191aa7c2 fix: lint 2021-05-30 03:37:55 +08:00
huanghongxun e7c9fcf767 feat: checkstyle in GitHub Actions 2021-05-30 03:35:26 +08:00
huanghongxun fc017515c5 feat: check style & format 2021-05-30 03:34:44 +08:00
huanghongxun 6d75c4d34f chore: C++ google style 2021-05-30 02:42:39 +08:00
huanghongxun 258a6628e4 fix: Cannot start application when code page is 65001. Closes #814.
This commit may cause breaking change: changing working directory to the directory where the executable locates in.
2021-05-30 02:25:31 +08:00
huanghongxun 228695d511 fix: NPE 2021-05-29 17:22:06 +08:00
Glavo c2af530abb feat: lookup JRE installation in more places. 2021-05-29 17:18:56 +08:00
huanghongxun 69bc826e30 feat: BMCLAPI now supports downloading Java 16. 2021-05-29 17:17:00 +08:00
huanghongxun 5d0944ed19 feat: lookup JRE installation in more places. 2021-05-29 17:06:08 +08:00
寒兮 43f4da5dd3 fix: GITHUB_SHA build version NullPointException 2021-05-29 17:05:12 +08:00
寒兮 c6ab2cec35 fix: short commit version use lower case 2021-05-29 17:05:12 +08:00
寒兮 b0dc689ba7 fix: use GITHUB_SHA instead of the git command 2021-05-29 17:05:12 +08:00
寒兮 e11b1f37b9 chore: artifacts version use git short commit 2021-05-29 17:05:12 +08:00
huanghongxun 3e5fac4df6 feat: lookup JRE installation in more places. 2021-05-29 16:50:07 +08:00
huanghongxun e63fa40822 feat: run github actions on pull request 2021-05-29 16:49:24 +08:00
huanghongxun e01096bff0 fix: lint 2021-05-29 15:15:40 +08:00
yaoxi-std 2a0cfb1125 fixed bugs 2021-05-29 01:23:58 +08:00
yaoxi-std d4f0a04ccd change NativesDir into LaunchOptions 2021-05-29 01:23:58 +08:00
yaoxi-std 7cbe24b35b add: allow customize LWJGL path (runs on M1) 2021-05-29 01:23:58 +08:00
huanghongxun b7da21e5ce fix: not select downloaded Java automatically. Closes #886. 2021-05-27 00:17:33 +08:00
Glavo 0ae1411f0a fix #861: Unable open zipfs for signed exe file 2021-05-25 15:05:42 +08:00
huanghongxun 66889cac27 chore: version 3.3 2021-05-25 13:25:43 +08:00
huanghongxun 67bd2ac386 fix: typo 2021-05-25 13:23:09 +08:00
huanghongxun 1f278c2a49 feat: publish unsigned artifacts by GitHub Actions 2021-05-25 13:20:33 +08:00
huanghongxun 1674f1828f fix: hmclauncher 2021-05-25 13:14:43 +08:00
huanghongxun 472a0a039f fix: fuck lfs 2021-05-25 13:13:33 +08:00
162 changed files with 4537 additions and 1637 deletions

No files matched your search

-1
View File
@@ -1 +0,0 @@
*.exe filter=lfs diff=lfs merge=lfs -text
+11 -2
View File
@@ -1,6 +1,6 @@
name: Java CI
on: [push]
on: [push, pull_request]
jobs:
build:
@@ -8,11 +8,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
java-package: jdk+fx
- name: Check style main
run: ./gradlew checkstyleMain
- name: Check style test
run: ./gradlew checkstyleTest
- name: Build with Gradle
run: ./gradlew build
- name: Upload Artifacts
uses: actions/upload-artifact@v2
with:
name: HMCL
path: HMCL/build/libs
+9 -2
View File
@@ -11,7 +11,6 @@ hs_err_pid*
*.log
.mine*
NVIDIA
*.json
# gradle build
/build/
@@ -38,4 +37,12 @@ NVIDIA
# netbeans
.nb-gradle
*.exe
*.exe
!/HMCL/src/main/resources/assets/HMCLauncher.exe
# macos
.DS_Store
# test
/hmcl.json
/.hmcl.json
+17 -8
View File
@@ -1,9 +1,11 @@
buildscript {
repositories {
gradlePluginPortal()
maven { url 'https://jitpack.io' }
}
dependencies {
classpath 'org.tukaani:xz:1.8'
classpath 'org.glavo:pack200:0.3.0'
}
}
@@ -19,18 +21,21 @@ import java.security.Signature
import java.security.spec.PKCS8EncodedKeySpec
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.jar.Pack200
import java.util.zip.GZIPOutputStream
import java.util.zip.ZipFile
import java.nio.file.Files
import org.tukaani.xz.LZMA2Options
import org.tukaani.xz.XZOutputStream
import org.glavo.pack200.Pack200
def buildnumber = System.getenv("BUILD_NUMBER") ?: "SNAPSHOT"
def dev = null
def shortcommit = System.getenv("GITHUB_SHA")?.toLowerCase()?.substring(0, 7) ?: null
if (shortcommit != null && !shortcommit.isEmpty()) dev = "dev-" + shortcommit
def buildnumber = System.getenv("BUILD_NUMBER") ?: dev ?: "SNAPSHOT"
if (System.getenv("BUILD_NUMBER") != null && System.getenv("BUILD_NUMBER_OFFSET") != null)
buildnumber = (Integer.parseInt(System.getenv("BUILD_NUMBER")) - Integer.parseInt(System.getenv("BUILD_NUMBER_OFFSET"))).toString()
def versionroot = System.getenv("VERSION_ROOT") ?: "3.2"
def versionroot = System.getenv("VERSION_ROOT") ?: "3.3"
version = versionroot + '.' + buildnumber
mainClassName = 'org.jackhuang.hmcl.Main'
@@ -96,8 +101,10 @@ sourceSets {
}
compileJava11Java {
javaCompiler = javaToolchains.compilerFor {
languageVersion = JavaLanguageVersion.of(11)
if(JavaVersion.current() < JavaVersion.VERSION_11) {
javaCompiler = javaToolchains.compilerFor {
languageVersion = JavaLanguageVersion.of(11)
}
}
options.compilerArgs.add('--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED')
sourceCompatibility = 11
@@ -117,6 +124,7 @@ shadowJar {
'Main-Class': mainClassName,
'Multi-Release': 'true',
'Implementation-Version': version,
'Class-Path': 'pack200.jar',
'Add-Opens': [
'java.base/java.lang',
'java.base/java.lang.reflect',
@@ -161,8 +169,9 @@ processResources {
def cssFile = new File(this.projectDir, "src/main/resources/" + resource)
def bssFile = new File(this.projectDir, "build/compiled-resources/" + resource[0..-4] + "bss")
bssFile.parentFile.mkdirs()
exec {
commandLine 'javapackager', '-createbss', '-outdir', bssFile.parent, '-srcfiles', cssFile.path
javaexec {
main = "com.sun.javafx.css.parser.Css2Bin"
args = [cssFile, bssFile]
}
}
}
@@ -207,4 +216,4 @@ task makeExecutables(dependsOn: jar) doLast {
build.dependsOn makePackXz
build.dependsOn makePackGz
build.dependsOn makeExecutables
build.dependsOn makeExecutables
@@ -36,6 +36,7 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.management.ManagementFactory;
import java.net.*;
import java.nio.file.Paths;
@@ -160,10 +161,15 @@ public final class Launcher extends Application {
return null;
}
} else {
File jarFile = new File(Launcher.class.getProtectionDomain().getCodeSource().getLocation().getPath());
String ext = FileUtils.getExtension(jarFile);
if ("jar".equals(ext) || "exe".equals(ext))
result.add(jarFile);
try {
File jarFile = new File(URLDecoder.decode(Launcher.class.getProtectionDomain().getCodeSource().getLocation().getPath(), "UTF-8"));
String ext = FileUtils.getExtension(jarFile);
if ("jar".equals(ext) || "exe".equals(ext))
result.add(jarFile);
} catch (UnsupportedEncodingException e) {
LOG.log(Level.WARNING, "Failed to decode jar path", e);
return null;
}
}
if (result.isEmpty())
return null;
@@ -43,6 +43,9 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Main {
private Main() {
}
public static void main(String[] args) {
System.setProperty("java.net.useSystemProxies", "true");
System.setProperty("http.agent", "HMCL/" + Metadata.VERSION);
@@ -64,7 +64,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
try {
// TODO: Dirty implementation here
if (I18n.getCurrentLocale().getLocale() == Locale.CHINA)
FileUtils.writeText(optionsFile, "lang:zh_cn\nforceUnicodeFont:true\n");
FileUtils.writeText(optionsFile, "lang:zh_CN\nforceUnicodeFont:true\n");
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to generate options.txt", e);
}
@@ -129,7 +129,9 @@ public class HMCLGameRepository extends DefaultGameRepository {
if (dstDir.exists()) throw new IOException("Version exists");
FileUtils.copyDirectory(srcDir.toPath(), dstDir.toPath());
Files.move(dstDir.toPath().resolve(srcId + ".jar"), dstDir.toPath().resolve(dstId + ".jar"));
if (Files.exists(dstDir.toPath().resolve(srcId + ".jar"))) {
Files.move(dstDir.toPath().resolve(srcId + ".jar"), dstDir.toPath().resolve(dstId + ".jar"));
}
Files.move(dstDir.toPath().resolve(srcId + ".json"), dstDir.toPath().resolve(dstId + ".json"));
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
@@ -235,10 +237,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
File iconFile = getVersionIconFile(id);
if (iconFile.exists())
return new Image("file:" + iconFile.getAbsolutePath());
else if (version.getMainClass() != null &&
(LibraryAnalyzer.LAUNCH_WRAPPER_MAIN.equals(version.getMainClass())
|| version.getMainClass().startsWith("net.fabricmc")
|| LibraryAnalyzer.MOD_LAUNCHER_MAIN.equals(version.getMainClass())))
else if (LibraryAnalyzer.isModded(this, version))
return newImage("/assets/img/furnace.png");
else
return newImage("/assets/img/grass.png");
@@ -306,7 +305,9 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setServerIp(vs.getServerIp())
.setWrapper(vs.getWrapper())
.setPrecalledCommand(vs.getPreLaunchCommand())
.setNoGeneratedJVMArgs(vs.isNoJVMArgs());
.setNoGeneratedJVMArgs(vs.isNoJVMArgs())
.setNativesDirType(vs.getNativesDirType())
.setNativesDir(vs.getNativesDir());
if (config().hasProxy()) {
builder.setProxy(ProxyManager.getProxy());
if (config().hasProxyAuth()) {
@@ -31,15 +31,17 @@ import java.nio.file.Path;
* @author huangyuhui
*/
public final class HMCLModpackManager {
private HMCLModpackManager() {
}
/**
* Read the manifest in a HMCL modpack.
*
* @param file a HMCL modpack file.
* @param file a HMCL modpack file.
* @param encoding encoding of modpack zip file.
* @throws IOException if the file is not a valid zip file.
* @throws JsonParseException if the manifest.json is missing or malformed.
* @return the manifest of HMCL modpack.
* @throws IOException if the file is not a valid zip file.
* @throws JsonParseException if the manifest.json is missing or malformed.
*/
public static Modpack readHMCLModpackManifest(Path file, Charset encoding) throws IOException, JsonParseException {
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json", encoding);
@@ -68,6 +68,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.RejectedExecutionException;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.mapOf;
@@ -325,9 +326,19 @@ public final class LauncherHelper {
if (acceptableJava.isPresent()) {
setting.setJavaVersion(acceptableJava.get());
} else {
Controllers.dialog(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), version.getJavaVersion().getMajorVersion()), i18n("message.warning"), MessageType.WARNING, () -> {
downloadJava(version.getJavaVersion(), profile).thenAccept(x -> onAccept.run());
});
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), version.getJavaVersion().getMajorVersion()), i18n("message.warning"), () -> {
downloadJava(version.getJavaVersion(), profile)
.thenAcceptAsync(x -> {
try {
Optional<JavaVersion> newAcceptableJava = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getParsedVersion() >= version.getJavaVersion().getMajorVersion())
.max(Comparator.comparing(JavaVersion::getVersionNumber));
newAcceptableJava.ifPresent(setting::setJavaVersion);
} catch (InterruptedException e) {
LOG.log(Level.SEVERE, "Cannot list javas", e);
}
}, Platform::runLater).thenAccept(x -> onAccept.run());
}, null);
flag = true;
}
}
@@ -477,7 +488,7 @@ public final class LauncherHelper {
super.onStop(success, executor);
Platform.runLater(() -> {
if (!success) {
future.completeExceptionally(executor.getException());
future.completeExceptionally(Optional.ofNullable(executor.getException()).orElseGet(InterruptedException::new));
} else {
future.complete(null);
}
@@ -26,11 +26,7 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.auth.yggdrasil.*;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.StringUtils;
@@ -39,13 +35,15 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
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.*;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -263,4 +261,4 @@ public final class TexturesLoader {
}
return wr;
}
}
}
@@ -208,6 +208,24 @@ public final class Profile implements Observable {
Platform.runLater(observableHelper::invalidate);
}
public static class ProfileVersion {
private final Profile profile;
private final String version;
public ProfileVersion(Profile profile, String version) {
this.profile = profile;
this.version = version;
}
public Profile getProfile() {
return profile;
}
public String getVersion() {
return version;
}
}
public static final class Serializer implements JsonSerializer<Profile>, JsonDeserializer<Profile> {
@Override
public JsonElement serialize(Profile src, Type typeOfSrc, JsonSerializationContext context) {
@@ -23,11 +23,11 @@ import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.io.FileUtils;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import java.util.Locale;
public class Settings {
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public final class Settings {
private static Settings instance;
@@ -22,6 +22,7 @@ import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*;
import org.jackhuang.hmcl.game.GameDirectoryType;
import org.jackhuang.hmcl.game.NativesDirectoryType;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
@@ -43,8 +44,6 @@ import java.util.stream.Collectors;
@JsonAdapter(VersionSetting.Serializer.class)
public final class VersionSetting implements Cloneable {
public transient String id;
private boolean global = false;
public boolean isGlobal() {
@@ -66,7 +65,7 @@ public final class VersionSetting implements Cloneable {
* 1. Global settings.
* 2. Version settings.
* If a version claims that it uses global settings, its version setting will be disabled.
*
* <p>
* Defaults false because if one version uses global first, custom version file will not be generated.
*/
public boolean isUsesGlobal() {
@@ -119,6 +118,39 @@ public final class VersionSetting implements Cloneable {
defaultJavaPathProperty.set(defaultJavaPath);
}
/**
* 0 - .minecraft/versions/&lt;version&gt;/natives/<br/>
*/
private final ObjectProperty<NativesDirectoryType> nativesDirTypeProperty = new SimpleObjectProperty<>(this, "nativesDirType", NativesDirectoryType.VERSION_FOLDER);
public ObjectProperty<NativesDirectoryType> nativesDirTypeProperty() {
return nativesDirTypeProperty;
}
public NativesDirectoryType getNativesDirType() {
return nativesDirTypeProperty.get();
}
public void setNativesDirType(NativesDirectoryType nativesDirType) {
nativesDirTypeProperty.set(nativesDirType);
}
// Path to lwjgl natives directory
private final StringProperty nativesDirProperty = new SimpleStringProperty(this, "nativesDirProperty", "");
public StringProperty nativesDirProperty() {
return nativesDirProperty;
}
public String getNativesDir() {
return nativesDirProperty.get();
}
public void setNativesDir(String nativesDir) {
nativesDirProperty.set(nativesDir);
}
private final StringProperty javaDirProperty = new SimpleStringProperty(this, "javaDir", "");
public StringProperty javaDirProperty() {
@@ -336,7 +368,7 @@ public final class VersionSetting implements Cloneable {
/**
* The server ip that will be entered after Minecraft successfully loaded ly.
*
* <p>
* Format: ip:port or without port.
*/
public String getServerIp() {
@@ -373,7 +405,7 @@ public final class VersionSetting implements Cloneable {
/**
* The width of Minecraft window, defaults 800.
*
* <p>
* The field saves int value.
* String type prevents unexpected value from JsonParseException.
* We can only reset this field instead of recreating the whole setting file.
@@ -395,7 +427,7 @@ public final class VersionSetting implements Cloneable {
/**
* The height of Minecraft window, defaults 480.
*
* <p>
* The field saves int value.
* String type prevents unexpected value from JsonParseException.
* We can only reset this field instead of recreating the whole setting file.
@@ -526,6 +558,8 @@ public final class VersionSetting implements Cloneable {
gameDirProperty.addListener(listener);
launcherVisibilityProperty.addListener(listener);
defaultJavaPathProperty.addListener(listener);
nativesDirProperty.addListener(listener);
nativesDirTypeProperty.addListener(listener);
}
@Override
@@ -553,6 +587,7 @@ public final class VersionSetting implements Cloneable {
versionSetting.setGameDirType(getGameDirType());
versionSetting.setGameDir(getGameDir());
versionSetting.setLauncherVisibility(getLauncherVisibility());
versionSetting.setNativesDir(getNativesDir());
return versionSetting;
}
@@ -584,6 +619,8 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal());
obj.addProperty("gameDirType", src.getGameDirType().ordinal());
obj.addProperty("defaultJavaPath", src.getDefaultJavaPath());
obj.addProperty("nativesDir", src.getNativesDir());
obj.addProperty("nativesDirType", src.getNativesDirType().ordinal());
return obj;
}
@@ -613,6 +650,7 @@ public final class VersionSetting implements Cloneable {
vs.setJava(Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse(""));
vs.setWrapper(Optional.ofNullable(obj.get("wrapper")).map(JsonElement::getAsString).orElse(""));
vs.setGameDir(Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse(""));
vs.setNativesDir(Optional.ofNullable(obj.get("nativesDir")).map(JsonElement::getAsString).orElse(""));
vs.setFullscreen(Optional.ofNullable(obj.get("fullscreen")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNoJVMArgs(Optional.ofNullable(obj.get("noJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotCheckGame(Optional.ofNullable(obj.get("notCheckGame")).map(JsonElement::getAsBoolean).orElse(false));
@@ -621,6 +659,7 @@ public final class VersionSetting implements Cloneable {
vs.setLauncherVisibility(LauncherVisibility.values()[Optional.ofNullable(obj.get("launcherVisibility")).map(JsonElement::getAsInt).orElse(1)]);
vs.setGameDirType(GameDirectoryType.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(0)]);
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
vs.setNativesDirType(NativesDirectoryType.values()[Optional.ofNullable(obj.get("nativesDirType")).map(JsonElement::getAsInt).orElse(0)]);
return vs;
}
@@ -3,7 +3,10 @@ package org.jackhuang.hmcl.ui;
import java.awt.*;
import java.lang.reflect.Method;
public class AwtUtils {
public final class AwtUtils {
private AwtUtils() {
}
public static void setAppleIcon(Image image) {
try {
Class<?> taskbarClass = Class.forName("java.awt.TaskBar");
@@ -28,7 +28,9 @@ import javafx.stage.StageStyle;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.java.JavaRepository;
import org.jackhuang.hmcl.mod.curse.CurseModManager;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.account.AuthlibInjectorServersPage;
@@ -40,13 +42,19 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.main.RootPage;
import org.jackhuang.hmcl.ui.versions.GameListPage;
import org.jackhuang.hmcl.ui.versions.ModDownloadListPage;
import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import java.io.File;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
@@ -61,10 +69,30 @@ public final class Controllers {
private static Scene scene;
private static Stage stage;
private static VersionPage versionPage = null;
private static Lazy<VersionPage> versionPage = new Lazy<>(VersionPage::new);
private static Lazy<GameListPage> gameListPage = new Lazy<>(() -> {
GameListPage gameListPage = new GameListPage();
gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
gameListPage.profilesProperty().bindContent(Profiles.profilesProperty());
FXUtils.applyDragListener(gameListPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
File modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
});
return gameListPage;
});
private static AuthlibInjectorServersPage serversPage = null;
private static RootPage rootPage;
private static Lazy<RootPage> rootPage = new Lazy<>(RootPage::new);
private static DecoratorController decorator;
private static Lazy<ModDownloadListPage> modDownloadListPage = new Lazy<>(() -> {
return new ModDownloadListPage(CurseModManager.SECTION_MODPACK, Versions::downloadModpackImpl) {
{
state.set(State.fromTitle(i18n("modpack.download")));
}
};
});
private Controllers() {
}
public static Scene getScene() {
return scene;
@@ -76,16 +104,17 @@ public final class Controllers {
// FXThread
public static VersionPage getVersionPage() {
if (versionPage == null)
versionPage = new VersionPage();
return versionPage;
return versionPage.get();
}
// FXThread
public static GameListPage getGameListPage() {
return gameListPage.get();
}
// FXThread
public static RootPage getRootPage() {
if (rootPage == null)
rootPage = new RootPage();
return rootPage;
return rootPage.get();
}
// FXThread
@@ -95,6 +124,11 @@ public final class Controllers {
return serversPage;
}
// FXThread
public static ModDownloadListPage getModpackDownloadListPage() {
return modDownloadListPage.get();
}
// FXThread
public static DecoratorController getDecorator() {
return decorator;
@@ -164,7 +198,11 @@ public final class Controllers {
}
public static void confirm(String text, String title, Runnable onAccept, Runnable onCancel) {
dialog(new MessageDialogPane(text, title, onAccept, onCancel));
confirm(text, title, MessageType.QUESTION, onAccept, onCancel);
}
public static void confirm(String text, String title, MessageType type, Runnable onAccept, Runnable onCancel) {
dialog(new MessageDialogPane(text, title, type, onAccept, onCancel));
}
public static CompletableFuture<String> prompt(String title, FutureCallback<String> onResult) {
@@ -207,6 +245,8 @@ public final class Controllers {
rootPage = null;
versionPage = null;
serversPage = null;
gameListPage = null;
modDownloadListPage = null;
decorator = null;
stage = null;
scene = null;
@@ -31,6 +31,8 @@ import java.util.concurrent.atomic.AtomicReference;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
public final class DialogController {
private DialogController() {
}
public static AuthInfo logIn(Account account) throws CancellationException, AuthenticationException, InterruptedException {
if (account instanceof YggdrasilAccount) {
@@ -38,8 +40,8 @@ public final class DialogController {
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
runInFX(() -> {
AccountLoginPane pane = new AccountLoginPane(account, it -> {
res.set(it);
latch.countDown();
res.set(it);
latch.countDown();
}, latch::countDown);
Controllers.dialog(pane);
});
@@ -24,6 +24,8 @@ import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
@@ -112,6 +114,15 @@ public final class FXUtils {
return onWeakChange(value, consumer);
}
public static WeakInvalidationListener observeWeak(Runnable runnable, Observable... observables) {
WeakInvalidationListener listener = new WeakInvalidationListener(observable -> runnable.run());
for (Observable observable : observables) {
observable.addListener(listener);
}
runnable.run();
return listener;
}
public static void runLaterIf(BooleanSupplier condition, Runnable runnable) {
if (condition.getAsBoolean()) Platform.runLater(() -> runLaterIf(condition, runnable));
else runnable.run();
@@ -430,6 +441,7 @@ public final class FXUtils {
* You should <b>only and always</b> use {@code bindEnum} as well as {@code unbindEnum} at the same time.
* @param comboBox the combo box being bound with the property which can be inferred by {@code bindEnum}.
* @see #bindEnum(JFXComboBox, Property)
* @deprecated Use {@link ExtendedProperties#selectedItemPropertyFor(ComboBox)}
*/
@SuppressWarnings("unchecked")
@Deprecated
@@ -102,11 +102,11 @@ public class InstallerItem extends Control {
}
public static class InstallerItemGroup {
public InstallerItem game = new InstallerItem(MINECRAFT);
public InstallerItem fabric = new InstallerItem(FABRIC);
public InstallerItem forge = new InstallerItem(FORGE);
public InstallerItem liteLoader = new InstallerItem(LITELOADER);
public InstallerItem optiFine = new InstallerItem(OPTIFINE);
public final InstallerItem game = new InstallerItem(MINECRAFT);
public final InstallerItem fabric = new InstallerItem(FABRIC);
public final InstallerItem forge = new InstallerItem(FORGE);
public final InstallerItem liteLoader = new InstallerItem(LITELOADER);
public final InstallerItem optiFine = new InstallerItem(OPTIFINE);
public InstallerItemGroup() {
forge.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
@@ -133,7 +133,7 @@ public class InstallerItem extends Control {
}
public InstallerItem[] getLibraries() {
return new InstallerItem[]{game, fabric, forge, liteLoader, optiFine};
return new InstallerItem[]{game, fabric, forge, liteLoader, optiFine};
}
}
@@ -61,10 +61,9 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
VBox vBox = new VBox();
{
vBox.getStyleClass().add("card-list");
vBox.setAlignment(Pos.BOTTOM_RIGHT);
vBox.setPickOnBounds(false);
vBox.setPadding(new Insets(15));
vBox.setSpacing(15);
JFXButton btnAdd = new JFXButton();
FXUtils.setLimitWidth(btnAdd, 40);
@@ -46,7 +46,7 @@ public final class SVG {
}
Group svg = new Group(path);
double scale = Math.min(width / svg.getBoundsInParent().getWidth(), height / svg.getBoundsInParent().getHeight());
double scale = Math.min(width / 24, height / 24);
svg.setScaleX(scale);
svg.setScaleY(scale);
@@ -59,6 +59,10 @@ public final class SVG {
return createSVGPath("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z", fill, width, height);
}
public static Node gearOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10M10,22C9.75,22 9.54,21.82 9.5,21.58L9.13,18.93C8.5,18.68 7.96,18.34 7.44,17.94L4.95,18.95C4.73,19.03 4.46,18.95 4.34,18.73L2.34,15.27C2.21,15.05 2.27,14.78 2.46,14.63L4.57,12.97L4.5,12L4.57,11L2.46,9.37C2.27,9.22 2.21,8.95 2.34,8.73L4.34,5.27C4.46,5.05 4.73,4.96 4.95,5.05L7.44,6.05C7.96,5.66 8.5,5.32 9.13,5.07L9.5,2.42C9.54,2.18 9.75,2 10,2H14C14.25,2 14.46,2.18 14.5,2.42L14.87,5.07C15.5,5.32 16.04,5.66 16.56,6.05L19.05,5.05C19.27,4.96 19.54,5.05 19.66,5.27L21.66,8.73C21.79,8.95 21.73,9.22 21.54,9.37L19.43,11L19.5,12L19.43,13L21.54,14.63C21.73,14.78 21.79,15.05 21.66,15.27L19.66,18.73C19.54,18.95 19.27,19.04 19.05,18.95L16.56,17.95C16.04,18.34 15.5,18.68 14.87,18.93L14.5,21.58C14.46,21.82 14.25,22 14,22H10M11.25,4L10.88,6.61C9.68,6.86 8.62,7.5 7.85,8.39L5.44,7.35L4.69,8.65L6.8,10.2C6.4,11.37 6.4,12.64 6.8,13.8L4.68,15.36L5.43,16.66L7.86,15.62C8.63,16.5 9.68,17.14 10.87,17.38L11.24,20H12.76L13.13,17.39C14.32,17.14 15.37,16.5 16.14,15.62L18.57,16.66L19.32,15.36L17.2,13.81C17.6,12.64 17.6,11.37 17.2,10.2L19.31,8.65L18.56,7.35L16.15,8.39C15.38,7.5 14.32,6.86 13.12,6.62L12.75,4H11.25Z", fill, width, height);
}
public static Node back(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z", fill, width, height);
}
@@ -83,6 +87,10 @@ public final class SVG {
return createSVGPath("M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z", fill, width, height);
}
public static Node deleteOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19M8,9H16V19H8V9M15.5,4L14.5,3H9.5L8.5,4H5V6H19V4H15.5Z", fill, width, height);
}
public static Node accountEdit(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M21.7,13.35L20.7,14.35L18.65,12.3L19.65,11.3C19.86,11.09 20.21,11.09 20.42,11.3L21.7,12.58C21.91,12.79 21.91,13.14 21.7,13.35M12,18.94L18.06,12.88L20.11,14.93L14.06,21H12V18.94M12,14C7.58,14 4,15.79 4,18V20H10V18.11L14,14.11C13.34,14.03 12.67,14 12,14M12,4A4,4 0 0,0 8,8A4,4 0 0,0 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4Z", fill, width, height);
}
@@ -103,7 +111,7 @@ public final class SVG {
return createSVGPath("M1008 6.286q18.857 13.714 15.429 36.571l-146.286 877.714q-2.857 16.571-18.286 25.714-8 4.571-17.714 4.571-6.286 0-13.714-2.857l-258.857-105.714-138.286 168.571q-10.286 13.143-28 13.143-7.429 0-12.571-2.286-10.857-4-17.429-13.429t-6.571-20.857v-199.429l493.714-605.143-610.857 528.571-225.714-92.571q-21.143-8-22.857-31.429-1.143-22.857 18.286-33.714l950.857-548.571q8.571-5.143 18.286-5.14311.429 0 20.571 6.286z", fill, width, height);
}
public static Node launch2(ObjectBinding<? extends Paint> fill, double width, double height) {
public static Node launchOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z", fill, width, height);
}
@@ -115,6 +123,10 @@ public final class SVG {
return createSVGPath("M20.71,4.04C21.1,3.65 21.1,3 20.71,2.63L18.37,0.29C18,-0.1 17.35,-0.1 16.96,0.29L15,2.25L18.75,6M17.75,7L14,3.25L4,13.25V17H7.75L17.75,7Z", fill, width, height);
}
public static Node pencilOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14.06,9L15,9.94L5.92,19H5V18.08L14.06,9M17.66,3C17.41,3 17.15,3.1 16.96,3.29L15.13,5.12L18.88,8.87L20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18.17,3.09 17.92,3 17.66,3M14.06,6.19L3,17.25V21H6.75L17.81,9.94L14.06,6.19Z", fill, width, height);
}
public static Node refresh(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z", fill, width, height);
}
@@ -123,6 +135,10 @@ public final class SVG {
return createSVGPath("M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z", fill, width, height);
}
public static Node folderOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M20,18H4V8H20M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z", fill, width, height);
}
public static Node update(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z", fill, width, height);
}
@@ -151,6 +167,10 @@ public final class SVG {
return createSVGPath("M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z", fill, width, height);
}
public static Node plusCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M13,7H11V11H7V13H11V17H13V13H17V11H13V7Z", fill, width, height);
}
public static Node importIcon(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14,12L10,8V11H2V13H10V16M20,18V6C20,4.89 19.1,4 18,4H6A2,2 0 0,0 4,6V9H6V6H18V18H6V15H4V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18Z", fill, width, height);
}
@@ -187,6 +207,10 @@ public final class SVG {
return createSVGPath("M22.7,19L13.6,9.9C14.5,7.6 14,4.9 12.1,3C10.1,1 7.1,0.6 4.7,1.7L9,6L6,9L1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1C4.8,14 7.5,14.5 9.8,13.6L18.9,22.7C19.3,23.1 19.9,23.1 20.3,22.7L22.6,20.4C23.1,20 23.1,19.3 22.7,19Z", fill, width, height);
}
public static Node wrenchOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M22.61,19L13.53,9.91C14.46,7.57 14,4.81 12.09,2.91C9.79,0.61 6.21,0.4 3.66,2.26L7.5,6.11L6.08,7.5L2.25,3.69C0.39,6.23 0.6,9.82 2.9,12.11C4.76,13.97 7.47,14.46 9.79,13.59L18.9,22.7C19.29,23.09 19.92,23.09 20.31,22.7L22.61,20.4C23,20 23,19.39 22.61,19M19.61,20.59L10.15,11.13C9.54,11.58 8.86,11.85 8.15,11.95C6.79,12.15 5.36,11.74 4.32,10.7C3.37,9.76 2.93,8.5 3,7.26L6.09,10.35L10.33,6.11L7.24,3C8.5,2.95 9.73,3.39 10.68,4.33C11.76,5.41 12.17,6.9 11.92,8.29C11.8,9 11.5,9.66 11.04,10.25L20.5,19.7L19.61,20.59Z", fill, width, height);
}
public static Node upload(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M9,16V10H5L12,3L19,10H15V16H9M5,20V18H19V20H5Z", fill, width, height);
}
@@ -194,4 +218,44 @@ public final class SVG {
public static Node hanger(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12 4A3.5 3.5 0 0 0 8.5 7.5H10.5A1.5 1.5 0 0 1 12 6A1.5 1.5 0 0 1 13.5 7.5A1.5 1.5 0 0 1 12 9C11.45 9 11 9.45 11 10V11.75L2.4 18.2A1 1 0 0 0 3 20H21A1 1 0 0 0 21.6 18.2L13 11.75V10.85A3.5 3.5 0 0 0 15.5 7.5A3.5 3.5 0 0 0 12 4M12 13.5L18 18H6Z", fill, width, height);
}
public static Node puzzle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M22,13.5C22,15.26 20.7,16.72 19,16.96V20A2,2 0 0,1 17,22H13.2V21.7A2.7,2.7 0 0,0 10.5,19C9,19 7.8,20.21 7.8,21.7V22H4A2,2 0 0,1 2,20V16.2H2.3C3.79,16.2 5,15 5,13.5C5,12 3.79,10.8 2.3,10.8H2V7A2,2 0 0,1 4,5H7.04C7.28,3.3 8.74,2 10.5,2C12.26,2 13.72,3.3 13.96,5H17A2,2 0 0,1 19,7V10.04C20.7,10.28 22,11.74 22,13.5M17,15H18.5A1.5,1.5 0 0,0 20,13.5A1.5,1.5 0 0,0 18.5,12H17V7H12V5.5A1.5,1.5 0 0,0 10.5,4A1.5,1.5 0 0,0 9,5.5V7H4V9.12C5.76,9.8 7,11.5 7,13.5C7,15.5 5.75,17.2 4,17.88V20H6.12C6.8,18.25 8.5,17 10.5,17C12.5,17 14.2,18.25 14.88,20H17V15Z", fill, width, height);
}
public static Node cube(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L6.04,7.5L12,10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z", fill, width, height);
}
public static Node pack(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L10.11,5.22L16,8.61L17.96,7.5L12,4.15M6.04,7.5L12,10.85L13.96,9.75L8.08,6.35L6.04,7.5M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z", fill, width, height);
}
public static Node gamepad(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M6,9H8V11H10V13H8V15H6V13H4V11H6V9M18.5,9A1.5,1.5 0 0,1 20,10.5A1.5,1.5 0 0,1 18.5,12A1.5,1.5 0 0,1 17,10.5A1.5,1.5 0 0,1 18.5,9M15.5,12A1.5,1.5 0 0,1 17,13.5A1.5,1.5 0 0,1 15.5,15A1.5,1.5 0 0,1 14,13.5A1.5,1.5 0 0,1 15.5,12M17,5A7,7 0 0,1 24,12A7,7 0 0,1 17,19C15.04,19 13.27,18.2 12,16.9C10.73,18.2 8.96,19 7,19A7,7 0 0,1 0,12A7,7 0 0,1 7,5H17M7,7A5,5 0 0,0 2,12A5,5 0 0,0 7,17C8.64,17 10.09,16.21 11,15H13C13.91,16.21 15.36,17 17,17A5,5 0 0,0 22,12A5,5 0 0,0 17,7H7Z", fill, width, height);
}
public static Node fire(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M17.66 11.2C17.43 10.9 17.15 10.64 16.89 10.38C16.22 9.78 15.46 9.35 14.82 8.72C13.33 7.26 13 4.85 13.95 3C13 3.23 12.17 3.75 11.46 4.32C8.87 6.4 7.85 10.07 9.07 13.22C9.11 13.32 9.15 13.42 9.15 13.55C9.15 13.77 9 13.97 8.8 14.05C8.57 14.15 8.33 14.09 8.14 13.93C8.08 13.88 8.04 13.83 8 13.76C6.87 12.33 6.69 10.28 7.45 8.64C5.78 10 4.87 12.3 5 14.47C5.06 14.97 5.12 15.47 5.29 15.97C5.43 16.57 5.7 17.17 6 17.7C7.08 19.43 8.95 20.67 10.96 20.92C13.1 21.19 15.39 20.8 17.03 19.32C18.86 17.66 19.5 15 18.56 12.72L18.43 12.46C18.22 12 17.66 11.2 17.66 11.2M14.5 17.5C14.22 17.74 13.76 18 13.4 18.1C12.28 18.5 11.16 17.94 10.5 17.28C11.69 17 12.4 16.12 12.61 15.23C12.78 14.43 12.46 13.77 12.33 13C12.21 12.26 12.23 11.63 12.5 10.94C12.69 11.32 12.89 11.7 13.13 12C13.9 13 15.11 13.44 15.37 14.8C15.41 14.94 15.43 15.08 15.43 15.23C15.46 16.05 15.1 16.95 14.5 17.5H14.5Z", fill, width, height);
}
public static Node monitorScreenshot(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M9,6H5V10H7V8H9M19,10H17V12H15V14H19M21,16H3V4H21M21,2H3C1.89,2 1,2.89 1,4V16A2,2 0 0,0 3,18H10V20H8V22H16V20H14V18H21A2,2 0 0,0 23,16V4C23,2.89 22.1,2 21,2", fill, width, height);
}
public static Node texture(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M9.29,21H12.12L21,12.12V9.29M19,21C19.55,21 20.05,20.78 20.41,20.41C20.78,20.05 21,19.55 21,19V17L17,21M5,3A2,2 0 0,0 3,5V7L7,3M11.88,3L3,11.88V14.71L14.71,3M19.5,3.08L3.08,19.5C3.17,19.85 3.35,20.16 3.59,20.41C3.84,20.65 4.15,20.83 4.5,20.92L20.93,4.5C20.74,3.8 20.2,3.26 19.5,3.08Z", fill, width, height);
}
public static Node alphaCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,1 11,7M11,9V11H13V9H11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z", fill, width, height);
}
public static Node betaCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M15,10.5C15,11.3 14.3,12 13.5,12C14.3,12 15,12.7 15,13.5V15A2,2 0 0,1 13,17H9V7H13A2,2 0 0,1 15,9V10.5M13,15V13H11V15H13M13,11V9H11V11H13M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z", fill, width, height);
}
public static Node releaseCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M9,7H13A2,2 0 0,1 15,9V11C15,11.84 14.5,12.55 13.76,12.85L15,17H13L11.8,13H11V17H9V7M11,9V11H13V9H11M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,16.41 7.58,20 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z", fill, width, height);
}
}
@@ -70,7 +70,13 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
root.setCenter(scrollPane);
}
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
FXUtils.onChangeAndOperate(skinnable.loadingProperty(), loading -> {
if (loading) {
spinnerPane.showSpinner();
} else {
spinnerPane.hideSpinner();
}
});
spinnerPane.setContent(root);
getChildren().setAll(spinnerPane);
@@ -18,25 +18,28 @@
package org.jackhuang.hmcl.ui.account;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.util.Pair;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AccountAdvancedListItem extends AdvancedListItem {
private final Tooltip tooltip;
private final ImageView imageView;
private ObjectProperty<Account> account = new SimpleObjectProperty<Account>() {
@@ -47,23 +50,26 @@ public class AccountAdvancedListItem extends AdvancedListItem {
titleProperty().unbind();
setTitle(i18n("account.missing"));
setSubtitle(i18n("account.missing.add"));
imageProperty().unbind();
setImage(newImage("/assets/img/craft_table.png"));
imageView.imageProperty().unbind();
imageView.setImage(newImage("/assets/img/craft_table.png"));
tooltip.setText("");
} else {
titleProperty().bind(Bindings.createStringBinding(account::getCharacter, account));
setSubtitle(accountSubtitle(account));
imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32));
imageView.imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32));
tooltip.setText(account.getCharacter() + " " + accountTooltip(account));
}
}
};
public AccountAdvancedListItem() {
setRightGraphic(SVG.viewList(Theme.blackFillBinding(), -1, -1));
tooltip = new Tooltip();
FXUtils.installFastTooltip(this, tooltip);
Pair<Node, ImageView> view = createImageView(null);
setLeftGraphic(view.getKey());
imageView = view.getValue();
setOnScroll(event -> {
Account current = account.get();
if (current == null) return;
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -87,8 +87,8 @@ public class AddAccountPane extends StackPane {
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
Bindings.bindContent(cboServers.getItems(), config().getAuthlibInjectorServers());
cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer));
selectDefaultServer();
cboServers.getItems().addListener(onInvalidating(this::resetServerSelection));
resetServerSelection();
cboType.getItems().setAll(Accounts.FACTORIES);
cboType.setConverter(stringConverter(Accounts::getLocalizedLoginTypeName));
@@ -174,12 +174,13 @@ public class AddAccountPane extends StackPane {
return unmodifiableList(result);
}
/**
* Selects the first server if no server is selected.
*/
private void selectDefaultServer() {
if (!cboServers.getItems().isEmpty() && cboServers.getSelectionModel().isEmpty()) {
cboServers.getSelectionModel().select(0);
private void resetServerSelection() {
if (!cboServers.getItems().isEmpty()) {
Platform.runLater(() -> {
// the selection will not be updated as expected
// if we call it immediately
cboServers.getSelectionModel().selectFirst();
});
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -129,7 +129,7 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
@FXML
private void onAddFinish() {
if (!config().getAuthlibInjectorServers().contains(serverBeingAdded)) {
config().getAuthlibInjectorServers().add(serverBeingAdded);
config().getAuthlibInjectorServers().add(0, serverBeingAdded);
}
fireEvent(new DialogCloseEvent());
}
@@ -23,7 +23,10 @@ import javafx.util.Duration;
public interface AnimationHandler {
Duration getDuration();
Pane getCurrentRoot();
Node getPreviousNode();
Node getCurrentNode();
}
@@ -173,7 +173,49 @@ public enum ContainerAnimations {
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH))));
new KeyValue(c.getPreviousNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH)))),
SWIPE_LEFT_FADE_SHORT(c -> {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth());
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getCurrentNode().translateXProperty(), 50, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), -50, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH)))),
SWIPE_RIGHT_FADE_SHORT(c -> {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth());
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getCurrentNode().translateXProperty(), -50, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), 50, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH))));
private final AnimationProducer animationProducer;
private ContainerAnimations opposite;
@@ -37,7 +37,6 @@ public class AdvancedListBox extends ScrollPane {
setFitToWidth(true);
setHbarPolicy(ScrollBarPolicy.NEVER);
container.setSpacing(5);
container.getStyleClass().add("advanced-list-box-content");
}
@@ -24,10 +24,16 @@ import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.Pair;
import static org.jackhuang.hmcl.util.Pair.pair;
public class AdvancedListItem extends Control {
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
private final ObjectProperty<Node> leftGraphic = new SimpleObjectProperty<>(this, "leftGraphic");
private final ObjectProperty<Node> rightGraphic = new SimpleObjectProperty<>(this, "rightGraphic");
private final StringProperty title = new SimpleStringProperty(this, "title");
private final BooleanProperty active = new SimpleBooleanProperty(this, "active");
@@ -39,16 +45,16 @@ public class AdvancedListItem extends Control {
addEventHandler(MouseEvent.MOUSE_CLICKED, e -> fireEvent(new ActionEvent()));
}
public Image getImage() {
return image.get();
public Node getLeftGraphic() {
return leftGraphic.get();
}
public ObjectProperty<Image> imageProperty() {
return image;
public ObjectProperty<Node> leftGraphicProperty() {
return leftGraphic;
}
public void setImage(Image image) {
this.image.set(image);
public void setLeftGraphic(Node leftGraphic) {
this.leftGraphic.set(leftGraphic);
}
public Node getRightGraphic() {
@@ -134,4 +140,17 @@ public class AdvancedListItem extends Control {
protected Skin<?> createDefaultSkin() {
return new AdvancedListItemSkin(this);
}
public static Pair<Node, ImageView> createImageView(Image image) {
StackPane imageViewContainer = new StackPane();
FXUtils.setLimitWidth(imageViewContainer, 32);
FXUtils.setLimitHeight(imageViewContainer, 32);
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, 32, 32);
imageView.setPreserveRatio(true);
imageView.setImage(image);
imageViewContainer.getChildren().setAll(imageView);
return pair(imageViewContainer, imageView);
}
}
@@ -18,16 +18,13 @@
package org.jackhuang.hmcl.ui.construct;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextAlignment;
import org.jackhuang.hmcl.ui.FXUtils;
public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
@@ -48,35 +45,22 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
root.setPickOnBounds(false);
HBox left = new HBox();
left.setAlignment(Pos.CENTER);
left.setAlignment(Pos.CENTER_LEFT);
left.setMouseTransparent(true);
StackPane imageViewContainer = new StackPane();
FXUtils.setLimitWidth(imageViewContainer, 32);
FXUtils.setLimitHeight(imageViewContainer, 32);
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, 32, 32);
imageView.setPreserveRatio(true);
imageView.imageProperty().bind(skinnable.imageProperty());
imageViewContainer.getChildren().setAll(imageView);
VBox vbox = new VBox();
root.setCenter(vbox);
vbox.setMouseTransparent(true);
vbox.setAlignment(Pos.CENTER_LEFT);
vbox.setPadding(new Insets(0, 0, 0, 10));
Label title = new Label();
title.textProperty().bind(skinnable.titleProperty());
title.setMaxWidth(90);
title.setStyle("-fx-font-size: 15;");
title.setTextAlignment(TextAlignment.JUSTIFY);
title.getStyleClass().add("title");
vbox.getChildren().add(title);
Label subtitle = new Label();
subtitle.textProperty().bind(skinnable.subtitleProperty());
subtitle.setMaxWidth(90);
subtitle.setStyle("-fx-font-size: 10;");
subtitle.setTextAlignment(TextAlignment.JUSTIFY);
subtitle.getStyleClass().add("subtitle");
vbox.getChildren().add(subtitle);
FXUtils.onChangeAndOperate(skinnable.subtitleProperty(), subtitleString -> {
@@ -84,7 +68,14 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
else vbox.getChildren().setAll(title, subtitle);
});
left.getChildren().setAll(imageViewContainer, vbox);
FXUtils.onChangeAndOperate(skinnable.leftGraphicProperty(),
newGraphic -> {
if (newGraphic == null) {
left.getChildren().clear();
} else {
left.getChildren().setAll(newGraphic);
}
});
root.setLeft(left);
HBox right = new HBox();
@@ -100,13 +91,10 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
right.getChildren().setAll(newGraphic);
}
});
root.setRight(right);
FXUtils.onChangeAndOperate(skinnable.actionButtonVisibleProperty(),
visible -> root.setRight(visible ? right : null));
stackPane.setStyle("-fx-padding: 10 16 10 16;");
stackPane.getStyleClass().add("transparent");
stackPane.setPickOnBounds(false);
stackPane.getChildren().setAll(root);
@@ -80,7 +80,7 @@ class ComponentListCell extends StackPane {
BorderPane groupNode = new BorderPane();
Node expandIcon = SVG.expand(Theme.blackFillBinding(), 10, 10);
Node expandIcon = SVG.expand(Theme.blackFillBinding(), 20, 20);
JFXButton expandButton = new JFXButton();
expandButton.setGraphic(expandIcon);
expandButton.getStyleClass().add("options-list-item-expand-button");
@@ -99,9 +99,10 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
// we're getting dragged without getting a mouse press
dragStart = thumb.localToParent(me.getX(), me.getY());
}
double dragPos = getSkinnable().getOrientation() == Orientation.VERTICAL ? cur.getY() - dragStart.getY(): cur.getX() - dragStart.getX();
double dragPos = getSkinnable().getOrientation() == Orientation.VERTICAL ? cur.getY() - dragStart.getY() : cur.getX() - dragStart.getX();
double position = preDragThumbPos + dragPos / (trackLength() - thumbLength());
if (!getSkinnable().isFocused() && getSkinnable().isFocusTraversable()) getSkinnable().requestFocus();
if (!getSkinnable().isFocused() && getSkinnable().isFocusTraversable())
getSkinnable().requestFocus();
double newValue = (position * (getSkinnable().getMax() - getSkinnable().getMin())) + getSkinnable().getMin();
if (!Double.isNaN(newValue)) {
getSkinnable().setValue(Lang.clamp(getSkinnable().getMin(), newValue, getSkinnable().getMax()));
@@ -54,12 +54,12 @@ public final class ImagePickerItem extends BorderPane {
imageView.setPreserveRatio(true);
JFXButton selectButton = new JFXButton();
selectButton.setGraphic(SVG.pencil(Theme.blackFillBinding(), 15, 15));
selectButton.setGraphic(SVG.pencil(Theme.blackFillBinding(), 20, 20));
selectButton.onMouseClickedProperty().bind(onSelectButtonClicked);
selectButton.getStyleClass().add("toggle-icon4");
JFXButton deleteButton = new JFXButton();
deleteButton.setGraphic(SVG.close(Theme.blackFillBinding(), 15, 15));
deleteButton.setGraphic(SVG.close(Theme.blackFillBinding(), 20, 20));
deleteButton.onMouseClickedProperty().bind(onDeleteButtonClicked);
deleteButton.getStyleClass().add("toggle-icon4");
@@ -88,8 +88,8 @@ public final class MessageDialogPane extends StackPane {
}
}
public MessageDialogPane(String text, String title, Runnable onAccept, Runnable onCancel) {
this(text, title, MessageType.QUESTION, onAccept);
public MessageDialogPane(String text, String title, MessageType type, Runnable onAccept, Runnable onCancel) {
this(text, title, type, onAccept);
cancelButton.setVisible(true);
cancelButton.setOnMouseClicked(e -> {
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.util.Logging;
import java.util.Optional;
@@ -48,7 +49,7 @@ public class Navigator extends TransitionPane {
backable.set(canGoBack());
getChildren().setAll(init);
fireEvent(new NavigationEvent(this, init, NavigationEvent.NAVIGATED));
fireEvent(new NavigationEvent(this, init, Navigation.NavigationDirection.START, NavigationEvent.NAVIGATED));
initialized = true;
}
@@ -68,14 +69,14 @@ public class Navigator extends TransitionPane {
stack.push(node);
backable.set(canGoBack());
NavigationEvent navigating = new NavigationEvent(this, from, NavigationEvent.NAVIGATING);
NavigationEvent navigating = new NavigationEvent(this, from, Navigation.NavigationDirection.NEXT, NavigationEvent.NAVIGATING);
fireEvent(navigating);
node.fireEvent(navigating);
node.getProperties().put("hmcl.navigator.animation", animationProducer);
setContent(node, animationProducer);
NavigationEvent navigated = new NavigationEvent(this, node, NavigationEvent.NAVIGATED);
NavigationEvent navigated = new NavigationEvent(this, node, Navigation.NavigationDirection.NEXT, NavigationEvent.NAVIGATED);
node.fireEvent(navigated);
EventHandler<PageCloseEvent> handler = event -> close(node);
@@ -111,7 +112,7 @@ public class Navigator extends TransitionPane {
backable.set(canGoBack());
Node node = stack.peek();
NavigationEvent navigating = new NavigationEvent(this, from, NavigationEvent.NAVIGATING);
NavigationEvent navigating = new NavigationEvent(this, from, Navigation.NavigationDirection.PREVIOUS, NavigationEvent.NAVIGATING);
fireEvent(navigating);
node.fireEvent(navigating);
@@ -122,7 +123,7 @@ public class Navigator extends TransitionPane {
setContent(node, ContainerAnimations.NONE.getAnimationProducer());
}
NavigationEvent navigated = new NavigationEvent(this, node, NavigationEvent.NAVIGATED);
NavigationEvent navigated = new NavigationEvent(this, node, Navigation.NavigationDirection.PREVIOUS, NavigationEvent.NAVIGATED);
node.fireEvent(navigated);
Optional.ofNullable(from.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER))
@@ -206,12 +207,14 @@ public class Navigator extends TransitionPane {
private final Navigator source;
private final Node node;
private final Navigation.NavigationDirection direction;
public NavigationEvent(Navigator source, Node target, EventType<? extends Event> eventType) {
public NavigationEvent(Navigator source, Node target, Navigation.NavigationDirection direction, EventType<? extends Event> eventType) {
super(source, target, eventType);
this.source = source;
this.node = target;
this.direction = direction;
}
@Override
@@ -222,5 +225,9 @@ public class Navigator extends TransitionPane {
public Node getNode() {
return node;
}
public Navigation.NavigationDirection getDirection() {
return direction;
}
}
}
@@ -18,35 +18,31 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXSpinner;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.DefaultProperty;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.*;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.AnimationHandler;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
@DefaultProperty("content")
public class SpinnerPane extends Control {
private final ObjectProperty<Node> content = new SimpleObjectProperty<>(this, "content");
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading");
private final StringProperty failedReason = new SimpleStringProperty(this, "failedReason");
public void showSpinner() {
setLoading(true);
setLoading(true);
}
public void hideSpinner() {
setLoading(false);
setFailedReason(null);
setLoading(false);
}
public Node getContent() {
@@ -62,15 +58,27 @@ public class SpinnerPane extends Control {
}
public boolean isLoading() {
return loading.get();
return loading.get();
}
public BooleanProperty loadingProperty() {
return loading;
return loading;
}
public void setLoading(boolean loading) {
this.loading.set(loading);
this.loading.set(loading);
}
public String getFailedReason() {
return failedReason.get();
}
public StringProperty failedReasonProperty() {
return failedReason;
}
public void setFailedReason(String failedReason) {
this.failedReason.set(failedReason);
}
@Override
@@ -82,62 +90,57 @@ public class SpinnerPane extends Control {
private final JFXSpinner spinner = new JFXSpinner();
private final StackPane contentPane = new StackPane();
private final StackPane topPane = new StackPane();
private final StackPane root = new StackPane();
private Timeline animation;
private final TransitionPane root = new TransitionPane();
private final StackPane failedPane = new StackPane();
private final Label failedReasonLabel = new Label();
@SuppressWarnings("FieldCanBeLocal") // prevent from gc.
private final WeakInvalidationListener observer;
protected Skin(SpinnerPane control) {
super(control);
root.getStyleClass().add("spinner-pane");
topPane.getChildren().setAll(spinner);
root.getChildren().setAll(contentPane, topPane);
FXUtils.onChangeAndOperate(getSkinnable().content, newValue -> contentPane.getChildren().setAll(newValue));
topPane.getStyleClass().add("notice-pane");
failedPane.getChildren().setAll(failedReasonLabel);
FXUtils.onChangeAndOperate(getSkinnable().content, newValue -> {
if (newValue == null) {
contentPane.getChildren().clear();
} else {
contentPane.getChildren().setAll(newValue);
}
});
getChildren().setAll(root);
FXUtils.onChangeAndOperate(getSkinnable().loadingProperty(), newValue -> {
Timeline prev = animation;
if (prev != null) prev.stop();
observer = FXUtils.observeWeak(() -> {
if (getSkinnable().getFailedReason() != null) {
root.setContent(failedPane, ContainerAnimations.FADE.getAnimationProducer());
failedReasonLabel.setText(getSkinnable().getFailedReason());
} else if (getSkinnable().isLoading()) {
root.setContent(topPane, ContainerAnimations.FADE.getAnimationProducer());
} else {
root.setContent(contentPane, ContainerAnimations.FADE.getAnimationProducer());
}
}, getSkinnable().loadingProperty(), getSkinnable().failedReasonProperty());
}
}
AnimationProducer transition;
topPane.setMouseTransparent(true);
topPane.setVisible(true);
topPane.getStyleClass().add("gray-background");
if (newValue)
transition = ContainerAnimations.FADE_IN.getAnimationProducer();
else
transition = ContainerAnimations.FADE_OUT.getAnimationProducer();
public interface State {}
AnimationHandler handler = new AnimationHandler() {
@Override
public Duration getDuration() {
return Duration.millis(160);
}
public static class LoadedState implements State {}
@Override
public Pane getCurrentRoot() {
return root;
}
public static class LoadingState implements State {}
@Override
public Node getPreviousNode() {
return null;
}
public static class FailedState implements State {
private final String reason;
@Override
public Node getCurrentNode() {
return topPane;
}
};
public FailedState(String reason) {
this.reason = reason;
}
Timeline now = new Timeline();
now.getKeyFrames().addAll(transition.animate(handler));
now.getKeyFrames().add(new KeyFrame(handler.getDuration(), e -> {
topPane.setMouseTransparent(!newValue);
topPane.setVisible(newValue);
}));
now.play();
animation = now;
});
public String getReason() {
return reason;
}
}
}
@@ -174,7 +174,7 @@ public interface TabControl {
}
}
public static class Tab {
class Tab {
private final StringProperty id = new SimpleStringProperty(this, "id");
private final StringProperty text = new SimpleStringProperty(this, "text");
private final ReadOnlyBooleanWrapper selected = new ReadOnlyBooleanWrapper(this, "selected");
@@ -49,6 +49,7 @@ public class TabHeader extends Control implements TabControl {
}
private ObservableList<Tab> tabs = FXCollections.observableArrayList();
private ObjectProperty<Side> side = new SimpleObjectProperty<>(Side.TOP);
@Override
public ObservableList<Tab> getTabs() {
@@ -69,6 +70,27 @@ public class TabHeader extends Control implements TabControl {
this.selectionModel.set(selectionModel);
}
/**
* The position to place the tabs.
*/
public Side getSide() {
return side.get();
}
/**
* The position of the tabs.
*/
public ObjectProperty<Side> sideProperty() {
return side;
}
/**
* The position the place the tabs in this TabHeader.
*/
public void setSide(Side side) {
this.side.set(side);
}
@Override
protected Skin<?> createDefaultSkin() {
return new TabHeaderSkin(this);
@@ -115,7 +137,7 @@ public class TabHeader extends Control implements TabControl {
protected class HeaderContainer extends StackPane {
private Timeline timeline;
private StackPane selectedTabLine;
private StackPane headersRegion;
private HeadersRegion headersRegion;
private Scale scale = new Scale(1, 1, 0, 0);
private Rotate rotate = new Rotate(0, 0, 1);
private double selectedTabLineOffset;
@@ -125,9 +147,115 @@ public class TabHeader extends Control implements TabControl {
getStyleClass().add("tab-header-area");
setPickOnBounds(false);
headersRegion = new StackPane() {
headersRegion = new HeadersRegion();
headersRegion.sideProperty().bind(getSkinnable().sideProperty());
selectedTabLine = new StackPane();
selectedTabLine.setManaged(false);
selectedTabLine.getTransforms().addAll(scale, rotate);
selectedTabLine.setCache(true);
selectedTabLine.getStyleClass().addAll("tab-selected-line");
selectedTabLine.setPrefHeight(2);
selectedTabLine.setPrefWidth(2);
selectedTabLine.setBackground(new Background(new BackgroundFill(ripplerColor, CornerRadii.EMPTY, Insets.EMPTY)));
getChildren().setAll(headersRegion, selectedTabLine);
headersRegion.setPickOnBounds(false);
headersRegion.prefHeightProperty().bind(heightProperty());
prefWidthProperty().bind(headersRegion.widthProperty());
rotate.pivotXProperty().bind(Bindings.createDoubleBinding(() -> getSkinnable().getSide().isHorizontal() ? 0.0 : 1, getSkinnable().sideProperty()));
rotate.pivotYProperty().bind(Bindings.createDoubleBinding(() -> getSkinnable().getSide().isHorizontal() ? 1.0 : 0, getSkinnable().sideProperty()));
Bindings.bindContent(headersRegion.getChildren(), binding = MappedObservableList.create(getSkinnable().getTabs(), tab -> {
TabHeaderContainer container = new TabHeaderContainer(tab);
container.setVisible(true);
return container;
}));
}
public void setNeedsLayout2(boolean value) {
setNeedsLayout(value);
}
private boolean isAnimating() {
return this.timeline != null && this.timeline.getStatus() == Animation.Status.RUNNING;
}
@Override
protected void layoutChildren() {
super.layoutChildren();
if (isSelectingTab) {
headersRegion.animateSelectionLine();
isSelectingTab = false;
}
}
private class HeadersRegion extends StackPane {
private SideAction action;
private final ObjectProperty<Side> side = new SimpleObjectProperty<Side>() {
@Override
protected double computePrefWidth(double height) {
protected void invalidated() {
super.invalidated();
switch (get()) {
case TOP: action = new Top(); break;
case BOTTOM: action = new Bottom(); break;
case LEFT: action = new Left(); break;
case RIGHT: action = new Right(); break;
default: throw new InternalError();
}
}
};
public Side getSide() {
return side.get();
}
public ObjectProperty<Side> sideProperty() {
return side;
}
public void setSide(Side side) {
this.side.set(side);
}
@Override
protected double computePrefWidth(double height) {
return action.computePrefWidth(height);
}
@Override
protected double computePrefHeight(double width) {
return action.computePrefHeight(width);
}
@Override
protected void layoutChildren() {
action.layoutChildren();
}
protected void animateSelectionLine() {
action.animateSelectionLine();
}
private abstract class SideAction {
abstract double computePrefWidth(double height);
abstract double computePrefHeight(double width);
void layoutChildren() {
if (isSelectingTab) {
animateSelectionLine();
isSelectingTab = false;
}
}
abstract void animateSelectionLine();
}
private abstract class Horizontal extends SideAction {
@Override
public double computePrefWidth(double height) {
double width = 0;
for (Node child : getChildren()) {
if (!(child instanceof TabHeaderContainer) || !child.isVisible()) continue;
@@ -137,7 +265,7 @@ public class TabHeader extends Control implements TabControl {
}
@Override
protected double computePrefHeight(double width) {
public double computePrefHeight(double width) {
double height = 0;
for (Node child : getChildren()) {
if (!(child instanceof TabHeaderContainer) || !child.isVisible()) continue;
@@ -146,13 +274,81 @@ public class TabHeader extends Control implements TabControl {
return snapSize(height) + snappedTopInset() + snappedBottomInset();
}
@Override
protected void layoutChildren() {
if (isSelectingTab) {
animateSelectionLine();
isSelectingTab = false;
private void runTimeline(double newTransX, double newWidth) {
double lineWidth = selectedTabLine.prefWidth(-1.0D);
if (isAnimating()) {
timeline.stop();
double tempScaleX = scale.getX();
if (rotate.getAngle() != 0.0D) {
rotate.setAngle(0.0D);
double tempWidth = tempScaleX * lineWidth;
selectedTabLine.setTranslateX(selectedTabLine.getTranslateX() - tempWidth);
}
}
double oldScaleX = scale.getX();
double oldWidth = lineWidth * oldScaleX;
double oldTransX = selectedTabLine.getTranslateX();
double newScaleX = newWidth * oldScaleX / oldWidth;
selectedTabLineOffset = newTransX;
// newTransX += offsetStart * (double)this.direction;
double transDiff = newTransX - oldTransX;
if (transDiff < 0.0D) {
selectedTabLine.setTranslateX(selectedTabLine.getTranslateX() + oldWidth);
newTransX += newWidth;
rotate.setAngle(180.0D);
}
timeline = new Timeline(
new KeyFrame(
Duration.ZERO,
new KeyValue(selectedTabLine.translateXProperty(), selectedTabLine.getTranslateX(), Interpolator.EASE_BOTH)
),
new KeyFrame(
Duration.seconds(0.24D),
new KeyValue(scale.xProperty(), newScaleX, Interpolator.EASE_BOTH),
new KeyValue(selectedTabLine.translateXProperty(), newTransX, Interpolator.EASE_BOTH)
)
);
timeline.setOnFinished((finish) -> {
if (rotate.getAngle() != 0.0D) {
rotate.setAngle(0.0D);
selectedTabLine.setTranslateX(selectedTabLine.getTranslateX() - newWidth);
}
});
timeline.play();
}
@Override
public void animateSelectionLine() {
double offset = 0.0D;
double selectedTabOffset = 0.0D;
double selectedTabWidth = 0.0D;
for (Node node : headersRegion.getChildren()) {
if (node instanceof TabHeaderContainer) {
TabHeaderContainer tabHeader = (TabHeaderContainer) node;
double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1.0D));
if (selectedTab != null && selectedTab.equals(tabHeader.tab)) {
selectedTabOffset = offset;
selectedTabWidth = tabHeaderPrefWidth;
break;
}
offset += tabHeaderPrefWidth;
}
}
this.runTimeline(selectedTabOffset, selectedTabWidth);
}
}
private class Top extends Horizontal {
@Override
public void layoutChildren() {
super.layoutChildren();
double headerHeight = snapSize(prefHeight(-1));
double tabStartX = 0;
for (Node node : getChildren()) {
@@ -171,106 +367,171 @@ public class TabHeader extends Control implements TabControl {
snapSize(selectedTabLine.prefWidth(-1)),
snapSize(selectedTabLine.prefHeight(-1)));
}
};
selectedTabLine = new StackPane();
selectedTabLine.setManaged(false);
selectedTabLine.getTransforms().addAll(scale, rotate);
selectedTabLine.setCache(true);
selectedTabLine.getStyleClass().addAll("tab-selected-line");
selectedTabLine.setPrefHeight(2);
selectedTabLine.setPrefWidth(1);
selectedTabLine.setBackground(new Background(new BackgroundFill(ripplerColor, CornerRadii.EMPTY, Insets.EMPTY)));
getChildren().setAll(headersRegion, selectedTabLine);
headersRegion.setPickOnBounds(false);
headersRegion.prefHeightProperty().bind(heightProperty());
prefWidthProperty().bind(headersRegion.widthProperty());
Bindings.bindContent(headersRegion.getChildren(), binding = MappedObservableList.create(getSkinnable().getTabs(), tab -> {
TabHeaderContainer container = new TabHeaderContainer(tab);
container.setVisible(true);
return container;
}));
}
public void setNeedsLayout2(boolean value) {
setNeedsLayout(value);
}
private void runTimeline(double newTransX, double newWidth) {
double tempScaleX = 0.0D;
double tempWidth = 0.0D;
double lineWidth = this.selectedTabLine.prefWidth(-1.0D);
if (this.isAnimating()) {
this.timeline.stop();
tempScaleX = this.scale.getX();
if (this.rotate.getAngle() != 0.0D) {
this.rotate.setAngle(0.0D);
tempWidth = tempScaleX * lineWidth;
this.selectedTabLine.setTranslateX(this.selectedTabLine.getTranslateX() - tempWidth);
}
}
double oldScaleX = this.scale.getX();
double oldWidth = lineWidth * oldScaleX;
double oldTransX = this.selectedTabLine.getTranslateX();
double newScaleX = newWidth * oldScaleX / oldWidth;
this.selectedTabLineOffset = newTransX;
// newTransX += offsetStart * (double)this.direction;
double transDiff = newTransX - oldTransX;
double midScaleX = tempScaleX != 0.0D ? tempScaleX : (Math.abs(transDiff) / 1.3D + oldWidth) * oldScaleX / oldWidth;
if (transDiff < 0.0D) {
this.selectedTabLine.setTranslateX(this.selectedTabLine.getTranslateX() + oldWidth);
newTransX += newWidth;
this.rotate.setAngle(180.0D);
}
private class Bottom extends Horizontal {
@Override
public void layoutChildren() {
super.layoutChildren();
this.timeline = new Timeline(new KeyFrame(Duration.ZERO, new KeyValue(this.selectedTabLine.translateXProperty(), this.selectedTabLine.getTranslateX(), Interpolator.EASE_BOTH)), new KeyFrame(Duration.seconds(0.12D), new KeyValue(this.scale.xProperty(), midScaleX, Interpolator.EASE_BOTH), new KeyValue(this.selectedTabLine.translateXProperty(), this.selectedTabLine.getTranslateX(), Interpolator.EASE_BOTH)), new KeyFrame(Duration.seconds(0.24D), new KeyValue(this.scale.xProperty(), newScaleX, Interpolator.EASE_BOTH), new KeyValue(this.selectedTabLine.translateXProperty(), newTransX, Interpolator.EASE_BOTH)));
this.timeline.setOnFinished((finish) -> {
if (this.rotate.getAngle() != 0.0D) {
this.rotate.setAngle(0.0D);
this.selectedTabLine.setTranslateX(this.selectedTabLine.getTranslateX() - newWidth);
}
double headerHeight = snapSize(prefHeight(-1));
double tabStartX = 0;
for (Node node : getChildren()) {
if (!(node instanceof TabHeaderContainer)) continue;
TabHeaderContainer child = (TabHeaderContainer) node;
double w = snapSize(child.prefWidth(-1));
double h = snapSize(child.prefHeight(-1));
child.resize(w, h);
});
this.timeline.play();
}
private boolean isAnimating() {
return this.timeline != null && this.timeline.getStatus() == Animation.Status.RUNNING;
}
@Override
protected void layoutChildren() {
super.layoutChildren();
if (isSelectingTab) {
animateSelectionLine();
isSelectingTab = false;
}
}
private void animateSelectionLine() {
double offset = 0.0D;
double selectedTabOffset = 0.0D;
double selectedTabWidth = 0.0D;
Side side = Side.TOP;
for (Node node : headersRegion.getChildren()) {
if (node instanceof TabHeaderContainer) {
TabHeaderContainer tabHeader = (TabHeaderContainer)node;
double tabHeaderPrefWidth = this.snapSize(tabHeader.prefWidth(-1.0D));
if (selectedTab != null && selectedTab.equals(tabHeader.tab)) {
selectedTabOffset = side != Side.LEFT && side != Side.BOTTOM ? offset : -offset - tabHeaderPrefWidth;
selectedTabWidth = tabHeaderPrefWidth;
break;
child.relocate(tabStartX, snappedTopInset());
tabStartX += w;
}
offset += tabHeaderPrefWidth;
selectedTabLine.resizeRelocate(0, 0,
snapSize(selectedTabLine.prefWidth(-1)),
snapSize(selectedTabLine.prefHeight(-1)));
}
}
private abstract class Vertical extends SideAction {
@Override
public double computePrefWidth(double height) {
double width = 0;
for (Node child : getChildren()) {
if (!(child instanceof TabHeaderContainer) || !child.isVisible()) continue;
width = Math.max(width, child.prefWidth(height));
}
return snapSize(width) + snappedLeftInset() + snappedRightInset();
}
@Override
public double computePrefHeight(double width) {
double height = 0;
for (Node child : getChildren()) {
if (!(child instanceof TabHeaderContainer) || !child.isVisible()) continue;
height += child.prefHeight(width);
}
return snapSize(height) + snappedTopInset() + snappedBottomInset();
}
private void runTimeline(double newTransY, double newHeight) {
double lineHeight = selectedTabLine.prefHeight(-1.0D);
if (isAnimating()) {
timeline.stop();
double tempScaleY = scale.getY();
if (rotate.getAngle() != 0.0D) {
rotate.setAngle(0.0D);
double tempHeight = tempScaleY * lineHeight;
selectedTabLine.setTranslateY(selectedTabLine.getTranslateY() - tempHeight);
}
}
double oldScaleY = scale.getY();
double oldHeight = lineHeight * oldScaleY;
double oldTransY = selectedTabLine.getTranslateY();
double newScaleY = newHeight * oldScaleY / oldHeight;
selectedTabLineOffset = newTransY;
// newTransY += offsetStart * (double)this.direction;
double transDiff = newTransY - oldTransY;
if (transDiff < 0.0D) {
selectedTabLine.setTranslateY(selectedTabLine.getTranslateY() + oldHeight);
newTransY += newHeight;
rotate.setAngle(180.0D);
}
timeline = new Timeline(
new KeyFrame(
Duration.ZERO,
new KeyValue(selectedTabLine.translateYProperty(), selectedTabLine.getTranslateY(), Interpolator.EASE_BOTH)
),
new KeyFrame(
Duration.seconds(1.24D),
new KeyValue(scale.yProperty(), newScaleY, Interpolator.EASE_BOTH),
new KeyValue(selectedTabLine.translateYProperty(), newTransY, Interpolator.EASE_BOTH)
)
);
timeline.setOnFinished((finish) -> {
if (rotate.getAngle() != 0.0D) {
rotate.setAngle(0.0D);
selectedTabLine.setTranslateY(selectedTabLine.getTranslateY() - newHeight);
}
});
timeline.play();
}
@Override
public void animateSelectionLine() {
double offset = 0.0D;
double selectedTabOffset = 0.0D;
double selectedTabHeight = 0.0D;
for (Node node : headersRegion.getChildren()) {
if (node instanceof TabHeaderContainer) {
TabHeaderContainer tabHeader = (TabHeaderContainer) node;
double tabHeaderPrefHeight = snapSize(tabHeader.prefHeight(-1.0D));
if (selectedTab != null && selectedTab.equals(tabHeader.tab)) {
selectedTabOffset = offset;
selectedTabHeight = tabHeaderPrefHeight;
break;
}
offset += tabHeaderPrefHeight;
}
}
this.runTimeline(selectedTabOffset, selectedTabHeight);
}
}
private class Left extends Vertical {
@Override
public void layoutChildren() {
super.layoutChildren();
double headerWidth = snapSize(prefWidth(-1));
double tabStartY = 0;
for (Node node : getChildren()) {
if (!(node instanceof TabHeaderContainer)) continue;
TabHeaderContainer child = (TabHeaderContainer) node;
double w = snapSize(child.prefWidth(-1));
double h = snapSize(child.prefHeight(-1));
child.resize(w, h);
child.relocate(headerWidth - w - snappedRightInset(), tabStartY);
tabStartY += h;
}
selectedTabLine.resizeRelocate(headerWidth - selectedTabLine.prefWidth(-1), 0,
snapSize(selectedTabLine.prefWidth(-1)),
snapSize(selectedTabLine.prefHeight(-1)));
}
}
private class Right extends Vertical {
@Override
public void layoutChildren() {
super.layoutChildren();
double headerWidth = snapSize(prefWidth(-1));
double tabStartY = 0;
for (Node node : getChildren()) {
if (!(node instanceof TabHeaderContainer)) continue;
TabHeaderContainer child = (TabHeaderContainer) node;
double w = snapSize(child.prefWidth(-1));
double h = snapSize(child.prefHeight(-1));
child.resize(w, h);
child.relocate(snappedLeftInset(), tabStartY);
tabStartY += h;
}
selectedTabLine.resizeRelocate(0, 0,
snapSize(selectedTabLine.prefWidth(-1)),
snapSize(selectedTabLine.prefHeight(-1)));
}
}
this.runTimeline(selectedTabOffset, selectedTabWidth);
}
}
@@ -305,4 +566,5 @@ public class TabHeader extends Control implements TabControl {
}
}
}
}
@@ -20,18 +20,22 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
public class TwoLineListItem extends VBox {
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
private final StringProperty title = new SimpleStringProperty(this, "title");
private final StringProperty tag = new SimpleStringProperty(this, "tag");
private final ObservableList<String> tags = FXCollections.observableArrayList();
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
private final ObservableList<Label> tagLabels;
public TwoLineListItem(String titleString, String subtitleString) {
this();
@@ -48,15 +52,17 @@ public class TwoLineListItem extends VBox {
lblTitle.getStyleClass().add("title");
lblTitle.textProperty().bind(title);
Label lblTag = new Label();
lblTag.getStyleClass().add("tag");
lblTag.textProperty().bind(tag);
HBox tagContainer = new HBox();
lblTag.visibleProperty().bind(Bindings.createBooleanBinding(
() -> StringUtils.isNotBlank(tag.getValue()),
tag));
tagLabels = MappedObservableList.create(tags, tag -> {
Label tagLabel = new Label();
tagLabel.getStyleClass().add("tag");
tagLabel.setText(tag);
return tagLabel;
});
Bindings.bindContent(tagContainer.getChildren(), tagLabels);
firstLine.getChildren().addAll(lblTitle, lblTag);
firstLine.getChildren().addAll(lblTitle, tagContainer);
Label lblSubtitle = new Label();
lblSubtitle.getStyleClass().add("subtitle");
@@ -90,16 +96,8 @@ public class TwoLineListItem extends VBox {
this.subtitle.set(subtitle);
}
public String getTag() {
return tag.get();
}
public StringProperty tagProperty() {
return tag;
}
public void setTag(String tag) {
this.tag.set(tag);
public ObservableList<String> getTags() {
return tags;
}
@Override
@@ -34,6 +34,7 @@ import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.jackhuang.hmcl.ui.wizard.Navigation;
public class Decorator extends Control {
private final ListProperty<Node> drawer = new SimpleListProperty<>(FXCollections.observableArrayList());
@@ -51,6 +52,7 @@ public class Decorator extends Control {
private final BooleanProperty canClose = new SimpleBooleanProperty(false);
private final BooleanProperty showCloseAsHome = new SimpleBooleanProperty(false);
private final Stage primaryStage;
private Navigation.NavigationDirection navigationDirection = Navigation.NavigationDirection.START;
private StackPane drawerWrapper;
private final ReadOnlyBooleanWrapper allowMove = new ReadOnlyBooleanWrapper();
@@ -238,4 +240,13 @@ public class Decorator extends Control {
e.consume();
});
}
// TODO: Dirty implementation.
public Navigation.NavigationDirection getNavigationDirection() {
return navigationDirection;
}
public void setNavigationDirection(Navigation.NavigationDirection navigationDirection) {
this.navigationDirection = navigationDirection;
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -18,26 +18,18 @@
package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXDialog;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.DragEvent;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
import org.jackhuang.hmcl.setting.Config;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -55,22 +47,22 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Random;
import java.util.logging.Level;
import java.util.stream.Stream;
import static java.util.logging.Level.WARNING;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.io.FileUtils.getExtension;
public class DecoratorController {
private static final String PROPERTY_DIALOG_CLOSE_HANDLER = DecoratorController.class.getName() + ".dialog.closeListener";
private final Decorator decorator;
private final ImageView welcomeView;
private final Navigator navigator;
private JFXDialog dialog;
@@ -89,27 +81,6 @@ public class DecoratorController {
decorator.onBackNavButtonActionProperty().set(e -> back());
decorator.onRefreshNavButtonActionProperty().set(e -> refresh());
welcomeView = new ImageView();
welcomeView.setImage(newImage("/assets/img/welcome.png"));
welcomeView.setCursor(Cursor.HAND);
FXUtils.limitSize(welcomeView, 796, 517);
welcomeView.setOnMouseClicked(e -> {
Timeline nowAnimation = new Timeline();
nowAnimation.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO, new KeyValue(welcomeView.opacityProperty(), 1.0D, Interpolator.EASE_BOTH)),
new KeyFrame(new Duration(300), new KeyValue(welcomeView.opacityProperty(), 0.0D, Interpolator.EASE_BOTH)),
new KeyFrame(new Duration(300), e2 -> decorator.getContainer().remove(welcomeView))
);
nowAnimation.play();
});
if (switchedToNewUI()) {
if (config().getLocalization().getLocale() == Locale.CHINA) {
// currently, user guide is only available in Chinese
decorator.getContainer().setAll(welcomeView);
}
}
setupBackground();
setupAuthlibInjectorDnD();
@@ -119,17 +90,6 @@ public class DecoratorController {
return decorator;
}
/**
* @return true if the user is seeing the current version of UI for the first time.
*/
private boolean switchedToNewUI() {
if (config().getUiVersion() < Config.CURRENT_UI_VERSION) {
config().setUiVersion(Config.CURRENT_UI_VERSION);
return true;
}
return false;
}
// ==== Background ====
private void setupBackground() {
@@ -174,10 +134,10 @@ public class DecoratorController {
List<Path> candidates;
try (Stream<Path> stream = Files.list(imageDir)) {
candidates = stream
.filter(Files::isRegularFile)
.filter(Files::isReadable)
.filter(it -> {
String filename = it.getFileName().toString();
return filename.endsWith(".png") || filename.endsWith(".jpg");
String ext = getExtension(it).toLowerCase();
return ext.equals("png") || ext.equals("jpg");
})
.collect(toList());
} catch (IOException e) {
@@ -199,13 +159,23 @@ public class DecoratorController {
}
private Optional<Image> tryLoadImage(Path path) {
if (Files.isRegularFile(path)) {
try {
return Optional.of(new Image(path.toAbsolutePath().toUri().toString()));
} catch (IllegalArgumentException ignored) {
}
if (!Files.isReadable(path))
return Optional.empty();
Image img;
try {
img = new Image(path.toAbsolutePath().toUri().toString());
} catch (IllegalArgumentException e) {
LOG.log(WARNING, "Couldn't load background image", e);
return Optional.empty();
}
return Optional.empty();
if (img.getException() != null) {
LOG.log(WARNING, "Couldn't load background image", img.getException());
return Optional.empty();
}
return Optional.of(img);
}
// ==== Navigation ====
@@ -273,6 +243,8 @@ public class DecoratorController {
decorator.showCloseAsHomeProperty().set(true);
}
decorator.setNavigationDirection(event.getDirection());
// state property should be updated at last.
if (to instanceof DecoratorPage) {
decorator.stateProperty().bind(((DecoratorPage) to).stateProperty());
@@ -41,8 +41,10 @@ import javafx.util.Duration;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.util.Lang;
public class DecoratorSkin extends SkinBase<Decorator> {
@@ -165,7 +167,16 @@ public class DecoratorSkin extends SkinBase<Decorator> {
Node node = createNavBar(skinnable, s.isTitleBarTransparent(), s.getLeftPaneWidth(), s.isBackable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.isRefreshable(), s.getTitle(), s.getTitleNode());
double targetOpacity = s.isTitleBarTransparent() ? 0 : 1;
if (s.isAnimate()) {
navBarPane.setContent(node, ContainerAnimations.FADE.getAnimationProducer());
AnimationProducer animation;
if (skinnable.getNavigationDirection() == Navigation.NavigationDirection.NEXT) {
animation = ContainerAnimations.SWIPE_LEFT_FADE_SHORT.getAnimationProducer();
} else if (skinnable.getNavigationDirection() == Navigation.NavigationDirection.PREVIOUS) {
animation = ContainerAnimations.SWIPE_RIGHT_FADE_SHORT.getAnimationProducer();
} else {
animation = ContainerAnimations.FADE.getAnimationProducer();
}
skinnable.setNavigationDirection(Navigation.NavigationDirection.START);
navBarPane.setContent(node, animation);
} else {
navBarPane.getChildren().setAll(node);
}
@@ -26,6 +26,7 @@ import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.construct.TabControl;
import org.jackhuang.hmcl.ui.construct.TabHeader;
import org.jackhuang.hmcl.ui.wizard.Navigation;
public abstract class DecoratorTabPage extends DecoratorTransitionPage implements TabControl {
@@ -36,10 +37,10 @@ public abstract class DecoratorTabPage extends DecoratorTransitionPage implement
}
if (newValue.getNode() != null) {
onNavigating(getCurrentPage());
if (getCurrentPage() != null) getCurrentPage().fireEvent(new Navigator.NavigationEvent(null, getCurrentPage(), Navigator.NavigationEvent.NAVIGATING));
if (getCurrentPage() != null) getCurrentPage().fireEvent(new Navigator.NavigationEvent(null, getCurrentPage(), Navigation.NavigationDirection.NEXT, Navigator.NavigationEvent.NAVIGATING));
navigate(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer());
onNavigated(getCurrentPage());
if (getCurrentPage() != null) getCurrentPage().fireEvent(new Navigator.NavigationEvent(null, getCurrentPage(), Navigator.NavigationEvent.NAVIGATED));
if (getCurrentPage() != null) getCurrentPage().fireEvent(new Navigator.NavigationEvent(null, getCurrentPage(), Navigation.NavigationDirection.NEXT, Navigator.NavigationEvent.NAVIGATED));
}
});
}
@@ -25,7 +25,6 @@ import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
import org.jackhuang.hmcl.ui.wizard.*;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements WizardDisplayer {
@@ -87,21 +87,24 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
if (file.isDirectory()) {
File[] files = file.listFiles();
if (files != null)
if (files != null) {
for (File it : files) {
CheckBoxTreeItem<String> subNode = getTreeItem(it, basePath + "/" + it.getName());
if (subNode != null) {
node.setSelected(subNode.isSelected() || node.isSelected());
if (!subNode.isSelected())
node.setIndeterminate(true);
node.getChildren().add(subNode);
if (subNode != null) {
node.setSelected(subNode.isSelected() || node.isSelected());
if (!subNode.isSelected()) {
node.setIndeterminate(true);
}
node.getChildren().add(subNode);
}
}
}
if (!node.isSelected()) node.setIndeterminate(false);
// Empty folder need not to be displayed.
if (node.getChildren().isEmpty())
if (node.getChildren().isEmpty()) {
return null;
}
}
HBox graphic = new HBox();
@@ -126,12 +129,13 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
private void getFilesNeeded(CheckBoxTreeItem<String> node, String basePath, List<String> list) {
if (node == null) return;
if (node.isSelected()) {
if (node.isSelected() || node.isIndeterminate()) {
if (basePath.length() > "minecraft/".length())
list.add(StringUtils.substringAfter(basePath, "minecraft/"));
for (TreeItem<String> child : node.getChildren()) {
if (child instanceof CheckBoxTreeItem)
if (child instanceof CheckBoxTreeItem) {
getFilesNeeded(((CheckBoxTreeItem<String>) child), basePath + "/" + child.getValue(), list);
}
}
}
}
@@ -78,13 +78,13 @@ public final class ModpackInfoPage extends Control implements WizardPage {
private final SimpleStringProperty url = new SimpleStringProperty("");
private final SimpleBooleanProperty forceUpdate = new SimpleBooleanProperty();
private final SimpleBooleanProperty packWithLauncher = new SimpleBooleanProperty();
private final SimpleStringProperty fileApi = new SimpleStringProperty();
private final SimpleStringProperty fileApi = new SimpleStringProperty("");
private final SimpleIntegerProperty minMemory = new SimpleIntegerProperty(0);
private final SimpleStringProperty authlibInjectorServer = new SimpleStringProperty();
private final SimpleStringProperty launchArguments = new SimpleStringProperty("");
private final SimpleStringProperty javaArguments = new SimpleStringProperty("");
private final ObjectProperty<EventHandler<? super MouseEvent>> next = new SimpleObjectProperty<>();
private final SimpleStringProperty mcbbsThreadId = new SimpleStringProperty();
private final SimpleStringProperty mcbbsThreadId = new SimpleStringProperty("");
public ModpackInfoPage(WizardController controller, HMCLGameRepository gameRepository, String version) {
this.controller = controller;
@@ -41,10 +41,7 @@ import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.TabHeader;
import org.jackhuang.hmcl.ui.decorator.DecoratorTabPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.profile.ProfileAdvancedListItem;
import org.jackhuang.hmcl.ui.profile.ProfileList;
import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem;
import org.jackhuang.hmcl.ui.versions.GameList;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.io.CompressingUtils;
@@ -65,15 +62,11 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class RootPage extends DecoratorTabPage {
private MainPage mainPage = null;
private SettingsPage settingsPage = null;
private GameList gameListPage = null;
private AccountList accountListPage = null;
private ProfileList profileListPage = null;
private final TabHeader.Tab mainTab = new TabHeader.Tab("main");
private final TabHeader.Tab settingsTab = new TabHeader.Tab("settings");
private final TabHeader.Tab gameTab = new TabHeader.Tab("game");
private final TabHeader.Tab accountTab = new TabHeader.Tab("account");
private final TabHeader.Tab profileTab = new TabHeader.Tab("profile");
public RootPage() {
setLeftPaneWidth(200);
@@ -86,10 +79,8 @@ public class RootPage extends DecoratorTabPage {
mainTab.setNodeSupplier(this::getMainPage);
settingsTab.setNodeSupplier(this::getSettingsPage);
gameTab.setNodeSupplier(this::getGameListPage);
accountTab.setNodeSupplier(this::getAccountListPage);
profileTab.setNodeSupplier(this::getProfileListPage);
getTabs().setAll(mainTab, settingsTab, gameTab, accountTab, profileTab);
getTabs().setAll(mainTab, settingsTab, accountTab);
}
@Override
@@ -151,17 +142,6 @@ public class RootPage extends DecoratorTabPage {
return settingsPage;
}
private GameList getGameListPage() {
if (gameListPage == null) {
gameListPage = new GameList();
FXUtils.applyDragListener(gameListPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
File modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
});
}
return gameListPage;
}
private AccountList getAccountListPage() {
if (accountListPage == null) {
accountListPage = new AccountList();
@@ -171,15 +151,6 @@ public class RootPage extends DecoratorTabPage {
return accountListPage;
}
private ProfileList getProfileListPage() {
if (profileListPage == null) {
profileListPage = new ProfileList();
profileListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
profileListPage.profilesProperty().bindContent(Profiles.profilesProperty());
}
return profileListPage;
}
public Tab getMainTab() {
return mainTab;
}
@@ -188,18 +159,10 @@ public class RootPage extends DecoratorTabPage {
return settingsTab;
}
public Tab getGameTab() {
return gameTab;
}
public Tab getAccountTab() {
return accountTab;
}
public Tab getProfileTab() {
return profileTab;
}
private void selectPage(Tab tab) {
if (getSelectionModel().getSelectedItem() == tab) {
getSelectionModel().select(getMainTab());
@@ -221,12 +184,11 @@ public class RootPage extends DecoratorTabPage {
// second item in left sidebar
GameAdvancedListItem gameListItem = new GameAdvancedListItem();
gameListItem.actionButtonVisibleProperty().bind(Profiles.selectedVersionProperty().isNotNull());
gameListItem.setOnAction(e -> {
Profile profile = Profiles.getSelectedProfile();
String version = Profiles.getSelectedVersion();
if (version == null) {
control.selectPage(control.gameTab);
Controllers.navigate(Controllers.getGameListPage());
} else {
Versions.modifyGameSettings(profile, version);
}
@@ -234,21 +196,14 @@ public class RootPage extends DecoratorTabPage {
// third item in left sidebar
AdvancedListItem gameItem = new AdvancedListItem();
gameItem.activeProperty().bind(control.gameTab.selectedProperty());
gameItem.setImage(newImage("/assets/img/bookshelf.png"));
gameItem.setLeftGraphic(AdvancedListItem.createImageView(newImage("/assets/img/bookshelf.png")).getKey());
gameItem.setTitle(i18n("version.manage"));
gameItem.setOnAction(e -> control.selectPage(control.gameTab));
// forth item in left sidebar
ProfileAdvancedListItem profileListItem = new ProfileAdvancedListItem();
profileListItem.activeProperty().bind(control.profileTab.selectedProperty());
profileListItem.setOnAction(e -> control.selectPage(control.profileTab));
profileListItem.profileProperty().bind(Profiles.selectedProfileProperty());
gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage()));
// fifth item in left sidebar
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
launcherSettingsItem.activeProperty().bind(control.settingsTab.selectedProperty());
launcherSettingsItem.setImage(newImage("/assets/img/command.png"));
launcherSettingsItem.setLeftGraphic(AdvancedListItem.createImageView(newImage("/assets/img/command.png")).getKey());
launcherSettingsItem.setTitle(i18n("settings.launcher"));
launcherSettingsItem.setOnAction(e -> control.selectPage(control.settingsTab));
@@ -259,8 +214,6 @@ public class RootPage extends DecoratorTabPage {
.startCategory(i18n("version").toUpperCase())
.add(gameListItem)
.add(gameItem)
.startCategory(i18n("profile.title").toUpperCase())
.add(profileListItem)
.startCategory(i18n("launcher").toUpperCase())
.add(launcherSettingsItem);
@@ -535,8 +535,12 @@ public abstract class SettingsView extends StackPane {
}
protected abstract void onUpdate();
protected abstract void onHelp();
protected abstract void onExportLogs();
protected abstract void onSponsor();
protected abstract void clearCacheDirectory();
}
@@ -42,7 +42,7 @@ public class ProfileAdvancedListItem extends AdvancedListItem {
};
public ProfileAdvancedListItem() {
setImage(newImage("/assets/img/craft_table.png"));
setLeftGraphic(createImageView(newImage("/assets/img/craft_table.png")).getKey());
setRightGraphic(SVG.viewList(Theme.blackFillBinding(), -1, -1));
}
@@ -1,58 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.profile;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.ListPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
public class ProfileList extends ListPage<ProfileListItem> implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("profile.manage")));
private final ListProperty<Profile> profiles = new SimpleListProperty<>(FXCollections.observableArrayList());
private ObjectProperty<Profile> selectedProfile;
public ProfileList() {
setItems(MappedObservableList.create(profilesProperty(), ProfileListItem::new));
selectedProfile = createSelectedItemPropertyFor(getItems(), Profile.class);
}
public ObjectProperty<Profile> selectedProfileProperty() {
return selectedProfile;
}
public ListProperty<Profile> profilesProperty() {
return profiles;
}
@Override
public void add() {
Controllers.navigate(new ProfilePage(null));
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
}
@@ -32,7 +32,7 @@ public class ProfileListItem extends RadioButton {
public ProfileListItem(Profile profile) {
this.profile = profile;
getStyleClass().clear();
getStyleClass().setAll("profile-list-item", "navigation-drawer-item");
setUserData(profile);
title.set(Profiles.getProfileDisplayName(profile));
@@ -18,49 +18,47 @@
package org.jackhuang.hmcl.ui.profile;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.SkinBase;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import org.jackhuang.hmcl.ui.versions.VersionPage;
public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
public ProfileListItemSkin(ProfileListItem skinnable) {
super(skinnable);
BorderPane root = new BorderPane();
root.setPickOnBounds(false);
RipplerContainer container = new RipplerContainer(root);
JFXRadioButton chkSelected = new JFXRadioButton() {
@Override
public void fire() {
skinnable.fire();
}
};
BorderPane.setAlignment(chkSelected, Pos.CENTER);
chkSelected.selectedProperty().bind(skinnable.selectedProperty());
root.setLeft(chkSelected);
FXUtils.onChangeAndOperate(skinnable.selectedProperty(), active -> {
skinnable.pseudoClassStateChanged(SELECTED, active);
});
HBox center = new HBox();
center.setSpacing(8);
center.setAlignment(Pos.CENTER_LEFT);
getSkinnable().addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
getSkinnable().setSelected(true);
});
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, 32, 32);
imageView.imageProperty().set(newImage("/assets/img/craft_table.png"));
Node left = VersionPage.wrap(SVG.folderOutline(null, 24, 24));
root.setLeft(left);
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
TwoLineListItem item = new TwoLineListItem();
item.setPickOnBounds(false);
BorderPane.setAlignment(item, Pos.CENTER);
center.getChildren().setAll(imageView, item);
root.setCenter(center);
root.setCenter(item);
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
@@ -69,16 +67,13 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
btnRemove.setOnMouseClicked(e -> skinnable.remove());
btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER);
btnRemove.setGraphic(SVG.delete(Theme.blackFillBinding(), -1, -1));
btnRemove.setGraphic(SVG.close(Theme.blackFillBinding(), 14, 14));
right.getChildren().add(btnRemove);
root.setRight(right);
root.getStyleClass().add("card");
root.setStyle("-fx-padding: 8 8 8 0");
JFXDepthManager.setDepth(root, 1);
item.titleProperty().bind(skinnable.titleProperty());
item.subtitleProperty().bind(skinnable.subtitleProperty());
getChildren().setAll(root);
getChildren().setAll(container);
}
}
@@ -68,6 +68,7 @@ public final class ProfilePage extends StackPane implements DecoratorPage {
{
setMessage(i18n("profile.already_exists"));
}
@Override
protected void eval() {
JFXTextField control = (JFXTextField) this.getSrcControl();
@@ -17,39 +17,45 @@
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.util.Pair;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameAdvancedListItem extends AdvancedListItem {
private final Tooltip tooltip;
private final ImageView imageView;
public GameAdvancedListItem() {
tooltip = new Tooltip();
Pair<Node, ImageView> view = createImageView(null);
setLeftGraphic(view.getKey());
imageView = view.getValue();
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> {
if (version != null && Profiles.getSelectedProfile() != null &&
Profiles.getSelectedProfile().getRepository().hasVersion(version)) {
FXUtils.installFastTooltip(this, tooltip);
setTitle(version);
setSubtitle(null);
setImage(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version));
imageView.setImage(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version));
tooltip.setText(version);
} else {
Tooltip.uninstall(this,tooltip);
setTitle(i18n("version.empty"));
setSubtitle(i18n("version.empty.add"));
setImage(newImage("/assets/img/grass.png"));
imageView.setImage(newImage("/assets/img/grass.png"));
tooltip.setText("");
}
});
setRightGraphic(SVG.gear(Theme.blackFillBinding(), -1, -1));
setActionButtonVisible(false);
}
}
@@ -45,7 +45,9 @@ public class GameItemSkin extends SkinBase<GameItem> {
TwoLineListItem item = new TwoLineListItem();
item.titleProperty().bind(skinnable.titleProperty());
item.tagProperty().bind(skinnable.tagProperty());
FXUtils.onChangeAndOperate(skinnable.tagProperty(), tag -> {
item.getTags().setAll(tag);
});
item.subtitleProperty().bind(skinnable.subtitleProperty());
BorderPane.setAlignment(item, Pos.CENTER);
center.getChildren().setAll(imageView, item);
@@ -1,147 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.Node;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.download.VanillaInstallWizardProvider;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameList extends ListPageBase<GameListItem> implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("version.manage")));
private ToggleGroup toggleGroup;
public GameList() {
EventBus.EVENT_BUS.channel(RefreshingVersionsEvent.class).register(event -> {
if (event.getSource() == Profiles.getSelectedProfile().getRepository())
runInFX(() -> setLoading(true));
});
Profiles.registerVersionsListener(this::loadVersions);
}
private void loadVersions(Profile profile) {
HMCLGameRepository repository = profile.getRepository();
toggleGroup = new ToggleGroup();
WeakListenerHolder listenerHolder = new WeakListenerHolder();
toggleGroup.getProperties().put("ReferenceHolder", listenerHolder);
List<GameListItem> children = repository.getVersions().parallelStream()
.filter(version -> !version.isHidden())
.sorted(Comparator.comparing((Version version) -> version.getReleaseTime() == null ? new Date(0L) : version.getReleaseTime())
.thenComparing(a -> VersionNumber.asVersion(a.getId())))
.map(version -> new GameListItem(toggleGroup, profile, version.getId()))
.collect(Collectors.toList());
runInFX(() -> {
if (profile == Profiles.getSelectedProfile()) {
setLoading(false);
itemsProperty().setAll(children);
children.forEach(GameListItem::checkSelection);
profile.selectedVersionProperty().addListener(listenerHolder.weak((a, b, newValue) -> {
FXUtils.checkFxUserThread();
children.forEach(it -> it.selectedProperty().set(false));
children.stream()
.filter(it -> it.getVersion().equals(newValue))
.findFirst()
.ifPresent(it -> it.selectedProperty().set(true));
}));
}
toggleGroup.selectedToggleProperty().addListener((o, a, toggle) -> {
if (toggle == null) return;
GameListItem model = (GameListItem) toggle.getUserData();
model.getProfile().setSelectedVersion(model.getVersion());
});
});
}
@Override
protected GameListSkin createDefaultSkin() {
return new GameListSkin();
}
public static void addNewGame() {
Profile profile = Profiles.getSelectedProfile();
if (profile.getRepository().isLoaded()) {
Controllers.getDecorator().startWizard(new VanillaInstallWizardProvider(profile), i18n("install.new_game"));
}
}
public static void importModpack() {
Profile profile = Profiles.getSelectedProfile();
if (profile.getRepository().isLoaded()) {
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(profile), i18n("install.modpack"));
}
}
public static void refreshList() {
Profiles.getSelectedProfile().getRepository().refreshVersionsAsync().start();
}
public void modifyGlobalGameSettings() {
Versions.modifyGlobalSettings(Profiles.getSelectedProfile());
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
private class GameListSkin extends ToolbarListPageSkin<GameList> {
public GameListSkin() {
super(GameList.this);
HBox hbox = new HBox(
createToolbarButton(i18n("install.new_game"), SVG::plus, GameList::addNewGame),
createToolbarButton(i18n("install.modpack"), SVG::importIcon, GameList::importModpack),
createToolbarButton(i18n("button.refresh"), SVG::refresh, GameList::refreshList),
createToolbarButton(i18n("settings.type.global.manage"), SVG::gear, GameList.this::modifyGlobalGameSettings)
);
hbox.setPickOnBounds(false);
state.set(new State(i18n("version.manage"), hbox, true, false, true));
}
@Override
protected List<Node> initializeToolbar(GameList skinnable) {
return Collections.emptyList();
}
}
}
@@ -62,17 +62,17 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
popup = new JFXPopup(menu);
menu.getContent().setAll(
new IconedMenuItem(FXUtils.limitingSize(SVG.launch(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch.test"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.launch(), popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.launchOutline(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch.test"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.launch(), popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.script(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch_script"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.generateLaunchScript(), popup)),
new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.gear(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.manage"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.modifyGameSettings(), popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.gearOutline(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.manage"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.modifyGameSettings(), popup)),
new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.pencil(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.rename"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.rename(), popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.pencilOutline(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.rename"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.rename(), popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.copy(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.duplicate"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.duplicate(), popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.delete(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.remove(), popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.deleteOutline(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.remove(), popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.export(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("modpack.export"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.export(), popup)),
new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.folderOpen(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.game"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.browse(), popup)));
new IconedMenuItem(FXUtils.limitingSize(SVG.folderOutline(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.game"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.browse(), popup)));
}
HBox right = new HBox();
@@ -81,7 +81,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
JFXButton btnUpgrade = new JFXButton();
btnUpgrade.setOnMouseClicked(e -> skinnable.update());
btnUpgrade.getStyleClass().add("toggle-icon4");
btnUpgrade.setGraphic(SVG.update(Theme.blackFillBinding(), -1, -1));
btnUpgrade.setGraphic(FXUtils.limitingSize(SVG.update(Theme.blackFillBinding(), 24, 24), 24, 24));
runInFX(() -> FXUtils.installFastTooltip(btnUpgrade, i18n("version.update")));
right.getChildren().add(btnUpgrade);
}
@@ -91,7 +91,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
btnLaunch.setOnMouseClicked(e -> skinnable.launch());
btnLaunch.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnLaunch, Pos.CENTER);
btnLaunch.setGraphic(SVG.launch(Theme.blackFillBinding(), 20, 20));
btnLaunch.setGraphic(FXUtils.limitingSize(SVG.launchOutline(Theme.blackFillBinding(), 24, 24), 24, 24));
right.getChildren().add(btnLaunch);
}
@@ -103,7 +103,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
});
btnManage.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnManage, Pos.CENTER);
btnManage.setGraphic(SVG.dotsVertical(Theme.blackFillBinding(), -1, -1));
btnManage.setGraphic(FXUtils.limitingSize(SVG.dotsVertical(Theme.blackFillBinding(), 24, 24), 24, 24));
right.getChildren().add(btnManage);
}
@@ -0,0 +1,257 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SkinBase;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.profile.ProfileListItem;
import org.jackhuang.hmcl.ui.profile.ProfilePage;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
public class GameListPage extends ListPageBase<GameListItem> implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("version.manage")));
private final ListProperty<Profile> profiles = new SimpleListProperty<>(FXCollections.observableArrayList());
@SuppressWarnings("FieldCanBeLocal")
private final ObservableList<ProfileListItem> profileListItems;
private final ObjectProperty<Profile> selectedProfile;
private ToggleGroup toggleGroup;
public GameListPage() {
EventBus.EVENT_BUS.channel(RefreshingVersionsEvent.class).register(event -> {
if (event.getSource() == Profiles.getSelectedProfile().getRepository())
runInFX(() -> setLoading(true));
});
profileListItems = MappedObservableList.create(profilesProperty(), profile -> {
ProfileListItem item = new ProfileListItem(profile);
FXUtils.setLimitWidth(item, 200);
return item;
});
selectedProfile = createSelectedItemPropertyFor(profileListItems, Profile.class);
}
public ObjectProperty<Profile> selectedProfileProperty() {
return selectedProfile;
}
public ObservableList<Profile> getProfiles() {
return profiles.get();
}
public ListProperty<Profile> profilesProperty() {
return profiles;
}
public void setProfiles(ObservableList<Profile> profiles) {
this.profiles.set(profiles);
}
@Override
protected GameListPageSkin createDefaultSkin() {
return new GameListPageSkin();
}
public void modifyGlobalGameSettings() {
Versions.modifyGlobalSettings(Profiles.getSelectedProfile());
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
private class GameListPageSkin extends SkinBase<GameListPage> {
protected GameListPageSkin() {
super(GameListPage.this);
BorderPane root = new BorderPane();
GameList gameList = new GameList();
{
BorderPane left = new BorderPane();
FXUtils.setLimitWidth(left, 200);
root.setLeft(left);
{
AdvancedListItem addProfileItem = new AdvancedListItem();
addProfileItem.getStyleClass().add("navigation-drawer-item");
addProfileItem.setTitle(i18n("profile.new"));
addProfileItem.setActionButtonVisible(false);
addProfileItem.setLeftGraphic(VersionPage.wrap(SVG.plusCircleOutline(Theme.blackFillBinding(), 24, 24)));
addProfileItem.setOnAction(e -> Controllers.navigate(new ProfilePage(null)));
ScrollPane pane = new ScrollPane();
VBox wrapper = new VBox();
wrapper.getStyleClass().add("advanced-list-box-content");
VBox box = new VBox();
Bindings.bindContent(box.getChildren(), profileListItems);
wrapper.getChildren().setAll(box, addProfileItem);
pane.setContent(wrapper);
left.setCenter(pane);
}
{
AdvancedListItem installNewGameItem = new AdvancedListItem();
installNewGameItem.getStyleClass().add("navigation-drawer-item");
installNewGameItem.setTitle(i18n("install.new_game"));
installNewGameItem.setActionButtonVisible(false);
installNewGameItem.setLeftGraphic(VersionPage.wrap(SVG.plusCircleOutline(Theme.blackFillBinding(), 24, 24)));
installNewGameItem.setOnAction(e -> Versions.addNewGame());
AdvancedListItem installModpackItem = new AdvancedListItem();
installModpackItem.getStyleClass().add("navigation-drawer-item");
installModpackItem.setTitle(i18n("install.modpack"));
installModpackItem.setActionButtonVisible(false);
installModpackItem.setLeftGraphic(VersionPage.wrap(SVG.pack(Theme.blackFillBinding(), 24, 24)));
installModpackItem.setOnAction(e -> Versions.importModpack());
AdvancedListItem downloadModpackItem = new AdvancedListItem();
downloadModpackItem.getStyleClass().add("navigation-drawer-item");
downloadModpackItem.setTitle(i18n("modpack.download"));
downloadModpackItem.setActionButtonVisible(false);
downloadModpackItem.setLeftGraphic(VersionPage.wrap(SVG.fire(Theme.blackFillBinding(), 24, 24)));
downloadModpackItem.setOnAction(e -> Versions.downloadModpack());
AdvancedListItem refreshItem = new AdvancedListItem();
refreshItem.getStyleClass().add("navigation-drawer-item");
refreshItem.setTitle(i18n("button.refresh"));
refreshItem.setActionButtonVisible(false);
refreshItem.setLeftGraphic(VersionPage.wrap(SVG.refresh(Theme.blackFillBinding(), 24, 24)));
refreshItem.setOnAction(e -> gameList.refreshList());
AdvancedListItem globalManageItem = new AdvancedListItem();
globalManageItem.getStyleClass().add("navigation-drawer-item");
globalManageItem.setTitle(i18n("settings.type.global.manage"));
globalManageItem.setActionButtonVisible(false);
globalManageItem.setLeftGraphic(VersionPage.wrap(SVG.gearOutline(Theme.blackFillBinding(), 24, 24)));
globalManageItem.setOnAction(e -> modifyGlobalGameSettings());
AdvancedListBox bottomLeftCornerList = new AdvancedListBox()
.add(installNewGameItem)
.add(installModpackItem)
.add(downloadModpackItem)
.add(refreshItem)
.add(globalManageItem);
FXUtils.setLimitHeight(bottomLeftCornerList, 40 * 5 + 12 * 2);
left.setBottom(bottomLeftCornerList);
}
}
root.setCenter(gameList);
getChildren().setAll(root);
}
}
private class GameList extends ListPageBase<GameListItem> {
public GameList() {
super();
Profiles.registerVersionsListener(this::loadVersions);
}
private void loadVersions(Profile profile) {
HMCLGameRepository repository = profile.getRepository();
toggleGroup = new ToggleGroup();
WeakListenerHolder listenerHolder = new WeakListenerHolder();
toggleGroup.getProperties().put("ReferenceHolder", listenerHolder);
List<GameListItem> children = repository.getVersions().parallelStream()
.filter(version -> !version.isHidden())
.sorted(Comparator.comparing((Version version) -> version.getReleaseTime() == null ? new Date(0L) : version.getReleaseTime())
.thenComparing(a -> VersionNumber.asVersion(a.getId())))
.map(version -> new GameListItem(toggleGroup, profile, version.getId()))
.collect(Collectors.toList());
runInFX(() -> {
if (profile == Profiles.getSelectedProfile()) {
setLoading(false);
itemsProperty().setAll(children);
children.forEach(GameListItem::checkSelection);
profile.selectedVersionProperty().addListener(listenerHolder.weak((a, b, newValue) -> {
FXUtils.checkFxUserThread();
children.forEach(it -> it.selectedProperty().set(false));
children.stream()
.filter(it -> it.getVersion().equals(newValue))
.findFirst()
.ifPresent(it -> it.selectedProperty().set(true));
}));
}
toggleGroup.selectedToggleProperty().addListener((o, a, toggle) -> {
if (toggle == null) return;
GameListItem model = (GameListItem) toggle.getUserData();
model.getProfile().setSelectedVersion(model.getVersion());
});
});
}
public void refreshList() {
Profiles.getSelectedProfile().getRepository().refreshVersionsAsync().start();
}
@Override
protected GameListSkin createDefaultSkin() {
return new GameListSkin();
}
private class GameListSkin extends ToolbarListPageSkin<GameList> {
public GameListSkin() {
super(GameList.this);
state.set(new State(i18n("version.manage"), null, true, false, true, false, 200));
}
@Override
protected List<Node> initializeToolbar(GameList skinnable) {
return Collections.emptyList();
}
}
}
}
@@ -0,0 +1,322 @@
/*
* 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.ui.versions;
import com.jfoenix.controls.*;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.mod.curse.CurseAddon;
import org.jackhuang.hmcl.mod.curse.CurseModManager;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.FloatListCell;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.util.*;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class ModDownloadListPage extends Control implements DecoratorPage {
protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
private final BooleanProperty loading = new SimpleBooleanProperty(false);
private final BooleanProperty failed = new SimpleBooleanProperty(false);
private final ObjectProperty<Profile.ProfileVersion> version = new SimpleObjectProperty<>();
private final ListProperty<CurseAddon> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final ModDownloadPage.DownloadCallback callback;
/**
* @see org.jackhuang.hmcl.mod.curse.CurseModManager#SECTION_MODPACK
* @see org.jackhuang.hmcl.mod.curse.CurseModManager#SECTION_MOD
*/
private final int section;
public ModDownloadListPage(int section, ModDownloadPage.DownloadCallback callback) {
this.section = section;
this.callback = callback;
}
public void loadVersion(Profile profile, String version) {
this.version.set(new Profile.ProfileVersion(profile, version));
setLoading(false);
setFailed(false);
}
public boolean isFailed() {
return failed.get();
}
public BooleanProperty failedProperty() {
return failed;
}
public void setFailed(boolean failed) {
this.failed.set(failed);
}
public boolean isLoading() {
return loading.get();
}
public BooleanProperty loadingProperty() {
return loading;
}
public void setLoading(boolean loading) {
this.loading.set(loading);
}
public void search(String userGameVersion, int category, int pageOffset, String searchFilter, int sort) {
setLoading(true);
File versionJar = StringUtils.isNotBlank(version.get().getVersion())
? version.get().getProfile().getRepository().getVersionJar(version.get().getVersion())
: null;
Task.supplyAsync(() -> {
String gameVersion;
if (StringUtils.isBlank(version.get().getVersion())) {
gameVersion = userGameVersion;
} else {
gameVersion = GameVersion.minecraftVersion(versionJar).orElse("");
}
return gameVersion;
}).thenApplyAsync(gameVersion -> {
return CurseModManager.searchPaginated(gameVersion, category, section, pageOffset, searchFilter, sort);
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
setLoading(false);
if (exception == null) {
items.setAll(result);
} else {
failed.set(true);
}
}).start();
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
@Override
protected Skin<?> createDefaultSkin() {
return new ModDownloadListPageSkin(this);
}
private static class ModDownloadListPageSkin extends SkinBase<ModDownloadListPage> {
protected ModDownloadListPageSkin(ModDownloadListPage control) {
super(control);
VBox pane = new VBox();
GridPane searchPane = new GridPane();
searchPane.getStyleClass().addAll("card");
VBox.setMargin(searchPane, new Insets(10, 10, 0, 10));
ColumnConstraints column1 = new ColumnConstraints();
column1.setPercentWidth(50);
column1.setHgrow(Priority.ALWAYS);
ColumnConstraints column2 = new ColumnConstraints();
column2.setHgrow(Priority.ALWAYS);
column2.setPercentWidth(50);
searchPane.getColumnConstraints().setAll(column1, column2);
searchPane.setHgap(16);
searchPane.setVgap(10);
{
JFXTextField nameField = new JFXTextField();
nameField.setPromptText(i18n("mods.name"));
searchPane.add(nameField, 0, 0);
JFXTextField gameVersionField = new JFXTextField();
gameVersionField.setPromptText(i18n("world.game_version"));
searchPane.add(gameVersionField, 1, 0);
FXUtils.onChangeAndOperate(getSkinnable().version, version -> {
searchPane.getChildren().remove(gameVersionField);
if (StringUtils.isNotBlank(version.getVersion())) {
GridPane.setColumnSpan(nameField, 2);
} else {
searchPane.add(gameVersionField, 1, 0);
GridPane.setColumnSpan(nameField, 1);
}
});
StackPane categoryStackPane = new StackPane();
JFXComboBox<CategoryIndented> categoryComboBox = new JFXComboBox<>();
categoryComboBox.getItems().setAll(new CategoryIndented(0, 0));
categoryStackPane.getChildren().setAll(categoryComboBox);
categoryComboBox.prefWidthProperty().bind(categoryStackPane.widthProperty());
categoryComboBox.getStyleClass().add("fit-width");
categoryComboBox.setPromptText(i18n("mods.category"));
categoryComboBox.getSelectionModel().select(0);
Task.supplyAsync(() -> CurseModManager.getCategories(getSkinnable().section))
.thenAcceptAsync(Schedulers.javafx(), categories -> {
List<CategoryIndented> result = new ArrayList<>();
result.add(new CategoryIndented(0, 0));
for (CurseModManager.Category category : categories) {
resolveCategory(category, 0, result);
}
categoryComboBox.getItems().setAll(result);
}).start();
searchPane.add(categoryStackPane, 0, 1);
StackPane sortStackPane = new StackPane();
JFXComboBox<String> sortComboBox = new JFXComboBox<>();
sortStackPane.getChildren().setAll(sortComboBox);
sortComboBox.prefWidthProperty().bind(sortStackPane.widthProperty());
sortComboBox.getStyleClass().add("fit-width");
sortComboBox.setPromptText(i18n("search.sort"));
sortComboBox.getItems().setAll(
i18n("curse.sort.date_created"),
i18n("curse.sort.popularity"),
i18n("curse.sort.last_updated"),
i18n("curse.sort.name"),
i18n("curse.sort.author"),
i18n("curse.sort.total_downloads"));
sortComboBox.getSelectionModel().select(0);
searchPane.add(sortStackPane, 1, 1);
VBox vbox = new VBox();
vbox.setAlignment(Pos.CENTER_RIGHT);
searchPane.add(vbox, 0, 2, 2, 1);
JFXButton searchButton = new JFXButton();
searchButton.setText(i18n("search"));
searchButton.setOnAction(e -> {
getSkinnable().search(gameVersionField.getText(),
Optional.ofNullable(categoryComboBox.getSelectionModel().getSelectedItem())
.map(CategoryIndented::getCategoryId)
.orElse(0),
0,
nameField.getText(),
sortComboBox.getSelectionModel().getSelectedIndex());
});
searchPane.add(searchButton, 0, 2);
vbox.getChildren().setAll(searchButton);
}
SpinnerPane spinnerPane = new SpinnerPane();
{
spinnerPane.loadingProperty().bind(getSkinnable().loadingProperty());
spinnerPane.failedReasonProperty().bind(Bindings.createStringBinding(() -> {
if (getSkinnable().isFailed()) {
return i18n("download.failed.refresh");
} else {
return null;
}
}, getSkinnable().failedProperty()));
JFXListView<CurseAddon> listView = new JFXListView<>();
spinnerPane.setContent(listView);
Bindings.bindContent(listView.getItems(), getSkinnable().items);
listView.setOnMouseClicked(e -> {
if (listView.getSelectionModel().getSelectedIndex() < 0)
return;
CurseAddon selectedItem = listView.getSelectionModel().getSelectedItem();
Controllers.navigate(new ModDownloadPage(selectedItem, getSkinnable().version.get(), getSkinnable().callback));
});
listView.setCellFactory(x -> new FloatListCell<CurseAddon>() {
TwoLineListItem content = new TwoLineListItem();
ImageView imageView = new ImageView();
{
Region clippedContainer = (Region) listView.lookup(".clipped-container");
setPrefWidth(0);
HBox container = new HBox(8);
container.setAlignment(Pos.CENTER_LEFT);
pane.getChildren().add(container);
if (clippedContainer != null) {
maxWidthProperty().bind(clippedContainer.widthProperty());
prefWidthProperty().bind(clippedContainer.widthProperty());
minWidthProperty().bind(clippedContainer.widthProperty());
}
container.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content);
}
@Override
protected void updateControl(CurseAddon dataItem, boolean empty) {
if (empty) return;
content.setTitle(dataItem.getName());
content.setSubtitle(dataItem.getSummary());
content.getTags().setAll(dataItem.getCategories().stream()
.map(category -> i18n("curse.category." + category.getCategoryId()))
.collect(Collectors.toList()));
for (CurseAddon.Attachment attachment : dataItem.getAttachments()) {
if (attachment.isDefault()) {
imageView.setImage(new Image(attachment.getThumbnailUrl(), 40, 40, true, true, true));
}
}
}
});
}
pane.getChildren().setAll(searchPane, spinnerPane);
getChildren().setAll(pane);
}
private static class CategoryIndented {
private final int indent;
private final int categoryId;
public CategoryIndented(int indent, int categoryId) {
this.indent = indent;
this.categoryId = categoryId;
}
public int getIndent() {
return indent;
}
public int getCategoryId() {
return categoryId;
}
@Override
public String toString() {
return StringUtils.repeats(' ', indent) + i18n("curse.category." + categoryId);
}
}
private static void resolveCategory(CurseModManager.Category category, int indent, List<CategoryIndented> result) {
result.add(new CategoryIndented(indent, category.getId()));
for (CurseModManager.Category subcategory : category.getSubcategories()) {
resolveCategory(subcategory, indent + 1, result);
}
}
}
}
@@ -0,0 +1,245 @@
/*
* 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.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXListView;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.mod.curse.CurseAddon;
import org.jackhuang.hmcl.mod.curse.CurseModManager;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.FloatListCell;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class ModDownloadPage extends Control implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
private final ListProperty<CurseAddon.LatestFile> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final BooleanProperty loading = new SimpleBooleanProperty(false);
private final BooleanProperty failed = new SimpleBooleanProperty(false);
private final CurseAddon addon;
private final Profile.ProfileVersion version;
private final DownloadCallback callback;
public ModDownloadPage(CurseAddon addon, Profile.ProfileVersion version, DownloadCallback callback) {
this.addon = addon;
this.version = version;
this.callback = callback;
File versionJar = StringUtils.isNotBlank(version.getVersion())
? version.getProfile().getRepository().getVersionJar(version.getVersion())
: null;
Task.runAsync(() -> {
if (StringUtils.isNotBlank(version.getVersion())) {
Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar);
if (gameVersion.isPresent()) {
List<CurseAddon.LatestFile> files = CurseModManager.getFiles(addon);
items.setAll(files.stream()
.filter(file -> file.getGameVersion().contains(gameVersion.get()))
.collect(Collectors.toList()));
return;
}
}
List<CurseAddon.LatestFile> files = CurseModManager.getFiles(addon);
items.setAll(files);
}).start();
this.state.set(State.fromTitle(i18n("mods.download.title", addon.getName())));
}
public CurseAddon getAddon() {
return addon;
}
public Profile.ProfileVersion getVersion() {
return version;
}
public boolean isLoading() {
return loading.get();
}
public BooleanProperty loadingProperty() {
return loading;
}
public void setLoading(boolean loading) {
this.loading.set(loading);
}
public boolean isFailed() {
return failed.get();
}
public BooleanProperty failedProperty() {
return failed;
}
public void setFailed(boolean failed) {
this.failed.set(failed);
}
public void download(CurseAddon.LatestFile file) {
this.callback.download(version.getProfile(), version.getVersion(), file);
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
@Override
protected Skin<?> createDefaultSkin() {
return new ModDownloadPageSkin(this);
}
private static class ModDownloadPageSkin extends SkinBase<ModDownloadPage> {
protected ModDownloadPageSkin(ModDownloadPage control) {
super(control);
BorderPane pane = new BorderPane();
HBox descriptionPane = new HBox(8);
descriptionPane.setAlignment(Pos.CENTER);
pane.setTop(descriptionPane);
descriptionPane.getStyleClass().add("card");
BorderPane.setMargin(descriptionPane, new Insets(11, 11, 0, 11));
TwoLineListItem content = new TwoLineListItem();
HBox.setHgrow(content, Priority.ALWAYS);
content.setTitle(getSkinnable().addon.getName());
content.setSubtitle(getSkinnable().addon.getSummary());
content.getTags().setAll(getSkinnable().addon.getCategories().stream()
.map(category -> i18n("curse.category." + category.getCategoryId()))
.collect(Collectors.toList()));
ImageView imageView = new ImageView();
for (CurseAddon.Attachment attachment : getSkinnable().addon.getAttachments()) {
if (attachment.isDefault()) {
imageView.setImage(new Image(attachment.getThumbnailUrl(), 40, 40, true, true, true));
}
}
JFXButton openUrlButton = new JFXButton();
openUrlButton.getStyleClass().add("toggle-icon4");
openUrlButton.setGraphic(SVG.launchOutline(Theme.blackFillBinding(), -1, -1));
openUrlButton.setOnAction(e -> FXUtils.openLink(getSkinnable().addon.getWebsiteUrl()));
descriptionPane.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content, openUrlButton);
SpinnerPane spinnerPane = new SpinnerPane();
pane.setCenter(spinnerPane);
{
spinnerPane.loadingProperty().bind(getSkinnable().loadingProperty());
spinnerPane.failedReasonProperty().bind(Bindings.createStringBinding(() -> {
if (getSkinnable().isFailed()) {
return i18n("download.failed.refresh");
} else {
return null;
}
}, getSkinnable().failedProperty()));
JFXListView<CurseAddon.LatestFile> listView = new JFXListView<>();
spinnerPane.setContent(listView);
Bindings.bindContent(listView.getItems(), getSkinnable().items);
listView.setCellFactory(x -> new FloatListCell<CurseAddon.LatestFile>() {
TwoLineListItem content = new TwoLineListItem();
StackPane graphicPane = new StackPane();
{
Region clippedContainer = (Region)listView.lookup(".clipped-container");
setPrefWidth(0);
HBox container = new HBox(8);
container.setAlignment(Pos.CENTER_LEFT);
pane.getChildren().add(container);
if (clippedContainer != null) {
maxWidthProperty().bind(clippedContainer.widthProperty());
prefWidthProperty().bind(clippedContainer.widthProperty());
minWidthProperty().bind(clippedContainer.widthProperty());
}
container.getChildren().setAll(graphicPane, content);
}
@Override
protected void updateControl(CurseAddon.LatestFile dataItem, boolean empty) {
if (empty) return;
content.setTitle(dataItem.getDisplayName());
content.getTags().setAll(dataItem.getGameVersion());
switch (dataItem.getReleaseType()) {
case 1: // release
graphicPane.getChildren().setAll(SVG.releaseCircleOutline(Theme.blackFillBinding(), 24, 24));
content.getTags().add(i18n("version.game.release"));
break;
case 2: // beta
graphicPane.getChildren().setAll(SVG.betaCircleOutline(Theme.blackFillBinding(), 24, 24));
content.getTags().add(i18n("version.game.snapshot"));
break;
case 3: // alpha
graphicPane.getChildren().setAll(SVG.alphaCircleOutline(Theme.blackFillBinding(), 24, 24));
content.getTags().add(i18n("version.game.snapshot"));
break;
}
}
});
listView.setOnMouseClicked(e -> {
if (listView.getSelectionModel().getSelectedIndex() < 0)
return;
CurseAddon.LatestFile selectedItem = listView.getSelectionModel().getSelectedItem();
getSkinnable().download(selectedItem);
});
}
getChildren().setAll(pane);
}
}
public interface DownloadCallback {
void download(Profile profile, @Nullable String version, CurseAddon.LatestFile file);
}
}
@@ -86,7 +86,6 @@ class ModListPageSkin extends SkinBase<ModListPage> {
BooleanProperty booleanProperty;
{
Region clippedContainer = (Region)listView.lookup(".clipped-container");
setPrefWidth(0);
HBox container = new HBox(8);
@@ -17,52 +17,50 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXListCell;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXPopup;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.input.MouseButton;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.util.Callback;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.mod.curse.CurseAddon;
import org.jackhuang.hmcl.mod.curse.CurseModManager;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.ToolbarListPageSkin;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class VersionPage extends Control implements DecoratorPage {
public class VersionPage extends Control implements DecoratorPage, ModDownloadPage.DownloadCallback {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
private final BooleanProperty loading = new SimpleBooleanProperty();
private final JFXListView<String> listView = new JFXListView<>();
private final TabHeader.Tab versionSettingsTab = new TabHeader.Tab("versionSettingsTab");
private final VersionSettingsPage versionSettingsPage = new VersionSettingsPage();
private final TabHeader.Tab modListTab = new TabHeader.Tab("modListTab");
private final ModListPage modListPage = new ModListPage(modListTab);
private final TabHeader.Tab curseModListTab = new TabHeader.Tab("modListTab");
private final ModDownloadListPage curseModListPage = new ModDownloadListPage(CurseModManager.SECTION_MOD, this);
private final TabHeader.Tab installerListTab = new TabHeader.Tab("installerListTab");
private final InstallerListPage installerListPage = new InstallerListPage();
private final TabHeader.Tab worldListTab = new TabHeader.Tab("worldList");
@@ -70,73 +68,43 @@ public class VersionPage extends Control implements DecoratorPage {
private final TransitionPane transitionPane = new TransitionPane();
private final ObjectProperty<TabHeader.Tab> selectedTab = new SimpleObjectProperty<>();
private final BooleanProperty currentVersionUpgradable = new SimpleBooleanProperty();
private Profile profile;
private String version;
private final ObjectProperty<Profile.ProfileVersion> version = new SimpleObjectProperty<>();
private String preferredVersionName = null;
{
Profiles.registerVersionsListener(this::loadVersions);
listView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null && !Objects.equals(oldValue, newValue))
loadVersion(newValue, profile);
if (newValue == null && !Objects.equals(oldValue, newValue)) {
if (listView.getItems().contains(preferredVersionName)) {
loadVersion(preferredVersionName, profile);
} else if (!listView.getItems().isEmpty()) {
loadVersion(listView.getItems().get(0), profile);
}
}
});
versionSettingsTab.setNode(versionSettingsPage);
modListTab.setNode(modListPage);
curseModListTab.setNode(curseModListPage);
installerListTab.setNode(installerListPage);
worldListTab.setNode(worldListPage);
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
}
private void loadVersions(Profile profile) {
HMCLGameRepository repository = profile.getRepository();
List<String> children = repository.getVersions().parallelStream()
.filter(version -> !version.isHidden())
.sorted(Comparator.comparing((Version version) -> version.getReleaseTime() == null ? new Date(0L) : version.getReleaseTime())
.thenComparing(a -> VersionNumber.asVersion(a.getId())))
.map(Version::getId)
.collect(Collectors.toList());
runInFX(() -> {
if (profile == Profiles.getSelectedProfile()) {
this.profile = profile;
loading.set(false);
listView.getItems().setAll(children);
}
selectedTab.set(versionSettingsTab);
FXUtils.onChangeAndOperate(selectedTab, newValue -> {
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer());
});
}
public void setVersion(String version, Profile profile) {
this.version = version;
this.profile = profile;
profile.setSelectedVersion(version);
this.version.set(new Profile.ProfileVersion(profile, version));
}
public void loadVersion(String version, Profile profile) {
// If we jumped to game list page and deleted this version
// and back to this page, we should return to main page.
if (!this.profile.getRepository().isLoaded() ||
!this.profile.getRepository().hasVersion(version)) {
if (this.version.get() != null && (!getProfile().getRepository().isLoaded() ||
!getProfile().getRepository().hasVersion(version))) {
Platform.runLater(() -> fireEvent(new PageCloseEvent()));
return;
}
setVersion(version, profile);
preferredVersionName = version;
listView.getSelectionModel().select(version);
versionSettingsPage.loadVersion(profile, version);
curseModListPage.loadVersion(profile, version);
currentVersionUpgradable.set(profile.getRepository().isModpack(version));
CompletableFuture.allOf(
@@ -146,63 +114,71 @@ public class VersionPage extends Control implements DecoratorPage {
}
private void onNavigated(Navigator.NavigationEvent event) {
if (this.version == null || this.profile == null)
if (this.version.get() == null)
throw new IllegalStateException();
// If we jumped to game list page and deleted this version
// and back to this page, we should return to main page.
if (!this.profile.getRepository().isLoaded() ||
!this.profile.getRepository().hasVersion(version)) {
if (!getProfile().getRepository().isLoaded() ||
!getProfile().getRepository().hasVersion(getVersion())) {
Platform.runLater(() -> fireEvent(new PageCloseEvent()));
return;
}
loadVersion(this.version, this.profile);
loadVersion(getVersion(), getProfile());
}
private void onBrowse(String sub) {
FXUtils.openFolder(new File(profile.getRepository().getRunDirectory(version), sub));
FXUtils.openFolder(new File(getProfile().getRepository().getRunDirectory(getVersion()), sub));
}
private void redownloadAssetIndex() {
Versions.updateGameAssets(profile, version);
Versions.updateGameAssets(getProfile(), getVersion());
}
private void clearLibraries() {
FileUtils.deleteDirectoryQuietly(new File(profile.getRepository().getBaseDirectory(), "libraries"));
FileUtils.deleteDirectoryQuietly(new File(getProfile().getRepository().getBaseDirectory(), "libraries"));
}
private void clearJunkFiles() {
Versions.cleanVersion(profile, version);
Versions.cleanVersion(getProfile(), getVersion());
}
private void testGame() {
Versions.testGame(profile, version);
Versions.testGame(getProfile(), getVersion());
}
private void updateGame() {
Versions.updateVersion(profile, version);
Versions.updateVersion(getProfile(), getVersion());
}
private void generateLaunchScript() {
Versions.generateLaunchScript(profile, version);
Versions.generateLaunchScript(getProfile(), getVersion());
}
private void export() {
Versions.exportVersion(profile, version);
Versions.exportVersion(getProfile(), getVersion());
}
private void rename() {
Versions.renameVersion(profile, version)
.thenApply(newVersionName -> this.preferredVersionName = newVersionName);
Versions.renameVersion(getProfile(), getVersion())
.thenApply(newVersionName -> this.preferredVersionName = newVersionName);
}
private void remove() {
Versions.deleteVersion(profile, version);
Versions.deleteVersion(getProfile(), getVersion());
}
private void duplicate() {
Versions.duplicateVersion(profile, version);
Versions.duplicateVersion(getProfile(), getVersion());
}
public Profile getProfile() {
return Optional.ofNullable(version.get()).map(Profile.ProfileVersion::getProfile).orElse(null);
}
public String getVersion() {
return Optional.ofNullable(version.get()).map(Profile.ProfileVersion::getVersion).orElse(null);
}
@Override
@@ -215,9 +191,30 @@ public class VersionPage extends Control implements DecoratorPage {
return state.getReadOnlyProperty();
}
@Override
public void download(Profile profile, @Nullable String version, CurseAddon.LatestFile file) {
if (version == null) {
throw new InternalError();
}
Path dest = profile.getRepository().getRunDirectory(version).toPath().resolve("mods").resolve(file.getFileName());
TaskExecutorDialogPane downloadingPane = new TaskExecutorDialogPane(it -> {
});
TaskExecutor executor = Task.composeAsync(() -> {
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getDownloadUrl()), dest.toFile());
task.setName(file.getDisplayName());
return task;
}).executor(false);
downloadingPane.setExecutor(executor, true);
Controllers.dialog(downloadingPane);
executor.start();
}
public static class Skin extends SkinBase<VersionPage> {
String currentVersion;
private JFXPopup listViewItemPopup;
/**
@@ -232,46 +229,30 @@ public class VersionPage extends Control implements DecoratorPage {
listViewItemPopup = new JFXPopup(menu);
menu.getContent().setAll(
new IconedMenuItem(FXUtils.limitingSize(SVG.launch(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch.test"), FXUtils.withJFXPopupClosing(() -> {
Versions.testGame(getSkinnable().profile, currentVersion);
Versions.testGame(getSkinnable().getProfile(), getSkinnable().getVersion());
}, listViewItemPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.script(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch_script"), FXUtils.withJFXPopupClosing(() -> {
Versions.generateLaunchScript(getSkinnable().profile, currentVersion);
Versions.generateLaunchScript(getSkinnable().getProfile(), getSkinnable().getVersion());
}, listViewItemPopup)),
new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.pencil(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.rename"), FXUtils.withJFXPopupClosing(() -> {
Versions.renameVersion(getSkinnable().profile, currentVersion).thenApply(name -> getSkinnable().preferredVersionName = name);
Versions.renameVersion(getSkinnable().getProfile(), getSkinnable().getVersion()).thenApply(name -> getSkinnable().preferredVersionName = name);
}, listViewItemPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.copy(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.duplicate"), FXUtils.withJFXPopupClosing(() -> {
Versions.duplicateVersion(getSkinnable().profile, currentVersion);
Versions.duplicateVersion(getSkinnable().getProfile(), getSkinnable().getVersion());
}, listViewItemPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.delete(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(() -> {
Versions.deleteVersion(getSkinnable().profile, currentVersion);
Versions.deleteVersion(getSkinnable().getProfile(), getSkinnable().getVersion());
}, listViewItemPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.export(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("modpack.export"), FXUtils.withJFXPopupClosing(() -> {
Versions.exportVersion(getSkinnable().profile, currentVersion);
Versions.exportVersion(getSkinnable().getProfile(), getSkinnable().getVersion());
}, listViewItemPopup)),
new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.folderOpen(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.game"), FXUtils.withJFXPopupClosing(() -> {
Versions.openFolder(getSkinnable().profile, currentVersion);
Versions.openFolder(getSkinnable().getProfile(), getSkinnable().getVersion());
}, listViewItemPopup))
);
control.listView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
control.listView.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
@Override
public ListCell<String> call(ListView<String> param) {
JFXListCell<String> cell = new JFXListCell<>();
cell.setOnMouseClicked(e -> {
if (cell.getItem() == null) return;
currentVersion = cell.getItem();
if (e.getButton() == MouseButton.SECONDARY) {
listViewItemPopup.show(cell, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, e.getX(), e.getY());
}
});
return cell;
}
});
SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.getStyleClass().add("large-spinner-pane");
@@ -281,65 +262,77 @@ public class VersionPage extends Control implements DecoratorPage {
{
BorderPane left = new BorderPane();
FXUtils.setLimitWidth(left, 200);
left.setCenter(control.listView);
root.setLeft(left);
JFXButton btnAddGame = ToolbarListPageSkin.createToolbarButton(null, SVG::plus, GameList::addNewGame);
FXUtils.installFastTooltip(btnAddGame, new Tooltip(i18n("install.new_game")));
AdvancedListItem versionSettingsItem = new AdvancedListItem();
versionSettingsItem.getStyleClass().add("navigation-drawer-item");
versionSettingsItem.setTitle(i18n("settings"));
versionSettingsItem.setLeftGraphic(wrap(SVG.gearOutline(null, 20, 20)));
versionSettingsItem.setActionButtonVisible(false);
versionSettingsItem.activeProperty().bind(control.selectedTab.isEqualTo(control.versionSettingsTab));
versionSettingsItem.setOnAction(e -> control.selectedTab.set(control.versionSettingsTab));
JFXButton btnImportModpack = ToolbarListPageSkin.createToolbarButton(null, SVG::importIcon, GameList::importModpack);
FXUtils.installFastTooltip(btnImportModpack, new Tooltip(i18n("install.modpack")));
AdvancedListItem modListItem = new AdvancedListItem();
modListItem.getStyleClass().add("navigation-drawer-item");
modListItem.setTitle(i18n("mods"));
modListItem.setLeftGraphic(wrap(SVG.puzzle(null, 20, 20)));
modListItem.setActionButtonVisible(false);
modListItem.activeProperty().bind(control.selectedTab.isEqualTo(control.modListTab));
modListItem.setOnAction(e -> control.selectedTab.set(control.modListTab));
JFXButton btnRefresh = ToolbarListPageSkin.createToolbarButton(null, SVG::refresh, GameList::refreshList);
FXUtils.installFastTooltip(btnRefresh, new Tooltip(i18n("button.refresh")));
AdvancedListItem curseModListItem = new AdvancedListItem();
curseModListItem.getStyleClass().add("navigation-drawer-item");
curseModListItem.setTitle(i18n("mods.download"));
curseModListItem.setLeftGraphic(wrap(SVG.fire(null, 20, 20)));
curseModListItem.setActionButtonVisible(false);
curseModListItem.activeProperty().bind(control.selectedTab.isEqualTo(control.curseModListTab));
curseModListItem.setOnAction(e -> control.selectedTab.set(control.curseModListTab));
HBox toolbar = new HBox();
toolbar.setPickOnBounds(false);
toolbar.getChildren().setAll(btnAddGame, btnImportModpack, btnRefresh);
left.setTop(toolbar);
}
AdvancedListItem installerListItem = new AdvancedListItem();
installerListItem.getStyleClass().add("navigation-drawer-item");
installerListItem.setTitle(i18n("settings.tabs.installers"));
installerListItem.setLeftGraphic(wrap(SVG.cube(null, 20, 20)));
installerListItem.setActionButtonVisible(false);
installerListItem.activeProperty().bind(control.selectedTab.isEqualTo(control.installerListTab));
installerListItem.setOnAction(e -> control.selectedTab.set(control.installerListTab));
AdvancedListItem worldListItem = new AdvancedListItem();
worldListItem.getStyleClass().add("navigation-drawer-item");
worldListItem.setTitle(i18n("world"));
worldListItem.setLeftGraphic(wrap(SVG.gamepad(null, 20, 20)));
worldListItem.setActionButtonVisible(false);
worldListItem.activeProperty().bind(control.selectedTab.isEqualTo(control.worldListTab));
worldListItem.setOnAction(e -> control.selectedTab.set(control.worldListTab));
AdvancedListBox sideBar = new AdvancedListBox()
.add(versionSettingsItem)
.add(modListItem)
.add(curseModListItem)
.add(installerListItem)
.add(worldListItem);
left.setCenter(sideBar);
TabHeader tabPane = new TabHeader();
tabPane.setPickOnBounds(false);
tabPane.getStyleClass().add("jfx-decorator-tab");
control.versionSettingsTab.setText(i18n("settings"));
control.modListTab.setText(i18n("mods"));
control.installerListTab.setText(i18n("settings.tabs.installers"));
control.worldListTab.setText(i18n("world"));
tabPane.getTabs().setAll(
control.versionSettingsTab,
control.modListTab,
control.installerListTab,
control.worldListTab);
control.selectedTab.bind(tabPane.getSelectionModel().selectedItemProperty());
FXUtils.onChangeAndOperate(tabPane.getSelectionModel().selectedItemProperty(), newValue -> {
control.transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer());
});
HBox toolBar = new HBox();
toolBar.setAlignment(Pos.TOP_RIGHT);
toolBar.setPickOnBounds(false);
{
PopupMenu browseList = new PopupMenu();
JFXPopup browsePopup = new JFXPopup(browseList);
browseList.getContent().setAll(
new IconedMenuItem(null, i18n("folder.game"), FXUtils.withJFXPopupClosing(() -> control.onBrowse(""), browsePopup)),
new IconedMenuItem(null, i18n("folder.mod"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("mods"), browsePopup)),
new IconedMenuItem(null, i18n("folder.config"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("config"), browsePopup)),
new IconedMenuItem(null, i18n("folder.resourcepacks"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("resourcepacks"), browsePopup)),
new IconedMenuItem(null, i18n("folder.screenshots"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("screenshots"), browsePopup)),
new IconedMenuItem(null, i18n("folder.saves"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("saves"), browsePopup))
new IconedMenuItem(FXUtils.limitingSize(SVG.gamepad(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.game"), FXUtils.withJFXPopupClosing(() -> control.onBrowse(""), browsePopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.puzzle(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.mod"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("mods"), browsePopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.gearOutline(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.config"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("config"), browsePopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.texture(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.resourcepacks"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("resourcepacks"), browsePopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.monitorScreenshot(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.screenshots"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("screenshots"), browsePopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.gamepad(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.saves"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("saves"), browsePopup))
);
PopupMenu managementList = new PopupMenu();
JFXPopup managementPopup = new JFXPopup(managementList);
managementList.getContent().setAll(
new IconedMenuItem(FXUtils.limitingSize(SVG.launch(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch.test"), FXUtils.withJFXPopupClosing(control::testGame, managementPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.launchOutline(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch.test"), FXUtils.withJFXPopupClosing(control::testGame, managementPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.script(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch_script"), FXUtils.withJFXPopupClosing(control::generateLaunchScript, managementPopup)),
new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.pencil(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.rename"), FXUtils.withJFXPopupClosing(control::rename, managementPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.copy(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.duplicate"), FXUtils.withJFXPopupClosing(control::duplicate, managementPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.delete(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(control::remove, managementPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.deleteOutline(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(control::remove, managementPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.export(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("modpack.export"), FXUtils.withJFXPopupClosing(control::export, managementPopup)),
new MenuSeparator(),
new IconedMenuItem(null, i18n("version.manage.redownload_assets_index"), FXUtils.withJFXPopupClosing(control::redownloadAssetIndex, managementPopup)),
@@ -347,44 +340,49 @@ public class VersionPage extends Control implements DecoratorPage {
new IconedMenuItem(null, i18n("version.manage.clean"), FXUtils.withJFXPopupClosing(control::clearJunkFiles, managementPopup)).addTooltip(i18n("version.manage.clean.tooltip"))
);
JFXButton upgradeButton = new JFXButton();
upgradeButton.setGraphic(SVG.update(null, 20, 20));
upgradeButton.getStyleClass().add("jfx-decorator-button");
upgradeButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
upgradeButton.setOnAction(event -> control.updateGame());
upgradeButton.visibleProperty().bind(control.currentVersionUpgradable);
FXUtils.installFastTooltip(upgradeButton, i18n("version.update"));
AdvancedListItem upgradeItem = new AdvancedListItem();
upgradeItem.getStyleClass().add("navigation-drawer-item");
upgradeItem.setTitle(i18n("version.update"));
upgradeItem.setLeftGraphic(wrap(SVG.update(Theme.blackFillBinding(), 20, 20)));
upgradeItem.setActionButtonVisible(false);
upgradeItem.visibleProperty().bind(control.currentVersionUpgradable);
upgradeItem.setOnAction(e -> control.updateGame());
JFXButton testGameButton = new JFXButton();
testGameButton.setGraphic(SVG.launch(null, 20, 20));
testGameButton.getStyleClass().add("jfx-decorator-button");
testGameButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
testGameButton.setOnAction(event -> control.testGame());
FXUtils.installFastTooltip(testGameButton, i18n("version.launch.test"));
AdvancedListItem testGameItem = new AdvancedListItem();
testGameItem.getStyleClass().add("navigation-drawer-item");
testGameItem.setTitle(i18n("version.launch.test"));
testGameItem.setLeftGraphic(wrap(SVG.launchOutline(Theme.blackFillBinding(), 20, 20)));
testGameItem.setActionButtonVisible(false);
testGameItem.setOnAction(e -> control.testGame());
JFXButton browseMenuButton = new JFXButton();
browseMenuButton.setGraphic(SVG.folderOpen(null, 20, 20));
browseMenuButton.getStyleClass().add("jfx-decorator-button");
browseMenuButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
browseMenuButton.setOnAction(event -> browsePopup.show(browseMenuButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, browseMenuButton.getHeight()));
FXUtils.installFastTooltip(browseMenuButton, i18n("settings.game.exploration"));
AdvancedListItem browseMenuItem = new AdvancedListItem();
browseMenuItem.getStyleClass().add("navigation-drawer-item");
browseMenuItem.setTitle(i18n("settings.game.exploration"));
browseMenuItem.setLeftGraphic(wrap(SVG.folderOutline(Theme.blackFillBinding(), 20, 20)));
browseMenuItem.setActionButtonVisible(false);
browseMenuItem.setOnAction(e -> browsePopup.show(browseMenuItem, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, browseMenuItem.getWidth(), 0));
JFXButton managementMenuButton = new JFXButton();
FXUtils.setLimitWidth(managementMenuButton, 40);
FXUtils.setLimitHeight(managementMenuButton, 40);
managementMenuButton.setGraphic(SVG.wrench(null, 20, 20));
managementMenuButton.getStyleClass().add("jfx-decorator-button");
managementMenuButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
managementMenuButton.setOnAction(event -> managementPopup.show(managementMenuButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, managementMenuButton.getHeight()));
FXUtils.installFastTooltip(managementMenuButton, i18n("settings.game.management"));
AdvancedListItem managementItem = new AdvancedListItem();
managementItem.getStyleClass().add("navigation-drawer-item");
managementItem.setTitle(i18n("settings.game.management"));
managementItem.setLeftGraphic(wrap(SVG.wrenchOutline(Theme.blackFillBinding(), 20, 20)));
managementItem.setActionButtonVisible(false);
managementItem.setOnAction(e -> managementPopup.show(managementItem, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, managementItem.getWidth(), 0));
toolBar.getChildren().setAll(upgradeButton, testGameButton, browseMenuButton, managementMenuButton);
AdvancedListBox toolbar = new AdvancedListBox()
.add(upgradeItem)
.add(testGameItem)
.add(browseMenuItem)
.add(managementItem);
toolbar.getStyleClass().add("advanced-list-box-clear-padding");
FXUtils.setLimitHeight(toolbar, 40 * 4 + 12 * 2);
left.setBottom(toolbar);
}
BorderPane titleBar = new BorderPane();
titleBar.setLeft(tabPane);
titleBar.setRight(toolBar);
control.state.set(new State(i18n("version.manage.manage"), titleBar, true, false, true, false, 200));
control.state.bind(Bindings.createObjectBinding(() ->
new State(i18n("version.manage.manage.title", getSkinnable().getVersion()), null, true, false, true, false, 200),
getSkinnable().version));
//control.transitionPane.getStyleClass().add("gray-background");
//FXUtils.setOverflowHidden(control.transitionPane, 8);
@@ -395,4 +393,14 @@ public class VersionPage extends Control implements DecoratorPage {
getChildren().setAll(spinnerPane);
}
}
public static Node wrap(Node node) {
StackPane stackPane = new StackPane();
FXUtils.setLimitWidth(stackPane, 30);
FXUtils.setLimitHeight(stackPane, 20);
stackPane.setPadding(new Insets(0, 10, 0, 0));
stackPane.getChildren().setAll(node);
return stackPane;
}
}
@@ -35,7 +35,11 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.GameDirectoryType;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.game.NativesDirectoryType;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -76,6 +80,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
@FXML private JFXTextField txtWidth;
@FXML private JFXTextField txtHeight;
@FXML private JFXTextField txtMaxMemory;
@FXML private JFXTextField txtNativesPath;
@FXML private JFXTextField txtJVMArgs;
@FXML private JFXTextField txtGameArgs;
@FXML private JFXTextField txtMetaspace;
@@ -88,11 +93,13 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
@FXML private JFXComboBox<LauncherVisibility> cboLauncherVisibility;
@FXML private JFXCheckBox chkFullscreen;
@FXML private Label lblPhysicalMemory;
@FXML private Label lblCustomizedNativesPath;
@FXML private JFXToggleButton chkNoJVMArgs;
@FXML private JFXToggleButton chkNoGameCheck;
@FXML private JFXToggleButton chkNoJVMCheck;
@FXML private MultiFileItem<JavaVersion> javaItem;
@FXML private MultiFileItem<GameDirectoryType> gameDirItem;
@FXML private MultiFileItem<NativesDirectoryType> nativesDirItem;
@FXML private JFXToggleButton chkShowLogs;
@FXML private ImagePickerItem iconPickerItem;
@FXML private JFXCheckBox chkEnableSpecificSettings;
@@ -138,6 +145,11 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
gameDirItem.createChildren(i18n("settings.advanced.game_dir.independent"), GameDirectoryType.VERSION_FOLDER)
));
nativesDirItem.setCustomUserData(NativesDirectoryType.CUSTOM);
nativesDirItem.loadChildren(Arrays.asList(
nativesDirItem.createChildren(i18n("settings.advanced.natives_directory.default"), NativesDirectoryType.VERSION_FOLDER)
));
chkEnableSpecificSettings.selectedProperty().addListener((a, b, newValue) -> {
if (versionId == null) return;
@@ -179,6 +191,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
FXUtils.unbindInt(txtMaxMemory, lastVersionSetting.maxMemoryProperty());
FXUtils.unbindString(javaItem.getTxtCustom(), lastVersionSetting.javaDirProperty());
FXUtils.unbindString(gameDirItem.getTxtCustom(), lastVersionSetting.gameDirProperty());
FXUtils.unbindString(nativesDirItem.getTxtCustom(), lastVersionSetting.nativesDirProperty());
FXUtils.unbindString(txtJVMArgs, lastVersionSetting.javaArgsProperty());
FXUtils.unbindString(txtGameArgs, lastVersionSetting.minecraftArgsProperty());
FXUtils.unbindString(txtMetaspace, lastVersionSetting.permSizeProperty());
@@ -198,6 +211,9 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
gameDirItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.gameDirTypeProperty());
gameDirItem.subtitleProperty().unbind();
nativesDirItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.nativesDirTypeProperty());
nativesDirItem.subtitleProperty().unbind();
}
// unbind data fields
@@ -209,6 +225,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
FXUtils.bindInt(txtMaxMemory, versionSetting.maxMemoryProperty());
FXUtils.bindString(javaItem.getTxtCustom(), versionSetting.javaDirProperty());
FXUtils.bindString(gameDirItem.getTxtCustom(), versionSetting.gameDirProperty());
FXUtils.bindString(nativesDirItem.getTxtCustom(), versionSetting.nativesDirProperty());
FXUtils.bindString(txtJVMArgs, versionSetting.javaArgsProperty());
FXUtils.bindString(txtGameArgs, versionSetting.minecraftArgsProperty());
FXUtils.bindString(txtMetaspace, versionSetting.permSizeProperty());
@@ -240,6 +257,10 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
gameDirItem.selectedDataProperty().bindBidirectional(versionSetting.gameDirTypeProperty());
gameDirItem.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath()).normalize().toString(),
versionSetting.gameDirProperty(), versionSetting.gameDirTypeProperty()));
nativesDirItem.selectedDataProperty().bindBidirectional(versionSetting.nativesDirTypeProperty());
nativesDirItem.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath() + "/natives").normalize().toString(),
versionSetting.nativesDirProperty(), versionSetting.nativesDirTypeProperty()));
lastVersionSetting = versionSetting;
@@ -22,8 +22,11 @@ import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.game.GameDirectoryType;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.mod.curse.CurseAddon;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
@@ -33,6 +36,7 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.download.VanillaInstallWizardProvider;
import org.jackhuang.hmcl.ui.export.ExportWizardProvider;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
@@ -41,12 +45,66 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class Versions {
public final class Versions {
private Versions() {
}
public static void addNewGame() {
Profile profile = Profiles.getSelectedProfile();
if (profile.getRepository().isLoaded()) {
Controllers.getDecorator().startWizard(new VanillaInstallWizardProvider(profile), i18n("install.new_game"));
}
}
public static void importModpack() {
Profile profile = Profiles.getSelectedProfile();
if (profile.getRepository().isLoaded()) {
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(profile), i18n("install.modpack"));
}
}
public static void downloadModpack() {
Profile profile = Profiles.getSelectedProfile();
if (profile.getRepository().isLoaded()) {
Controllers.getModpackDownloadListPage().loadVersion(profile, null);
Controllers.navigate(Controllers.getModpackDownloadListPage());
}
}
public static void downloadModpackImpl(Profile profile, String version, CurseAddon.LatestFile file) {
Path modpack;
URL downloadURL;
try {
modpack = Files.createTempFile("modpack", ".zip");
downloadURL = new URL(file.getDownloadUrl());
} catch (IOException e) {
Controllers.dialog(
i18n("install.failed.downloading.detail", file.getDownloadUrl()) + "\n" + StringUtils.getStackTrace(e),
i18n("download.failed"), MessageDialogPane.MessageType.ERROR);
return;
}
Controllers.taskDialog(
new FileDownloadTask(downloadURL, modpack.toFile())
.whenComplete(Schedulers.javafx(), e -> {
if (e == null) {
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack.toFile()));
} else {
Controllers.dialog(
i18n("install.failed.downloading.detail", file.getDownloadUrl()) + "\n" + StringUtils.getStackTrace(e),
i18n("download.failed"), MessageDialogPane.MessageType.ERROR);
}
}).executor(true),
i18n("message.downloading")
);
}
public static void deleteVersion(Profile profile, String version) {
boolean isIndependent = profile.getVersionSetting(version).getGameDirType() == GameDirectoryType.VERSION_FOLDER;
@@ -54,7 +112,7 @@ public class Versions {
String message = isIndependent ? i18n("version.manage.remove.confirm.independent", version) :
isMovingToTrashSupported ? i18n("version.manage.remove.confirm.trash", version, version + "_removed") :
i18n("version.manage.remove.confirm", version);
Controllers.confirm(message, i18n("message.confirm"), () -> {
Controllers.confirm(message, i18n("message.warning"), MessageDialogPane.MessageType.WARNING, () -> {
profile.getRepository().removeVersionFromDisk(version);
}, null);
}
@@ -156,7 +214,7 @@ public class Versions {
Controllers.getRootPage().checkAccount();
else if (id == null || !profile.getRepository().isLoaded() || !profile.getRepository().hasVersion(id))
Controllers.dialog(i18n("version.empty.launch"), i18n("launch.failed"), MessageDialogPane.MessageType.ERROR, () -> {
Controllers.getRootPage().getSelectionModel().select(Controllers.getRootPage().getGameTab());
Controllers.navigate(Controllers.getGameListPage());
});
else
return true;
@@ -50,7 +50,6 @@ public class WorldExportPage extends WizardSinglePage {
return new WorldExportPageSkin(this);
}
public StringProperty pathProperty() {
return path;
}
@@ -94,5 +94,4 @@ public class WorldExportPageSkin extends SkinBase<WorldExportPage> {
getChildren().setAll(container);
}
}
@@ -22,11 +22,17 @@ import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
public interface Navigation {
void onStart();
void onNext();
void onPrev(boolean cleanUp);
boolean canPrev();
void onFinish();
void onEnd();
void onCancel();
enum NavigationDirection {
@@ -23,9 +23,13 @@ import java.util.Map;
public interface WizardProvider {
void start(Map<String, Object> settings);
Object finish(Map<String, Object> settings);
Node createPage(WizardController controller, int step, Map<String, Object> settings);
boolean cancel();
default boolean cancelIfCannotGoBack() {
return false;
}
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.upgrade;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.Pack200Utils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.tukaani.xz.XZInputStream;
@@ -26,7 +27,6 @@ import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
class HMCLDownloadTask extends FileDownloadTask {
@@ -52,7 +52,7 @@ class HMCLDownloadTask extends FileDownloadTask {
byte[] raw = Files.readAllBytes(target);
try (InputStream in = new XZInputStream(new ByteArrayInputStream(raw));
JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) {
Pack200.newUnpacker().unpack(in, out);
Pack200Utils.unpack(in, out);
}
break;
@@ -21,9 +21,9 @@ 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 org.jackhuang.hmcl.util.platform.SystemUtils;
import java.io.IOException;
import java.util.Optional;
@@ -38,7 +38,7 @@ public class RemoteVersion {
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);
if (SystemUtils.JRE_CAPABILITY_PACK200 && packXZUrl != null && packXZHash != null) {
if (Pack200Utils.isSupported() && packXZUrl != null && packXZHash != null) {
return new RemoteVersion(version, packXZUrl, Type.PACK_XZ, new IntegrityCheck("SHA-1", packXZHash));
} else if (jarUrl != null && jarHash != null) {
return new RemoteVersion(version, jarUrl, Type.JAR, new IntegrityCheck("SHA-1", jarHash));
@@ -1,3 +1,20 @@
/*
* 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.nio.file.Path;
@@ -8,7 +25,10 @@ import java.util.Set;
*
* @author ZekerZhayard
*/
public class JavaFXPatcher {
public final class JavaFXPatcher {
private JavaFXPatcher() {
}
public static void patch(Set<String> modules, Path... jarPaths) {
// Nothing to do with Java 8
}
@@ -0,0 +1,44 @@
/*
* 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.util.Objects;
import java.util.function.Supplier;
/**
* Non thread-safe lazy initialization wrapper.
*
* @param <T> value type
*/
public class Lazy<T> {
private Supplier<T> supplier;
private T value = null;
public Lazy(Supplier<T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
public T get() {
if (value == null) {
value = Objects.requireNonNull(supplier.get());
supplier = null;
}
return value;
}
}
@@ -1,55 +1,144 @@
/*
* 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/>.
*/
/*
* The MIT License (MIT)
*
* Copyright (c) 2017-2021 Matthew Coley
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.jackhuang.hmcl.util;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.swing.*;
import java.awt.*;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.*;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.Collectors;
import static java.lang.Class.forName;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toSet;
import static org.jackhuang.hmcl.Metadata.HMCL_DIRECTORY;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.platform.JavaVersion.CURRENT_JAVA;
/**
* Utility for patching self when missing dependencies.
* Copy from https://github.com/Col-E/Recaf/blob/master/src/main/java/me/coley/recaf/util/self/SelfDependencyPatcher.java
*
* @author Matt
*/
public class SelfDependencyPatcher {
private static final Path DEPENDENCIES_DIR_PATH = HMCL_DIRECTORY.resolve("dependencies");
private static final String DEFAULT_JFX_VERSION = "16";
private static final Map<String, String> JFX_DEPENDENCIES = new HashMap<>();
import java.awt.Dialog;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
static {
addJfxDependency("base");
addJfxDependency("controls");
addJfxDependency("fxml");
addJfxDependency("graphics");
addJfxDependency("media");
// Fix #874: Remove the dependency on javafx.swing
// addJfxDependency("swing");
addJfxDependency("web");
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
// From: https://github.com/Col-E/Recaf/blob/7378b397cee664ae81b7963b0355ef8ff013c3a7/src/main/java/me/coley/recaf/util/self/SelfDependencyPatcher.java
public final class SelfDependencyPatcher {
private SelfDependencyPatcher() {
}
private static void addJfxDependency(String name) {
JFX_DEPENDENCIES.put("javafx." + name, jfxUrl(name));
static class DependencyDescriptor {
private static final String REPOSITORY_URL = "https://maven.aliyun.com/repository/central/";
private static final Path DEPENDENCIES_DIR_PATH = HMCL_DIRECTORY.resolve("dependencies");
private static String currentArchClassifier() {
switch (OperatingSystem.CURRENT_OS) {
case LINUX:
return "linux";
case OSX:
return "mac";
default:
return "win";
}
}
public String module;
public String groupId;
public String artifactId;
public String version;
public Map<String, String> sha1;
public String filename() {
return artifactId + "-" + version + "-" + currentArchClassifier() + ".jar";
}
public String sha1() {
return sha1.get(currentArchClassifier());
}
public String url() {
return REPOSITORY_URL + groupId.replace('.', '/') + "/" + artifactId + "/" + version + "/" + filename();
}
public Path localPath() {
return DEPENDENCIES_DIR_PATH.resolve(filename());
}
}
private static final String DEPENDENCIES_LIST_FILE = "/assets/openjfx-dependencies.json";
private static List<DependencyDescriptor> readDependencies() {
String content;
try (InputStream in = SelfDependencyPatcher.class.getResourceAsStream(DEPENDENCIES_LIST_FILE)) {
content = IOUtils.readFullyAsString(in, UTF_8);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return new Gson().fromJson(content, TypeToken.getParameterized(List.class, DependencyDescriptor.class).getType());
}
private static final List<DependencyDescriptor> JFX_DEPENDENCIES = readDependencies();
/**
* Patch in any missing dependencies, if any.
*/
@@ -81,17 +170,17 @@ public class SelfDependencyPatcher {
// Otherwise we're free to download in Java 11+
LOG.info("Missing JavaFX dependencies, attempting to patch in missing classes");
// Check if dependencies need to be downloaded
if (!hasCachedDependencies()) {
LOG.info(" - No local cache, downloading dependencies...");
// Download missing dependencies
List<DependencyDescriptor> missingDependencies = checkMissingDependencies();
if (!missingDependencies.isEmpty()) {
try {
fetchDependencies();
} catch (Exception ex) {
throw new PatchException("Failed to download dependencies", ex);
fetchDependencies(missingDependencies);
} catch (IOException e) {
throw new PatchException("Failed to download dependencies", e);
}
} else {
LOG.info(" - Local cache found!");
}
// Add the dependencies
try {
loadFromCache();
@@ -106,17 +195,21 @@ public class SelfDependencyPatcher {
/**
* Inject them into the current classpath.
*
* @throws IOException
* When the locally cached dependency urls cannot be resolved.
* @throws ReflectiveOperationException
* When the call to add these urls to the system classpath failed.
* @throws IOException When the locally cached dependency urls cannot be resolved.
* @throws ReflectiveOperationException When the call to add these urls to the system classpath failed.
*/
private static void loadFromCache() throws IOException, ReflectiveOperationException {
LOG.info(" - Loading dependencies...");
List<Path> jarPaths = new ArrayList<>();
List<String> jfxDepFile = JFX_DEPENDENCIES.values().stream().map(SelfDependencyPatcher::getFileName).collect(Collectors.toList());
Files.walk(DEPENDENCIES_DIR_PATH).filter(p -> jfxDepFile.contains(p.getFileName().toString())).forEach(jarPaths::add);
JavaFXPatcher.patch(JFX_DEPENDENCIES.keySet(), jarPaths.toArray(new Path[0]));
Set<String> modules = JFX_DEPENDENCIES.stream()
.map(it -> it.module)
.collect(toSet());
Path[] jars = JFX_DEPENDENCIES.stream()
.map(it -> it.localPath())
.toArray(Path[]::new);
JavaFXPatcher.patch(modules, jars);
}
/**
@@ -124,106 +217,57 @@ public class SelfDependencyPatcher {
*
* @throws IOException When the files cannot be fetched or saved.
*/
private static void fetchDependencies() throws Exception {
// Get dir to store dependencies in
Path dependenciesDir = DEPENDENCIES_DIR_PATH;
if (!Files.isDirectory(dependenciesDir)) {
Files.createDirectories(dependenciesDir);
}
private static void fetchDependencies(List<DependencyDescriptor> dependencies) throws IOException {
ProgressFrame dialog = new ProgressFrame(i18n("download.javafx"));
ForkJoinTask<Void> task = ForkJoinPool.commonPool().submit(() -> {
// Download each dependency
Collection<String> dependencies = JFX_DEPENDENCIES.values();
int i = 1;
for (String dependencyUrlPath : dependencies) {
URL depURL = new URL(dependencyUrlPath);
Path dependencyFilePath = DEPENDENCIES_DIR_PATH.resolve(getFileName(dependencyUrlPath));
int finalI = i;
SwingUtilities.invokeLater(() -> {
dialog.setStatus(dependencyUrlPath);
dialog.setProgress(finalI, dependencies.size());
});
Files.copy(depURL.openStream(), dependencyFilePath, StandardCopyOption.REPLACE_EXISTING);
checksum(dependencyFilePath, dependencyUrlPath);
i++;
}
dialog.dispose();
return null;
});
dialog.setVisible(true);
task.get();
int progress = 0;
for (DependencyDescriptor dependency : dependencies) {
int currentProgress = ++progress;
SwingUtilities.invokeLater(() -> {
dialog.setStatus(dependency.url());
dialog.setProgress(currentProgress, dependencies.size());
});
LOG.info("Downloading " + dependency.url());
Files.createDirectories(dependency.localPath().getParent());
Files.copy(new URL(dependency.url()).openStream(), dependency.localPath(), StandardCopyOption.REPLACE_EXISTING);
verifyChecksum(dependency);
}
dialog.dispose();
}
/**
* @return {@code true} when the dependencies directory has files in it.
*/
private static boolean hasCachedDependencies() {
if (!Files.isDirectory(DEPENDENCIES_DIR_PATH))
return false;
private static List<DependencyDescriptor> checkMissingDependencies() {
List<DependencyDescriptor> missing = new ArrayList<>();
for (String url : JFX_DEPENDENCIES.values()) {
Path dependencyFilePath = DEPENDENCIES_DIR_PATH.resolve(getFileName(url));
if (!Files.exists(dependencyFilePath))
return false;
for (DependencyDescriptor dependency : JFX_DEPENDENCIES) {
if (!Files.exists(dependency.localPath())) {
missing.add(dependency);
continue;
}
try {
checksum(dependencyFilePath, url);
verifyChecksum(dependency);
} catch (ChecksumMismatchException e) {
return false;
} catch (IOException ignored) {
// Ignore other situations
LOG.warning("Corrupted dependency " + dependency.filename() + ": " + e.getMessage());
missing.add(dependency);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
return true;
return missing;
}
private static void checksum(Path dependencyFilePath, String dependencyUrlPath) throws IOException {
String expectedHash = NetworkUtils.doGet(NetworkUtils.toURL(dependencyUrlPath + ".sha1"));
String actualHash = Hex.encodeHex(DigestUtils.digest("SHA-1", dependencyFilePath));
private static void verifyChecksum(DependencyDescriptor dependency) throws IOException, ChecksumMismatchException {
String expectedHash = dependency.sha1();
String actualHash = Hex.encodeHex(DigestUtils.digest("SHA-1", dependency.localPath()));
if (!expectedHash.equalsIgnoreCase(actualHash)) {
throw new ChecksumMismatchException("SHA-1", expectedHash, actualHash);
}
}
/**
* @param url Full url path.
* @return Name of file at url.
*/
private static String getFileName(String url) {
return url.substring(url.lastIndexOf('/') + 1);
}
/**
* @param component Name of the component.
* @return Formed URL for the component.
*/
private static String jfxUrl(String component) {
return jfxUrl(component, DEFAULT_JFX_VERSION);
}
private static String jfxUrl(String component, String version) {
// https://repo1.maven.org/maven2/org/openjfx/javafx-%s/%s/javafx-%s-%s-%s.jar
return String.format("https://maven.aliyun.com/repository/central/org/openjfx/javafx-%s/%s/javafx-%s-%s-%s.jar",
component, version, component, version, getMvnName());
// return String.format("https://bmclapi.bangbang93.com/maven/org/openjfx/javafx-%s/%s/javafx-%s-%s-%s.jar",
// component, version, component, version, getMvnName());
}
private static String getMvnName() {
switch (OperatingSystem.CURRENT_OS) {
case LINUX:
return "linux";
case OSX:
return "mac";
default:
return "win";
}
}
public static class PatchException extends Exception {
PatchException(String message, Throwable cause) {
super(message, cause);
@@ -234,8 +278,6 @@ public class SelfDependencyPatcher {
}
public static class ProgressFrame extends JDialog {
private int totalTasks = 0;
private int finishedTasks = 0;
private final JProgressBar progressBar;
private final JLabel progressText;
@@ -253,10 +295,10 @@ public class SelfDependencyPatcher {
setLocationRelativeTo(null);
GridBagLayout gridBagLayout = new GridBagLayout();
gridBagLayout.columnWidths = new int[]{600, 0};
gridBagLayout.rowHeights = new int[]{0, 0, 0, 200};
gridBagLayout.columnWeights = new double[]{1.0, Double.MIN_VALUE};
gridBagLayout.rowWeights = new double[]{0.0, 0.0, 0.0, 1.0};
gridBagLayout.columnWidths = new int[] { 600, 0 };
gridBagLayout.rowHeights = new int[] { 0, 0, 0, 200 };
gridBagLayout.columnWeights = new double[] { 1.0, Double.MIN_VALUE };
gridBagLayout.rowWeights = new double[] { 0.0, 0.0, 0.0, 1.0 };
panel.setLayout(gridBagLayout);
progressText = new JLabel("");
@@ -28,6 +28,8 @@ import java.util.Locale;
import java.util.ResourceBundle;
public final class Locales {
private Locales() {
}
public static final SupportedLocale DEFAULT = new SupportedLocale(Locale.getDefault(), "lang.default");
@@ -61,12 +63,18 @@ public final class Locales {
public static SupportedLocale getLocaleByName(String name) {
if (name == null) return DEFAULT;
switch (name.toLowerCase()) {
case "en": return EN;
case "zh": return ZH;
case "zh_cn": return ZH_CN;
case "es": return ES;
case "ru": return RU;
default: return DEFAULT;
case "en":
return EN;
case "zh":
return ZH;
case "zh_cn":
return ZH_CN;
case "es":
return ES;
case "ru":
return RU;
default:
return DEFAULT;
}
}
@@ -1,22 +1,31 @@
/*
* 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.InputStream;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.module.Configuration;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import jdk.internal.loader.BuiltinClassLoader;
@@ -25,11 +34,34 @@ import jdk.internal.loader.BuiltinClassLoader;
*
* @author ZekerZhayard
*/
public class JavaFXPatcher {
public final class JavaFXPatcher {
private JavaFXPatcher() {
}
private static final String[] addOpens = {
"javafx.base/com.sun.javafx.runtime",
"javafx.base/com.sun.javafx.binding",
"javafx.base/com.sun.javafx.event",
"javafx.graphics/javafx.css",
"javafx.graphics/com.sun.javafx.stage",
"javafx.controls/com.sun.javafx.scene.control.behavior",
"javafx.controls/javafx.scene.control.skin",
"javafx.controls/com.sun.javafx.scene.control"
};
private static final String[] addExports = {
"javafx.base/com.sun.javafx.binding",
"javafx.base/com.sun.javafx.event",
"javafx.graphics/com.sun.javafx.stage",
"javafx.controls/com.sun.javafx.scene.control.behavior",
"javafx.controls/javafx.scene.control.skin",
"javafx.controls/com.sun.javafx.scene.control"
};
/**
* Add JavaFX to module path at runtime.
*
* @param modules All module names
* @param modules All module names
* @param jarPaths All jar paths
* @throws ReflectiveOperationException When the call to add these jars to the system module path failed.
*/
@@ -47,32 +79,26 @@ public class JavaFXPatcher {
ModuleLayer layer = ModuleLayer.defineModules(config, List.of(ModuleLayer.boot()), name -> ClassLoader.getSystemClassLoader()).layer();
// Add-Exports and Add-Opens
try (
FileSystem fs = FileSystems.newFileSystem(URI.create("jar:" + JavaFXPatcher.class.getProtectionDomain().getCodeSource().getLocation().toURI()), Map.of("create", "true"));
InputStream stream = Files.newInputStream(fs.getPath("/META-INF/MANIFEST.MF"))
) {
try {
// Some hacks
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Module.class, MethodHandles.lookup());
Attributes attributes = new Manifest(stream).getMainAttributes();
addExportsOrOpens(attributes.getValue("Add-Exports"), layer, lookup.findVirtual(Module.class, "implAddExportsToAllUnnamed", MethodType.methodType(void.class, String.class)));
addExportsOrOpens(attributes.getValue("Add-Opens"), layer, lookup.findVirtual(Module.class, "implAddOpensToAllUnnamed", MethodType.methodType(void.class, String.class)));
addExportsOrOpens(addExports, layer, lookup.findVirtual(Module.class, "implAddExportsToAllUnnamed", MethodType.methodType(void.class, String.class)));
addExportsOrOpens(addOpens, layer, lookup.findVirtual(Module.class, "implAddOpensToAllUnnamed", MethodType.methodType(void.class, String.class)));
} catch (Throwable t) {
throw new ReflectiveOperationException(t);
}
}
private static void addExportsOrOpens(String targets, ModuleLayer layer, MethodHandle handle) {
for (String target : targets.split("\\s+")) {
private static void addExportsOrOpens(String[] targets, ModuleLayer layer, MethodHandle handle) {
for (String target : targets) {
String[] name = target.split("/", 2); // <module>/<package>
if (name[0].startsWith("javafx.")) {
layer.findModule(name[0]).ifPresent(m -> {
try {
handle.invokeWithArguments(m, name[1]);
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
});
}
layer.findModule(name[0]).ifPresent(m -> {
try {
handle.invokeWithArguments(m, name[1]);
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
});
}
}
}
Binary file not shown.
@@ -19,7 +19,7 @@
-fx-base-color: %base-color%;
-fx-base-darker-color: derive(-fx-base-color, -10%);
-fx-base-check-color: derive(-fx-base-color, 30%);
-fx-base-rippler-color: %base-rippler-color%;
-fx-base-rippler-color: derive(%base-rippler-color%, 100%);
-fx-base-disabled-text-fill: %disabled-font-color%;
-fx-base-text-fill: %font-color%;
+104 -5
View File
@@ -18,6 +18,10 @@
.root {
}
.svg {
-fx-fill: black;
}
.scroll-bar .thumb {
-fx-fill: rgba(255, 255, 255, 0.5);
-fx-arc-width: 5px;
@@ -49,17 +53,81 @@
}
.rippler-container {
-jfx-rippler-fill: -fx-base-check-color;
-jfx-rippler-fill: derive(-fx-base-color, 100%);
}
.rippler-container:selected .label {
-fx-text-fill: -fx-base-text-fill;
}
.advanced-list-item .container {
-fx-padding: 10 16 10 16;
-fx-background-color: null;
}
.advanced-list-item .container VBox {
-fx-padding: 0 0 0 10;
}
.advanced-list-item .container .image-container {
-fx-max-width: 32;
-fx-min-width: 32;
-fx-max-height: 32;
-fx-min-height: 32;
}
.advanced-list-item .container .title {
-fx-font-size: 13;
-fx-text-alignment: justify;
}
.advanced-list-item .container .subtitle {
-fx-font-size: 10;
-fx-text-alignment: justify;
}
.advanced-list-item:selected .container {
-fx-background-color: -fx-base-rippler-color;
}
.advanced-list-item:selected .container .title {
-fx-text-fill: -fx-base-color;
-fx-font-weight: bold;
}
.advanced-list-item:selected .svg {
-fx-fill: -fx-base-color;
}
.navigation-drawer-item .container VBox {
-fx-padding: 0 0 0 0;
}
.navigation-drawer-item .two-line-list-item .title {
-fx-font-size: 13;
}
.navigation-drawer-item .two-line-list-item .subtitle {
-fx-font-size: 10;
}
.profile-list-item BorderPane {
-fx-padding: 8 0 8 16;
}
.profile-list-item:selected BorderPane {
-fx-background-color: -fx-base-rippler-color;
}
.profile-list-item:selected .two-line-list-item .title {
-fx-text-fill: -fx-base-color;
-fx-font-weight: bold;
}
.profile-list-item:selected .svg {
-fx-fill: -fx-base-color;
}
.notice-pane > .label {
-fx-text-fill: #0079FF;
-fx-font-size: 20;
@@ -69,15 +137,15 @@
.class-title {
-fx-font-size: 12px;
-fx-padding: 0 16 0 16;
-fx-padding: 16 16 8 16;
}
.advanced-list-box-item {
-fx-padding: 0 16 0 16;
}
.advanced-list-box-content {
-fx-padding: 20 0 20 0;
-fx-padding: 12 0 12 0;
-fx-spacing: 0;
}
.iconed-item {
@@ -722,16 +790,47 @@
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 5, 0.06, -0.5, 1);
}
.depth-0 {
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0), 0, 0, 0, 0);
}
.depth-1 {
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 10, 0.12, -1, 2);
}
.depth-2 {
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 15, 0.16, 0, 4);
}
.depth-3 {
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 20, 0.19, 0, 6);
}
.depth-4 {
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 25, 0.25, 0, 8);
}
.depth-5 {
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 30, 0.3, 0, 10);
}
.card {
-fx-background-color: WHITE;
-fx-background-color: rgba(255, 255, 255, 0.8);
-fx-background-radius: 4;
-fx-padding: 8px;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 10, 0.12, -1, 2);
}
.card:selected {
-fx-background-color: derive(-fx-base-color, 60%);
}
.card-list {
-fx-padding: 10;
-fx-spacing: 10;
}
.options-sublist {
-fx-background-color: white;
}
@@ -11,7 +11,7 @@
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
<ScrollPane fx:id="scroll" fitToHeight="true" fitToWidth="true" vbarPolicy="ALWAYS">
<VBox fx:id="rootPane" style="-fx-padding: 10;" spacing="12">
<VBox fx:id="rootPane" styleClass="card-list">
<ComponentList fx:id="iconPickerItemWrapper">
<ImagePickerItem fx:id="iconPickerItem" title="%settings.icon" onSelectButtonClicked="#onExploreIcon" onDeleteButtonClicked="#onDeleteIcon">
@@ -53,7 +53,7 @@
</JFXTextField>
</right>
</BorderPane>
<BorderPane> <!-- Launcher Visibility -->
<left>
<Label text="%settings.advanced.launcher_visible" BorderPane.alignment="CENTER_LEFT"/>
@@ -128,6 +128,9 @@
fx:id="txtPrecallingCommand" StackPane.margin="$insets"/>
<JFXTextField labelFloat="true" promptText="%settings.advanced.server_ip" styleClass="fit-width"
fx:id="txtServerIP" StackPane.margin="$insets"/>
<MultiFileItem fx:id="nativesDirItem" title="%settings.advanced.natives_directory" chooserTitle="%settings.advanced.natives_directory.choose"
hasSubtitle="true" customText="%settings.custom" directory="true" />
<BorderPane>
<left>
<Label BorderPane.alignment="CENTER_LEFT" text="%settings.advanced.no_jvm_args"/>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -18,7 +18,7 @@
# Contributors: dxNeil, machinesmith42
about.copyright=Copyright
about.copyright.statement=Copyright (c) 2020 huangyuhui.
about.copyright.statement=Copyright (c) 2021 huangyuhui.
about.author=Author
about.author.statement=huanghongxun (hmcl@huangyuhui.net)
about.thanks_to=Thanks to
@@ -98,6 +98,74 @@ color.custom=Custom Color
crash.NoClassDefFound=Please verify that the "Hello Minecraft! Launcher" software is not corrupted.
crash.user_fault=Your OS or Java environment may not be properly installed which may result in a crash, please check your Java Runtime Environment or your computer!
curse.category.0=All
# https://addons-ecs.forgesvc.net/api/v2/category/section/4471
curse.category.4474=Sci-Fi
curse.category.4481=Small / Light
curse.category.4483=Combat
curse.category.4477=Mini Game
curse.category.4478=Quests
curse.category.4484=Multiplayer
curse.category.4476=Exploration
curse.category.4736=Skyblock
curse.category.4475=Adventure and RPG
curse.category.4487=FTB
curse.category.4480=Map Based
curse.category.4479=Hardcore
curse.category.4482=Extra Large
curse.category.4472=Tech
curse.category.4473=Magic
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
curse.category.423=Map and Information
curse.category.426=Addons
curse.category.434=Armor, Tools, and Weapons
curse.category.409=Structures
curse.category.4485=Blood Magic
curse.category.420=Storage
curse.category.429=Industrial Craft
curse.category.419=Magic
curse.category.412=Technology
curse.category.4557=Redstone
curse.category.428=Tinker's Construct
curse.category.414=Player Transport
curse.category.4486=Lucky Blocks
curse.category.432=Buildcraft
curse.category.418=Genetics
curse.category.4671=Twitch Integration
curse.category.408=Ores and Resources
curse.category.4773=CraftTweaker
curse.category.430=Thaumcraft
curse.category.422=Adventure and RPG
curse.category.413=Processing
curse.category.417=Energy
curse.category.415=Energy, Fluid and Item Transport
curse.category.433=Forestry
curse.category.425=Miscellaneous
curse.category.4545=Applied Energistics 2
curse.category.416=Farming
curse.category.421=API and Library
curse.category.4780=Fabric
curse.category.424=Cosmetic
curse.category.406=World gen
curse.category.435=Server Utility
curse.category.411=Mobs
curse.category.407=Biomes
curse.category.427=Thermal Expansion
curse.category.410=Dimensions
curse.category.436=Food
curse.category.4558=Redstone
curse.category.4843=Automation
curse.category.4906=MCreator
curse.sort.author=Author
curse.sort.date_created=Date Created
curse.sort.last_updated=Last Updated
curse.sort.name=Name
curse.sort.popularity=Popularity
curse.sort.total_downloads=Total Downloads
download=Download
download.code.404=File not found on the remote server: %s
download.failed=Failed to download %1$s, response code: %2$d
@@ -196,7 +264,7 @@ launch.state.dependencies=Dependencies
launch.state.done=Done
launch.state.logging_in=Logging In
launch.state.modpack=Loading modpack
launch.state.waiting_launching=Launching modpack
launch.state.waiting_launching=Launching Game
launch.wrong_javadir=Invalid Java directory, default Java path will be applied.
launcher=Launcher
@@ -242,6 +310,7 @@ modpack.choose.remote.detail=Requires a direct download link to the remote modpa
modpack.choose.remote.tooltip=A direct download link to the remote modpack file
modpack.desc=Describe your modpack, including precautions and changelog. Markdown and online pictures are supported.
modpack.description=Description
modpack.download=Modpack Downloads
modpack.enter_name=Enter a name for this modpack.
modpack.export=Export Modpack
modpack.export.as=Export Modpack As...
@@ -307,6 +376,7 @@ mods.add=Install mods
mods.add.failed=Failed to install mods %s.
mods.add.success=Successfully installed mods %s.
mods.choose_mod=Choose your mods
mods.download=Mod Downloads
mods.enable=Enable
mods.disable=Disable
mods.name=Name
@@ -364,13 +434,16 @@ settings.advanced.dont_check_jvm_validity=Don't check whether JVM can launch the
settings.advanced.game_dir.default=Standard (.minecraft/)
settings.advanced.game_dir.independent=Independent (.minecraft/versions/<version name>/, except for assets and libraries)
settings.advanced.java_permanent_generation_space=PermGen Space/MB
settings.advanced.jvm_args=Java VM Arguments
settings.advanced.jvm_args=Java VM Arguments (overriding defaults)
settings.advanced.launcher_visibility.close=Close the launcher when the game launched.
settings.advanced.launcher_visibility.hide=Hide the launcher when the game launched.
settings.advanced.launcher_visibility.hide_and_reopen=Hide the launcher and re-open when game closes.
settings.advanced.launcher_visibility.keep=Keep the launcher visible.
settings.advanced.launcher_visible=Launcher Visibility
settings.advanced.minecraft_arguments=Minecraft Arguments
settings.advanced.natives_directory=Local Library Path (LWJGL)
settings.advanced.natives_directory.choose=Choose Local Library Path
settings.advanced.natives_directory.default=Standerd (.minecraft/versions/<version name>/natives/)
settings.advanced.no_jvm_args=No Default JVM Args
settings.advanced.precall_command=Pre-Launch command (will be executed before game starts)
settings.advanced.server_ip=Server Address
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -18,7 +18,7 @@
# Contributors: poopingpenis
about.copyright=Copyright
about.copyright.statement=Copyright (c) 2020 huangyuhui.
about.copyright.statement=Copyright (c) 2021 huangyuhui.
about.author=Autor
about.author.statement=huanghongxun (hmcl@huangyuhui.net)
about.thanks_to=Gracias a
@@ -335,13 +335,16 @@ settings.advanced.dont_check_jvm_validity=No verificar si JVM puede lanzar el ju
settings.advanced.game_dir.default=Por defecto (.minecraft/)
settings.advanced.game_dir.independent=Independiente (.minecraft/versions/<version name>/, excepto assets,libraries)
settings.advanced.java_permanent_generation_space=PermGen Space/MB
settings.advanced.jvm_args=Java VM Arguments
settings.advanced.jvm_args=Java VM Arguments (overriding defaults)
settings.advanced.launcher_visibility.close=Cerrar launcher cuando juego es iniciado.
settings.advanced.launcher_visibility.hide=Esconder launcher cuando juego is iniciado.
settings.advanced.launcher_visibility.hide_and_reopen=Esconder launcher y reabrir cuando juego cierre.
settings.advanced.launcher_visibility.keep=Mantener launcher visible.
settings.advanced.launcher_visible=Visibilidad de launcher
settings.advanced.minecraft_arguments=Minecraft Arguments
settings.advanced.natives_directory=Loka biblioteko- vojo (LWJGL)
settings.advanced.natives_directory.choose=Elektu lokan bibliotekvojn
settings.advanced.natives_directory.default=Por defecto (.minecraft/versions/<version name>/natives/)
settings.advanced.no_jvm_args=No JVM Args por defecto
settings.advanced.precall_command=Comando pre-inicio (será ejecutado antes de inicio del juego)
settings.advanced.server_ip=Host de servidor
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -18,7 +18,7 @@
# Содействующие: Botylyov Yury, The_BadUser
about.copyright=Авторские права
about.copyright.statement=(C) 2020 huangyuhui.
about.copyright.statement=(C) 2021 huangyuhui.
about.author=Автор
about.author.statement=huanghongxun (hmcl@huangyuhui.net)
about.thanks_to=Отдельное спасибо
@@ -359,6 +359,9 @@ settings.advanced.launcher_visibility.hide_and_reopen=Сворачивать л
settings.advanced.launcher_visibility.keep=Оставлять лаунчер видимым.
settings.advanced.launcher_visible=Видимость лаунчера
settings.advanced.minecraft_arguments=Параметры Minecraft
settings.advanced.natives_directory=путь к локальной библиотеке (LWJGL)
settings.advanced.natives_directory.choose=Выберите путь к локальной библиотеке
settings.advanced.natives_directory.default=По умолчанию (.minecraft/versions/<версия>/natives/)
settings.advanced.no_jvm_args=По умолчанию параметры JVM отсутствуют
settings.advanced.precall_command=Команда предзапуска (будет выполнено до запуска игры)
settings.advanced.server_ip=Адрес сервера
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -18,7 +18,7 @@
# Contributors: pan93412
about.copyright=著作權
about.copyright.statement=著作權所有 (c) 2020 huangyuhui。
about.copyright.statement=著作權所有 (c) 2021 huangyuhui。
about.author=作者
about.author.statement=huanghongxun (hmcl@huangyuhui.net)
about.thanks_to=鳴謝
@@ -358,13 +358,16 @@ settings.advanced.dont_check_jvm_validity=不檢查 JVM 與遊戲的相容性
settings.advanced.game_dir.default=預設(.minecraft/)
settings.advanced.game_dir.independent=各版本獨立(.minecraft/versions/<版本名>/,除 assets、libraries)
settings.advanced.java_permanent_generation_space=記憶體永久儲存區域(不必填寫,格式: MB)
settings.advanced.jvm_args=Java 虛擬機參數(不必填寫)
settings.advanced.jvm_args=Java 虛擬機參數(不必填寫,覆蓋默認設置)
settings.advanced.launcher_visibility.close=遊戲啟動後結束啟動器
settings.advanced.launcher_visibility.hide=遊戲啟動後隱藏啟動器
settings.advanced.launcher_visibility.hide_and_reopen=隱藏啟動器並在遊戲結束後重新開啟
settings.advanced.launcher_visibility.keep=不隱藏啟動器
settings.advanced.launcher_visible=啟動器可見性
settings.advanced.minecraft_arguments=Minecraft 額外參數(不必填寫)
settings.advanced.natives_directory=本地庫路徑(LWJGL)
settings.advanced.natives_directory.choose=選擇本地庫路徑
settings.advanced.natives_directory.default=預設(.minecraft/versions/<版本名>/natives/)
settings.advanced.no_jvm_args=不使用預設的 JVM 參數
settings.advanced.precall_command=啟動前執行指令(不必填寫,將在遊戲啟動前呼叫使用)
settings.advanced.server_ip=直接進入伺服器 IP 位址(不必填寫,啟動遊戲後直接進入對應伺服器)
@@ -458,6 +461,7 @@ version.manage.duplicate.duplicate_save=複製存檔
version.manage.duplicate.prompt=請輸入新遊戲實例名稱
version.manage.duplicate.confirm=將鎖定複製產生的新遊戲實例:強制版本隔離、遊戲設置獨立。
version.manage.manage=遊戲管理
version.manage.manage.title=遊戲管理 - %1s
version.manage.redownload_assets_index=更新遊戲資源檔案
version.manage.remove=刪除該版本
version.manage.remove.confirm=真的要刪除版本 %s 嗎? 你將無法找回被刪除的檔案!
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -18,11 +18,11 @@
# Contributors: huangyuhui
about.copyright=版权
about.copyright.statement=版权所有 (c) 2020 huangyuhui.
about.copyright.statement=版权所有 (c) 2021 huangyuhui.
about.author=作者
about.author.statement=huanghongxun (hmcl@huangyuhui.net)
about.thanks_to=鸣谢
about.thanks_to.statement=bangbang93 (BMCLAPI, https://bmclapi2.bangbang93.com/)\ngamerteam (默认背景图)\n所有通过 Issues、Pull Requests 等方式参与本项目的贡献者
about.thanks_to.statement=yushijinhun (authlib-injector 相关支持)\nbangbang93 (BMCLAPI, https://bmclapi2.bangbang93.com/)\ngamerteam (默认背景图)\n所有通过 Issues、Pull Requests 等方式参与本项目的贡献者
about.dependency=依赖
# 由于篇幅限制,仅列出第一作者
about.dependency.statement=JFoenix (Shadi Shaheen, Apache 2.0)\nGson (Google, Apache 2.0)\nApache Commons Compress (ASF, Apache 2.0)\nXZ for Java (Lasse Collin, Public Domain)\nfx-gson (Joffrey Bion, MIT)\nConstant Pool Scanner (jenkins-ci, CDDL, GPL 2)\nOpenNBT (Steveice10, BSD 3-Clause)
@@ -98,6 +98,74 @@ color.custom=自定义颜色
crash.NoClassDefFound=请确认 Hello Minecraft! Launcher 本体是否完整,或更新您的 Java。
crash.user_fault=您的系统或 Java 环境可能安装不当导致本软件崩溃,请检查您的 Java 环境或您的电脑!可以尝试重新安装 Java。
curse.category.0=全部
# https://addons-ecs.forgesvc.net/api/v2/category/section/4471
curse.category.4474=科幻
curse.category.4481=轻量整合包
curse.category.4483=战斗 PVP
curse.category.4477=小游戏
curse.category.4478=任务
curse.category.4484=多人
curse.category.4476=探索
curse.category.4736=空岛
curse.category.4475=冒险 RPG
curse.category.4487=FTB 整合包
curse.category.4480=有特定地图
curse.category.4479=高难度
curse.category.4482=大型整合包
curse.category.4472=科技
curse.category.4473=魔法
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
curse.category.423=信息展示
curse.category.426=模组扩展
curse.category.434=装备武器
curse.category.409=自然生成
curse.category.4485=血魔法
curse.category.420=存储
curse.category.429=工业 (Industrialcraft)
curse.category.419=魔法
curse.category.412=科技
curse.category.4557=红石
curse.category.428=匠魂
curse.category.414=交通运输
curse.category.4486=Lucky Blocks
curse.category.432=建筑 (Buildcraft)
curse.category.418=基因
curse.category.4671=Twitch
curse.category.408=矿物资源
curse.category.4773=CraftTweaker
curse.category.430=神秘 (Thaumcraft)
curse.category.422=冒险 RPG
curse.category.413=机器处理
curse.category.417=能源
curse.category.415=物流运输
curse.category.433=林业 (Forestry)
curse.category.425=其他
curse.category.4545=应用能源 2 (Applied Energistics 2)
curse.category.416=农业
curse.category.421=支持库
curse.category.4780=Fabric
curse.category.424=装饰
curse.category.406=世界生成
curse.category.435=服务器
curse.category.411=生物
curse.category.407=生物群系
curse.category.427=热力膨胀 (Thermal Expansion)
curse.category.410=维度
curse.category.436=食物
curse.category.4558=红石
curse.category.4843=自动化
curse.category.4906=MCreator
curse.sort.author=作者
curse.sort.date_created=创建日期
curse.sort.last_updated=最近更新
curse.sort.name=名称
curse.sort.popularity=热度
curse.sort.total_downloads=下载量
download=下载
download.code.404=远程服务器不包含需要下载的文件: %s
download.failed=下载失败: %1$s,错误码:%2$d
@@ -242,6 +310,7 @@ modpack.choose.remote.detail=需要提供整合包的下载链接
modpack.choose.remote.tooltip=要下载的整合包的链接
modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录,支持 HTML(图片请用网络图)。
modpack.description=整合包描述
modpack.download=下载整合包
modpack.enter_name=给游戏起个你喜欢的名字
modpack.export=导出整合包
modpack.export.as=请选择整合包类型 (若无法决定,请选择我的世界中文论坛整合包标准)
@@ -307,9 +376,12 @@ mods.add=添加模组
mods.add.failed=添加模组 %s 失败。
mods.add.success=成功添加模组 %s。
mods.choose_mod=选择模组
mods.download=模组下载
mods.download.title=模组下载 - %1s
mods.enable=启用
mods.disable=禁用
mods.name=名称
mods.category=类别
mods.not_modded=你需要先在自动安装页面安装 Fabric、Forge 或 LiteLoader 才能进行模组管理。
datapack=数据包
@@ -347,11 +419,14 @@ profile.instance_directory=游戏路径
profile.instance_directory.choose=选择游戏路径
profile.manage=游戏目录列表
profile.name=名称
profile.new=新建配置
profile.new=添加游戏目录
profile.title=游戏目录
profile.selected=已选中
profile.use_relative_path=若可能,游戏目录使用相对路径
search=搜索
search.sort=排序
selector.choose=选择
selector.choose_file=选择文件
selector.custom=自定义
@@ -364,13 +439,16 @@ settings.advanced.dont_check_jvm_validity=不检查 JVM 与游戏的兼容性
settings.advanced.game_dir.default=默认(.minecraft/)
settings.advanced.game_dir.independent=各版本独立(.minecraft/versions/<版本名>/,除 assets、libraries)
settings.advanced.java_permanent_generation_space=内存永久保存区域(不必填写,单位 MB)
settings.advanced.jvm_args=Java 虚拟机参数(不必填写)
settings.advanced.jvm_args=Java 虚拟机参数(不必填写,覆盖默认设置)
settings.advanced.launcher_visibility.close=游戏启动后结束启动器
settings.advanced.launcher_visibility.hide=游戏启动后隐藏启动器
settings.advanced.launcher_visibility.hide_and_reopen=隐藏启动器并在游戏结束后重新打开
settings.advanced.launcher_visibility.keep=保持启动器可见
settings.advanced.launcher_visible=启动器可见性
settings.advanced.minecraft_arguments=Minecraft 额外参数(不必填写)
settings.advanced.natives_directory=本地库路径(LWJGL)
settings.advanced.natives_directory.choose=选择本地库路径
settings.advanced.natives_directory.default=预设(.minecraft/versions/<版本名>/natives/)
settings.advanced.no_jvm_args=不添加默认的 JVM 参数
settings.advanced.precall_command=启动前执行命令(不必填写,将在游戏启动前调用)
settings.advanced.server_ip=直入服务器 IP 地址(不必填写,启动游戏后直接进入对应服务器)
@@ -449,7 +527,7 @@ version.empty.add=进入版本列表安装
version.empty.launch=没有可启动的游戏,你可以点击左侧游戏栏内的设置按钮进入版本列表安装游戏
version.forbidden_name=此版本名称不受支持,请换一个名字
version.game.old=远古版
version.game.release=稳定版
version.game.release=正式版
version.game.snapshot=测试版
version.launch=启动游戏
version.launch.test=测试游戏
@@ -465,6 +543,7 @@ version.manage.duplicate.duplicate_save=复制存档
version.manage.duplicate.prompt=请输入新游戏实例名称
version.manage.duplicate.confirm=将锁定复制产生的新游戏实例:强制版本隔离、游戏设置独立。
version.manage.manage=游戏管理
version.manage.manage.title=游戏管理 - %1s
version.manage.redownload_assets_index=更新游戏资源文件
version.manage.remove=删除该版本
version.manage.remove.confirm=真的要删除版本 %s 吗?你将无法找回被删除的文件!
@@ -0,0 +1,68 @@
[
{
"module": "javafx.base",
"groupId": "org.openjfx",
"artifactId": "javafx-base",
"version": "16",
"sha1": {
"linux": "7d6b85ec89e99ea40c2a63bb1f23d43e05cd2557",
"mac": "ea97d8a5ab95d070df86de1215f29f08378c98c8",
"win": "e0564fea3f27dd3a10fa62e5005612333fc244bc"
}
},
{
"module": "javafx.controls",
"groupId": "org.openjfx",
"artifactId": "javafx-controls",
"version": "16",
"sha1": {
"linux": "116b127e512d23ddb84c62017c256e7f67f5b9eb",
"mac": "8df933b095bdd8203dc937e39e3a9f199654e47e",
"win": "65cdeae29c67d25932dcc66ca4f7d269923631ba"
}
},
{
"module": "javafx.fxml",
"groupId": "org.openjfx",
"artifactId": "javafx-fxml",
"version": "16",
"sha1": {
"linux": "c7147c450773c3d4fd038ac9d1a6fdebbc3c11e0",
"mac": "7e4f07a331991485560673a398d6988cc805b67f",
"win": "369f646fc29d2223cc98cfc3c059f2409b3e8fcb"
}
},
{
"module": "javafx.graphics",
"groupId": "org.openjfx",
"artifactId": "javafx-graphics",
"version": "16",
"sha1": {
"linux": "01bf2fd9f083daa2f492c4c58c4f7d5acb6f4b7d",
"mac": "a45de3d40e217f7ba22cecf54abc281d13a172f6",
"win": "0cd18f8477818b40cfdbcfec7e2fc15f632f0a2f"
}
},
{
"module": "javafx.media",
"groupId": "org.openjfx",
"artifactId": "javafx-media",
"version": "16",
"sha1": {
"linux": "10edb5fd8eb64b3312b31225c396e1850b9d178b",
"mac": "9e0248083267cdc22b2b698f0b0b62b7399ad32d",
"win": "6d776658906fc25051734bc6b03f214cc72c808e"
}
},
{
"module": "javafx.web",
"groupId": "org.openjfx",
"artifactId": "javafx-web",
"version": "16",
"sha1": {
"linux": "522f626e1798a7d589d9b187ca5c74c5c2f8a0ee",
"mac": "32b0e13b649ea828e7d185187f178680cc04e564",
"win": "d2f80089a4ae1629a5ec14637abf4f6d70b96dab"
}
}
]
@@ -17,19 +17,19 @@
*/
package org.jackhuang.hmcl.auth.authlibinjector;
import static org.jackhuang.hmcl.util.io.NetworkUtils.decodeURL;
import java.util.Optional;
import java.util.function.Consumer;
import javafx.event.EventHandler;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import java.util.Optional;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.util.io.NetworkUtils.decodeURL;
/**
* @author yushijinhun
* @see https://github.com/yushijinhun/authlib-injector/wiki/%E5%90%AF%E5%8A%A8%E5%99%A8%E6%8A%80%E6%9C%AF%E8%A7%84%E8%8C%83#dnd-%E6%96%B9%E5%BC%8F%E6%B7%BB%E5%8A%A0-yggdrasil-%E6%9C%8D%E5%8A%A1%E7%AB%AF
* @see <a href="https://github.com/yushijinhun/authlib-injector/wiki/%E5%90%AF%E5%8A%A8%E5%99%A8%E6%8A%80%E6%9C%AF%E8%A7%84%E8%8C%83#dnd-%E6%96%B9%E5%BC%8F%E6%B7%BB%E5%8A%A0-yggdrasil-%E6%9C%8D%E5%8A%A1%E7%AB%AF">Launcher Technical Specification for Authlib-Injector</a>
*/
public final class AuthlibInjectorDnD {
@@ -31,7 +31,7 @@ import static org.jackhuang.hmcl.util.Lang.tryCast;
*
* @author huangyuhui
*/
public class OfflineAccountFactory extends AccountFactory<OfflineAccount> {
public final class OfflineAccountFactory extends AccountFactory<OfflineAccount> {
public static final OfflineAccountFactory INSTANCE = new OfflineAccountFactory();
private OfflineAccountFactory() {
@@ -17,20 +17,13 @@
*/
package org.jackhuang.hmcl.auth.yggdrasil;
import static java.util.Collections.unmodifiableMap;
import com.google.gson.*;
import java.lang.reflect.Type;
import java.util.LinkedHashMap;
import java.util.Map;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import static java.util.Collections.unmodifiableMap;
public class PropertyMapSerializer implements JsonSerializer<Map<String, String>>, JsonDeserializer<Map<String, String>> {
@@ -57,4 +50,4 @@ public class PropertyMapSerializer implements JsonSerializer<Map<String, String>
});
return result;
}
}
}
@@ -17,8 +17,6 @@
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
@@ -78,7 +78,6 @@ public class BMCLAPIDownloadProvider implements DownloadProvider {
@Override
public String injectURL(String baseURL) {
if (baseURL.contains("v1/products/java-runtime")) return baseURL;
return baseURL
.replace("https://bmclapi2.bangbang93.com", apiRoot)
.replace("https://launchermeta.mojang.com", apiRoot)
@@ -33,6 +33,7 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.OPTIFINE;
@@ -130,9 +131,14 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
public Task<Version> installLibraryAsync(Version baseVersion, RemoteVersion libraryVersion) {
if (baseVersion.isResolved()) throw new IllegalArgumentException("Version should not be resolved");
AtomicReference<Version> removedLibraryVersion = new AtomicReference<>();
return removeLibraryAsync(baseVersion.resolvePreservingPatches(repository), libraryVersion.getLibraryId())
.thenComposeAsync(version -> libraryVersion.getInstallTask(this, version))
.thenApplyAsync(baseVersion::addPatch)
.thenComposeAsync(version -> {
removedLibraryVersion.set(version);
return libraryVersion.getInstallTask(this, version);
})
.thenApplyAsync(patch -> removedLibraryVersion.get().addPatch(patch))
.withStage(String.format("hmcl.install.%s:%s", libraryVersion.getLibraryId(), libraryVersion.getSelfVersion()));
}
@@ -38,7 +38,7 @@ public abstract class GameBuilder {
}
/**
* The new game version name, for .minecraft/<version name>.
* The new game version name, for .minecraft/&lt;version name&gt;.
*
* @param name the name of new game version.
*/
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.game.VersionProvider;
import org.jackhuang.hmcl.util.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -84,6 +85,11 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
return modLauncher.equals(version.getMainClass()) || version.getPatches().stream().anyMatch(patch -> modLauncher.equals(patch.getMainClass()));
}
public boolean hasBootstrapLauncher() {
final String bootstrapLauncher = "cpw.mods.bootstraplauncher.BootstrapLauncher";
return bootstrapLauncher.equals(version.getMainClass()) || version.getPatches().stream().anyMatch(patch -> bootstrapLauncher.equals(patch.getMainClass()));
}
private Version removingMatchedLibrary(Version version, String libraryId) {
LibraryType type = LibraryType.fromPatchId(libraryId);
if (type == null) return version;
@@ -141,10 +147,16 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
return new LibraryAnalyzer(version, libraries);
}
public static boolean isModded(VersionProvider provider, Version version) {
Version resolvedVersion = version.resolve(provider);
String mainClass = resolvedVersion.getMainClass();
return mainClass != null && (LAUNCH_WRAPPER_MAIN.equals(mainClass) || mainClass.startsWith("net.fabricmc") || mainClass.startsWith("cpw.mods"));
}
public enum LibraryType {
MINECRAFT(true, "game", Pattern.compile("^$"), Pattern.compile("^$")),
FABRIC(true, "fabric", Pattern.compile("net\\.fabricmc"), Pattern.compile("fabric-loader")),
FORGE(true, "forge", Pattern.compile("net\\.minecraftforge"), Pattern.compile("forge")),
FORGE(true, "forge", Pattern.compile("net\\.minecraftforge"), Pattern.compile("(forge|fmlloader)")),
LITELOADER(true, "liteloader", Pattern.compile("com\\.mumfrey"), Pattern.compile("liteloader")),
OPTIFINE(false, "optifine", Pattern.compile("(net\\.)?optifine"), Pattern.compile("^(?!.*launchwrapper).*$"));
@@ -202,4 +214,5 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
public static final String VANILLA_MAIN = "net.minecraft.client.main.Main";
public static final String LAUNCH_WRAPPER_MAIN = "net.minecraft.launchwrapper.Launch";
public static final String MOD_LAUNCHER_MAIN = "cpw.mods.modlauncher.Launcher";
public static final String BOOTSTRAP_LAUNCHER_MAIN = "cpw.mods.bootstraplauncher.BootstrapLauncher";
}
@@ -17,29 +17,26 @@
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.game.Artifact;
import org.jackhuang.hmcl.game.CompatibilityRule;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.game.VersionLibraryBuilder;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.SimpleMultimap;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.FORGE;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.LITELOADER;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.OPTIFINE;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
public class MaintainTask extends Task<Version> {
private final GameRepository repository;
@@ -68,7 +65,10 @@ public class MaintainTask extends Task<Version> {
return maintainOptiFineLibrary(repository, maintainGameWithLaunchWrapper(unique(version), true), false);
} else if (mainClass != null && mainClass.equals(LibraryAnalyzer.MOD_LAUNCHER_MAIN)) {
// Forge 1.13 and OptiFine
return maintainOptiFineLibrary(repository, maintainGameWithModLauncher(repository, unique(version)), true);
return maintainOptiFineLibrary(repository, maintainGameWithCpwModLauncher(repository, unique(version)), true);
} else if (mainClass != null && mainClass.equals(LibraryAnalyzer.BOOTSTRAP_LAUNCHER_MAIN)) {
// Forge 1.17
return maintainGameWithCpwBoostrapLauncher(repository, unique(version));
} else {
// Vanilla Minecraft does not need maintain
// Fabric does not need maintain, nothing compatible with fabric now.
@@ -122,7 +122,7 @@ public class MaintainTask extends Task<Version> {
return mainClass == null ? ret : ret.setMainClass(mainClass);
}
private static Version maintainGameWithModLauncher(GameRepository repository, Version version) {
private static Version maintainGameWithCpwModLauncher(GameRepository repository, Version version) {
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version);
VersionLibraryBuilder builder = new VersionLibraryBuilder(version);
@@ -133,7 +133,7 @@ public class MaintainTask extends Task<Version> {
Optional<Library> optiFine = version.getLibraries().stream().filter(library -> library.is("optifine", "OptiFine")).findAny();
boolean libraryExisting = version.getLibraries().stream().anyMatch(library -> library.is("org.jackhuang.hmcl", "transformer-discovery-service"));
optiFine.ifPresent(library -> {
builder.addJvmArgument("-Dhmcl.transformer.candidates=${libraries_directory}/" + library.getPath());
builder.addJvmArgument("-Dhmcl.transformer.candidates=${library_directory}/" + library.getPath());
if (!libraryExisting) builder.addLibrary(hmclTransformerDiscoveryService);
Path libraryPath = repository.getLibraryFile(version, hmclTransformerDiscoveryService).toPath();
try {
@@ -149,6 +149,59 @@ public class MaintainTask extends Task<Version> {
return builder.build();
}
private static String updateIgnoreList(GameRepository repository, Version version, String ignoreList) {
String[] ignores = ignoreList.split(",");
List<String> newIgnoreList = new ArrayList<>();
// To resolve the the problem that name of primary jar may conflict with the module naming convention,
// we need to manually ignore ${primary_jar}.
newIgnoreList.add("${primary_jar}");
Path libraryDirectory = repository.getLibrariesDirectory(version).toPath().toAbsolutePath();
// The default ignoreList is too loose and may cause some problems, we replace them with the absolute version.
// For example, if "client-extra" is in ignoreList, and game directory contains "client-extra" component, all
// libraries will be ignored, which is not expected.
for (String classpathName : repository.getClasspath(version)) {
Path classpathFile = Paths.get(classpathName).toAbsolutePath();
String fileName = classpathFile.getFileName().toString();
if (Stream.of(ignores).anyMatch(fileName::contains)) {
// This library should be ignored for Jigsaw module finding by Forge.
String absolutePath;
if (classpathFile.startsWith(libraryDirectory)) {
// Note: It's assumed using "/" instead of File.separator in classpath
absolutePath = "${library_directory}${file_separator}" + libraryDirectory.relativize(classpathFile).toString().replace(File.separator, "${file_separator}");
} else {
absolutePath = classpathFile.toString();
}
newIgnoreList.add(StringUtils.substringBefore(absolutePath, ","));
}
}
return String.join(",", newIgnoreList);
}
// Fix wrong configurations when launching 1.17+ with Forge.
private static Version maintainGameWithCpwBoostrapLauncher(GameRepository repository, Version version) {
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version);
VersionLibraryBuilder builder = new VersionLibraryBuilder(version);
if (!libraryAnalyzer.has(FORGE)) return version;
// The default ignoreList set by Forge installer does not fulfill our requirements
List<Argument> jvm = builder.getMutableJvmArguments();
for (int i = 0; i < jvm.size(); i++) {
Argument jvmArg = jvm.get(i);
if (jvmArg instanceof StringArgument) {
String jvmArgStr = jvmArg.toString();
if (jvmArgStr.startsWith("-DignoreList=")) {
jvm.set(i, new StringArgument("-DignoreList=" + updateIgnoreList(repository, version, jvmArgStr.substring("-DignoreList=".length()))));
}
}
}
return builder.build();
}
private static Version maintainOptiFineLibrary(GameRepository repository, Version version, boolean remove) {
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version);
List<Library> libraries = new ArrayList<>(version.getLibraries());
@@ -173,7 +173,7 @@ public final class ForgeInstallTask extends Task<Version> {
ForgeNewInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeNewInstallProfile.class);
if (!gameVersion.get().equals(profile.getMinecraft()))
throw new VersionMismatchException(profile.getMinecraft(), gameVersion.get());
return new ForgeNewInstallTask(dependencyManager, version, modifyVersion(gameVersion.get(), profile.getPath().getVersion().replaceAll("(?i)forge", "")), installer);
return new ForgeNewInstallTask(dependencyManager, version, modifyVersion(gameVersion.get(), profile.getVersion()), installer);
} else if (installProfile.containsKey("install") && installProfile.containsKey("versionInfo")) {
ForgeInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeInstallProfile.class);
if (!gameVersion.get().equals(profile.getInstall().getMinecraft()))
@@ -25,10 +25,7 @@ import org.jackhuang.hmcl.util.function.ExceptionalFunction;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
@Immutable
@@ -37,15 +34,17 @@ public class ForgeNewInstallProfile implements Validation {
private final int spec;
private final String minecraft;
private final String json;
private final String version;
private final Artifact path;
private final List<Library> libraries;
private final List<Processor> processors;
private final Map<String, Datum> data;
public ForgeNewInstallProfile(int spec, String minecraft, String json, Artifact path, List<Library> libraries, List<Processor> processors, Map<String, Datum> data) {
public ForgeNewInstallProfile(int spec, String minecraft, String json, String version, Artifact path, List<Library> libraries, List<Processor> processors, Map<String, Datum> data) {
this.spec = spec;
this.minecraft = minecraft;
this.json = json;
this.version = version;
this.path = path;
this.libraries = libraries;
this.processors = processors;
@@ -74,12 +73,20 @@ public class ForgeNewInstallProfile implements Validation {
return json;
}
/**
*
* @return forge version.
*/
public String getVersion() {
return version;
}
/**
* Maven artifact path for the main jar to install.
* @return artifact path of the main jar.
*/
public Artifact getPath() {
return path;
public Optional<Artifact> getPath() {
return Optional.ofNullable(path);
}
/**
@@ -112,7 +119,7 @@ public class ForgeNewInstallProfile implements Validation {
@Override
public void validate() throws JsonParseException, TolerableValidationException {
if (minecraft == null || json == null || path == null)
if (minecraft == null || json == null || version == null)
throw new JsonParseException("ForgeNewInstallProfile is malformed");
}
@@ -76,6 +76,50 @@ public class ForgeNewInstallTask extends Task<Version> {
setSignificance(TaskSignificance.MINOR);
}
private static String replaceTokens(Map<String, String> tokens, String value) {
StringBuilder buf = new StringBuilder();
for (int x = 0; x < value.length(); x++) {
char c = value.charAt(x);
if (c == '\\') {
if (x == value.length() - 1)
throw new IllegalArgumentException("Illegal pattern (Bad escape): " + value);
buf.append(value.charAt(++x));
} else if (c == '{' || c == '\'') {
StringBuilder key = new StringBuilder();
for (int y = x + 1; y <= value.length(); y++) {
if (y == value.length())
throw new IllegalArgumentException("Illegal pattern (Unclosed " + c + "): " + value);
char d = value.charAt(y);
if (d == '\\') {
if (y == value.length() - 1)
throw new IllegalArgumentException("Illegal pattern (Bad escape): " + value);
key.append(value.charAt(++y));
} else {
if (c == '{' && d == '}') {
x = y;
break;
}
if (c == '\'' && d == '\'') {
x = y;
break;
}
key.append(d);
}
}
if (c == '\'') {
buf.append(key);
} else {
if (!tokens.containsKey(key.toString()))
throw new IllegalArgumentException("Illegal pattern: " + value + " Missing Key: " + key);
buf.append(tokens.get(key.toString()));
}
} else {
buf.append(c);
}
}
return buf.toString();
}
private <E extends Exception> String parseLiteral(String literal, Map<String, String> var, ExceptionalFunction<String, String, E> plainConverter) throws E {
if (StringUtils.isSurrounded(literal, "{", "}"))
return var.get(StringUtils.removeSurrounding(literal, "{", "}"));
@@ -84,7 +128,7 @@ public class ForgeNewInstallTask extends Task<Version> {
else if (StringUtils.isSurrounded(literal, "[", "]"))
return gameRepository.getArtifactFile(version, Artifact.fromDescriptor(StringUtils.removeSurrounding(literal, "[", "]"))).toString();
else
return plainConverter.apply(literal);
return plainConverter.apply(replaceTokens(var, literal));
}
@Override
@@ -116,10 +160,10 @@ public class ForgeNewInstallTask extends Task<Version> {
}
}
{
Path mainJar = profile.getPath().getPath(fs.getPath("maven"));
if (profile.getPath().isPresent()) {
Path mainJar = profile.getPath().get().getPath(fs.getPath("maven"));
if (Files.exists(mainJar)) {
Path dest = gameRepository.getArtifactFile(version, profile.getPath());
Path dest = gameRepository.getArtifactFile(version, profile.getPath().get());
FileUtils.copyFile(mainJar, dest);
}
}
@@ -130,7 +174,7 @@ public class ForgeNewInstallTask extends Task<Version> {
@Override
public void execute() throws Exception {
Path temp = Files.createTempDirectory("forge_installer");
Path temp = Files.createTempDirectory("forge_installer");
int finished = 0;
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
List<ForgeNewInstallProfile.Processor> processors = profile.getProcessors();
@@ -153,6 +197,10 @@ public class ForgeNewInstallTask extends Task<Version> {
data.put("SIDE", "client");
data.put("MINECRAFT_JAR", gameRepository.getVersionJar(version).getAbsolutePath());
data.put("MINECRAFT_VERSION", gameRepository.getVersionJar(version).getAbsolutePath());
data.put("ROOT", gameRepository.getBaseDirectory().getAbsolutePath());
data.put("INSTALLER", installer.toAbsolutePath().toString());
data.put("LIBRARY_DIR", gameRepository.getLibrariesDirectory(version).getAbsolutePath());
for (ForgeNewInstallProfile.Processor processor : processors) {
Map<String, String> outputs = new HashMap<>();
@@ -105,7 +105,6 @@ public final class GameAssetIndexDownloadTask extends Task<Void> {
dependencies.add(task);
}
public static class GameAssetIndexMalformedException extends IOException {
}
}
@@ -19,12 +19,8 @@ package org.jackhuang.hmcl.download.game;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
@@ -33,11 +29,9 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
/**
* Remove class digital verification file in game jar
Loaded 100 of 162 files, more files were not shown because too many files have changed in this diff. Show more