Compare commits
141
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fb9fa0af9 | ||
|
|
d35fb9a486 | ||
|
|
37fb9a0d65 | ||
|
|
af7cf393dc | ||
|
|
7a20af462b | ||
|
|
0939ed819e | ||
|
|
5da4ad7a62 | ||
|
|
123c863e3d | ||
|
|
6fc0b13dc7 | ||
|
|
59bf8d3ebc | ||
|
|
805393b184 | ||
|
|
5d0d77aff4 | ||
|
|
d2e4ddc876 | ||
|
|
5b7ca8c751 | ||
|
|
16c7558497 | ||
|
|
9b6b3f8938 | ||
|
|
331d8d1bb6 | ||
|
|
e5cca24740 | ||
|
|
6f40335322 | ||
|
|
b6de9d4ea2 | ||
|
|
5f8f01e0ab | ||
|
|
0d69e944f6 | ||
|
|
1dc4e3f730 | ||
|
|
14d1bccd14 | ||
|
|
5de150b06e | ||
|
|
e34dd39b9a | ||
|
|
e7d5faeeac | ||
|
|
46a7269087 | ||
|
|
dc31674721 | ||
|
|
afe6bc7456 | ||
|
|
378fd04463 | ||
|
|
3e751ba5c6 | ||
|
|
8fc1ec7bb8 | ||
|
|
e5326a3c4d | ||
|
|
fe279234d8 | ||
|
|
ef9b041310 | ||
|
|
9db576d4bc | ||
|
|
a84971abfc | ||
|
|
a7a79db842 | ||
|
|
d5da14fb1e | ||
|
|
0d2e287f67 | ||
|
|
c329f1cafb | ||
|
|
ab1a2f6fcb | ||
|
|
463dc56702 | ||
|
|
88e23cd0d2 | ||
|
|
9776aafab6 | ||
|
|
fbb5a03e5d | ||
|
|
7c11ce53f3 | ||
|
|
66f4c022d5 | ||
|
|
4b36450f9a | ||
|
|
36b58bd1bb | ||
|
|
0296b6f73d | ||
|
|
447afaa76f | ||
|
|
a0191aa7c2 | ||
|
|
e7c9fcf767 | ||
|
|
fc017515c5 | ||
|
|
6d75c4d34f | ||
|
|
258a6628e4 | ||
|
|
228695d511 | ||
|
|
c2af530abb | ||
|
|
69bc826e30 | ||
|
|
5d0944ed19 | ||
|
|
43f4da5dd3 | ||
|
|
c6ab2cec35 | ||
|
|
b0dc689ba7 | ||
|
|
e11b1f37b9 | ||
|
|
3e5fac4df6 | ||
|
|
e63fa40822 | ||
|
|
e01096bff0 | ||
|
|
2a0cfb1125 | ||
|
|
d4f0a04ccd | ||
|
|
7cbe24b35b | ||
|
|
b7da21e5ce | ||
|
|
0ae1411f0a | ||
|
|
66889cac27 | ||
|
|
67bd2ac386 | ||
|
|
1f278c2a49 | ||
|
|
1674f1828f | ||
|
|
472a0a039f | ||
|
|
61597cb83d | ||
|
|
2f17552b2c | ||
|
|
f2d3d24338 | ||
|
|
e8a64f52f0 | ||
|
|
f55fe0fc45 | ||
|
|
1e3a5f9515 | ||
|
|
a9c7eb0954 | ||
|
|
289472d8ba | ||
|
|
f191e80100 | ||
|
|
607248e8b3 | ||
|
|
cecabe5bc4 | ||
|
|
947a2e3d91 | ||
|
|
382e0f3b1d | ||
|
|
6e440783dc | ||
|
|
fcd7d8ba83 | ||
|
|
a53d2a1370 | ||
|
|
20148d0b02 | ||
|
|
3114e33067 | ||
|
|
e137d72b76 | ||
|
|
cc087dee81 | ||
|
|
eed475cb28 | ||
|
|
d07c45f0b5 | ||
|
|
0aa0fa1b78 | ||
|
|
e91ec13dbd | ||
|
|
e36105d937 | ||
|
|
07d4df312d | ||
|
|
b521c5b76a | ||
|
|
c1596443f5 | ||
|
|
cadeb1f59c | ||
|
|
2d4223f7ed | ||
|
|
c453bbc7cd | ||
|
|
2184961e3c | ||
|
|
8afd8bde8b | ||
|
|
65952a5d81 | ||
|
|
ac1f2ec1ed | ||
|
|
454cc1e702 | ||
|
|
e3fa7428bf | ||
|
|
7ece35e28a | ||
|
|
d22dae9834 | ||
|
|
63bd707741 | ||
|
|
734c1e14f0 | ||
|
|
46869ad5f7 | ||
|
|
13fa713d58 | ||
|
|
94cd33af5c | ||
|
|
7efd9607e5 | ||
|
|
94539ad77b | ||
|
|
f2a86857bc | ||
|
|
3f2be63d08 | ||
|
|
2d7c1393d6 | ||
|
|
3d33230e25 | ||
|
|
dd4683e693 | ||
|
|
8bc5d2112f | ||
|
|
a828896989 | ||
|
|
5e8734aa95 | ||
|
|
1ac844f398 | ||
|
|
3f85ac208c | ||
|
|
c191186023 | ||
|
|
4715a95a54 | ||
|
|
8108f11c45 | ||
|
|
f4f1850271 | ||
|
|
5a81972f68 | ||
|
|
674dfca34d |
No files matched your search
@@ -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
@@ -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
|
||||
+56
-22
@@ -1,12 +1,17 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
dependencies {
|
||||
classpath 'org.tukaani:xz:1.8'
|
||||
classpath 'org.glavo:pack200:0.3.0'
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'com.github.johnrengelman.shadow' version '4.0.0'
|
||||
id 'application'
|
||||
id 'com.github.johnrengelman.shadow' version '7.0.0'
|
||||
}
|
||||
|
||||
import java.nio.file.FileSystems
|
||||
@@ -16,25 +21,28 @@ 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'
|
||||
|
||||
dependencies {
|
||||
compile project(":HMCLCore")
|
||||
compile rootProject.files("lib/JFoenix.jar")
|
||||
implementation project(":HMCLCore")
|
||||
implementation rootProject.files("lib/JFoenix.jar")
|
||||
}
|
||||
|
||||
def digest(String algorithm, byte[] bytes) {
|
||||
@@ -75,19 +83,50 @@ ext.packer = Pack200.newPacker()
|
||||
packer.properties()["pack.effort"] = "9"
|
||||
ext.unpacker = Pack200.newUnpacker()
|
||||
|
||||
// Pack200 does not guarantee that unpacked .class file is bit-wise same as the .class file before packing
|
||||
// because of shrinking. So we should pack .class files and unpack it to make sure that after unpacking
|
||||
// .class files remain the same.
|
||||
def repack(File file) {
|
||||
def packed = new ByteArrayOutputStream()
|
||||
new JarFile(file).withCloseable { packer.pack(it, packed) }
|
||||
new JarOutputStream(file.newOutputStream()).withCloseable { unpacker.unpack(new ByteArrayInputStream(packed.toByteArray()), it) }
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
java11 {
|
||||
java {
|
||||
srcDirs = ['src/main/java11']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileJava11Java {
|
||||
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
|
||||
targetCompatibility = 11
|
||||
}
|
||||
|
||||
jar {
|
||||
enabled = false
|
||||
dependsOn shadowJar
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
classifier = null
|
||||
|
||||
manifest {
|
||||
attributes 'Created-By': 'Copyright(c) 2013-2020 huangyuhui.',
|
||||
'Main-Class': mainClassName,
|
||||
'Multi-Release': 'true',
|
||||
'Implementation-Version': version,
|
||||
'Class-Path': 'pack200.jar',
|
||||
'Add-Opens': [
|
||||
'java.base/java.lang',
|
||||
'java.base/java.lang.reflect',
|
||||
'javafx.graphics/javafx.css',
|
||||
'javafx.base/com.sun.javafx.runtime',
|
||||
@@ -99,6 +138,7 @@ jar {
|
||||
'javafx.graphics/com.sun.javafx.stage'
|
||||
].join(" "),
|
||||
'Add-Exports': [
|
||||
'java.base/jdk.internal.loader',
|
||||
'javafx.controls/com.sun.javafx.scene.control.behavior',
|
||||
'javafx.controls/javafx.scene.control.skin',
|
||||
'javafx.controls/com.sun.javafx.scene.control',
|
||||
@@ -107,22 +147,9 @@ jar {
|
||||
'javafx.base/com.sun.javafx.event'
|
||||
].join(" ")
|
||||
}
|
||||
finalizedBy shadowJar
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
classifier = null
|
||||
|
||||
exclude 'META-INF/maven/**'
|
||||
exclude 'META-INF/NOTICE.txt'
|
||||
exclude 'META-INF/LICENSE.txt'
|
||||
|
||||
dependencies {
|
||||
exclude(dependency('org.jetbrains:annotations'))
|
||||
}
|
||||
|
||||
doLast {
|
||||
repack(jar.archivePath)
|
||||
repack(jar.archivePath) // see repack()
|
||||
attachSignature(jar.archivePath)
|
||||
createChecksum(jar.archivePath)
|
||||
}
|
||||
@@ -142,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]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,6 +179,11 @@ processResources {
|
||||
|
||||
convertToBSS "assets/css/root.css"
|
||||
convertToBSS "assets/css/blue.css"
|
||||
|
||||
into('META-INF/versions/11') {
|
||||
from sourceSets.java11.output
|
||||
}
|
||||
dependsOn java11Classes
|
||||
}
|
||||
|
||||
task makePack(dependsOn: jar) {
|
||||
@@ -165,6 +198,7 @@ task makePack(dependsOn: jar) {
|
||||
|
||||
task makePackXz(dependsOn: makePack) doLast {
|
||||
def packXz = new File(makePack.outputPath.parentFile, makePack.outputPath.name + ".xz")
|
||||
// Our CI server does not have enough memory space to compress file at highest level.
|
||||
new XZOutputStream(packXz.newOutputStream(), new LZMA2Options(5)).withCloseable { it << makePack.outputPath.bytes }
|
||||
createChecksum(packXz)
|
||||
}
|
||||
|
||||
@@ -23,19 +23,22 @@ import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.AwtUtils;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.CrashReporter;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
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.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.net.*;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@@ -47,11 +50,14 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class Launcher extends Application {
|
||||
public static final CookieManager COOKIE_MANAGER = new CookieManager();
|
||||
|
||||
@Override
|
||||
public void start(Stage primaryStage) {
|
||||
Thread.currentThread().setUncaughtExceptionHandler(CRASH_REPORTER);
|
||||
|
||||
CookieHandler.setDefault(COOKIE_MANAGER);
|
||||
|
||||
try {
|
||||
try {
|
||||
ConfigHolder.init();
|
||||
@@ -67,6 +73,8 @@ public final class Launcher extends Application {
|
||||
Platform.setImplicitExit(false);
|
||||
Controllers.initialize(primaryStage);
|
||||
|
||||
initIcon();
|
||||
|
||||
UpdateChecker.init();
|
||||
|
||||
primaryStage.show();
|
||||
@@ -82,7 +90,17 @@ public final class Launcher extends Application {
|
||||
Controllers.onApplicationStop();
|
||||
}
|
||||
|
||||
private void initIcon() {
|
||||
Toolkit toolkit = Toolkit.getDefaultToolkit();
|
||||
Image image = toolkit.getImage(Launcher.class.getResource("/assets/img/icon.png"));
|
||||
AwtUtils.setAppleIcon(image);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (UpdateHandler.processArguments(args)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
|
||||
AsyncTaskExecutor.setUncaughtExceptionHandler(new CrashReporter(false));
|
||||
|
||||
@@ -142,6 +160,16 @@ public final class Launcher extends Application {
|
||||
} catch (URISyntaxException e) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
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;
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
|
||||
|
||||
import javax.net.ssl.*;
|
||||
import javax.swing.*;
|
||||
@@ -43,12 +43,14 @@ 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);
|
||||
System.setProperty("javafx.autoproxy.disable", "true");
|
||||
|
||||
checkJavaFX();
|
||||
checkDirectoryPath();
|
||||
|
||||
// This environment check will take ~300ms
|
||||
@@ -59,9 +61,10 @@ public final class Main {
|
||||
|
||||
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
|
||||
|
||||
if (UpdateHandler.processArguments(args)) {
|
||||
return;
|
||||
}
|
||||
checkJavaFX();
|
||||
|
||||
// Fix title bar not displaying in GTK systems
|
||||
System.setProperty("jdk.gtk.version", "2");
|
||||
|
||||
Launcher.main(args);
|
||||
}
|
||||
@@ -77,9 +80,13 @@ public final class Main {
|
||||
|
||||
private static void checkJavaFX() {
|
||||
try {
|
||||
Class.forName("javafx.application.Application");
|
||||
} catch (ClassNotFoundException e) {
|
||||
showErrorAndExit(i18n("fatal.missing_javafx"));
|
||||
SelfDependencyPatcher.patch();
|
||||
} catch (SelfDependencyPatcher.PatchException e) {
|
||||
LOG.log(Level.SEVERE, "unable to patch JVM", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.missing"));
|
||||
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
|
||||
LOG.log(Level.SEVERE, "unable to patch JVM", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.incompatible"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,12 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* Stores metadata about this application.
|
||||
*/
|
||||
@@ -39,5 +40,14 @@ public final class Metadata {
|
||||
public static final String PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html";
|
||||
|
||||
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
|
||||
public static final Path HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
|
||||
public static final Path HMCL_DIRECTORY = getHMCLDirectory();
|
||||
|
||||
private static Path getHMCLDirectory() {
|
||||
String home = System.getProperty("user.home", ".");
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
// to fulfill XDG standard.
|
||||
return Paths.get(home, ".cache", "hmcl");
|
||||
}
|
||||
return OperatingSystem.getWorkingDirectory("hmcl");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -19,13 +19,24 @@ package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.ProxyManager;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -33,6 +44,7 @@ import java.nio.file.Files;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
|
||||
public class HMCLGameRepository extends DefaultGameRepository {
|
||||
@@ -117,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();
|
||||
@@ -223,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");
|
||||
@@ -273,6 +284,56 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
return FORBIDDEN.contains(id);
|
||||
}
|
||||
|
||||
public LaunchOptions getLaunchOptions(String version, File gameDir, boolean checkJava) throws InterruptedException {
|
||||
VersionSetting vs = getVersionSetting(version);
|
||||
|
||||
JavaVersion javaVersion = Optional.ofNullable(vs.getJavaVersion(checkJava)).orElse(JavaVersion.fromCurrentEnvironment());
|
||||
LaunchOptions.Builder builder = new LaunchOptions.Builder()
|
||||
.setGameDir(gameDir)
|
||||
.setJava(javaVersion)
|
||||
.setVersionName(Metadata.TITLE)
|
||||
.setVersionType(Metadata.TITLE)
|
||||
.setProfileName(Metadata.TITLE)
|
||||
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
|
||||
.setJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
|
||||
.setMaxMemory(vs.getMaxMemory())
|
||||
.setMinMemory(vs.getMinMemory())
|
||||
.setMetaspace(Lang.toIntOrNull(vs.getPermSize()))
|
||||
.setWidth(vs.getWidth())
|
||||
.setHeight(vs.getHeight())
|
||||
.setFullscreen(vs.isFullscreen())
|
||||
.setServerIp(vs.getServerIp())
|
||||
.setWrapper(vs.getWrapper())
|
||||
.setPrecalledCommand(vs.getPreLaunchCommand())
|
||||
.setNoGeneratedJVMArgs(vs.isNoJVMArgs())
|
||||
.setNativesDirType(vs.getNativesDirType())
|
||||
.setNativesDir(vs.getNativesDir());
|
||||
if (config().hasProxy()) {
|
||||
builder.setProxy(ProxyManager.getProxy());
|
||||
if (config().hasProxyAuth()) {
|
||||
builder.setProxyUser(config().getProxyUser());
|
||||
builder.setProxyPass(config().getProxyPass());
|
||||
}
|
||||
}
|
||||
|
||||
File json = getModpackConfiguration(version);
|
||||
if (json.exists()) {
|
||||
try {
|
||||
String jsonText = FileUtils.readText(json);
|
||||
ModpackConfiguration<?> modpackConfiguration = JsonUtils.GSON.fromJson(jsonText, ModpackConfiguration.class);
|
||||
if (McbbsModpackLocalInstallTask.MODPACK_TYPE.equals(modpackConfiguration.getType())) {
|
||||
ModpackConfiguration<McbbsModpackManifest> config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<McbbsModpackManifest>>() {
|
||||
}.getType());
|
||||
config.getManifest().injectLaunchOptions(builder);
|
||||
}
|
||||
} catch (IOException | JsonParseException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getModpackConfiguration(String version) {
|
||||
return new File(getVersionRoot(version), "modpack.cfg");
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <[email protected]> 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.game;
|
||||
|
||||
import org.jackhuang.hmcl.mod.ModAdviser;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.Zipper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Export the game to a mod pack file.
|
||||
*/
|
||||
public class HMCLModpackExportTask extends Task<Void> {
|
||||
private final DefaultGameRepository repository;
|
||||
private final String version;
|
||||
private final List<String> whitelist;
|
||||
private final Modpack modpack;
|
||||
private final File output;
|
||||
|
||||
/**
|
||||
* @param output mod pack file.
|
||||
* @param version to locate version.json
|
||||
*/
|
||||
public HMCLModpackExportTask(DefaultGameRepository repository, String version, List<String> whitelist, Modpack modpack, File output) {
|
||||
this.repository = repository;
|
||||
this.version = version;
|
||||
this.whitelist = whitelist;
|
||||
this.modpack = modpack;
|
||||
this.output = output;
|
||||
|
||||
onDone().register(event -> {
|
||||
if (event.isFailed()) output.delete();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
ArrayList<String> blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST);
|
||||
blackList.add(version + ".jar");
|
||||
blackList.add(version + ".json");
|
||||
Logging.LOG.info("Compressing game files without some files in blacklist, including files or directories: usernamecache.json, asm, logs, backups, versions, assets, usercache.json, libraries, crash-reports, launcher_profiles.json, NVIDIA, TCNodeTracker");
|
||||
try (Zipper zip = new Zipper(output.toPath())) {
|
||||
zip.putDirectory(repository.getRunDirectory(version).toPath(), "minecraft", path -> Modpack.acceptFile(path, blackList, whitelist));
|
||||
|
||||
Version mv = repository.getResolvedPreservingPatchesVersion(version);
|
||||
String gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version))
|
||||
.orElseThrow(() -> new IOException("Cannot parse the version of " + version));
|
||||
zip.putTextFile(JsonUtils.GSON.toJson(mv.setJar(gameVersion)), "minecraft/pack.json"); // Making "jar" to gameVersion is to be compatible with old HMCL.
|
||||
zip.putTextFile(JsonUtils.GSON.toJson(modpack.setGameVersion(gameVersion)), "modpack.json"); // Newer HMCL only reads 'gameVersion' field.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.download.MaintainTask;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameVerificationFixTask;
|
||||
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
|
||||
import org.jackhuang.hmcl.download.java.JavaRepository;
|
||||
import org.jackhuang.hmcl.launch.NotDecompressingNativesException;
|
||||
import org.jackhuang.hmcl.launch.PermissionException;
|
||||
import org.jackhuang.hmcl.launch.ProcessCreationException;
|
||||
@@ -35,7 +36,11 @@ import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseCompletionTask;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackCompletionTask;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackCompletionTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
|
||||
import org.jackhuang.hmcl.setting.LauncherVisibility;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
@@ -50,10 +55,7 @@ import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
@@ -62,9 +64,11 @@ import java.io.IOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
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;
|
||||
@@ -140,10 +144,12 @@ public final class LauncherHelper {
|
||||
}), Task.composeAsync(() -> {
|
||||
try {
|
||||
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
|
||||
if ("Curse".equals(configuration.getType()))
|
||||
if (CurseInstallTask.MODPACK_TYPE.equals(configuration.getType()))
|
||||
return new CurseCompletionTask(dependencyManager, selectedVersion);
|
||||
else if ("Server".equals(configuration.getType()))
|
||||
else if (ServerModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType()))
|
||||
return new ServerModpackCompletionTask(dependencyManager, selectedVersion);
|
||||
else if (McbbsModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType()))
|
||||
return new McbbsModpackCompletionTask(dependencyManager, selectedVersion);
|
||||
else
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
@@ -169,7 +175,7 @@ public final class LauncherHelper {
|
||||
repository,
|
||||
version.getPatches().isEmpty() ? repository.getResolvedVersion(selectedVersion) : version,
|
||||
authInfo,
|
||||
setting.toLaunchOptions(profile.getGameDir(), !setting.isNotCheckJVM()),
|
||||
repository.getLaunchOptions(selectedVersion, profile.getGameDir(), !setting.isNotCheckJVM()),
|
||||
launcherVisibility == LauncherVisibility.CLOSE
|
||||
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
|
||||
: new HMCLProcessListener(repository, selectedVersion, authInfo, launchingLatch, gameVersion.isPresent())
|
||||
@@ -312,6 +318,32 @@ public final class LauncherHelper {
|
||||
flag = true;
|
||||
}
|
||||
|
||||
if (!flag && version.getJavaVersion() != null) {
|
||||
if (java.getParsedVersion() < version.getJavaVersion().getMajorVersion()) {
|
||||
Optional<JavaVersion> acceptableJava = JavaVersion.getJavas().stream()
|
||||
.filter(javaVersion -> javaVersion.getParsedVersion() >= version.getJavaVersion().getMajorVersion())
|
||||
.max(Comparator.comparing(JavaVersion::getVersionNumber));
|
||||
if (acceptableJava.isPresent()) {
|
||||
setting.setJavaVersion(acceptableJava.get());
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Game later than 1.7.2 accepts Java 8.
|
||||
if (!flag && java.getParsedVersion() < JavaVersion.JAVA_8 && gameVersion.compareTo(VersionNumber.asVersion("1.7.2")) > 0) {
|
||||
Optional<JavaVersion> java8 = JavaVersion.getJavas().stream()
|
||||
@@ -334,8 +366,8 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
// LaunchWrapper 1.12 will crash because of assuming the system class loader is an instance of URLClassLoader.
|
||||
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9_AND_LATER
|
||||
&& version.getMainClass().equals(LibraryAnalyzer.LAUNCH_WRAPPER_MAIN)
|
||||
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9
|
||||
&& LibraryAnalyzer.LAUNCH_WRAPPER_MAIN.equals(version.getMainClass())
|
||||
&& version.getLibraries().stream()
|
||||
.filter(library -> "launchwrapper".equals(library.getArtifactId()))
|
||||
.anyMatch(library -> VersionNumber.asVersion(library.getVersion()).compareTo(VersionNumber.asVersion("1.13")) < 0)) {
|
||||
@@ -367,7 +399,7 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_32 &&
|
||||
org.jackhuang.hmcl.util.platform.Platform.IS_64_BIT) {
|
||||
Architecture.CURRENT.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_64) {
|
||||
final JavaVersion java32 = java;
|
||||
|
||||
// First find if same java version but whose platform is 64-bit installed.
|
||||
@@ -442,6 +474,36 @@ public final class LauncherHelper {
|
||||
onAccept.run();
|
||||
}
|
||||
|
||||
private static CompletableFuture<Void> downloadJava(GameJavaVersion javaVersion, Profile profile) {
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
|
||||
TaskExecutorDialogPane javaDownloadingPane = new TaskExecutorDialogPane(it -> {
|
||||
});
|
||||
|
||||
TaskExecutor executor = JavaRepository.downloadJava(javaVersion,
|
||||
profile.getDependency().getDownloadProvider()).executor(false);
|
||||
executor.addTaskListener(new TaskListener() {
|
||||
@Override
|
||||
public void onStop(boolean success, TaskExecutor executor) {
|
||||
super.onStop(success, executor);
|
||||
Platform.runLater(() -> {
|
||||
if (!success) {
|
||||
future.completeExceptionally(Optional.ofNullable(executor.getException()).orElseGet(InterruptedException::new));
|
||||
} else {
|
||||
future.complete(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
javaDownloadingPane.setExecutor(executor, true);
|
||||
Controllers.dialog(javaDownloadingPane);
|
||||
executor.start();
|
||||
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
private void checkExit() {
|
||||
switch (launcherVisibility) {
|
||||
case HIDE_AND_REOPEN:
|
||||
|
||||
@@ -23,6 +23,8 @@ import org.jackhuang.hmcl.mod.*;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseManifest;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
|
||||
@@ -43,12 +45,20 @@ import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class ModpackHelper {
|
||||
private ModpackHelper() {}
|
||||
|
||||
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException {
|
||||
try {
|
||||
return McbbsModpackManifest.readManifest(file, charset);
|
||||
} catch (Exception ignored) {
|
||||
ignored.printStackTrace();
|
||||
// ignore it, not a valid MCBBS modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return CurseManifest.readCurseForgeModpackManifest(file, charset);
|
||||
} catch (Exception e) {
|
||||
@@ -148,12 +158,16 @@ public final class ModpackHelper {
|
||||
return new HMCLModpackInstallTask(profile, zipFile, modpack, name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure);
|
||||
else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, ((MultiMCInstanceConfiguration) modpack.getManifest()), name)
|
||||
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, (MultiMCInstanceConfiguration) modpack.getManifest(), name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name));
|
||||
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
|
||||
else if (modpack.getManifest() instanceof ServerModpackManifest)
|
||||
return new ServerModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, ((ServerModpackManifest) modpack.getManifest()), name)
|
||||
return new ServerModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (ServerModpackManifest) modpack.getManifest(), name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure);
|
||||
else if (modpack.getManifest() instanceof McbbsModpackManifest)
|
||||
return new McbbsModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (McbbsModpackManifest) modpack.getManifest(), name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name));
|
||||
else throw new IllegalArgumentException("Unrecognized modpack: " + modpack.getManifest());
|
||||
}
|
||||
|
||||
@@ -185,6 +199,11 @@ public final class ModpackHelper {
|
||||
throw new MismatchedModpackTypeException(HMCLModpackInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
|
||||
|
||||
return new ModpackUpdateTask(profile.getRepository(), name, new HMCLModpackInstallTask(profile, zipFile, modpack, name));
|
||||
case McbbsModpackLocalInstallTask.MODPACK_TYPE:
|
||||
if (!(modpack.getManifest() instanceof McbbsModpackManifest))
|
||||
throw new MismatchedModpackTypeException(McbbsModpackLocalInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
|
||||
|
||||
return new ModpackUpdateTask(profile.getRepository(), name, new McbbsModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (McbbsModpackManifest) modpack.getManifest(), name));
|
||||
case ServerModpackLocalInstallTask.MODPACK_TYPE:
|
||||
if (!(modpack.getManifest() instanceof ServerModpackManifest))
|
||||
throw new MismatchedModpackTypeException(ServerModpackLocalInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
|
||||
@@ -232,5 +251,18 @@ public final class ModpackHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static Task<Void> createMultiMCPostInstallTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
|
||||
return Task.runAsync(Schedulers.javafx(), () -> {
|
||||
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
|
||||
ModpackHelper.toVersionSetting(manifest, vs);
|
||||
});
|
||||
}
|
||||
|
||||
private static Task<Void> createMcbbsPostInstallTask(Profile profile, McbbsModpackManifest manifest, String version) {
|
||||
return Task.runAsync(Schedulers.javafx(), () -> {
|
||||
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
|
||||
if (manifest.getLaunchInfo().getMinMemory() > vs.getMaxMemory())
|
||||
vs.setMaxMemory(manifest.getLaunchInfo().getMinMemory());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <[email protected]> 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.game;
|
||||
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class MultiMCInstallVersionSettingTask extends Task<Void> {
|
||||
private final Profile profile;
|
||||
private final MultiMCInstanceConfiguration manifest;
|
||||
private final String version;
|
||||
|
||||
public MultiMCInstallVersionSettingTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
|
||||
this.profile = profile;
|
||||
this.manifest = manifest;
|
||||
this.version = version;
|
||||
|
||||
setExecutor(Schedulers.javafx());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
|
||||
ModpackHelper.toVersionSetting(manifest, vs);
|
||||
}
|
||||
}
|
||||
@@ -19,16 +19,14 @@ package org.jackhuang.hmcl.game;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
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.microsoft.MicrosoftAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.*;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.util.ResourceNotFoundError;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
@@ -42,7 +40,10 @@ 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;
|
||||
@@ -134,6 +135,7 @@ public final class TexturesLoader {
|
||||
|
||||
// ==== Skins ====
|
||||
private final static Map<TextureModel, LoadedTexture> DEFAULT_SKINS = new EnumMap<>(TextureModel.class);
|
||||
|
||||
static {
|
||||
loadDefaultSkin("/assets/img/steve.png", TextureModel.STEVE);
|
||||
loadDefaultSkin("/assets/img/alex.png", TextureModel.ALEX);
|
||||
@@ -181,6 +183,30 @@ public final class TexturesLoader {
|
||||
}
|
||||
}, uuidFallback);
|
||||
}
|
||||
|
||||
public static ObjectBinding<LoadedTexture> skinBinding(Account account) {
|
||||
LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(account.getUUID()));
|
||||
return BindingMapping.of(account.getTextures())
|
||||
.map(textures -> textures
|
||||
.flatMap(it -> Optional.ofNullable(it.get(TextureType.SKIN)))
|
||||
.filter(it -> StringUtils.isNotBlank(it.getUrl())))
|
||||
.asyncMap(it -> {
|
||||
if (it.isPresent()) {
|
||||
Texture texture = it.get();
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return loadTexture(texture);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
|
||||
return uuidFallback;
|
||||
}
|
||||
}, POOL);
|
||||
} else {
|
||||
return CompletableFuture.completedFuture(uuidFallback);
|
||||
}
|
||||
}, uuidFallback);
|
||||
}
|
||||
|
||||
// ====
|
||||
|
||||
// ==== Avatar ====
|
||||
@@ -205,16 +231,34 @@ public final class TexturesLoader {
|
||||
public static ObjectBinding<Image> fxAvatarBinding(YggdrasilService service, UUID uuid, int size) {
|
||||
return BindingMapping.of(skinBinding(service, uuid))
|
||||
.map(it -> toAvatar(it.image, size))
|
||||
.map(img -> SwingFXUtils.toFXImage(img, null));
|
||||
.map(TexturesLoader::toFXImage);
|
||||
}
|
||||
|
||||
public static ObjectBinding<Image> fxAvatarBinding(Account account, int size) {
|
||||
if (account instanceof YggdrasilAccount) {
|
||||
return fxAvatarBinding(((YggdrasilAccount) account).getYggdrasilService(), account.getUUID(), size);
|
||||
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount) {
|
||||
return BindingMapping.of(skinBinding(account))
|
||||
.map(it -> toAvatar(it.image, size))
|
||||
.map(TexturesLoader::toFXImage);
|
||||
} else {
|
||||
return Bindings.createObjectBinding(
|
||||
() -> SwingFXUtils.toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size), null));
|
||||
() -> toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size)));
|
||||
}
|
||||
}
|
||||
// ====
|
||||
|
||||
// Based on https://stackoverflow.com/a/57552025
|
||||
// Fix #874: Use it instead of SwingFXUtils.toFXImage
|
||||
private static WritableImage toFXImage(BufferedImage image) {
|
||||
WritableImage wr = new WritableImage(image.getWidth(), image.getHeight());
|
||||
PixelWriter pw = wr.getPixelWriter();
|
||||
|
||||
final int iw = image.getWidth();
|
||||
final int ih = image.getHeight();
|
||||
for (int x = 0; x < iw; x++) {
|
||||
for (int y = 0; y < ih; y++) {
|
||||
pw.setArgb(x, y, image.getRGB(x, y));
|
||||
}
|
||||
}
|
||||
return wr;
|
||||
}
|
||||
}
|
||||
@@ -34,15 +34,20 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactProvider;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloader;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.SimpleAuthlibInjectorArtifactProvider;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginStage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
@@ -51,6 +56,7 @@ import static java.util.stream.Collectors.toList;
|
||||
import static javafx.collections.FXCollections.observableArrayList;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
|
||||
import static org.jackhuang.hmcl.util.Lang.immutableListOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
@@ -78,6 +84,8 @@ public final class Accounts {
|
||||
public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE;
|
||||
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
|
||||
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
|
||||
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(MicrosoftAccountLoginStage.INSTANCE));
|
||||
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_AUTHLIB_INJECTOR, FACTORY_MICROSOFT);
|
||||
|
||||
// ==== login type / account factory mapping ====
|
||||
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
|
||||
@@ -86,6 +94,7 @@ public final class Accounts {
|
||||
type2factory.put("offline", FACTORY_OFFLINE);
|
||||
type2factory.put("yggdrasil", FACTORY_MOJANG);
|
||||
type2factory.put("authlibInjector", FACTORY_AUTHLIB_INJECTOR);
|
||||
type2factory.put("microsoft", FACTORY_MICROSOFT);
|
||||
|
||||
type2factory.forEach((type, factory) -> factory2type.put(factory, type));
|
||||
}
|
||||
@@ -108,6 +117,8 @@ public final class Accounts {
|
||||
return FACTORY_AUTHLIB_INJECTOR;
|
||||
else if (account instanceof YggdrasilAccount)
|
||||
return FACTORY_MOJANG;
|
||||
else if (account instanceof MicrosoftAccount)
|
||||
return FACTORY_MICROSOFT;
|
||||
else
|
||||
throw new IllegalArgumentException("Failed to determine account type: " + account);
|
||||
}
|
||||
@@ -309,7 +320,8 @@ public final class Accounts {
|
||||
private static Map<AccountFactory<?>, String> unlocalizedLoginTypeNames = mapOf(
|
||||
pair(Accounts.FACTORY_OFFLINE, "account.methods.offline"),
|
||||
pair(Accounts.FACTORY_MOJANG, "account.methods.yggdrasil"),
|
||||
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, "account.methods.authlib_injector"));
|
||||
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, "account.methods.authlib_injector"),
|
||||
pair(Accounts.FACTORY_MICROSOFT, "account.methods.microsoft"));
|
||||
|
||||
public static String getLocalizedLoginTypeName(AccountFactory<?> factory) {
|
||||
return i18n(Optional.ofNullable(unlocalizedLoginTypeNames.get(factory))
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -21,16 +21,14 @@ import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.*;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.GameDirectoryType;
|
||||
import org.jackhuang.hmcl.game.LaunchOptions;
|
||||
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;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.file.InvalidPathException;
|
||||
@@ -39,8 +37,6 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
@@ -48,8 +44,6 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
@JsonAdapter(VersionSetting.Serializer.class)
|
||||
public final class VersionSetting implements Cloneable {
|
||||
|
||||
public transient String id;
|
||||
|
||||
private boolean global = false;
|
||||
|
||||
public boolean isGlobal() {
|
||||
@@ -71,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() {
|
||||
@@ -124,6 +118,39 @@ public final class VersionSetting implements Cloneable {
|
||||
defaultJavaPathProperty.set(defaultJavaPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 0 - .minecraft/versions/<version>/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() {
|
||||
@@ -341,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() {
|
||||
@@ -378,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.
|
||||
@@ -400,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.
|
||||
@@ -531,36 +558,8 @@ public final class VersionSetting implements Cloneable {
|
||||
gameDirProperty.addListener(listener);
|
||||
launcherVisibilityProperty.addListener(listener);
|
||||
defaultJavaPathProperty.addListener(listener);
|
||||
}
|
||||
|
||||
public LaunchOptions toLaunchOptions(File gameDir, boolean checkJava) throws InterruptedException {
|
||||
JavaVersion javaVersion = Optional.ofNullable(getJavaVersion(checkJava)).orElse(JavaVersion.fromCurrentEnvironment());
|
||||
LaunchOptions.Builder builder = new LaunchOptions.Builder()
|
||||
.setGameDir(gameDir)
|
||||
.setJava(javaVersion)
|
||||
.setVersionName(Metadata.TITLE)
|
||||
.setVersionType(Metadata.TITLE)
|
||||
.setProfileName(Metadata.TITLE)
|
||||
.setMinecraftArgs(getMinecraftArgs())
|
||||
.setJavaArgs(getJavaArgs())
|
||||
.setMaxMemory(getMaxMemory())
|
||||
.setMinMemory(getMinMemory())
|
||||
.setMetaspace(Lang.toIntOrNull(getPermSize()))
|
||||
.setWidth(getWidth())
|
||||
.setHeight(getHeight())
|
||||
.setFullscreen(isFullscreen())
|
||||
.setServerIp(getServerIp())
|
||||
.setWrapper(getWrapper())
|
||||
.setPrecalledCommand(getPreLaunchCommand())
|
||||
.setNoGeneratedJVMArgs(isNoJVMArgs());
|
||||
if (config().hasProxy()) {
|
||||
builder.setProxy(ProxyManager.getProxy());
|
||||
if (config().hasProxyAuth()) {
|
||||
builder.setProxyUser(config().getProxyUser());
|
||||
builder.setProxyPass(config().getProxyPass());
|
||||
}
|
||||
}
|
||||
return builder.create();
|
||||
nativesDirProperty.addListener(listener);
|
||||
nativesDirTypeProperty.addListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -588,6 +587,7 @@ public final class VersionSetting implements Cloneable {
|
||||
versionSetting.setGameDirType(getGameDirType());
|
||||
versionSetting.setGameDir(getGameDir());
|
||||
versionSetting.setLauncherVisibility(getLauncherVisibility());
|
||||
versionSetting.setNativesDir(getNativesDir());
|
||||
return versionSetting;
|
||||
}
|
||||
|
||||
@@ -619,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;
|
||||
}
|
||||
@@ -648,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));
|
||||
@@ -656,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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import java.awt.*;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public final class AwtUtils {
|
||||
private AwtUtils() {
|
||||
}
|
||||
|
||||
public static void setAppleIcon(Image image) {
|
||||
try {
|
||||
Class<?> taskbarClass = Class.forName("java.awt.TaskBar");
|
||||
Method getTaskBarMethod = taskbarClass.getDeclaredMethod("getTaskBar");
|
||||
Object taskBar = getTaskBarMethod.invoke(null);
|
||||
Method setIconImageMethod = taskbarClass.getDeclaredMethod("setIconImage", Image.class);
|
||||
setIconImageMethod.invoke(taskBar, image);
|
||||
} catch (Throwable ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,10 +27,14 @@ import javafx.stage.Stage;
|
||||
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;
|
||||
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginStage;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
@@ -38,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;
|
||||
@@ -59,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;
|
||||
@@ -74,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
|
||||
@@ -93,6 +124,11 @@ public final class Controllers {
|
||||
return serversPage;
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static ModDownloadListPage getModpackDownloadListPage() {
|
||||
return modDownloadListPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static DecoratorController getDecorator() {
|
||||
return decorator;
|
||||
@@ -107,6 +143,7 @@ public final class Controllers {
|
||||
Logging.LOG.info("Start initializing application");
|
||||
|
||||
Controllers.stage = stage;
|
||||
MicrosoftAccountLoginStage.INSTANCE.initOwner(stage);
|
||||
|
||||
stage.setHeight(config().getHeight());
|
||||
stageHeight.bind(stage.heightProperty());
|
||||
@@ -123,7 +160,7 @@ public final class Controllers {
|
||||
dialog(i18n("launcher.cache_directory.invalid"));
|
||||
}
|
||||
|
||||
Task.runAsync(JavaVersion::initialize).start();
|
||||
Task.runAsync(JavaVersion::initialize).thenRunAsync(JavaRepository::initialize).start();
|
||||
|
||||
scene = new Scene(decorator.getDecorator());
|
||||
scene.setFill(Color.TRANSPARENT);
|
||||
@@ -161,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) {
|
||||
@@ -204,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);
|
||||
|
||||
@@ -198,7 +198,7 @@ public final class LogWindow extends Stage {
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
try {
|
||||
Desktop.getDesktop().open(logFile.toFile());
|
||||
} catch (IOException ignored) {
|
||||
} catch (IOException | IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.jfoenix.controls.JFXDialogLayout;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.web.WebEngine;
|
||||
import javafx.scene.web.WebView;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
|
||||
import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
|
||||
@@ -29,8 +30,6 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class UpgradeDialog extends JFXDialogLayout {
|
||||
private final WebView webView = new WebView();
|
||||
|
||||
public UpgradeDialog(Runnable updateRunnable) {
|
||||
{
|
||||
setHeading(new Label(i18n("update.changelog")));
|
||||
@@ -38,6 +37,7 @@ public class UpgradeDialog extends JFXDialogLayout {
|
||||
|
||||
{
|
||||
WebView webView = new WebView();
|
||||
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
|
||||
WebEngine engine = webView.getEngine();
|
||||
engine.load(CHANGELOG_URL + config().getUpdateChannel().channelName);
|
||||
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
|
||||
|
||||
@@ -17,20 +17,53 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXProgressBar;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.web.WebEngine;
|
||||
import javafx.scene.web.WebView;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
|
||||
public class WebStage extends Stage {
|
||||
private final WebView webView = new WebView();
|
||||
protected final StackPane pane = new StackPane();
|
||||
protected final JFXProgressBar progressBar = new JFXProgressBar();
|
||||
protected final WebView webView = new WebView();
|
||||
protected final WebEngine webEngine = webView.getEngine();
|
||||
|
||||
public WebStage() {
|
||||
setScene(new Scene(webView, 800, 480));
|
||||
this(800, 480);
|
||||
}
|
||||
|
||||
public WebStage(int width, int height) {
|
||||
setScene(new Scene(pane, width, height));
|
||||
getScene().getStylesheets().addAll(config().getTheme().getStylesheets());
|
||||
getIcons().add(newImage("/assets/img/icon.png"));
|
||||
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
|
||||
webView.setContextMenuEnabled(false);
|
||||
progressBar.progressProperty().bind(webView.getEngine().getLoadWorker().progressProperty());
|
||||
|
||||
progressBar.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
|
||||
switch (webView.getEngine().getLoadWorker().getState()) {
|
||||
case SUCCEEDED:
|
||||
case FAILED:
|
||||
case CANCELLED:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}, webEngine.getLoadWorker().stateProperty()));
|
||||
|
||||
BorderPane borderPane = new BorderPane();
|
||||
borderPane.setPickOnBounds(false);
|
||||
borderPane.setTop(progressBar);
|
||||
progressBar.prefWidthProperty().bind(borderPane.widthProperty());
|
||||
pane.getChildren().setAll(webView, borderPane);
|
||||
}
|
||||
|
||||
public WebView getWebView() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -18,8 +18,10 @@
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.image.Image;
|
||||
@@ -30,6 +32,8 @@ import org.jackhuang.hmcl.auth.CredentialExpiredException;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
@@ -37,13 +41,23 @@ import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.DialogController;
|
||||
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import static java.util.Collections.emptySet;
|
||||
import static javafx.beans.binding.Bindings.createBooleanBinding;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -73,7 +87,9 @@ public class AccountListItem extends RadioButton {
|
||||
if (account instanceof OfflineAccount) {
|
||||
title.bind(characterName);
|
||||
} else {
|
||||
title.bind(Bindings.concat(account.getUsername(), " - ", characterName));
|
||||
title.bind(
|
||||
account.getUsername().isEmpty() ? characterName :
|
||||
Bindings.concat(account.getUsername(), " - ", characterName));
|
||||
}
|
||||
|
||||
image.bind(TexturesLoader.fxAvatarBinding(account, 32));
|
||||
@@ -84,7 +100,7 @@ public class AccountListItem extends RadioButton {
|
||||
return new AccountListItemSkin(this);
|
||||
}
|
||||
|
||||
private Task<?> refreshAsync() {
|
||||
public Task<?> refreshAsync() {
|
||||
return Task.runAsync(() -> {
|
||||
account.clearCache();
|
||||
try {
|
||||
@@ -105,17 +121,32 @@ public class AccountListItem extends RadioButton {
|
||||
});
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
refreshAsync().whenComplete(e -> {}).start();
|
||||
public ObservableBooleanValue canUploadSkin() {
|
||||
if (account instanceof YggdrasilAccount) {
|
||||
if (account instanceof AuthlibInjectorAccount) {
|
||||
AuthlibInjectorAccount aiAccount = (AuthlibInjectorAccount) account;
|
||||
ObjectBinding<Optional<CompleteGameProfile>> profile = aiAccount.getYggdrasilService().getProfileRepository().binding(aiAccount.getUUID());
|
||||
return createBooleanBinding(() -> {
|
||||
Set<TextureType> uploadableTextures = profile.get()
|
||||
.map(AuthlibInjectorAccount::getUploadableTextures)
|
||||
.orElse(emptySet());
|
||||
return uploadableTextures.contains(TextureType.SKIN);
|
||||
}, profile);
|
||||
} else {
|
||||
return createBooleanBinding(() -> true);
|
||||
}
|
||||
} else {
|
||||
return createBooleanBinding(() -> false);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean canUploadSkin() {
|
||||
return account instanceof YggdrasilAccount && !(account instanceof AuthlibInjectorAccount);
|
||||
}
|
||||
|
||||
public void uploadSkin() {
|
||||
/**
|
||||
* @return the skin upload task, null if no file is selected
|
||||
*/
|
||||
@Nullable
|
||||
public Task<?> uploadSkin() {
|
||||
if (!(account instanceof YggdrasilAccount)) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
FileChooser chooser = new FileChooser();
|
||||
@@ -123,23 +154,31 @@ public class AccountListItem extends RadioButton {
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("account.skin.file"), "*.png"));
|
||||
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
|
||||
if (selectedFile == null) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
Controllers.prompt(new PromptDialogPane.Builder(i18n("account.skin.upload"), (questions, resolve, reject) -> {
|
||||
PromptDialogPane.Builder.CandidatesQuestion q = (PromptDialogPane.Builder.CandidatesQuestion) questions.get(0);
|
||||
String model = q.getValue() == 0 ? "" : "slim";
|
||||
refreshAsync()
|
||||
.thenRunAsync(() -> ((YggdrasilAccount) account).uploadSkin(model, selectedFile.toPath()))
|
||||
.thenComposeAsync(this::refreshAsync)
|
||||
.thenRunAsync(Schedulers.javafx(), resolve::run)
|
||||
.whenComplete(Schedulers.javafx(), e -> {
|
||||
if (e != null) {
|
||||
reject.accept(AddAccountPane.accountException(e));
|
||||
}
|
||||
}).start();
|
||||
}).addQuestion(new PromptDialogPane.Builder.CandidatesQuestion(i18n("account.skin.model"),
|
||||
i18n("account.skin.model.default"), i18n("account.skin.model.slim"))));
|
||||
return refreshAsync()
|
||||
.thenRunAsync(() -> {
|
||||
BufferedImage skinImg;
|
||||
try {
|
||||
skinImg = ImageIO.read(selectedFile);
|
||||
} catch (IOException e) {
|
||||
throw new InvalidSkinException("Failed to read skin image", e);
|
||||
}
|
||||
if (skinImg == null) {
|
||||
throw new InvalidSkinException("Failed to read skin image");
|
||||
}
|
||||
NormalizedSkin skin = new NormalizedSkin(skinImg);
|
||||
String model = skin.isSlim() ? "slim" : "";
|
||||
LOG.info("Uploading skin [" + selectedFile + "], model [" + model + "]");
|
||||
((YggdrasilAccount) account).uploadSkin(model, selectedFile.toPath());
|
||||
})
|
||||
.thenComposeAsync(refreshAsync())
|
||||
.whenComplete(Schedulers.javafx(), e -> {
|
||||
if (e != null) {
|
||||
Controllers.dialog(AddAccountPane.accountException(e), i18n("account.skin.upload.failed"), MessageType.ERROR);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void remove() {
|
||||
|
||||
@@ -31,8 +31,11 @@ import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
@@ -86,20 +89,38 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
JFXButton btnRefresh = new JFXButton();
|
||||
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
|
||||
SpinnerPane spinnerRefresh = new SpinnerPane();
|
||||
btnRefresh.setOnMouseClicked(e -> {
|
||||
spinnerRefresh.showSpinner();
|
||||
skinnable.refreshAsync()
|
||||
.whenComplete(ex -> spinnerRefresh.hideSpinner())
|
||||
.start();
|
||||
});
|
||||
btnRefresh.getStyleClass().add("toggle-icon4");
|
||||
btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), -1, -1));
|
||||
runInFX(() -> FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh")));
|
||||
right.getChildren().add(btnRefresh);
|
||||
spinnerRefresh.setContent(btnRefresh);
|
||||
right.getChildren().add(spinnerRefresh);
|
||||
|
||||
if (skinnable.canUploadSkin()) {
|
||||
JFXButton btnUpload = new JFXButton();
|
||||
btnUpload.setOnMouseClicked(e -> skinnable.uploadSkin());
|
||||
btnUpload.getStyleClass().add("toggle-icon4");
|
||||
btnUpload.setGraphic(SVG.hanger(Theme.blackFillBinding(), -1, -1));
|
||||
runInFX(() -> FXUtils.installFastTooltip(btnUpload, i18n("account.skin.upload")));
|
||||
right.getChildren().add(btnUpload);
|
||||
}
|
||||
JFXButton btnUpload = new JFXButton();
|
||||
SpinnerPane spinnerUpload = new SpinnerPane();
|
||||
btnUpload.setOnMouseClicked(e -> {
|
||||
Task<?> uploadTask = skinnable.uploadSkin();
|
||||
if (uploadTask != null) {
|
||||
spinnerUpload.showSpinner();
|
||||
uploadTask
|
||||
.whenComplete(Schedulers.javafx(), ex -> spinnerUpload.hideSpinner())
|
||||
.start();
|
||||
}
|
||||
});
|
||||
btnUpload.getStyleClass().add("toggle-icon4");
|
||||
btnUpload.setGraphic(SVG.hanger(Theme.blackFillBinding(), -1, -1));
|
||||
runInFX(() -> FXUtils.installFastTooltip(btnUpload, i18n("account.skin.upload")));
|
||||
spinnerUpload.managedProperty().bind(spinnerUpload.visibleProperty());
|
||||
spinnerUpload.visibleProperty().bind(skinnable.canUploadSkin());
|
||||
spinnerUpload.setContent(btnUpload);
|
||||
spinnerUpload.getStyleClass().add("small-spinner-pane");
|
||||
right.getChildren().add(spinnerUpload);
|
||||
|
||||
JFXButton btnRemove = new JFXButton();
|
||||
btnRemove.setOnMouseClicked(e -> skinnable.remove());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <[email protected]> and contributors
|
||||
* Copyright (C) 2021 huangyuhui <[email protected]> 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
|
||||
@@ -49,6 +49,7 @@ import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -68,6 +69,7 @@ public class AddAccountPane extends StackPane {
|
||||
@FXML private JFXPasswordField txtPassword;
|
||||
@FXML private Label lblCreationWarning;
|
||||
@FXML private Label lblPassword;
|
||||
@FXML private Label lblUsername;
|
||||
@FXML private JFXComboBox<AccountFactory<?>> cboType;
|
||||
@FXML private JFXComboBox<AuthlibInjectorServer> cboServers;
|
||||
@FXML private Label lblInjectorServer;
|
||||
@@ -85,10 +87,10 @@ 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.FACTORY_OFFLINE, Accounts.FACTORY_MOJANG, Accounts.FACTORY_AUTHLIB_INJECTOR);
|
||||
cboType.getItems().setAll(Accounts.FACTORIES);
|
||||
cboType.setConverter(stringConverter(Accounts::getLocalizedLoginTypeName));
|
||||
// try selecting the preferred login type
|
||||
cboType.getSelectionModel().select(
|
||||
@@ -108,24 +110,26 @@ public class AddAccountPane extends StackPane {
|
||||
// remember the last used login type
|
||||
loginType.addListener((observable, oldValue, newValue) -> config().setPreferredLoginType(Accounts.getLoginType(newValue)));
|
||||
|
||||
txtPassword.visibleProperty().bind(loginType.isNotEqualTo(Accounts.FACTORY_OFFLINE));
|
||||
txtUsername.visibleProperty().bind(Bindings.createBooleanBinding(() -> loginType.get().getLoginType().requiresUsername, loginType));
|
||||
lblUsername.visibleProperty().bind(txtUsername.visibleProperty());
|
||||
txtPassword.visibleProperty().bind(Bindings.createBooleanBinding(() -> loginType.get().getLoginType().requiresPassword, loginType));
|
||||
lblPassword.visibleProperty().bind(txtPassword.visibleProperty());
|
||||
|
||||
cboServers.visibleProperty().bind(loginType.isEqualTo(Accounts.FACTORY_AUTHLIB_INJECTOR));
|
||||
lblInjectorServer.visibleProperty().bind(cboServers.visibleProperty());
|
||||
|
||||
txtUsername.getValidators().add(new Validator(i18n("input.email"), str -> !txtPassword.isVisible() || str.contains("@")));
|
||||
txtUsername.getValidators().add(new Validator(i18n("input.email"), this::validateUsername));
|
||||
|
||||
btnAccept.disableProperty().bind(Bindings.createBooleanBinding(
|
||||
() -> !( // consider the opposite situation: input is valid
|
||||
txtUsername.validate() &&
|
||||
(!txtUsername.isVisible() || txtUsername.validate()) &&
|
||||
// invisible means the field is not needed, neither should it be validated
|
||||
(!txtPassword.isVisible() || txtPassword.validate()) &&
|
||||
(!cboServers.isVisible() || cboServers.getSelectionModel().getSelectedItem() != null)
|
||||
),
|
||||
txtUsername.textProperty(),
|
||||
txtPassword.textProperty(), txtPassword.visibleProperty(),
|
||||
cboServers.getSelectionModel().selectedItemProperty(), cboServers.visibleProperty()));
|
||||
txtUsername.textProperty(), txtPassword.textProperty(),
|
||||
loginType, cboServers.getSelectionModel().selectedItemProperty(),
|
||||
txtPassword.visibleProperty(), cboServers.visibleProperty()));
|
||||
|
||||
// authlib-injector links
|
||||
links.bind(BindingMapping.of(cboServers.getSelectionModel().selectedItemProperty())
|
||||
@@ -135,6 +139,20 @@ public class AddAccountPane extends StackPane {
|
||||
linksContainer.visibleProperty().bind(cboServers.visibleProperty());
|
||||
}
|
||||
|
||||
private boolean validateUsername(String username) {
|
||||
AccountFactory<?> loginType = cboType.getSelectionModel().getSelectedItem();
|
||||
if (loginType == Accounts.FACTORY_OFFLINE) {
|
||||
return true;
|
||||
} else if (loginType == Accounts.FACTORY_AUTHLIB_INJECTOR) {
|
||||
AuthlibInjectorServer server = cboServers.getSelectionModel().getSelectedItem();
|
||||
if (server != null && server.isNonEmailLogin()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return username.contains("@");
|
||||
}
|
||||
|
||||
private static final String[] ALLOWED_LINKS = { "register" };
|
||||
|
||||
public static List<Hyperlink> createHyperlinks(AuthlibInjectorServer server) {
|
||||
@@ -156,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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +260,7 @@ public class AddAccountPane extends StackPane {
|
||||
Controllers.dialog(new AddAuthlibInjectorServerPane());
|
||||
}
|
||||
|
||||
private class Selector extends BorderPane implements CharacterSelector {
|
||||
private static class Selector extends BorderPane implements CharacterSelector {
|
||||
|
||||
private final AdvancedListBox listBox = new AdvancedListBox();
|
||||
private final JFXButton cancel = new JFXButton();
|
||||
@@ -325,6 +344,8 @@ public class AddAccountPane extends StackPane {
|
||||
return i18n("account.failed.injector_download_failure");
|
||||
} else if (exception instanceof CharacterDeletedException) {
|
||||
return i18n("account.failed.character_deleted");
|
||||
} else if (exception instanceof InvalidSkinException) {
|
||||
return i18n("account.skin.invalid_skin");
|
||||
} else if (exception.getClass() == AuthenticationException.class) {
|
||||
return exception.getLocalizedMessage();
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <[email protected]> and contributors
|
||||
* Copyright (C) 2021 huangyuhui <[email protected]> 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());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <[email protected]> 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.account;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Modality;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
|
||||
import org.jackhuang.hmcl.ui.WebStage;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static org.jackhuang.hmcl.Launcher.COOKIE_MANAGER;
|
||||
|
||||
public class MicrosoftAccountLoginStage extends WebStage implements MicrosoftService.WebViewCallback {
|
||||
public static final MicrosoftAccountLoginStage INSTANCE = new MicrosoftAccountLoginStage();
|
||||
|
||||
CompletableFuture<String> future;
|
||||
Predicate<String> urlTester;
|
||||
|
||||
public MicrosoftAccountLoginStage() {
|
||||
super(600, 600);
|
||||
initModality(Modality.APPLICATION_MODAL);
|
||||
|
||||
titleProperty().bind(webEngine.titleProperty());
|
||||
|
||||
webEngine.locationProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (urlTester != null && urlTester.test(newValue)) {
|
||||
future.complete(newValue);
|
||||
hide();
|
||||
}
|
||||
});
|
||||
|
||||
showingProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (!newValue) {
|
||||
if (future != null) {
|
||||
future.completeExceptionally(new InterruptedException());
|
||||
}
|
||||
future = null;
|
||||
urlTester = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<String> show(MicrosoftService service, Predicate<String> urlTester, String initialURL) {
|
||||
Platform.runLater(() -> {
|
||||
COOKIE_MANAGER.getCookieStore().removeAll();
|
||||
|
||||
webEngine.load(initialURL);
|
||||
show();
|
||||
});
|
||||
this.future = new CompletableFuture<>();
|
||||
this.urlTester = urlTester;
|
||||
return future;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.scene.control.TextInputControl;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
public class RequiredValidator extends ValidatorBase {
|
||||
|
||||
public RequiredValidator() {
|
||||
}
|
||||
|
||||
public RequiredValidator(@NamedArg("message") String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void eval() {
|
||||
if (srcControl.get() instanceof TextInputControl) {
|
||||
evalTextInputField();
|
||||
}
|
||||
}
|
||||
|
||||
private void evalTextInputField() {
|
||||
TextInputControl textField = ((TextInputControl) srcControl.get());
|
||||
|
||||
hasErrors.set(StringUtils.isBlank(textField.getText()));
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,7 +24,6 @@ import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.task.DownloadManager;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.task.TaskListener;
|
||||
@@ -38,7 +37,7 @@ import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
public class TaskExecutorDialogPane extends StackPane {
|
||||
private TaskExecutor executor;
|
||||
private Consumer<Region> onCancel;
|
||||
private final Consumer<DownloadManager.SpeedEvent> speedEventHandler;
|
||||
private final Consumer<FileDownloadTask.SpeedEvent> speedEventHandler;
|
||||
|
||||
@FXML
|
||||
private Label lblTitle;
|
||||
@@ -76,7 +75,7 @@ public class TaskExecutorDialogPane extends StackPane {
|
||||
lblProgress.setText(String.format("%.1f %s", finalSpeed, finalUnit));
|
||||
});
|
||||
};
|
||||
DownloadManager.speedEvent.channel(DownloadManager.SpeedEvent.class).registerWeak(speedEventHandler);
|
||||
FileDownloadTask.speedEvent.channel(FileDownloadTask.SpeedEvent.class).registerWeak(speedEventHandler);
|
||||
}
|
||||
|
||||
public void setExecutor(TaskExecutor executor) {
|
||||
|
||||
@@ -34,14 +34,16 @@ import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameInstallTask;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
|
||||
import org.jackhuang.hmcl.game.HMCLModpackExportTask;
|
||||
import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
|
||||
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
|
||||
import org.jackhuang.hmcl.mod.ModpackInstallTask;
|
||||
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseCompletionTask;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
@@ -128,7 +130,7 @@ public final class TaskListPane extends StackPane {
|
||||
task.setName(i18n("modpack.install", i18n("modpack.type.multimc")));
|
||||
} else if (task instanceof HMCLModpackInstallTask) {
|
||||
task.setName(i18n("modpack.install", i18n("modpack.type.hmcl")));
|
||||
} else if (task instanceof HMCLModpackExportTask) {
|
||||
} else if (task instanceof McbbsModpackExportTask || task instanceof MultiMCModpackExportTask || task instanceof ServerModpackExportTask) {
|
||||
task.setName(i18n("modpack.export"));
|
||||
} else if (task instanceof MinecraftInstanceTask) {
|
||||
task.setName(i18n("modpack.scan"));
|
||||
@@ -274,7 +276,7 @@ public final class TaskListPane extends StackPane {
|
||||
|
||||
public void setThrowable(Throwable throwable) {
|
||||
unbind();
|
||||
state.setText(throwable == null ? "" : throwable.getLocalizedMessage());
|
||||
state.setText(throwable.getLocalizedMessage());
|
||||
bar.setProgress(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.download;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Region;
|
||||
@@ -90,8 +91,17 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
|
||||
if (name.isPresent()) {
|
||||
txtModpackName.setText(name.get());
|
||||
txtModpackName.setDisable(true);
|
||||
} else {
|
||||
txtModpackName.getValidators().addAll(
|
||||
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().hasVersion(str) && StringUtils.isNotBlank(str)),
|
||||
new Validator(i18n("version.forbidden_name"), str -> !profile.getRepository().forbidsVersion(str))
|
||||
);
|
||||
}
|
||||
|
||||
btnInstall.disableProperty().bind(Bindings.createBooleanBinding(
|
||||
() -> !txtModpackName.validate(),
|
||||
txtModpackName.textProperty()));
|
||||
|
||||
Optional<File> filePath = tryCast(controller.getSettings().get(MODPACK_FILE), File.class);
|
||||
if (filePath.isPresent()) {
|
||||
selectedFile = filePath.get();
|
||||
@@ -122,11 +132,6 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
|
||||
|
||||
if (!name.isPresent()) {
|
||||
txtModpackName.setText(manifest.getName());
|
||||
txtModpackName.getValidators().addAll(
|
||||
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().hasVersion(str) && StringUtils.isNotBlank(str)),
|
||||
new Validator(i18n("version.forbidden_name"), str -> !profile.getRepository().forbidsVersion(str))
|
||||
);
|
||||
txtModpackName.textProperty().addListener(e -> btnInstall.setDisable(!txtModpackName.validate()));
|
||||
}
|
||||
}, e -> {
|
||||
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageType.ERROR);
|
||||
|
||||
@@ -114,7 +114,7 @@ public final class ModpackSelectionPage extends StackPane implements WizardPage
|
||||
resolve.run();
|
||||
|
||||
Controllers.taskDialog(
|
||||
new FileDownloadTask(url, modpack.toFile())
|
||||
new FileDownloadTask(url, modpack.toFile(), null)
|
||||
.whenComplete(Schedulers.javafx(), e -> {
|
||||
if (e == null) {
|
||||
resolve.run();
|
||||
|
||||
@@ -19,9 +19,9 @@ package org.jackhuang.hmcl.ui.export;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.game.HMCLModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.ModAdviser;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.mod.ModpackExportInfo;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
|
||||
@@ -36,7 +36,6 @@ import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.io.Zipper;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@@ -63,72 +62,97 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> whitelist = (List<String>) settings.get(ModpackFileSelectionPage.MODPACK_FILE_SELECTION);
|
||||
File modpackFile = (File) settings.get(ModpackInfoPage.MODPACK_FILE);
|
||||
String modpackName = (String) settings.get(ModpackInfoPage.MODPACK_NAME);
|
||||
String modpackAuthor = (String) settings.get(ModpackInfoPage.MODPACK_AUTHOR);
|
||||
String modpackFileApi = (String) settings.get(ModpackInfoPage.MODPACK_FILE_API);
|
||||
String modpackVersion = (String) settings.get(ModpackInfoPage.MODPACK_VERSION);
|
||||
String modpackDescription = (String) settings.get(ModpackInfoPage.MODPACK_DESCRIPTION);
|
||||
ModpackExportInfo exportInfo = (ModpackExportInfo) settings.get(ModpackInfoPage.MODPACK_INFO);
|
||||
exportInfo.setWhitelist(whitelist);
|
||||
String modpackType = (String) settings.get(ModpackTypeSelectionPage.MODPACK_TYPE);
|
||||
boolean includeLauncher = (Boolean) settings.get(ModpackInfoPage.MODPACK_INCLUDE_LAUNCHER);
|
||||
|
||||
switch (modpackType) {
|
||||
case ModpackTypeSelectionPage.MODPACK_TYPE_HMCL:
|
||||
return exportAsHMCL(whitelist, modpackFile, modpackName, modpackAuthor, modpackVersion, modpackDescription, includeLauncher);
|
||||
case ModpackTypeSelectionPage.MODPACK_TYPE_MULTIMC:
|
||||
return exportAsMultiMC(whitelist, modpackFile, modpackName, modpackAuthor, modpackVersion, modpackDescription);
|
||||
case ModpackTypeSelectionPage.MODPACK_TYPE_SERVER:
|
||||
return exportAsServer(whitelist, modpackFile, modpackName, modpackAuthor, modpackVersion, modpackDescription, modpackFileApi);
|
||||
default:
|
||||
throw new IllegalStateException("Unrecognized modpack type " + modpackType);
|
||||
}
|
||||
return exportWithLauncher(modpackType, exportInfo, modpackFile);
|
||||
}
|
||||
|
||||
private Task<?> exportAsHMCL(List<String> whitelist, File modpackFile, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription, boolean includeLauncherRaw) {
|
||||
private Task<?> exportWithLauncher(String modpackType, ModpackExportInfo exportInfo, File modpackFile) {
|
||||
List<File> launcherJar = Launcher.getCurrentJarFiles();
|
||||
boolean includeLauncher = includeLauncherRaw && launcherJar != null;
|
||||
boolean packWithLauncher = exportInfo.isPackWithLauncher() && launcherJar != null;
|
||||
return new Task<Object>() {
|
||||
File tempModpack;
|
||||
Task<?> exportTask;
|
||||
|
||||
@Override
|
||||
public boolean doPreExecute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preExecute() throws Exception {
|
||||
File dest;
|
||||
if (packWithLauncher) {
|
||||
dest = tempModpack = Files.createTempFile("hmcl", ".zip").toFile();
|
||||
} else {
|
||||
dest = modpackFile;
|
||||
}
|
||||
|
||||
switch (modpackType) {
|
||||
case ModpackTypeSelectionPage.MODPACK_TYPE_MCBBS:
|
||||
exportTask = exportAsMcbbs(exportInfo, dest);
|
||||
break;
|
||||
case ModpackTypeSelectionPage.MODPACK_TYPE_MULTIMC:
|
||||
exportTask = exportAsMultiMC(exportInfo, dest);
|
||||
break;
|
||||
case ModpackTypeSelectionPage.MODPACK_TYPE_SERVER:
|
||||
exportTask = exportAsServer(exportInfo, dest);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unrecognized modpack type " + modpackType);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task<?>> getDependents() {
|
||||
return Collections.singleton(exportTask);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
if (!packWithLauncher) return;
|
||||
try (Zipper zip = new Zipper(modpackFile.toPath())) {
|
||||
Config exported = new Config();
|
||||
|
||||
exported.setBackgroundImageType(config().getBackgroundImageType());
|
||||
exported.setBackgroundImage(config().getBackgroundImage());
|
||||
exported.setTheme(config().getTheme());
|
||||
exported.setDownloadType(config().getDownloadType());
|
||||
exported.setPreferredLoginType(config().getPreferredLoginType());
|
||||
exported.getAuthlibInjectorServers().setAll(config().getAuthlibInjectorServers());
|
||||
|
||||
zip.putTextFile(exported.toJson(), ConfigHolder.CONFIG_FILENAME);
|
||||
zip.putFile(tempModpack, "modpack.zip");
|
||||
|
||||
File bg = new File("bg").getAbsoluteFile();
|
||||
if (bg.isDirectory())
|
||||
zip.putDirectory(bg.toPath(), "bg");
|
||||
|
||||
File background_png = new File("background.png").getAbsoluteFile();
|
||||
if (background_png.isFile())
|
||||
zip.putFile(background_png, "background.png");
|
||||
|
||||
File background_jpg = new File("background.jpg").getAbsoluteFile();
|
||||
if (background_jpg.isFile())
|
||||
zip.putFile(background_jpg, "background.jpg");
|
||||
|
||||
for (File jar : launcherJar)
|
||||
zip.putFile(jar, jar.getName());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Task<?> exportAsMcbbs(ModpackExportInfo exportInfo, File modpackFile) {
|
||||
return new Task<Void>() {
|
||||
Task<?> dependency = null;
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
File tempModpack = includeLauncher ? Files.createTempFile("hmcl", ".zip").toFile() : modpackFile;
|
||||
|
||||
dependency = new HMCLModpackExportTask(profile.getRepository(), version, whitelist,
|
||||
new Modpack(modpackName, modpackAuthor, modpackVersion, null, modpackDescription, StandardCharsets.UTF_8, null), tempModpack);
|
||||
|
||||
if (includeLauncher) {
|
||||
dependency = dependency.thenRunAsync(() -> {
|
||||
try (Zipper zip = new Zipper(modpackFile.toPath())) {
|
||||
Config exported = new Config();
|
||||
|
||||
exported.setBackgroundImageType(config().getBackgroundImageType());
|
||||
exported.setBackgroundImage(config().getBackgroundImage());
|
||||
exported.setTheme(config().getTheme());
|
||||
exported.setDownloadType(config().getDownloadType());
|
||||
exported.setPreferredLoginType(config().getPreferredLoginType());
|
||||
exported.getAuthlibInjectorServers().setAll(config().getAuthlibInjectorServers());
|
||||
|
||||
zip.putTextFile(exported.toJson(), ConfigHolder.CONFIG_FILENAME);
|
||||
zip.putFile(tempModpack, "modpack.zip");
|
||||
|
||||
File bg = new File("bg").getAbsoluteFile();
|
||||
if (bg.isDirectory())
|
||||
zip.putDirectory(bg.toPath(), "bg");
|
||||
|
||||
File background_png = new File("background.png").getAbsoluteFile();
|
||||
if (background_png.isFile())
|
||||
zip.putFile(background_png, "background.png");
|
||||
|
||||
File background_jpg = new File("background.jpg").getAbsoluteFile();
|
||||
if (background_jpg.isFile())
|
||||
zip.putFile(background_jpg, "background.jpg");
|
||||
|
||||
for (File jar : launcherJar)
|
||||
zip.putFile(jar, jar.getName());
|
||||
}
|
||||
});
|
||||
}
|
||||
public void execute() {
|
||||
dependency = new McbbsModpackExportTask(profile.getRepository(), version, exportInfo, modpackFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -138,30 +162,30 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
};
|
||||
}
|
||||
|
||||
private Task<?> exportAsMultiMC(List<String> whitelist, File modpackFile, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription) {
|
||||
private Task<?> exportAsMultiMC(ModpackExportInfo exportInfo, File modpackFile) {
|
||||
return new Task<Void>() {
|
||||
Task<?> dependency;
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
VersionSetting vs = profile.getVersionSetting(version);
|
||||
dependency = new MultiMCModpackExportTask(profile.getRepository(), version, whitelist,
|
||||
dependency = new MultiMCModpackExportTask(profile.getRepository(), version, exportInfo.getWhitelist(),
|
||||
new MultiMCInstanceConfiguration(
|
||||
"OneSix",
|
||||
modpackName + "-" + modpackVersion,
|
||||
exportInfo.getName() + "-" + exportInfo.getVersion(),
|
||||
null,
|
||||
Lang.toIntOrNull(vs.getPermSize()),
|
||||
vs.getWrapper(),
|
||||
vs.getPreLaunchCommand(),
|
||||
null,
|
||||
modpackDescription,
|
||||
exportInfo.getDescription(),
|
||||
null,
|
||||
vs.getJavaArgs(),
|
||||
exportInfo.getJavaArguments(),
|
||||
vs.isFullscreen(),
|
||||
vs.getWidth(),
|
||||
vs.getHeight(),
|
||||
vs.getMaxMemory(),
|
||||
vs.getMinMemory(),
|
||||
exportInfo.getMinMemory(),
|
||||
vs.isShowLogs(),
|
||||
/* showConsoleOnError */ true,
|
||||
/* autoCloseConsole */ false,
|
||||
@@ -181,13 +205,13 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
};
|
||||
}
|
||||
|
||||
private Task<?> exportAsServer(List<String> whitelist, File modpackFile, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription, String modpackFileApi) {
|
||||
private Task<?> exportAsServer(ModpackExportInfo exportInfo, File modpackFile) {
|
||||
return new Task<Void>() {
|
||||
Task<?> dependency;
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
dependency = new ServerModpackExportTask(profile.getRepository(), version, whitelist, modpackName, modpackAuthor, modpackVersion, modpackDescription, modpackFileApi, modpackFile);
|
||||
dependency = new ServerModpackExportTask(profile.getRepository(), version, exportInfo, modpackFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -203,7 +227,7 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
case 0:
|
||||
return new ModpackTypeSelectionPage(controller);
|
||||
case 1:
|
||||
return new ModpackInfoPage(controller, version);
|
||||
return new ModpackInfoPage(controller, profile.getRepository(), version);
|
||||
case 2:
|
||||
return new ModpackFileSelectionPage(controller, profile, version, ModAdviser::suggestMod);
|
||||
default:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,76 +17,96 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.export;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextArea;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import com.jfoenix.controls.JFXToggleButton;
|
||||
import com.jfoenix.validation.RequiredFieldValidator;
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
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 javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.mod.ModpackExportInfo;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.*;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE;
|
||||
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_SERVER;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
private final WizardController controller;
|
||||
private final HMCLGameRepository gameRepository;
|
||||
private final ModpackExportInfo.Options options;
|
||||
private final String versionName;
|
||||
private final boolean canIncludeLauncher;
|
||||
private final boolean showFileApi;
|
||||
|
||||
private SimpleStringProperty versionName = new SimpleStringProperty();
|
||||
private SimpleStringProperty modpackName = new SimpleStringProperty();
|
||||
private SimpleStringProperty modpackFileApi = new SimpleStringProperty();
|
||||
private SimpleStringProperty modpackAuthor = new SimpleStringProperty();
|
||||
private SimpleStringProperty modpackVersion = new SimpleStringProperty("1.0");
|
||||
private SimpleStringProperty modpackDescription = new SimpleStringProperty();
|
||||
private SimpleBooleanProperty includingLauncher = new SimpleBooleanProperty();
|
||||
private ObjectProperty<EventHandler<? super MouseEvent>> next = new SimpleObjectProperty<>();
|
||||
private final ModpackExportInfo exportInfo = new ModpackExportInfo();
|
||||
|
||||
public ModpackInfoPage(WizardController controller, String version) {
|
||||
private final SimpleStringProperty name = new SimpleStringProperty("");
|
||||
private final SimpleStringProperty author = new SimpleStringProperty("");
|
||||
private final SimpleStringProperty version = new SimpleStringProperty("1.0");
|
||||
private final SimpleStringProperty description = new SimpleStringProperty("");
|
||||
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 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("");
|
||||
|
||||
public ModpackInfoPage(WizardController controller, HMCLGameRepository gameRepository, String version) {
|
||||
this.controller = controller;
|
||||
modpackName.set(version);
|
||||
modpackAuthor.set(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getUsername).orElse(""));
|
||||
versionName.set(version);
|
||||
this.gameRepository = gameRepository;
|
||||
this.options = tryCast(controller.getSettings().get(MODPACK_INFO_OPTION), ModpackExportInfo.Options.class)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Settings.MODPACK_INFO_OPTION is required"));
|
||||
this.versionName = version;
|
||||
|
||||
name.set(version);
|
||||
author.set(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getUsername).orElse(""));
|
||||
|
||||
VersionSetting versionSetting = gameRepository.getVersionSetting(versionName);
|
||||
minMemory.set(Optional.ofNullable(versionSetting.getMinMemory()).orElse(0));
|
||||
launchArguments.set(versionSetting.getMinecraftArgs());
|
||||
javaArguments.set(versionSetting.getJavaArgs());
|
||||
|
||||
List<File> launcherJar = Launcher.getCurrentJarFiles();
|
||||
canIncludeLauncher = launcherJar != null;
|
||||
showFileApi = controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_SERVER;
|
||||
|
||||
next.set(e -> onNext());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onNext() {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(i18n("modpack.wizard.step.initialization.save"));
|
||||
@@ -96,24 +116,35 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
controller.onEnd();
|
||||
return;
|
||||
}
|
||||
controller.getSettings().put(MODPACK_NAME, modpackName.get());
|
||||
controller.getSettings().put(MODPACK_FILE_API, modpackFileApi.get());
|
||||
controller.getSettings().put(MODPACK_VERSION, modpackVersion.get());
|
||||
controller.getSettings().put(MODPACK_AUTHOR, modpackAuthor.get());
|
||||
|
||||
exportInfo.setName(name.get());
|
||||
exportInfo.setFileApi(fileApi.get());
|
||||
exportInfo.setVersion(version.get());
|
||||
exportInfo.setAuthor(author.get());
|
||||
exportInfo.setDescription(description.get());
|
||||
exportInfo.setPackWithLauncher(packWithLauncher.get());
|
||||
exportInfo.setUrl(url.get());
|
||||
exportInfo.setForceUpdate(forceUpdate.get());
|
||||
exportInfo.setPackWithLauncher(packWithLauncher.get());
|
||||
exportInfo.setMinMemory(minMemory.get());
|
||||
exportInfo.setLaunchArguments(launchArguments.get());
|
||||
exportInfo.setJavaArguments(javaArguments.get());
|
||||
exportInfo.setAuthlibInjectorServer(authlibInjectorServer.get());
|
||||
|
||||
if (StringUtils.isNotBlank(mcbbsThreadId.get())) {
|
||||
exportInfo.setOrigins(Collections.singletonList(new McbbsModpackManifest.Origin(
|
||||
"mcbbs", Integer.parseInt(mcbbsThreadId.get())
|
||||
)));
|
||||
}
|
||||
|
||||
controller.getSettings().put(MODPACK_INFO, exportInfo);
|
||||
controller.getSettings().put(MODPACK_FILE, file);
|
||||
controller.getSettings().put(MODPACK_DESCRIPTION, modpackDescription.get());
|
||||
controller.getSettings().put(MODPACK_INCLUDE_LAUNCHER, includingLauncher.get());
|
||||
controller.onNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(Map<String, Object> settings) {
|
||||
controller.getSettings().remove(MODPACK_NAME);
|
||||
controller.getSettings().remove(MODPACK_VERSION);
|
||||
controller.getSettings().remove(MODPACK_AUTHOR);
|
||||
controller.getSettings().remove(MODPACK_DESCRIPTION);
|
||||
controller.getSettings().remove(MODPACK_INCLUDE_LAUNCHER);
|
||||
controller.getSettings().remove(MODPACK_FILE);
|
||||
controller.getSettings().remove(MODPACK_INFO);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -126,19 +157,12 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
return new ModpackInfoPageSkin(this);
|
||||
}
|
||||
|
||||
public static final String MODPACK_NAME = "modpack.name";
|
||||
public static final String MODPACK_FILE_API = "modpack.file_api";
|
||||
public static final String MODPACK_VERSION = "modpack.version";
|
||||
public static final String MODPACK_AUTHOR = "archive.author";
|
||||
public static final String MODPACK_DESCRIPTION = "modpack.description";
|
||||
public static final String MODPACK_INCLUDE_LAUNCHER = "modpack.include_launcher";
|
||||
public static final String MODPACK_INFO = "modpack.info";
|
||||
public static final String MODPACK_FILE = "modpack.file";
|
||||
public static final String MODPACK_INFO_OPTION = "modpack.info.option";
|
||||
|
||||
public static class ModpackInfoPageSkin extends SkinBase<ModpackInfoPage> {
|
||||
private final JFXTextField txtModpackName;
|
||||
private final JFXTextField txtModpackFileApi;
|
||||
private final JFXTextField txtModpackAuthor;
|
||||
private final JFXTextField txtModpackVersion;
|
||||
private ObservableList<Node> originList;
|
||||
|
||||
public ModpackInfoPageSkin(ModpackInfoPage skinnable) {
|
||||
super(skinnable);
|
||||
@@ -151,6 +175,8 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
scroll.setFitToHeight(true);
|
||||
getChildren().setAll(scroll);
|
||||
|
||||
List<JFXTextField> validatingFields = new ArrayList<>();
|
||||
|
||||
{
|
||||
BorderPane borderPane = new BorderPane();
|
||||
borderPane.setStyle("-fx-padding: 16;");
|
||||
@@ -179,31 +205,37 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
borderPane1.setLeft(new Label(i18n("modpack.wizard.step.initialization.exported_version")));
|
||||
|
||||
Label versionNameLabel = new Label();
|
||||
versionNameLabel.textProperty().bind(skinnable.versionName);
|
||||
versionNameLabel.setText(skinnable.versionName);
|
||||
borderPane1.setRight(versionNameLabel);
|
||||
list.getContent().add(borderPane1);
|
||||
}
|
||||
|
||||
{
|
||||
txtModpackName = new JFXTextField();
|
||||
txtModpackName.textProperty().bindBidirectional(skinnable.modpackName);
|
||||
JFXTextField txtModpackName = new JFXTextField();
|
||||
txtModpackName.textProperty().bindBidirectional(skinnable.name);
|
||||
txtModpackName.setLabelFloat(true);
|
||||
txtModpackName.setPromptText(i18n("modpack.name"));
|
||||
RequiredFieldValidator validator = new RequiredFieldValidator();
|
||||
validator.setMessage(i18n("modpack.not_a_valid_name"));
|
||||
txtModpackName.getValidators().add(validator);
|
||||
txtModpackName.getValidators().add(new RequiredValidator(i18n("modpack.not_a_valid_name")));
|
||||
StackPane.setMargin(txtModpackName, insets);
|
||||
list.getContent().add(txtModpackName);
|
||||
|
||||
validatingFields.add(txtModpackName);
|
||||
}
|
||||
|
||||
if (skinnable.showFileApi) {
|
||||
txtModpackFileApi = new JFXTextField();
|
||||
txtModpackFileApi.textProperty().bindBidirectional(skinnable.modpackFileApi);
|
||||
if (skinnable.options.isRequireFileApi()) {
|
||||
JFXTextField txtModpackFileApi = new JFXTextField();
|
||||
txtModpackFileApi.textProperty().bindBidirectional(skinnable.fileApi);
|
||||
txtModpackFileApi.setLabelFloat(true);
|
||||
txtModpackFileApi.setPromptText(i18n("modpack.file_api"));
|
||||
RequiredFieldValidator validator = new RequiredFieldValidator();
|
||||
txtModpackFileApi.getValidators().add(validator);
|
||||
|
||||
if (skinnable.options.isValidateFileApi()) {
|
||||
txtModpackFileApi.getValidators().add(new RequiredValidator());
|
||||
}
|
||||
|
||||
txtModpackFileApi.getValidators().add(new Validator(s -> {
|
||||
if (s.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
new URL(s).toURI();
|
||||
return true;
|
||||
@@ -213,35 +245,37 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
}));
|
||||
StackPane.setMargin(txtModpackFileApi, insets);
|
||||
list.getContent().add(txtModpackFileApi);
|
||||
} else {
|
||||
txtModpackFileApi = null;
|
||||
|
||||
validatingFields.add(txtModpackFileApi);
|
||||
}
|
||||
|
||||
{
|
||||
txtModpackAuthor = new JFXTextField();
|
||||
txtModpackAuthor.textProperty().bindBidirectional(skinnable.modpackAuthor);
|
||||
JFXTextField txtModpackAuthor = new JFXTextField();
|
||||
txtModpackAuthor.textProperty().bindBidirectional(skinnable.author);
|
||||
txtModpackAuthor.setLabelFloat(true);
|
||||
txtModpackAuthor.setPromptText(i18n("archive.author"));
|
||||
RequiredFieldValidator validator = new RequiredFieldValidator();
|
||||
txtModpackAuthor.getValidators().add(validator);
|
||||
txtModpackAuthor.getValidators().add(new RequiredValidator());
|
||||
StackPane.setMargin(txtModpackAuthor, insets);
|
||||
list.getContent().add(txtModpackAuthor);
|
||||
|
||||
validatingFields.add(txtModpackAuthor);
|
||||
}
|
||||
|
||||
{
|
||||
txtModpackVersion = new JFXTextField();
|
||||
txtModpackVersion.textProperty().bindBidirectional(skinnable.modpackVersion);
|
||||
JFXTextField txtModpackVersion = new JFXTextField();
|
||||
txtModpackVersion.textProperty().bindBidirectional(skinnable.version);
|
||||
txtModpackVersion.setLabelFloat(true);
|
||||
txtModpackVersion.setPromptText(i18n("archive.version"));
|
||||
RequiredFieldValidator validator = new RequiredFieldValidator();
|
||||
txtModpackVersion.getValidators().add(validator);
|
||||
txtModpackVersion.getValidators().add(new RequiredValidator());
|
||||
StackPane.setMargin(txtModpackVersion, insets);
|
||||
list.getContent().add(txtModpackVersion);
|
||||
|
||||
validatingFields.add(txtModpackVersion);
|
||||
}
|
||||
|
||||
{
|
||||
JFXTextArea area = new JFXTextArea();
|
||||
area.textProperty().bindBidirectional(skinnable.modpackDescription);
|
||||
area.textProperty().bindBidirectional(skinnable.description);
|
||||
area.setLabelFloat(true);
|
||||
area.setPromptText(i18n("modpack.desc"));
|
||||
area.setMinHeight(400);
|
||||
@@ -249,18 +283,123 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
list.getContent().add(area);
|
||||
}
|
||||
|
||||
if (skinnable.controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_HMCL) {
|
||||
BorderPane borderPane1 = new BorderPane();
|
||||
borderPane1.setLeft(new Label(i18n("modpack.wizard.step.initialization.include_launcher")));
|
||||
list.getContent().add(borderPane1);
|
||||
if (skinnable.options.isRequireForceUpdate()) {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.force_update")));
|
||||
list.getContent().add(pane);
|
||||
|
||||
JFXToggleButton button = new JFXToggleButton();
|
||||
button.setDisable(!skinnable.canIncludeLauncher);
|
||||
button.selectedProperty().bindBidirectional(skinnable.includingLauncher);
|
||||
button.selectedProperty().bindBidirectional(skinnable.forceUpdate);
|
||||
button.setSize(8);
|
||||
button.setMinHeight(16);
|
||||
button.setMaxHeight(16);
|
||||
borderPane1.setRight(button);
|
||||
pane.setRight(button);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireAuthlibInjectorServer()) {
|
||||
JFXComboBox<AuthlibInjectorServer> cboServers = new JFXComboBox<>();
|
||||
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
|
||||
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
|
||||
Bindings.bindContent(cboServers.getItems(), config().getAuthlibInjectorServers());
|
||||
|
||||
skinnable.authlibInjectorServer.bind(Bindings.createStringBinding(() ->
|
||||
Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem())
|
||||
.map(AuthlibInjectorServer::getUrl)
|
||||
.orElse(null)));
|
||||
|
||||
BorderPane pane = new BorderPane();
|
||||
|
||||
Label left = new Label(i18n("account.injector.server"));
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
pane.setLeft(left);
|
||||
pane.setRight(cboServers);
|
||||
|
||||
list.getContent().add(pane);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireMinMemory()) {
|
||||
JFXTextField txtMinMemory = new JFXTextField();
|
||||
FXUtils.bindInt(txtMinMemory, skinnable.minMemory);
|
||||
txtMinMemory.getValidators().add(new NumberValidator(i18n("input.number"), false));
|
||||
FXUtils.setLimitWidth(txtMinMemory, 300);
|
||||
validatingFields.add(txtMinMemory);
|
||||
|
||||
BorderPane pane = new BorderPane();
|
||||
Label label = new Label(i18n("settings.min_memory"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
pane.setLeft(label);
|
||||
BorderPane.setAlignment(txtMinMemory, Pos.CENTER_RIGHT);
|
||||
pane.setRight(txtMinMemory);
|
||||
|
||||
list.getContent().add(pane);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireLaunchArguments()) {
|
||||
JFXTextField txtLaunchArguments = new JFXTextField();
|
||||
txtLaunchArguments.textProperty().bindBidirectional(skinnable.launchArguments);
|
||||
txtLaunchArguments.setLabelFloat(true);
|
||||
txtLaunchArguments.setPromptText(i18n("settings.advanced.minecraft_arguments"));
|
||||
StackPane.setMargin(txtLaunchArguments, insets);
|
||||
list.getContent().add(txtLaunchArguments);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireJavaArguments()) {
|
||||
JFXTextField txtJavaArguments = new JFXTextField();
|
||||
txtJavaArguments.textProperty().bindBidirectional(skinnable.javaArguments);
|
||||
txtJavaArguments.setLabelFloat(true);
|
||||
txtJavaArguments.setPromptText(i18n("settings.advanced.jvm_args"));
|
||||
StackPane.setMargin(txtJavaArguments, insets);
|
||||
list.getContent().add(txtJavaArguments);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireOrigins() || skinnable.options.isRequireUrl()) {
|
||||
BorderPane originPane = new BorderPane();
|
||||
Label title = new Label(i18n("modpack.origin"));
|
||||
originPane.setTop(title);
|
||||
VBox container = new VBox();
|
||||
BorderPane.setMargin(container, new Insets(0, 0, 0, 16));
|
||||
originPane.setCenter(container);
|
||||
list.getContent().add(originPane);
|
||||
|
||||
if (skinnable.options.isRequireUrl()) {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setPadding(new Insets(8, 0, 8, 0));
|
||||
Label left = new Label(i18n("modpack.origin.url"));
|
||||
pane.setLeft(left);
|
||||
JFXTextField txtModpackUrl = new JFXTextField();
|
||||
txtModpackUrl.textProperty().bindBidirectional(skinnable.url);
|
||||
pane.setRight(txtModpackUrl);
|
||||
container.getChildren().add(pane);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireOrigins()) {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setPadding(new Insets(8, 0, 8, 0));
|
||||
Label left = new Label(i18n("modpack.origin.mcbbs"));
|
||||
pane.setLeft(left);
|
||||
JFXTextField txtMcbbs = new JFXTextField();
|
||||
FXUtils.setValidateWhileTextChanged(txtMcbbs, true);
|
||||
txtMcbbs.getValidators().add(new NumberValidator(i18n("input.number"), true));
|
||||
txtMcbbs.textProperty().bindBidirectional(skinnable.mcbbsThreadId);
|
||||
pane.setRight(txtMcbbs);
|
||||
container.getChildren().add(pane);
|
||||
|
||||
validatingFields.add(txtMcbbs);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.include_launcher")));
|
||||
list.getContent().add(pane);
|
||||
|
||||
JFXToggleButton button = new JFXToggleButton();
|
||||
button.setDisable(!skinnable.canIncludeLauncher);
|
||||
button.selectedProperty().bindBidirectional(skinnable.packWithLauncher);
|
||||
button.setSize(8);
|
||||
button.setMinHeight(16);
|
||||
button.setMaxHeight(16);
|
||||
pane.setRight(button);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,15 +415,13 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
nextButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
nextButton.setText(i18n("wizard.next"));
|
||||
nextButton.getStyleClass().add("jfx-button-raised");
|
||||
if (skinnable.showFileApi) {
|
||||
nextButton.disableProperty().bind(Bindings.createBooleanBinding(() ->
|
||||
!txtModpackName.validate() || !txtModpackVersion.validate() || !txtModpackAuthor.validate() || !txtModpackFileApi.validate(),
|
||||
txtModpackName.textProperty(), txtModpackAuthor.textProperty(), txtModpackVersion.textProperty(), txtModpackFileApi.textProperty()));
|
||||
} else {
|
||||
nextButton.disableProperty().bind(Bindings.createBooleanBinding(() ->
|
||||
!txtModpackName.validate() || !txtModpackVersion.validate() || !txtModpackAuthor.validate(),
|
||||
txtModpackName.textProperty(), txtModpackAuthor.textProperty(), txtModpackVersion.textProperty()));
|
||||
}
|
||||
nextButton.disableProperty().bind(
|
||||
// Disable nextButton if any text of JFXTextFields in validatingFields does not fulfill
|
||||
// our requirement.
|
||||
Bindings.createBooleanBinding(() -> validatingFields.stream()
|
||||
.map(field -> !field.validate())
|
||||
.reduce(false, (left, right) -> left || right),
|
||||
validatingFields.stream().map(JFXTextField::textProperty).toArray(StringProperty[]::new)));
|
||||
hbox.getChildren().add(nextButton);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,18 +20,23 @@ package org.jackhuang.hmcl.ui.export;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.mod.ModpackExportInfo;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.export.ModpackInfoPage.MODPACK_INFO_OPTION;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class ModpackTypeSelectionPage extends StackPane implements WizardPage {
|
||||
private final WizardController controller;
|
||||
@FXML
|
||||
private JFXButton btnHMCL;
|
||||
private JFXButton btnMCBBS;
|
||||
@FXML
|
||||
private JFXButton btnMultiMC;
|
||||
@FXML
|
||||
@@ -41,12 +46,15 @@ public final class ModpackTypeSelectionPage extends StackPane implements WizardP
|
||||
this.controller = controller;
|
||||
FXUtils.loadFXML(this, "/assets/fxml/modpack/type.fxml");
|
||||
|
||||
JFXButton[] buttons = new JFXButton[]{btnHMCL, btnMultiMC, btnServer};
|
||||
String[] types = new String[]{MODPACK_TYPE_HMCL, MODPACK_TYPE_MULTIMC, MODPACK_TYPE_SERVER};
|
||||
JFXButton[] buttons = new JFXButton[]{btnMCBBS, btnMultiMC, btnServer};
|
||||
String[] types = new String[]{MODPACK_TYPE_MCBBS, MODPACK_TYPE_MULTIMC, MODPACK_TYPE_SERVER};
|
||||
ModpackExportInfo.Options[] options = new ModpackExportInfo.Options[]{McbbsModpackExportTask.OPTION, MultiMCModpackExportTask.OPTION, ServerModpackExportTask.OPTION};
|
||||
for (int i = 0; i < types.length; ++i) {
|
||||
String type = types[i];
|
||||
ModpackExportInfo.Options option = options[i];
|
||||
buttons[i].setOnMouseClicked(e -> {
|
||||
controller.getSettings().put(MODPACK_TYPE, type);
|
||||
controller.getSettings().put(MODPACK_INFO_OPTION, option);
|
||||
controller.onNext();
|
||||
});
|
||||
}
|
||||
@@ -63,7 +71,7 @@ public final class ModpackTypeSelectionPage extends StackPane implements WizardP
|
||||
|
||||
public static final String MODPACK_TYPE = "modpack.type";
|
||||
|
||||
public static final String MODPACK_TYPE_HMCL = "hmcl";
|
||||
public static final String MODPACK_TYPE_MCBBS = "mcbbs";
|
||||
public static final String MODPACK_TYPE_MULTIMC = "multimc";
|
||||
public static final String MODPACK_TYPE_SERVER = "server";
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -132,7 +132,7 @@ class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
}
|
||||
|
||||
String getSubtitle() {
|
||||
return StringUtils.parseColorEscapes(packInfo.getDescription());
|
||||
return StringUtils.parseColorEscapes(packInfo.getDescription().toString());
|
||||
}
|
||||
|
||||
Datapack.Pack getPackInfo() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ public class GameItem extends Control {
|
||||
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
ModpackConfiguration<Void> config = profile.getRepository().readModpackConfiguration(version);
|
||||
ModpackConfiguration<?> config = profile.getRepository().readModpackConfiguration(version);
|
||||
if (config == null) return;
|
||||
tag.set(config.getVersion());
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.stage.FileChooser;
|
||||
@@ -40,10 +39,8 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
|
||||
@@ -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,7 +18,7 @@
|
||||
package org.jackhuang.hmcl.upgrade;
|
||||
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Pack200Utils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.tukaani.xz.XZInputStream;
|
||||
|
||||
@@ -26,31 +26,24 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.jar.Pack200;
|
||||
|
||||
class HMCLDownloadTask extends Task<Void> {
|
||||
class HMCLDownloadTask extends FileDownloadTask {
|
||||
|
||||
private final FileDownloadTask task;
|
||||
private final RemoteVersion.Type archiveFormat;
|
||||
private final Path target;
|
||||
private RemoteVersion.Type archiveFormat;
|
||||
|
||||
public HMCLDownloadTask(RemoteVersion version, Path target) {
|
||||
this.task = new FileDownloadTask(NetworkUtils.toURL(version.getUrl()), target.toFile(), version.getIntegrityCheck());
|
||||
this.archiveFormat = version.getType();
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task<?>> getDependents() {
|
||||
return Collections.singletonList(this);
|
||||
super(NetworkUtils.toURL(version.getUrl()), target.toFile(), version.getIntegrityCheck());
|
||||
archiveFormat = version.getType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
super.execute();
|
||||
|
||||
try {
|
||||
Path target = getFile().toPath();
|
||||
|
||||
switch (archiveFormat) {
|
||||
case JAR:
|
||||
break;
|
||||
@@ -59,7 +52,7 @@ class HMCLDownloadTask extends Task<Void> {
|
||||
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;
|
||||
|
||||
@@ -67,7 +60,7 @@ class HMCLDownloadTask extends Task<Void> {
|
||||
throw new IllegalArgumentException("Unknown format: " + archiveFormat);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Files.deleteIfExists(target);
|
||||
getFile().delete();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -199,7 +199,7 @@ public final class UpdateHandler {
|
||||
StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace();
|
||||
for (int i = 0; i < stacktrace.length; i++) {
|
||||
StackTraceElement element = stacktrace[i];
|
||||
if (Main.class.getName().equals(element.getClassName())) {
|
||||
if (Main.class.getName().equals(element.getClassName()) && "main".equals(element.getMethodName())) {
|
||||
// we've reached the main method
|
||||
return i + 1 != stacktrace.length;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Utility for Adding JavaFX to module path.
|
||||
*
|
||||
* @author ZekerZhayard
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
/*
|
||||
* 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 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;
|
||||
|
||||
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;
|
||||
|
||||
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() {
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
public static void patch() throws PatchException, IncompatibleVersionException {
|
||||
// Do nothing if JavaFX is detected
|
||||
try {
|
||||
try {
|
||||
forName("javafx.application.Application");
|
||||
return;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
} catch (UnsupportedClassVersionError error) {
|
||||
// Loading the JavaFX class was unsupported.
|
||||
// We are probably on 8 and its on 11
|
||||
throw new IncompatibleVersionException();
|
||||
}
|
||||
// So the problem with Java 8 is that some distributions DO NOT BUNDLE JAVAFX
|
||||
// Why is this a problem? OpenJFX does not come in public bundles prior to Java 11
|
||||
// So you're out of luck unless you change your JDK or update Java.
|
||||
if (CURRENT_JAVA.getParsedVersion() < 11) {
|
||||
throw new IncompatibleVersionException();
|
||||
}
|
||||
|
||||
// We can only self-patch JavaFX on x86-64 platform.
|
||||
// For ARM support, user's manual patch is required.
|
||||
if (Architecture.CURRENT != Architecture.X86_64) {
|
||||
throw new IncompatibleVersionException();
|
||||
}
|
||||
|
||||
// Otherwise we're free to download in Java 11+
|
||||
LOG.info("Missing JavaFX dependencies, attempting to patch in missing classes");
|
||||
|
||||
// Download missing dependencies
|
||||
List<DependencyDescriptor> missingDependencies = checkMissingDependencies();
|
||||
if (!missingDependencies.isEmpty()) {
|
||||
try {
|
||||
fetchDependencies(missingDependencies);
|
||||
} catch (IOException e) {
|
||||
throw new PatchException("Failed to download dependencies", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the dependencies
|
||||
try {
|
||||
loadFromCache();
|
||||
} catch (IOException ex) {
|
||||
throw new PatchException("Failed to load JavaFX cache", ex);
|
||||
} catch (ReflectiveOperationException | NoClassDefFoundError ex) {
|
||||
throw new PatchException("Failed to add dependencies to classpath!", ex);
|
||||
}
|
||||
LOG.info(" - Done!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private static void loadFromCache() throws IOException, ReflectiveOperationException {
|
||||
LOG.info(" - Loading dependencies...");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download dependencies.
|
||||
*
|
||||
* @throws IOException When the files cannot be fetched or saved.
|
||||
*/
|
||||
private static void fetchDependencies(List<DependencyDescriptor> dependencies) throws IOException {
|
||||
ProgressFrame dialog = new ProgressFrame(i18n("download.javafx"));
|
||||
dialog.setVisible(true);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private static List<DependencyDescriptor> checkMissingDependencies() {
|
||||
List<DependencyDescriptor> missing = new ArrayList<>();
|
||||
|
||||
for (DependencyDescriptor dependency : JFX_DEPENDENCIES) {
|
||||
if (!Files.exists(dependency.localPath())) {
|
||||
missing.add(dependency);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
verifyChecksum(dependency);
|
||||
} catch (ChecksumMismatchException e) {
|
||||
LOG.warning("Corrupted dependency " + dependency.filename() + ": " + e.getMessage());
|
||||
missing.add(dependency);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
return missing;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public static class PatchException extends Exception {
|
||||
PatchException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
public static class IncompatibleVersionException extends Exception {
|
||||
}
|
||||
|
||||
public static class ProgressFrame extends JDialog {
|
||||
|
||||
private final JProgressBar progressBar;
|
||||
private final JLabel progressText;
|
||||
|
||||
public ProgressFrame(String title) {
|
||||
super((Dialog) null);
|
||||
|
||||
JPanel panel = new JPanel();
|
||||
|
||||
setResizable(false);
|
||||
setTitle(title);
|
||||
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
||||
setBounds(100, 100, 600, 150);
|
||||
setContentPane(panel);
|
||||
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 };
|
||||
panel.setLayout(gridBagLayout);
|
||||
|
||||
progressText = new JLabel("");
|
||||
GridBagConstraints gbc_lblProgressText = new GridBagConstraints();
|
||||
gbc_lblProgressText.insets = new Insets(10, 0, 5, 0);
|
||||
gbc_lblProgressText.gridx = 0;
|
||||
gbc_lblProgressText.gridy = 0;
|
||||
panel.add(progressText, gbc_lblProgressText);
|
||||
|
||||
progressBar = new JProgressBar();
|
||||
GridBagConstraints gbc_progressBar = new GridBagConstraints();
|
||||
gbc_progressBar.insets = new Insets(0, 25, 5, 25);
|
||||
gbc_progressBar.fill = GridBagConstraints.HORIZONTAL;
|
||||
gbc_progressBar.gridx = 0;
|
||||
gbc_progressBar.gridy = 1;
|
||||
panel.add(progressBar, gbc_progressBar);
|
||||
|
||||
JButton btnCancel = new JButton(i18n("button.cancel"));
|
||||
btnCancel.addActionListener(e -> {
|
||||
System.exit(-1);
|
||||
});
|
||||
GridBagConstraints gbc_btnCancel = new GridBagConstraints();
|
||||
gbc_btnCancel.insets = new Insets(0, 25, 5, 25);
|
||||
gbc_btnCancel.fill = GridBagConstraints.HORIZONTAL;
|
||||
gbc_btnCancel.gridx = 0;
|
||||
gbc_btnCancel.gridy = 2;
|
||||
panel.add(btnCancel, gbc_btnCancel);
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
progressText.setText(status);
|
||||
}
|
||||
|
||||
public void setProgress(int current, int total) {
|
||||
progressBar.setValue(current);
|
||||
progressBar.setMaximum(total);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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.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.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import jdk.internal.loader.BuiltinClassLoader;
|
||||
|
||||
/**
|
||||
* Utility for Adding JavaFX to module path.
|
||||
*
|
||||
* @author ZekerZhayard
|
||||
*/
|
||||
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 jarPaths All jar paths
|
||||
* @throws ReflectiveOperationException When the call to add these jars to the system module path failed.
|
||||
*/
|
||||
public static void patch(Set<String> modules, Path... jarPaths) throws ReflectiveOperationException {
|
||||
// Find all modules
|
||||
ModuleFinder finder = ModuleFinder.of(jarPaths);
|
||||
|
||||
// Load all modules as unnamed module
|
||||
for (ModuleReference mref : finder.findAll()) {
|
||||
((BuiltinClassLoader) ClassLoader.getSystemClassLoader()).loadModule(mref);
|
||||
}
|
||||
|
||||
// Define all modules
|
||||
Configuration config = Configuration.resolveAndBind(finder, List.of(ModuleLayer.boot().configuration()), finder, modules);
|
||||
ModuleLayer layer = ModuleLayer.defineModules(config, List.of(ModuleLayer.boot()), name -> ClassLoader.getSystemClassLoader()).layer();
|
||||
|
||||
// Add-Exports and Add-Opens
|
||||
try {
|
||||
// Some hacks
|
||||
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Module.class, MethodHandles.lookup());
|
||||
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) {
|
||||
String[] name = target.split("/", 2); // <module>/<package>
|
||||
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%;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
<?import com.jfoenix.validation.RequiredFieldValidator?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import org.jackhuang.hmcl.ui.FXUtils?>
|
||||
<?import javafx.scene.shape.SVGPath?>
|
||||
<?import org.jackhuang.hmcl.ui.construct.RequiredValidator?>
|
||||
<?import org.jackhuang.hmcl.ui.construct.SpinnerPane?>
|
||||
<?import org.jackhuang.hmcl.ui.FXUtils?>
|
||||
<fx:root xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
type="StackPane">
|
||||
@@ -45,13 +47,13 @@
|
||||
</JFXButton>
|
||||
</HBox>
|
||||
|
||||
<Label text="%account.username" GridPane.rowIndex="2" GridPane.columnIndex="0"/>
|
||||
<Label fx:id="lblUsername" text="%account.username" GridPane.rowIndex="2" GridPane.columnIndex="0"/>
|
||||
|
||||
<JFXTextField fx:id="txtUsername" GridPane.columnIndex="1" GridPane.rowIndex="2" GridPane.columnSpan="2"
|
||||
FXUtils.validateWhileTextChanged="true" onAction="#onCreationAccept">
|
||||
<validators>
|
||||
<RequiredFieldValidator message="%input.not_empty">
|
||||
</RequiredFieldValidator>
|
||||
<RequiredValidator message="%input.not_empty">
|
||||
</RequiredValidator>
|
||||
</validators>
|
||||
</JFXTextField>
|
||||
|
||||
|
||||
Loaded 100 of 248 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user