diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml
new file mode 100644
index 000000000..378766f67
--- /dev/null
+++ b/.idea/codeStyleSettings.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 000000000..853b5f825
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/fileTemplates/JavaFXApplication.java b/.idea/fileTemplates/JavaFXApplication.java
new file mode 100644
index 000000000..f9b5a47c2
--- /dev/null
+++ b/.idea/fileTemplates/JavaFXApplication.java
@@ -0,0 +1,17 @@
+#parse("HMCL.java")
+#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "") package ${PACKAGE_NAME};#end
+
+import javafx.application.Application;
+import javafx.stage.Stage;
+
+public class ${NAME} extends Application {
+
+ public static void main(String[] args) {
+ launch(args);
+ }
+
+ @Override
+ public void start(Stage primaryStage) {
+
+ }
+}
diff --git a/.idea/fileTemplates/Singleton.java b/.idea/fileTemplates/Singleton.java
new file mode 100644
index 000000000..9f8dcba1d
--- /dev/null
+++ b/.idea/fileTemplates/Singleton.java
@@ -0,0 +1,12 @@
+#parse("HMCL.java")
+#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
+public class ${NAME}{
+ private static ${NAME} ourInstance = new ${NAME}();
+
+ public static ${NAME} getInstance() {
+ return ourInstance;
+ }
+
+ private ${NAME}() {
+ }
+}
diff --git a/.idea/fileTemplates/includes/HMCL.java b/.idea/fileTemplates/includes/HMCL.java
new file mode 100644
index 000000000..8ba77f53c
--- /dev/null
+++ b/.idea/fileTemplates/includes/HMCL.java
@@ -0,0 +1,18 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+
\ No newline at end of file
diff --git a/.idea/fileTemplates/internal/AnnotationType.java b/.idea/fileTemplates/internal/AnnotationType.java
new file mode 100644
index 000000000..4c8f31d13
--- /dev/null
+++ b/.idea/fileTemplates/internal/AnnotationType.java
@@ -0,0 +1,4 @@
+#parse("HMCL.java")
+#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
+public @interface ${NAME} {
+}
diff --git a/.idea/fileTemplates/internal/Class.java b/.idea/fileTemplates/internal/Class.java
new file mode 100644
index 000000000..307d238f1
--- /dev/null
+++ b/.idea/fileTemplates/internal/Class.java
@@ -0,0 +1,4 @@
+#parse("HMCL.java")
+#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
+public class ${NAME} {
+}
diff --git a/.idea/fileTemplates/internal/Enum.java b/.idea/fileTemplates/internal/Enum.java
new file mode 100644
index 000000000..f9ed71e48
--- /dev/null
+++ b/.idea/fileTemplates/internal/Enum.java
@@ -0,0 +1,4 @@
+#parse("HMCL.java")
+#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
+public enum ${NAME} {
+}
diff --git a/.idea/fileTemplates/internal/Interface.java b/.idea/fileTemplates/internal/Interface.java
new file mode 100644
index 000000000..013564ecb
--- /dev/null
+++ b/.idea/fileTemplates/internal/Interface.java
@@ -0,0 +1,4 @@
+#parse("HMCL.java")
+#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
+public interface ${NAME} {
+}
diff --git a/.idea/fileTemplates/internal/Kotlin Class.kt b/.idea/fileTemplates/internal/Kotlin Class.kt
new file mode 100644
index 000000000..3ea22c045
--- /dev/null
+++ b/.idea/fileTemplates/internal/Kotlin Class.kt
@@ -0,0 +1,5 @@
+#parse("HMCL.java")
+#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
+#end
+class ${NAME} {
+}
\ No newline at end of file
diff --git a/.idea/fileTemplates/internal/Kotlin Enum.kt b/.idea/fileTemplates/internal/Kotlin Enum.kt
new file mode 100644
index 000000000..8c3294b5a
--- /dev/null
+++ b/.idea/fileTemplates/internal/Kotlin Enum.kt
@@ -0,0 +1,5 @@
+#parse("HMCL.java")
+#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
+#end
+enum class ${NAME} {
+}
\ No newline at end of file
diff --git a/.idea/fileTemplates/internal/Kotlin File.kt b/.idea/fileTemplates/internal/Kotlin File.kt
new file mode 100644
index 000000000..5d389eb39
--- /dev/null
+++ b/.idea/fileTemplates/internal/Kotlin File.kt
@@ -0,0 +1,3 @@
+#parse("HMCL.java")
+#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
+#end
diff --git a/.idea/fileTemplates/internal/Kotlin Interface.kt b/.idea/fileTemplates/internal/Kotlin Interface.kt
new file mode 100644
index 000000000..0c119e2ee
--- /dev/null
+++ b/.idea/fileTemplates/internal/Kotlin Interface.kt
@@ -0,0 +1,5 @@
+#parse("HMCL.java")
+#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
+#end
+interface ${NAME} {
+}
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 000000000..273897151
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 000000000..0dd4b3546
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/Gradle__com_google_code_gson_gson_2_8_1.xml b/.idea/libraries/Gradle__com_google_code_gson_gson_2_8_1.xml
new file mode 100644
index 000000000..623ed41a8
--- /dev/null
+++ b/.idea/libraries/Gradle__com_google_code_gson_gson_2_8_1.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/Gradle__junit_junit_4_12.xml b/.idea/libraries/Gradle__junit_junit_4_12.xml
new file mode 100644
index 000000000..04c10dd5a
--- /dev/null
+++ b/.idea/libraries/Gradle__junit_junit_4_12.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/Gradle__org_hamcrest_hamcrest_core_1_3.xml b/.idea/libraries/Gradle__org_hamcrest_hamcrest_core_1_3.xml
new file mode 100644
index 000000000..8262f729c
--- /dev/null
+++ b/.idea/libraries/Gradle__org_hamcrest_hamcrest_core_1_3.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/Gradle__org_jetbrains_annotations_13_0.xml b/.idea/libraries/Gradle__org_jetbrains_annotations_13_0.xml
new file mode 100644
index 000000000..4f32fdef2
--- /dev/null
+++ b/.idea/libraries/Gradle__org_jetbrains_annotations_13_0.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_1_1_3_2.xml b/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_1_1_3_2.xml
new file mode 100644
index 000000000..c3c06992f
--- /dev/null
+++ b/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_1_1_3_2.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jre7_1_1_3_2.xml b/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jre7_1_1_3_2.xml
new file mode 100644
index 000000000..1747c57f6
--- /dev/null
+++ b/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jre7_1_1_3_2.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jre8_1_1_3_2.xml b/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jre8_1_1_3_2.xml
new file mode 100644
index 000000000..dcafaf334
--- /dev/null
+++ b/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jre8_1_1_3_2.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 000000000..d5d79e0ca
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 000000000..03dc4bb5b
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/HMCL/HMCL.iml b/.idea/modules/HMCL/HMCL.iml
new file mode 100644
index 000000000..09637b25a
--- /dev/null
+++ b/.idea/modules/HMCL/HMCL.iml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/HMCL/HMCL_main.iml b/.idea/modules/HMCL/HMCL_main.iml
new file mode 100644
index 000000000..5aa7bbdc8
--- /dev/null
+++ b/.idea/modules/HMCL/HMCL_main.iml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/HMCL/HMCL_test.iml b/.idea/modules/HMCL/HMCL_test.iml
new file mode 100644
index 000000000..df96bc3ce
--- /dev/null
+++ b/.idea/modules/HMCL/HMCL_test.iml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/HMCLCore/HMCLCore.iml b/.idea/modules/HMCLCore/HMCLCore.iml
new file mode 100644
index 000000000..b66228a14
--- /dev/null
+++ b/.idea/modules/HMCLCore/HMCLCore.iml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/HMCLCore/HMCLCore_main.iml b/.idea/modules/HMCLCore/HMCLCore_main.iml
new file mode 100644
index 000000000..63c541d43
--- /dev/null
+++ b/.idea/modules/HMCLCore/HMCLCore_main.iml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/HMCLCore/HMCLCore_test.iml b/.idea/modules/HMCLCore/HMCLCore_test.iml
new file mode 100644
index 000000000..d25a4ddf2
--- /dev/null
+++ b/.idea/modules/HMCLCore/HMCLCore_test.iml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/HMCLKotlin.iml b/.idea/modules/HMCLKotlin.iml
new file mode 100644
index 000000000..5b1923fb2
--- /dev/null
+++ b/.idea/modules/HMCLKotlin.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/HMCLKotlin_main.iml b/.idea/modules/HMCLKotlin_main.iml
new file mode 100644
index 000000000..29ae031a6
--- /dev/null
+++ b/.idea/modules/HMCLKotlin_main.iml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/HMCLKotlin_test.iml b/.idea/modules/HMCLKotlin_test.iml
new file mode 100644
index 000000000..56ff6708f
--- /dev/null
+++ b/.idea/modules/HMCLKotlin_test.iml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/jfoenix.iml b/.idea/modules/jfoenix.iml
new file mode 100644
index 000000000..fb9e6d307
--- /dev/null
+++ b/.idea/modules/jfoenix.iml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/tornadofx.iml b/.idea/modules/tornadofx.iml
new file mode 100644
index 000000000..b15457137
--- /dev/null
+++ b/.idea/modules/tornadofx.iml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/tornadofx_main.iml b/.idea/modules/tornadofx_main.iml
new file mode 100644
index 000000000..eb25c54a8
--- /dev/null
+++ b/.idea/modules/tornadofx_main.iml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/tornadofx_test.iml b/.idea/modules/tornadofx_test.iml
new file mode 100644
index 000000000..dba773c9b
--- /dev/null
+++ b/.idea/modules/tornadofx_test.iml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml
new file mode 100644
index 000000000..e96534fb2
--- /dev/null
+++ b/.idea/uiDesigner.xml
@@ -0,0 +1,124 @@
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 000000000..94a25f7f4
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 000000000..b6766f5f9
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,2536 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ EventHelper
+ getSource()
+ cancel
+ cached
+ isCancelled
+ running
+ ceiling
+ close()
+ listener`
+ updateMessage
+ fixed
+ Parallel
+ createNewFile
+ copyTo
+ terminate
+ ParallelTask
+ finest
+ hidden
+ toggle-icon
+ toggle-icon3
+ get(
+ title
+ fullscreen
+ jfx-decorator-buttons-container
+ padding
+ isRes
+ doDependentsSucceeded: Boolean
+ white
+ jfx-list-view
+ isNull
+
+
+ E:\sources\java\JFoenix\jfoenix\build\libs\jfoenix-0.0.0-SNAPSHOT.jar!\com\jfoenix
+ D:\Develop\Java\jdk1.8.0_131\jre\lib\ext\jfxrt.jar!\com\sun\javafx\scene\control\skin
+ E:\sources\java\HMCLKotlin\HMCL
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1499579974315
+
+
+ 1499579974315
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ file://$PROJECT_DIR$/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionListItem.kt
+ 38
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Kotlin (HMCL_main)|Kotlin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1.8
+
+
+
+
+
+
+
+
+
+
+
+ HMCLCore_test|HMCLCore
+
+
+
+
+
+
+
+
+
+
+
+ 1.8
+
+
+
+
+
+
+
+
+
+
+
+ Gradle: com.google.code.gson:gson:2.8.1
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/HMCL/.travis.yml b/HMCL/.travis.yml
new file mode 100644
index 000000000..8b9ae134a
--- /dev/null
+++ b/HMCL/.travis.yml
@@ -0,0 +1,14 @@
+language: java
+jdk:
+- oraclejdk8
+branches:
+ only:
+ - master
+before_install:
+- chmod +x gradlew
+script: "bash ./gradlew clean build --stacktrace"
+before_cache:
+- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
+cache:
+ directories:
+ - "$HOME/.gradle/caches/"
\ No newline at end of file
diff --git a/HMCL/out/production/classes/META-INF/HMCL_main.kotlin_module b/HMCL/out/production/classes/META-INF/HMCL_main.kotlin_module
new file mode 100644
index 000000000..69495ad34
Binary files /dev/null and b/HMCL/out/production/classes/META-INF/HMCL_main.kotlin_module differ
diff --git a/HMCL/out/production/resources/assets/css/jfoenix-components.css b/HMCL/out/production/resources/assets/css/jfoenix-components.css
new file mode 100644
index 000000000..1c02f8720
--- /dev/null
+++ b/HMCL/out/production/resources/assets/css/jfoenix-components.css
@@ -0,0 +1,394 @@
+.root {
+ -fx-font-family: Roboto;
+ src: "/resources/roboto/Roboto-Regular.ttf";
+}
+
+/* Burgers Demo */
+
+.jfx-hamburger {
+ -fx-spacing: 5;
+ -fx-cursor: hand;
+}
+
+.jfx-hamburger StackPane {
+ -fx-pref-width: 40px;
+ -fx-pref-height: 7px;
+ -fx-background-color: #D63333;
+ -fx-background-radius: 5px;
+}
+
+/* Input Demo */
+
+.text-field {
+ -fx-max-width: 300;
+}
+
+.jfx-text-field, .jfx-password-field {
+ -fx-background-color: WHITE;
+ -fx-font-weight: BOLD;
+ -fx-prompt-text-fill: #808080;
+ -fx-alignment: top-left;
+ -jfx-focus-color: #4059A9;
+ -jfx-unfocus-color: #4d4d4d;
+ -fx-max-width: 300;
+}
+
+.jfx-decorator {
+ -fx-decorator-color: RED;
+}
+
+.jfx-decorator .jfx-decorator-buttons-container {
+ -fx-background-color: -fx-decorator-color;
+}
+
+.jfx-decorator .resize-border {
+ -fx-border-color: -fx-decorator-color;
+ -fx-border-width: 0 4 4 4;
+}
+
+.jfx-text-area, .text-area {
+ -fx-font-weight: BOLD;
+}
+
+.jfx-text-field:error, .jfx-password-field:error, .jfx-text-area:error {
+ -jfx-focus-color: #D34336;
+ -jfx-unfocus-color: #D34336;
+}
+
+.jfx-text-field .error-label, .jfx-password-field .error-label, .jfx-text-area .error-label {
+ -fx-text-fill: #D34336;
+ -fx-font-size: 0.75em;
+}
+
+.jfx-text-field .error-icon, .jfx-password-field .error-icon, .jfx-text-area .error-icon {
+ -fx-text-fill: #D34336;
+ -fx-font-size: 1em;
+}
+
+/* Progress Bar Demo */
+
+.progress-bar > .bar {
+ -fx-min-width: 500;
+}
+
+.jfx-progress-bar > .bar {
+ -fx-min-width: 500;
+}
+
+.jfx-progress-bar {
+ -fx-progress-color: #0F9D58;
+ -fx-stroke-width: 3;
+}
+
+/* Icons Demo */
+.icon {
+ -fx-text-fill: #FE774D;
+ -fx-padding: 10;
+ -fx-cursor: hand;
+}
+
+.icons-rippler {
+ -jfx-rippler-fill: BLUE;
+ -jfx-mask-type: CIRCLE;
+}
+
+.icons-rippler:hover {
+ -fx-cursor: hand;
+}
+
+.jfx-check-box {
+ -fx-font-weight: BOLD;
+}
+
+.custom-jfx-check-box {
+ -jfx-checked-color: RED;
+ -jfx-unchecked-color: BLACK;
+}
+
+/* Button */
+.button {
+ -fx-padding: 0.7em 0.57em;
+ -fx-font-size: 14px;
+}
+
+.button-raised {
+ -fx-padding: 0.7em 0.57em;
+ -fx-font-size: 14px;
+ -jfx-button-type: RAISED;
+ -fx-background-color: rgb(77, 102, 204);
+ -fx-pref-width: 200;
+ -fx-text-fill: WHITE;
+}
+
+/* The main scrollbar **track** CSS class */
+.mylistview .scroll-bar:horizontal .track,
+.mylistview .scroll-bar:vertical .track {
+ -fx-background-color: transparent;
+ -fx-border-color: transparent;
+ -fx-background-radius: 0em;
+ -fx-border-radius: 2em;
+}
+
+/* The increment and decrement button CSS class of scrollbar */
+.mylistview .scroll-bar:horizontal .increment-button,
+.mylistview .scroll-bar:horizontal .decrement-button {
+ -fx-background-color: transparent;
+ -fx-background-radius: 0em;
+ -fx-padding: 0 0 10 0;
+}
+
+/* The increment and decrement button CSS class of scrollbar */
+
+.mylistview .scroll-bar:vertical .increment-button,
+.mylistview .scroll-bar:vertical .decrement-button {
+ -fx-background-color: transparent;
+ -fx-background-radius: 0em;
+ -fx-padding: 0 10 0 0;
+
+}
+
+.mylistview .scroll-bar .increment-arrow,
+.mylistview .scroll-bar .decrement-arrow {
+ -fx-shape: " ";
+ -fx-padding: 0;
+}
+
+/* The main scrollbar **thumb** CSS class which we drag every time (movable) */
+.mylistview .scroll-bar:horizontal .thumb,
+.mylistview .scroll-bar:vertical .thumb {
+ -fx-background-color: derive(black, 90%);
+ -fx-background-insets: 2, 0, 0;
+ -fx-background-radius: 2em;
+}
+
+.jfx-list-cell-container {
+ -fx-alignment: center-left;
+}
+
+.jfx-list-cell-container > .label {
+ -fx-text-fill: BLACK;
+}
+
+.jfx-list-cell:odd:selected > .jfx-rippler > StackPane, .jfx-list-cell:even:selected > .jfx-rippler > StackPane {
+ -fx-background-color: rgba(0, 0, 255, 0.2);
+}
+
+.jfx-list-cell {
+ -fx-background-insets: 0.0;
+ -fx-text-fill: BLACK;
+}
+
+.jfx-list-cell:odd, .jfx-list-cell:even {
+ -fx-background-color: WHITE;
+}
+
+.jfx-list-cell:filled:hover {
+ -fx-text-fill: black;
+}
+
+.jfx-list-cell .jfx-rippler {
+ -jfx-rippler-fill: BLUE;
+}
+
+.jfx-list-view {
+ -fx-background-insets: 0;
+ -jfx-cell-horizontal-margin: 0.0;
+ -jfx-cell-vertical-margin: 5.0;
+ -jfx-vertical-gap: 10;
+ -jfx-expanded: false;
+ -fx-pref-width: 200;
+}
+
+.jfx-toggle-button {
+ -jfx-toggle-color: RED;
+}
+
+.jfx-tool-bar {
+ -fx-font-size: 15;
+ -fx-background-color: #5264AE;
+ -fx-pref-width: 100%;
+ -fx-pref-height: 64px;
+}
+
+.jfx-tool-bar HBox {
+ -fx-alignment: center;
+ -fx-spacing: 25;
+ -fx-padding: 0 10;
+}
+
+.jfx-tool-bar Label {
+ -fx-text-fill: WHITE;
+}
+
+.jfx-popup-container {
+ -fx-background-color: WHITE;
+}
+
+.jfx-snackbar-content {
+ -fx-background-color: #323232;
+ -fx-padding: 5;
+ -fx-spacing: 5;
+}
+
+.jfx-snackbar-toast {
+ -fx-text-fill: WHITE;
+}
+
+.jfx-snackbar-action {
+ -fx-text-fill: #ff4081;
+}
+
+.jfx-list-cell-content-container {
+ -fx-alignment: center-left;
+}
+
+.jfx-list-cell-container .label {
+ -fx-text-fill: BLACK;
+}
+
+.combo-box-popup .list-view .jfx-list-cell:odd:selected .jfx-list-cell-container,
+.combo-box-popup .list-view .jfx-list-cell:even:selected .jfx-list-cell-container {
+ -fx-background-color: rgba(0.0, 0.0, 255.0, 0.2);
+}
+
+.combo-box-popup .list-view .jfx-list-cell {
+ -fx-background-insets: 0.0;
+ -fx-text-fill: BLACK;
+}
+
+.combo-box-popup .list-view .jfx-list-cell:odd,
+.combo-box-popup .list-view .jfx-list-cell:even {
+ -fx-background-color: WHITE;
+}
+
+.combo-box-popup .list-view .jfx-list-cell:filled:hover {
+ -fx-text-fill: black;
+}
+
+.combo-box-popup .list-view .jfx-list-cell .jfx-rippler {
+ -fx-rippler-fill: #5264AE;
+}
+
+/*.combo-box .combo-box-button-container{
+ -fx-border-color:BLACK;-fx-border-width: 0 0 1 0;
+}
+.combo-box .combo-box-selected-value-container{
+ -fx-border-color:BLACK;
+} */
+
+/*
+ * TREE TABLE CSS
+ */
+
+.tree-table-view {
+ -fx-tree-table-color: rgba(255, 0, 0, 0.2);
+ -fx-tree-table-rippler-color: rgba(255, 0, 0, 0.4);
+}
+
+.tree-table-view:focused .tree-table-row-cell:selected {
+ -fx-background-color: -fx-tree-table-color;
+ -fx-table-cell-border-color: -fx-tree-table-color;
+ -fx-text-fill: BLACK;
+}
+
+.tree-table-view:focused .tree-table-row-cell:selected .tree-table-cell {
+ -fx-text-fill: BLACK;
+}
+
+.tree-table-view .jfx-rippler {
+ -jfx-rippler-fill: -fx-tree-table-rippler-color;
+}
+
+.tree-table-view .column-header,
+.tree-table-view .column-header-background,
+.tree-table-view .column-header-background .filler {
+ -fx-background-color: TRANSPARENT;
+}
+
+.tree-table-view .column-header {
+ -fx-border-width: 0 1 0 1;
+ -fx-border-color: #F3F3F3;
+}
+
+.tree-table-view .column-header .label {
+ -fx-text-fill: #949494;
+ -fx-padding: 16 0 16 0;
+}
+
+.tree-table-view .column-header .arrow, .tree-table-view .column-header .sort-order-dot {
+ -fx-background-color: #949494;
+}
+
+.tree-table-view .column-header:last-visible {
+ -fx-border-width: 0 2 0 1;
+}
+
+.tree-table-view .column-header-background {
+ -fx-border-width: 0 0.0 1 0;
+ -fx-border-color: #F3F3F3;
+}
+
+.tree-table-view .tree-table-cell {
+ -fx-border-width: 0 0 0 0;
+ -fx-padding: 16 0 16 0;
+}
+
+.tree-table-view .column-overlay {
+ -fx-background-color: -fx-tree-table-color;
+}
+
+.tree-table-view .column-resize-line, .tree-table-view .column-drag-header {
+ -fx-background-color: -fx-tree-table-rippler-color;
+}
+
+.tree-table-view:focused {
+ -fx-background-color: -fx-tree-table-color, -fx-box-border, -fx-control-inner-background;
+ -fx-background-insets: -1.4, 0, 1;
+ -fx-background-radius: 1.4, 0, 0;
+ /*....*/
+ -fx-padding: 1; /* 0.083333em; */
+}
+
+.tree-table-row-cell > .tree-disclosure-node > .arrow {
+ -fx-background-color: -fx-text-fill;
+ -fx-padding: 0.333333em 0.229em 0.333333em 0.229em; /* 4 */
+ -fx-shape: "M 0 -3.5 L 4 0 L 0 3.5 z";
+}
+
+.tree-table-row-cell .jfx-text-field {
+ -fx-focus-color: rgba(240, 40, 40);
+}
+
+.tree-table-row-group {
+ -fx-background-color: rgba(230, 230, 230);
+}
+
+.animated-option-button {
+ -fx-pref-width: 50px;
+ -fx-background-color: #44B449;
+ -fx-background-radius: 50px;
+ -fx-pref-height: 50px;
+ -fx-text-fill: white;
+ -fx-border-color: WHITE;
+ -fx-border-radius: 50px;
+ -fx-border-width: 4px;
+}
+
+.animated-option-sub-button {
+ -fx-background-color: #43609C;
+}
+
+.animated-option-sub-button2 {
+ -fx-background-color: rgb(203, 104, 96);
+}
+
+.tree-table-view .menu-item:focused {
+ -fx-background-color: -fx-tree-table-color;
+
+}
+
+.tree-table-view .menu-item .label {
+ -fx-padding: 5 0 5 0;
+}
+
+
diff --git a/HMCL/out/production/resources/assets/css/jfoenix-main-demo.css b/HMCL/out/production/resources/assets/css/jfoenix-main-demo.css
new file mode 100644
index 000000000..6c5c8d875
--- /dev/null
+++ b/HMCL/out/production/resources/assets/css/jfoenix-main-demo.css
@@ -0,0 +1,1033 @@
+/*
+ * Apply google Roboto font to all UI components
+ */
+
+.root {
+ -fx-font-family: "Roboto";
+}
+
+.side-menu {
+ -fx-padding: 20.0, 10.0;
+ -fx-font-size: 15.0;
+ -fx-font-weight: NORMAL;
+ -fx-text-fill: #444;
+}
+
+.title-label {
+ -fx-font-size: 16.0px;
+ -fx-padding: 14.0px;
+ -fx-text-fill: rgba(0.0, 0.0, 0.0, 0.87);
+}
+
+.radio-button-title-label {
+ -fx-font-size: 16.0px;
+ -fx-padding: 14.0 0.0 -20.0 0.0;
+ -fx-text-fill: rgba(0.0, 0.0, 0.0, 0.87);
+}
+
+/*******************************************************************************
+ * *
+ * JFX Drawer *
+ * *
+ ******************************************************************************/
+
+.jfx-drawer-overlay-pane {
+ -fx-background-color: rgba(0.0, 0.0, 0.0, 0.1)
+}
+
+.jfx-drawer-side-pane {
+ -fx-background-color: WHITE;
+}
+
+/*******************************************************************************
+ * *
+ * JFX Dialog Layout *
+ * *
+ ******************************************************************************/
+
+.jfx-dialog-overlay-pane {
+ -fx-background-color: rgba(0.0, 0.0, 0.0, 0.1);
+}
+
+.dialog-trigger {
+ -fx-background-color: WHITE;
+ -jfx-button-type: RAISED;
+ -fx-font-size: 14.0px;
+}
+
+.jfx-dialog-layout {
+ -fx-padding: 24.0px 24.0px 16.0px 24.0px;
+ -fx-text-fill: rgba(0.0, 0.0, 0.0, 0.87);
+}
+
+.jfx-layout-heading {
+ -fx-font-size: 20.0px;
+ -fx-font-weight: BOLD;
+ -fx-alignment: center-left;
+ -fx-padding: 5.0 0.0 5.0 0.0;
+}
+
+.jfx-layout-body .label {
+ -fx-font-size: 14.0px;
+ -fx-pref-width: 400.0px;
+ -fx-wrap-text: true;
+}
+
+.jfx-layout-actions {
+ -fx-padding: 10.0px 0.0 0.0 0.0;
+ -fx-alignment: center-right;
+}
+
+.dialog-accept {
+ -fx-text-fill: #03A9F4;
+ -fx-font-weight: BOLD;
+ -fx-padding: 0.7em 0.8em;
+}
+
+/*******************************************************************************
+ * *
+ * JFX Pop Up *
+ * *
+ ******************************************************************************/
+
+.jfx-popup-overlay-pane {
+ -fx-background-color: transparent;
+}
+
+.jfx-popup-container {
+ -fx-background-color: WHITE;
+ -fx-pref-width: 151.0px;
+}
+
+.popup-list-view {
+ -fx-pref-width: 150.0px;
+}
+
+.jfx-snackbar-content {
+ -fx-background-color: #323232;
+}
+
+.jfx-snackbar-toast {
+ -fx-text-fill: WHITE;
+}
+
+.jfx-snackbar-action {
+ -fx-text-fill: #ff4081;
+}
+
+/*******************************************************************************
+ * *
+ * JFX Icons *
+ * *
+ ******************************************************************************/
+
+.icon {
+ -fx-fill: #FE774D;
+ -fx-padding: 10.0;
+ -fx-cursor: hand;
+}
+
+.icons-rippler {
+ -jfx-rippler-fill: #FE774D;
+ -jfx-mask-type: CIRCLE;
+}
+
+.icons-rippler1 {
+ -jfx-rippler-fill: #4285F4;
+ -jfx-mask-type: CIRCLE;
+}
+
+.icons-rippler:hover {
+ -fx-cursor: hand;
+}
+
+.animated-burgers .jfx-hamburger StackPane {
+ -fx-background-color: #5264AE;
+ -fx-background-radius: 4px;
+}
+
+.animated-burgers .jfx-rippler {
+ -jfx-rippler-fill: #5264AE;
+}
+
+.icons-badge .badge-pane {
+ -fx-background-color: #ff4081;
+ -fx-background-radius: 23;
+ -fx-pref-width: 23;
+ -fx-pref-height: 23;
+ -fx-alignment: center;
+}
+
+.icons-badge Label {
+ -fx-font-weight: BOLD;
+ -fx-font-size: 14.0px;
+ -fx-text-fill: WHITE;
+}
+
+/*******************************************************************************
+* *
+* JFX Hamburger Icon *
+* *
+*******************************************************************************/
+
+.jfx-hamburger {
+ -fx-padding: 10.0;
+ -fx-spacing: 4px;
+ -fx-cursor: hand;
+}
+
+.jfx-hamburger StackPane {
+ -fx-pref-width: 30px;
+ -fx-pref-height: 4px;
+ -fx-background-color: #FFF;
+ -fx-background-radius: 5px;
+}
+
+/*******************************************************************************
+ * *
+ * JFX Tool Bar *
+ * *
+ ******************************************************************************/
+
+.jfx-tool-bar {
+ -fx-font-size: 13.0;
+ -fx-font-weight: BOLD;
+ -fx-background-color: #5264AE;
+ -fx-pref-width: 100.0%;
+ -fx-pref-height: 32.0px;
+}
+
+.jfx-tool-bar HBox {
+ -fx-alignment: center;
+ -fx-spacing: 25.0;
+ -fx-padding: 0.0 10.0;
+}
+
+.jfx-tool-bar Label {
+ -fx-text-fill: WHITE;
+}
+
+.jfx-tool-bar .jfx-options-burger {
+ -fx-padding: 22px;
+}
+
+.jfx-tool-bar .jfx-options-burger StackPane {
+ -fx-pref-width: 4px;
+}
+
+.jfx-tool-bar .jfx-rippler {
+ -jfx-rippler-fill: WHITE;
+}
+
+.option-list-view {
+ -fx-pref-width: 160.0px;
+ -fx-background-color: WHITE;
+}
+
+.option-jfx-list-view-icon {
+ -fx-fill: #5264AE;
+ -fx-padding: 0.0 10.0 0.0 5.0;
+ -fx-cursor: hand;
+}
+
+/*******************************************************************************
+ * *
+ * JFX Radio Button *
+ * *
+ ******************************************************************************/
+
+.jfx-radio-button .radio {
+ -fx-stroke-width: 2.0px;
+ -fx-fill: transparent;
+}
+
+.jfx-radio-button .dot {
+ -fx-fill: #0F9D58;
+}
+
+.custom-jfx-radio-button {
+ -fx-font-size: 16.0px;
+}
+
+.custom-jfx-radio-button .radio {
+ -fx-stroke-width: 2.0px;
+ -fx-fill: transparent;
+}
+
+.custom-jfx-radio-button-blue {
+ -fx-text-fill: #5264AE;
+ -jfx-selected-color: #5264AE;
+ -jfx-unselected-color: #212121;
+}
+
+.custom-jfx-radio-button-red {
+ -fx-text-fill: #f44336;
+ -jfx-selected-color: #f44336;
+ -jfx-unselected-color: #b71c1c;
+}
+
+.custom-jfx-radio-button-green {
+ -fx-text-fill: #4caf50;
+ -jfx-selected-color: #4caf50;
+ -jfx-unselected-color: #1b5e20;
+}
+
+/*******************************************************************************
+ * *
+ * JFX Slider *
+ * *
+ ******************************************************************************/
+
+.svg-slider .thumb {
+ -fx-stroke: black;
+ -fx-fill: black;
+}
+
+.jfx-slider:disabled {
+ -fx-opacity: 0.4;
+}
+
+/*******************************************************
+* *
+* For the main demo sliders *
+* *
+*******************************************************/
+
+.jfx-slider-style {
+ -jfx-indicator-position: right;
+}
+
+.jfx-slider-style > .thumb {
+ -fx-background-color: #03a9f4;
+}
+
+.jfx-slider-style .track {
+ -fx-background-color: #ff5252;
+ -fx-pref-height: 5px;
+ -fx-pref-width: 5px;
+}
+
+.jfx-slider-style .sliderValue {
+ -fx-stroke: WHITE;
+ -fx-font-size: 10.0;
+}
+
+/******************************************************/
+
+/*******************************************************************************
+* *
+* JFX Rippler *
+* *
+*******************************************************************************/
+
+/*.jfx-rippler {
+ -fx-rippler-fill: #5264AE;
+ -fx-mask-type: RECT;
+}*/
+.jfx-rippler:hover {
+ -fx-cursor: hand;
+}
+
+/*******************************************************************************
+* *
+* JFX Button *
+* *
+*******************************************************************************/
+
+.custom-jfx-button-raised .jfx-rippler {
+ -jfx-rippler-fill: YELLOW;
+}
+
+.custom-jfx-button-raised {
+ -fx-padding: 0.7em 0.57em;
+ -fx-font-size: 14.0px;
+ -jfx-button-type: RAISED;
+ -fx-background-color: rgb(102.0, 153.0, 102.0);
+ -fx-pref-width: 200.0;
+ -fx-text-fill: WHITE;
+}
+
+.circle-jfx-button-raised .jfx-rippler {
+ -jfx-rippler-fill: YELLOW;
+}
+
+.circle-jfx-button-raised {
+ -fx-padding: 0.7em 0.57em;
+ -fx-font-size: 14.0px;
+ -jfx-button-type: RAISED;
+ -fx-background-color: rgb(102.0, 153.0, 102.0);
+ -fx-pref-width: 200.0;
+ -fx-text-fill: WHITE;
+ -jfx-mask-type: CIRCLE;
+}
+
+/*******************************************************************************
+* *
+* JFX Check Box *
+* *
+*******************************************************************************/
+
+.jfx-check-box {
+ -fx-font-weight: BOLD;
+}
+
+.custom-jfx-check-box {
+ -jfx-checked-color: RED;
+ -jfx-unchecked-color: BLACK;
+}
+
+.custom-jfx-check-box-all-colored {
+ -jfx-checked-color: #5264AE;
+ -jfx-unchecked-color: #5264AE;
+ -fx-text-fill: #5264AE;
+}
+
+.custom-jfx-check-box-text-colored {
+ -fx-text-fill: rgb(153.0, 0.0, 0.0);
+}
+
+/*******************************************************************************
+* *
+* JFX Progress Bar *
+* *
+*******************************************************************************/
+
+.jfx-progress-bar {
+ -fx-pref-width: 500.0;
+}
+
+.jfx-progress-bar > .track, .jfx-progress-bar > .bar {
+ -fx-background-radius: 0;
+ -fx-background-insets: 0;
+}
+
+.jfx-progress-bar > .track {
+ -fx-background-color: #E0E0E0;
+}
+
+.jfx-progress-bar > .bar {
+ -fx-background-color: #0F9D58;
+}
+
+.custom-jfx-progress-bar > .bar {
+ -fx-background-color: rgb(255.0, 128.0, 0.0);
+}
+
+.custom-jfx-progress-bar-stroke > .bar {
+ -fx-background-color: #5264AE;
+ -fx-padding: 6;
+}
+
+/*******************************************************************************
+* *
+* JFX Textfield *
+* *
+*******************************************************************************/
+
+.jfx-text-field, .jfx-password-field, .jfx-text-area {
+ -fx-background-color: transparent;
+ -fx-font-weight: BOLD;
+ -fx-prompt-text-fill: #808080;
+ -fx-alignment: top-left;
+ -fx-max-width: 300.0;
+ -fx-pref-width: 300.0;
+ -jfx-focus-color: #4059A9;
+ -jfx-unfocus-color: #4d4d4d;
+}
+
+.jfx-text-area .viewport {
+ -fx-background-color: #F4F4F4;
+}
+
+/*******************************************************************************
+* *
+* JFX List View *
+* *
+*******************************************************************************/
+
+.jfx-list-cell {
+ -fx-background-insets: 0.0;
+ -fx-text-fill: BLACK;
+}
+
+.jfx-list-cell:odd,
+.jfx-list-cell:even {
+ -fx-background-color: WHITE;
+}
+
+.jfx-list-cell:filled:hover,
+.jfx-list-cell:selected .label {
+ -fx-text-fill: black;
+}
+
+.jfx-list-cell .jfx-rippler {
+ -jfx-rippler-fill: #5264AE;
+}
+
+.jfx-list-cell:odd:selected > .jfx-rippler > StackPane,
+.jfx-list-cell:even:selected > .jfx-rippler > StackPane {
+ -fx-background-color: rgba(0.0, 0.0, 255.0, 0.2);
+}
+
+.jfx-list-view {
+ -fx-background-insets: 0.0;
+ -jfx-cell-horizontal-margin: 0.0;
+ -jfx-cell-vertical-margin: 5.0;
+ -jfx-vertical-gap: 10.0;
+ -jfx-expanded: false;
+}
+
+.custom-jfx-list-view .jfx-list-cell:odd:selected > .jfx-rippler > StackPane,
+.custom-jfx-list-view .jfx-list-cell:even:selected > .jfx-rippler > StackPane {
+ -fx-background-color: rgba(255, 0, 0, 0.2);
+}
+
+.custom-jfx-list-view {
+ -fx-background-insets: 0.0;
+ -jfx-cell-horizontal-margin: 0.0;
+ -jfx-cell-vertical-margin: 5.0;
+ -jfx-expanded: false;
+ -fx-max-width: 200.0px;
+ /* important to hide the list change of height */
+ -fx-background-color: TRANSPARENT;
+}
+
+.custom-jfx-list-view .jfx-rippler {
+ -jfx-rippler-fill: RED;
+}
+
+.custom-jfx-list-view1 {
+ -jfx-vertical-gap: 10.0;
+ -fx-pref-width: 150px;
+ -fx-background-color: transparent;
+}
+
+.custom-jfx-list-view-icon,
+.jfx-list-cell:selected .label .custom-jfx-list-view-icon {
+ /*-fx-text-fill: #5264AE;*/
+ -fx-fill: #5264AE;
+ -fx-padding: 0.0 10.0 0.0 5.0;
+ -fx-cursor: hand;
+}
+
+.custom-jfx-list-view-icon-container {
+ -fx-pref-width: 40px;
+}
+
+.custom-jfx-list-view .sublist-item {
+ -fx-border-color: #e0e0e0;
+ -fx-border-width: 1 0 1 0;
+}
+
+.custom-jfx-list-view .jfx-list-cell .sublist-header > .drop-icon {
+ -fx-background-color: GRAY;
+}
+
+.custom-jfx-list-view .jfx-list-cell:filled:hover .sublist-header > .drop-icon {
+ -fx-background-color: BLACK;
+}
+
+/*******************************************************************************/
+/*******************************************************************************/
+/*******************************************************************************/
+/*******************************************************************************/
+/*******************************************************************************/
+/*******************************************************************************/
+/*******************************************************************************
+* *
+* JFX SUBLIST IMPORTANT *
+* *
+*******************************************************************************/
+.sublist .scroll-bar:vertical .increment-arrow,
+.sublist .scroll-bar:vertical .decrement-arrow,
+.sublist .scroll-bar:vertical .increment-button,
+.sublist .scroll-bar:vertical .decrement-button {
+ -fx-padding: 0;
+}
+
+.sublist .scroll-bar:vertical .track,
+.sublist .scroll-bar:vertical .thumb {
+ -fx-background-color: WHITE;
+}
+
+.sublist {
+ -fx-background-color: TRANSPARENT;
+}
+
+.sublist .jfx-list-cell:odd:selected > .jfx-rippler > StackPane,
+.sublist .jfx-list-cell:even:selected > .jfx-rippler > StackPane {
+ -fx-background-color: rgba(255, 0, 0, 0.2);
+}
+
+.sublist .jfx-list-cell {
+ -fx-background-insets: 0.0;
+ -fx-text-fill: BLACK;
+}
+
+.sublist-header {
+ -fx-alignment: center-left;
+}
+
+.sublist-header .sub-label {
+ -fx-padding: 0 0 0 12;
+}
+
+/*.custom-jfx-list-view .sublist-container {
+ -fx-padding : 0 0 5 0;
+}*/
+
+/*******************************************************************************
+* *
+* JFX Toggle Button *
+* *
+*******************************************************************************/
+
+.jfx-toggle-button {
+ -jfx-toggle-color: #0F9D58;
+}
+
+.custom-jfx-toggle-button {
+ -jfx-toggle-color: #4285F4;
+}
+
+.custom-jfx-toggle-button-red {
+ -jfx-toggle-color: red;
+}
+
+.toggle-label {
+ -fx-font-size: 14.0px;
+}
+
+.toggle-icon1 .icon {
+ -fx-fill: #4285F4;
+ -fx-padding: 20.0;
+}
+
+.toggle-icon1 {
+ -fx-pref-width: 80px;
+ -fx-background-radius: 80px;
+ -fx-pref-height: 80px;
+ -fx-background-color: transparent;
+ -jfx-toggle-color: rgba(66.0, 133.0, 244.0, 0.29885056614875793);
+ -jfx-untoggle-color: transparent;
+}
+
+.toggle-icon1 .jfx-rippler {
+ -jfx-rippler-fill: rgba(66.0, 133.0, 244.0, 0.29885056614875793);
+ -jfx-mask-type: CIRCLE;
+}
+
+.toggle-icon2, .toggle-icon3 {
+ -fx-pref-width: 50px;
+ -fx-background-radius: 50px;
+ -fx-pref-height: 50px;
+ -fx-background-color: transparent;
+}
+
+.toggle-icon2 .icon {
+ -fx-fill: RED;
+}
+
+.toggle-icon2 .jfx-rippler {
+ -jfx-rippler-fill: RED;
+ -jfx-mask-type: CIRCLE;
+}
+
+.toggle-icon3 {
+ -fx-pref-width: 40px;
+ -fx-background-radius: 50px;
+ -fx-pref-height: 30px;
+ -fx-background-color: transparent;
+ -jfx-toggle-color: rgba(128, 128, 255, 0.2);
+ -jfx-untoggle-color: transparent;
+}
+
+.toggle-icon3 .icon {
+ -fx-fill: rgb(204.0, 204.0, 51.0);
+ -fx-padding: 10.0;
+}
+
+.toggle-icon3 .jfx-rippler {
+ -jfx-rippler-fill: #0F9D58;
+ -jfx-mask-type: CIRCLE;
+}
+
+/*******************************************************************************
+* *
+* JFX Spinner *
+* *
+*******************************************************************************/
+
+.jfx-spinner > .arc {
+ -fx-stroke-width: 3.0;
+ -fx-fill: transparent;
+}
+
+.first-spinner {
+ -jfx-radius: 20;
+}
+
+.first-spinner > .arc {
+ -fx-stroke-width: 5.0;
+}
+
+.second-spinner {
+ -jfx-radius: 30;
+}
+
+.second-spinner > .arc {
+ -fx-stroke-width: 5.0;
+}
+
+.third-spinner {
+ -jfx-radius: 40;
+}
+
+.third-spinner > .arc {
+ -fx-stroke-width: 5.0;
+}
+
+.fourth-spinner {
+ -jfx-radius: 50;
+}
+
+.fourth-spinner > .arc {
+ -fx-stroke-width: 5.0;
+}
+
+.fifth-spinner {
+ -jfx-radius: 60;
+}
+
+.fifth-spinner > .arc {
+ -fx-stroke-width: 5.0;
+}
+
+.sixth-spinner {
+ -jfx-radius: 70;
+}
+
+.sixth-spinner > .arc {
+ -fx-stroke-width: 5.0;
+}
+
+.seventh-spinner {
+ -jfx-radius: 80;
+}
+
+.seventh-spinner > .arc {
+ -fx-stroke-width: 5.0;
+}
+
+.blue-spinner .arc {
+ -fx-stroke: #4285f4;
+}
+
+.red-spinner .arc {
+ -fx-stroke: #db4437;
+}
+
+.green-spinner .arc {
+ -fx-stroke: #f4b400;
+}
+
+.yellow-spinner .arc {
+ -fx-stroke: #0F9D58;
+}
+
+.materialDesign-purple .arc {
+ -fx-stroke: #ab47bc;
+}
+
+.materialDesign-blue .arc {
+ -fx-stroke: #2962ff;
+}
+
+.materialDesign-cyan .arc {
+ -fx-stroke: #00b8d4;
+}
+
+.materialDesign-green .arc {
+ -fx-stroke: #00c853;
+}
+
+.materialDesign-yellow .arc {
+ -fx-stroke: #ffd600;
+}
+
+.materialDesign-orange .arc {
+ -fx-stroke: #ff6d00;
+}
+
+.materialDesign-red .arc {
+ -fx-stroke: #d50000;
+}
+
+/*******************************************************************************
+* *
+* JFX Combo Box *
+* *
+*******************************************************************************/
+
+.combo-box-popup .list-view .jfx-list-cell .label,
+.combo-box-popup .list-view .jfx-list-cell:filled:hover .label {
+ -fx-text-fill: BLACK;
+}
+
+.combo-box-popup .list-view .jfx-list-cell .custom-jfx-list-view-icon,
+.combo-box-popup .list-view .jfx-list-cell:filled:hover .custom-jfx-list-view-icon,
+.combo-box-popup .list-view .jfx-list-cell:selected .custom-jfx-list-view-icon {
+ -fx-fill: #5264AE;
+}
+
+.combo-box-popup .list-view .jfx-list-cell:odd:selected > .jfx-rippler > StackPane,
+.combo-box-popup .list-view .jfx-list-cell:even:selected > .jfx-rippler > StackPane {
+ -fx-background-color: rgba(0.0, 0.0, 255.0, 0.2);
+}
+
+.combo-box-popup .list-view .jfx-list-cell {
+ -fx-background-insets: 0.0;
+}
+
+.combo-box-popup .list-view .jfx-list-cell:odd,
+.combo-box-popup .list-view .jfx-list-cell:even {
+ -fx-background-color: WHITE;
+}
+
+.combo-box-popup .list-view .jfx-list-cell .jfx-rippler {
+ -jfx-rippler-fill: #5264AE;
+}
+
+/*******************************************************************************
+* *
+* JFX Decorator *
+* *
+*******************************************************************************/
+
+.jfx-decorator {
+ -fx-decorator-color: derive(#5264AE, -20%);
+}
+
+.jfx-decorator .jfx-decorator-buttons-container {
+ -fx-background-color: -fx-decorator-color;
+}
+
+.jfx-decorator .resize-border {
+ -fx-border-color: -fx-decorator-color;
+ -fx-border-width: 0 4 4 4;
+}
+
+/*******************************************************************************
+* *
+* Tree Table View *
+* *
+*******************************************************************************/
+
+.tree-table-view {
+ -fx-tree-table-color: rgba(82, 100, 174, 0.4);
+ -fx-tree-table-rippler-color: rgba(82, 100, 174, 0.6);
+}
+
+.tree-table-view:focused .tree-table-row-cell:selected {
+ -fx-background-color: -fx-tree-table-color;
+ -fx-table-cell-border-color: -fx-tree-table-color;
+ -fx-text-fill: BLACK;
+}
+
+.tree-table-view:focused .tree-table-row-cell:selected .tree-table-cell {
+ -fx-text-fill: BLACK;
+}
+
+.tree-table-view .jfx-rippler {
+ -jfx-rippler-fill: -fx-tree-table-rippler-color;
+}
+
+.tree-table-view .column-header,
+.tree-table-view .column-header-background,
+.tree-table-view .column-header-background .filler {
+ -fx-background-color: TRANSPARENT;
+}
+
+.tree-table-view .column-header {
+ -fx-border-width: 0 1 0 1;
+ -fx-border-color: #F3F3F3;
+}
+
+.tree-table-view .column-header .label {
+ -fx-text-fill: #949494;
+ -fx-padding: 16 0 16 0;
+}
+
+.tree-table-view .column-header .arrow, .tree-table-view .column-header .sort-order-dot {
+ -fx-background-color: #949494;
+}
+
+.tree-table-view .column-header:last-visible {
+ -fx-border-width: 0 2 0 1;
+}
+
+.tree-table-view .column-header-background {
+ -fx-border-width: 0 0.0 1 0;
+ -fx-border-color: #F3F3F3;
+}
+
+.tree-table-view .tree-table-cell {
+ -fx-border-width: 0 0 0 0;
+ -fx-padding: 16 0 16 0;
+ -fx-alignment: top-center;
+}
+
+.tree-table-view .column-overlay {
+ -fx-background-color: -fx-tree-table-color;
+}
+
+.tree-table-view .column-resize-line, .tree-table-view .column-drag-header {
+ -fx-background-color: -fx-tree-table-rippler-color;
+}
+
+.tree-table-view:focused {
+ -fx-background-color: -fx-tree-table-color, -fx-box-border, -fx-control-inner-background;
+ -fx-background-insets: -1.4, 0, 1;
+ -fx-background-radius: 1.4, 0, 0;
+ /*....*/
+ -fx-padding: 1; /* 0.083333em; */
+}
+
+.tree-table-row-cell > .tree-disclosure-node > .arrow {
+ -fx-background-color: -fx-text-fill;
+ -fx-padding: 0.333333em 0.229em 0.333333em 0.229em; /* 4 */
+ -fx-shape: "M 0 -3.5 L 4 0 L 0 3.5 z";
+}
+
+.tree-table-row-cell .jfx-text-field {
+ -fx-focus-color: rgba(82, 100, 174);
+}
+
+.tree-table-row-cell .jfx-text-field:error {
+ -jfx-focus-color: #D34336;
+ -jfx-unfocus-color: #D34336;
+}
+
+.tree-table-row-cell .jfx-text-field .error-label {
+ -fx-text-fill: #D34336;
+ -fx-font-size: 0.75em;
+}
+
+.tree-table-row-cell .jfx-text-field .error-icon {
+ -fx-fill: #D34336;
+ -fx-font-size: 1.0em;
+}
+
+.tree-table-row-group {
+ -fx-background-color: rgba(230, 230, 230);
+}
+
+.tree-table-view .menu-item:focused {
+ -fx-background-color: -fx-tree-table-color;
+
+}
+
+.tree-table-view .menu-item .label {
+ -fx-padding: 5 0 5 0;
+}
+
+/*******************************************************************************
+ * *
+ * Scroll Bar *
+ * *
+ ******************************************************************************/
+
+.scroll-bar:vertical > .track-background, .scroll-bar:horizontal > .track-background {
+ -fx-background-color: #F1F1F1;
+ -fx-background-insets: 0.0;
+}
+
+.scroll-bar:vertical > .thumb, .scroll-bar:horizontal > .thumb {
+ -fx-background-color: #BCBCBC;
+ -fx-background-insets: 0.0;
+ -fx-background-radius: 1.0;
+}
+
+.scroll-bar > .increment-button, .scroll-bar > .decrement-button {
+ -fx-padding: 5 2 5 2;
+}
+
+.scroll-bar > .increment-button, .scroll-bar > .decrement-button, .scroll-bar:hover > .increment-button, .scroll-bar:hover > .decrement-button {
+ -fx-background-color: transparent;
+}
+
+.scroll-bar > .increment-button > .increment-arrow, .scroll-bar > .decrement-button > .decrement-arrow {
+ -fx-background-color: rgb(150.0, 150.0, 150.0);
+}
+
+.scroll-bar > .increment-button > .increment-arrow {
+ -fx-shape: "M298 426h428l-214 214z";
+}
+
+.scroll-bar > .decrement-button > .decrement-arrow {
+ -fx-shape: "M298 598l214-214 214 214h-428z";
+}
+
+/*******************************************************************************
+ * *
+ * Scroll Pane *
+ * *
+ ******************************************************************************/
+
+.scroll-pane {
+ -fx-background-insets: 0;
+ -fx-padding: 0;
+}
+
+.scroll-pane:focused {
+ -fx-background-insets: 0;
+}
+
+.scroll-pane .corner {
+ -fx-background-insets: 0;
+}
+
+/*******************************************************************************
+* *
+* Error Facade *
+* *
+*******************************************************************************/
+
+.error-label {
+ -fx-text-fill: #D34336;
+ -fx-font-size: 0.75em;
+ -fx-font-weight: bold;
+}
+
+.error-icon {
+ -fx-fill: #D34336;
+ -fx-font-size: 1.0em;
+}
+
+.jfx-text-field:error, .jfx-password-field:error, .jfx-text-area:error, .jfx-combo-box:error {
+ -jfx-focus-color: #D34336;
+ -jfx-unfocus-color: #D34336;
+}
+
+.jfx-text-field .error-label, .jfx-password-field .error-label, .jfx-text-area .error-label {
+ -fx-text-fill: #D34336;
+ -fx-font-size: 0.75em;
+}
+
+.jfx-text-field .error-icon, .jfx-password-field .error-icon, .jfx-text-area .error-icon {
+ -fx-fill: #D34336;
+ -fx-font-size: 1.0em;
+}
+
+.fit-width {
+ -fx-pref-width: 100%;
+}
+/*
+.jfx-scroll-pane .main-header {
+ -fx-background-image: url("../bg1.jpg");
+}
+
+.jfx-scroll-pane .condensed-header {
+ -fx-background-image: url("../bg4.jpg");
+}
+*/
\ No newline at end of file
diff --git a/HMCL/out/production/resources/assets/fxml/download/dltype.fxml b/HMCL/out/production/resources/assets/fxml/download/dltype.fxml
new file mode 100644
index 000000000..5da12c5d5
--- /dev/null
+++ b/HMCL/out/production/resources/assets/fxml/download/dltype.fxml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+ Install New Game
+
+
+
+
+
+
+
+ Install Modpack (CurseForge supported)
+
+
+
+
+
+
+
diff --git a/HMCL/out/production/resources/assets/fxml/download/installers.fxml b/HMCL/out/production/resources/assets/fxml/download/installers.fxml
new file mode 100644
index 000000000..e81963ce1
--- /dev/null
+++ b/HMCL/out/production/resources/assets/fxml/download/installers.fxml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Install Forge
+
+
+
+
+
+
+
+ Install LiteLoader
+
+
+
+
+
+
+
+ Install OptiFine
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HMCL/out/production/resources/assets/fxml/download/versions-list-item.fxml b/HMCL/out/production/resources/assets/fxml/download/versions-list-item.fxml
new file mode 100644
index 000000000..c39c1fd4a
--- /dev/null
+++ b/HMCL/out/production/resources/assets/fxml/download/versions-list-item.fxml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HMCL/out/production/resources/assets/fxml/download/versions.fxml b/HMCL/out/production/resources/assets/fxml/download/versions.fxml
new file mode 100644
index 000000000..ab7abbe50
--- /dev/null
+++ b/HMCL/out/production/resources/assets/fxml/download/versions.fxml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/HMCL/out/production/resources/assets/fxml/main.fxml b/HMCL/out/production/resources/assets/fxml/main.fxml
new file mode 100644
index 000000000..4cd42eb03
--- /dev/null
+++ b/HMCL/out/production/resources/assets/fxml/main.fxml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HMCL/out/production/resources/assets/fxml/version-list-item.fxml b/HMCL/out/production/resources/assets/fxml/version-list-item.fxml
new file mode 100644
index 000000000..682f54fa8
--- /dev/null
+++ b/HMCL/out/production/resources/assets/fxml/version-list-item.fxml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HMCL/out/production/resources/assets/fxml/version.fxml b/HMCL/out/production/resources/assets/fxml/version.fxml
new file mode 100644
index 000000000..c1eb0c969
--- /dev/null
+++ b/HMCL/out/production/resources/assets/fxml/version.fxml
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Java Directory
+ Max Memory
+ Launcher Visibility
+ Run Directory
+ Dimension
+
+
+
+
+
+
+
+
+ Physical Memory: 16000MB
+
+
+
+
+
+
+
+
+
+ Close
+ Hide
+ Keep
+ Hide and Reopen
+
+
+
+
+
+
+ Default(.minecraft/)
+ Divided(.minecraft/versions/<versionName>)
+
+
+
+
+
+
+
+ x
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HMCL/out/production/resources/assets/fxml/wizard.fxml b/HMCL/out/production/resources/assets/fxml/wizard.fxml
new file mode 100644
index 000000000..5bfc439c2
--- /dev/null
+++ b/HMCL/out/production/resources/assets/fxml/wizard.fxml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HMCL/out/production/resources/assets/svg/arrow-left.fxml b/HMCL/out/production/resources/assets/svg/arrow-left.fxml
new file mode 100644
index 000000000..008914a29
--- /dev/null
+++ b/HMCL/out/production/resources/assets/svg/arrow-left.fxml
@@ -0,0 +1,2 @@
+
+
diff --git a/HMCL/out/production/resources/assets/svg/arrow-right.fxml b/HMCL/out/production/resources/assets/svg/arrow-right.fxml
new file mode 100644
index 000000000..535557cc7
--- /dev/null
+++ b/HMCL/out/production/resources/assets/svg/arrow-right.fxml
@@ -0,0 +1,2 @@
+
+
diff --git a/HMCL/out/production/resources/assets/svg/close.fxml b/HMCL/out/production/resources/assets/svg/close.fxml
new file mode 100644
index 000000000..b045a2a2a
--- /dev/null
+++ b/HMCL/out/production/resources/assets/svg/close.fxml
@@ -0,0 +1,2 @@
+
+
diff --git a/HMCL/out/production/resources/assets/svg/dots-vertical.fxml b/HMCL/out/production/resources/assets/svg/dots-vertical.fxml
new file mode 100644
index 000000000..d156a7f9b
--- /dev/null
+++ b/HMCL/out/production/resources/assets/svg/dots-vertical.fxml
@@ -0,0 +1,2 @@
+
+
diff --git a/HMCL/out/production/resources/assets/svg/gear.fxml b/HMCL/out/production/resources/assets/svg/gear.fxml
new file mode 100644
index 000000000..e777fa296
--- /dev/null
+++ b/HMCL/out/production/resources/assets/svg/gear.fxml
@@ -0,0 +1,2 @@
+
+
diff --git a/HMCL/out/production/resources/assets/svg/plus.fxml b/HMCL/out/production/resources/assets/svg/plus.fxml
new file mode 100644
index 000000000..498e812f4
--- /dev/null
+++ b/HMCL/out/production/resources/assets/svg/plus.fxml
@@ -0,0 +1,2 @@
+
+
diff --git a/HMCL/out/production/resources/assets/svg/refresh.fxml b/HMCL/out/production/resources/assets/svg/refresh.fxml
new file mode 100644
index 000000000..f1c6b5ff7
--- /dev/null
+++ b/HMCL/out/production/resources/assets/svg/refresh.fxml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/HMCL/out/production/resources/assets/svg/rocket.fxml b/HMCL/out/production/resources/assets/svg/rocket.fxml
new file mode 100644
index 000000000..8558b678b
--- /dev/null
+++ b/HMCL/out/production/resources/assets/svg/rocket.fxml
@@ -0,0 +1,2 @@
+
+
diff --git a/HMCL/settings.gradle b/HMCL/settings.gradle
new file mode 100644
index 000000000..3b884e584
--- /dev/null
+++ b/HMCL/settings.gradle
@@ -0,0 +1,2 @@
+rootProject.name = 'HMCL'
+
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Main.kt b/HMCL/src/main/java/org/jackhuang/hmcl/Main.kt
new file mode 100644
index 000000000..e6c38d2dd
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/Main.kt
@@ -0,0 +1,61 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl
+
+import javafx.application.Application
+import javafx.stage.Stage
+import org.jackhuang.hmcl.ui.Controllers
+import org.jackhuang.hmcl.util.DEFAULT_USER_AGENT
+import org.jackhuang.hmcl.util.OS
+import java.io.File
+
+class MainApplication : Application() {
+
+ override fun start(stage: Stage) {
+ Controllers.initialize(stage)
+
+ stage.isResizable = false
+ stage.scene = Controllers.scene
+ stage.show()
+ }
+
+ companion object {
+
+ @JvmStatic
+ fun main(args: Array) {
+ DEFAULT_USER_AGENT = "Hello Minecraft! Launcher"
+
+ launch(MainApplication::class.java, *args)
+ }
+
+ fun getWorkingDirectory(folder: String): File {
+ val userhome = System.getProperty("user.home", ".")
+ return when (OS.CURRENT_OS) {
+ OS.LINUX -> File(userhome, ".$folder/")
+ OS.WINDOWS -> {
+ val appdata: String? = System.getenv("APPDATA")
+ File(appdata ?: userhome, ".$folder/")
+ }
+ OS.OSX -> File(userhome, "Library/Application Support/" + folder)
+ else -> File(userhome, "$folder/")
+ }
+ }
+
+ fun getMinecraftDirectory(): File = getWorkingDirectory("minecraft")
+ }
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.kt b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.kt
new file mode 100644
index 000000000..895140fc3
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.kt
@@ -0,0 +1,43 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.game
+
+import org.jackhuang.hmcl.util.LOG
+import java.io.File
+import java.io.IOException
+import java.util.logging.Level
+
+class HMCLGameRepository(baseDirectory: File)
+ : DefaultGameRepository(baseDirectory) {
+
+ val PROFILE = "{\"selectedProfile\": \"(Default)\",\"profiles\": {\"(Default)\": {\"name\": \"(Default)\"}},\"clientToken\": \"88888888-8888-8888-8888-888888888888\"}"
+
+ @Synchronized
+ override fun refreshVersions() {
+ super.refreshVersions()
+
+ try {
+ val file = baseDirectory.resolve("launcher_profiles.json")
+ if (!file.exists())
+ file.writeText(PROFILE)
+ } catch (ex: IOException) {
+ LOG.log(Level.WARNING, "Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex)
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/EnumGameDirectory.kt b/HMCL/src/main/java/org/jackhuang/hmcl/setting/EnumGameDirectory.kt
new file mode 100644
index 000000000..c3fff72c1
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/EnumGameDirectory.kt
@@ -0,0 +1,23 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.setting
+
+enum class EnumGameDirectory {
+ ROOT_FOLDER,
+ VERSION_FOLDER
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/LauncherVisibility.kt b/HMCL/src/main/java/org/jackhuang/hmcl/setting/LauncherVisibility.kt
new file mode 100644
index 000000000..0d3cce324
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/LauncherVisibility.kt
@@ -0,0 +1,47 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.setting
+
+/**
+ * The visibility of launcher.
+ * @author huangyuhui
+ */
+enum class LauncherVisibility {
+
+ /**
+ * Close the launcher anyway when the game process created even if failed to
+ * launch game.
+ */
+ CLOSE,
+
+ /**
+ * Hide the launcher when the game process created, if failed to launch
+ * game, will show the log window.
+ */
+ HIDE,
+
+ /**
+ * Keep the launcher visible even if the game launched successfully.
+ */
+ KEEP,
+
+ /**
+ * Hide the launcher and reopen it when game closes.
+ */
+ HIDE_AND_REOPEN
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.kt b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.kt
new file mode 100644
index 000000000..32ed64045
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.kt
@@ -0,0 +1,232 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.setting
+
+import com.google.gson.*
+import javafx.beans.property.*
+import org.jackhuang.hmcl.util.*
+import java.lang.reflect.Type
+
+class VersionSetting() {
+
+ /**
+ * The displayed name.
+ */
+ val nameProperty = SimpleStringProperty(this, "name", "")
+ var name: String by nameProperty
+
+ /**
+ * HMCL Version Settings have been divided into 2 parts.
+ * 1. Global settings.
+ * 2. Version settings.
+ * If a version claims that it uses global settings, its version setting will be disabled.
+ *
+ * Defaults false because if one version uses global first, custom version file will not be generated.
+ */
+ val usesGlobalProperty = SimpleBooleanProperty(this, "usesGlobal", false)
+ var usesGlobal: Boolean by usesGlobalProperty
+
+ // java
+
+ /**
+ * Java version or null if user customizes java directory.
+ */
+ val javaProperty = SimpleStringProperty(this, "java", null)
+ var java: String? by javaProperty
+
+ /**
+ * User customized java directory or null if user uses system Java.
+ */
+ val javaDirProperty = SimpleStringProperty(this, "javaDir", "")
+ var javaDir: String by javaDirProperty
+
+ /**
+ * The command to launch java, i.e. optirun.
+ */
+ val wrapperProperty = SimpleStringProperty(this, "wrapper", "")
+ var wrapper: String by wrapperProperty
+
+ /**
+ * The permanent generation size of JVM garbage collection.
+ */
+ val permSizeProperty = SimpleIntegerProperty(this, "permSize", 0)
+ var permSize: Int by permSizeProperty
+
+ /**
+ * The maximum memory that JVM can allocate.
+ * The size of JVM heap.
+ */
+ val maxMemoryProperty = SimpleIntegerProperty(this, "maxMemory", 0)
+ var maxMemory: Int by maxMemoryProperty
+
+ /**
+ * The command that will be executed before launching the Minecraft.
+ * Operating system relevant.
+ */
+ val precalledCommandProperty = SimpleStringProperty(this, "precalledCommand", "")
+ var precalledCommand: String by precalledCommandProperty
+
+ // options
+
+ /**
+ * The user customized arguments passed to JVM.
+ */
+ val javaArgsProperty = SimpleStringProperty(this, "javaArgs", "")
+ var javaArgs: String by javaArgsProperty
+
+ /**
+ * The user customized arguments passed to Minecraft.
+ */
+ val minecraftArgsProperty = SimpleStringProperty(this, "minecraftArgs", "")
+ var minecraftArgs: String by minecraftArgsProperty
+
+ /**
+ * True if disallow HMCL use default JVM arguments.
+ */
+ val noJVMArgsProperty = SimpleBooleanProperty(this, "noJVMArgs", false)
+ var noJVMArgs: Boolean by noJVMArgsProperty
+
+ /**
+ * True if HMCL does not check game's completeness.
+ */
+ val notCheckGameProperty = SimpleBooleanProperty(this, "notCheckGame", false)
+ var notCheckGame: Boolean by notCheckGameProperty
+
+ // Minecraft settings.
+
+ /**
+ * The server ip that will be entered after Minecraft successfully loaded immediately.
+ *
+ * Format: ip:port or without port.
+ */
+ val serverIpProperty = SimpleStringProperty(this, "serverIp", "")
+ var serverIp: String by serverIpProperty
+
+ /**
+ * True if Minecraft started in fullscreen mode.
+ */
+ val fullscreenProperty = SimpleBooleanProperty(this, "fullscreen", false)
+ var fullscreen: Boolean by fullscreenProperty
+
+ /**
+ * The width of Minecraft window, defaults 800.
+ *
+ * The field saves int value.
+ * String type prevents unexpected value from causing JsonSyntaxException.
+ * We can only reset this field instead of recreating the whole setting file.
+ */
+ val widthProperty = SimpleIntegerProperty(this, "width", 0)
+ var width: Int by widthProperty
+
+
+ /**
+ * The height of Minecraft window, defaults 480.
+ *
+ * The field saves int value.
+ * String type prevents unexpected value from causing JsonSyntaxException.
+ * We can only reset this field instead of recreating the whole setting file.
+ */
+ val heightProperty = SimpleIntegerProperty(this, "height", 0)
+ var height: Int by heightProperty
+
+
+ /**
+ * 0 - .minecraft
+ * 1 - .minecraft/versions/<version>/
+ */
+ val gameDirTypeProperty = SimpleObjectProperty(this, "gameDirTypeProperty", EnumGameDirectory.ROOT_FOLDER)
+ var gameDirType: EnumGameDirectory by gameDirTypeProperty
+
+ // launcher settings
+
+ /**
+ * 0 - Close the launcher when the game starts.
+ * 1 - Hide the launcher when the game starts.
+ * 2 - Keep the launcher open.
+ */
+ val launcherVisibilityProperty = SimpleObjectProperty(this, "launcherVisibility", LauncherVisibility.HIDE)
+ var launcherVisibility: LauncherVisibility by launcherVisibilityProperty
+
+ val gameVersion: String
+ get() = "1.7.10"
+
+ companion object Serializer: JsonSerializer, JsonDeserializer {
+ override fun serialize(src: VersionSetting?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
+ if (src == null) return JsonNull.INSTANCE
+ val jsonObject = JsonObject()
+ with(jsonObject) {
+ addProperty("name", src.name)
+ addProperty("usesGlobal", src.usesGlobal)
+ addProperty("javaArgs", src.javaArgs)
+ addProperty("minecraftArgs", src.minecraftArgs)
+ addProperty("maxMemory", src.maxMemory)
+ addProperty("permSize", src.permSize)
+ addProperty("width", src.width)
+ addProperty("height", src.height)
+ addProperty("javaDir", src.javaDir)
+ addProperty("precalledCommand", src.precalledCommand)
+ addProperty("serverIp", src.serverIp)
+ addProperty("java", src.java)
+ addProperty("wrapper", src.wrapper)
+ addProperty("fullscreen", src.fullscreen)
+ addProperty("noJVMArgs", src.noJVMArgs)
+ addProperty("notCheckGame", src.notCheckGame)
+ addProperty("launcherVisibility", src.launcherVisibility.ordinal)
+ addProperty("gameDirType", src.gameDirType.ordinal)
+ }
+
+ return jsonObject
+ }
+
+ override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): VersionSetting? {
+ if (json == null || json == JsonNull.INSTANCE || json !is JsonObject) return null
+
+ return VersionSetting().apply {
+ name = json["name"]?.asString ?: ""
+ usesGlobal = json["usesGlobal"]?.asBoolean ?: false
+ javaArgs = json["javaArgs"]?.asString ?: ""
+ minecraftArgs = json["minecraftArgs"]?.asString ?: ""
+ maxMemory = parseJsonPrimitive(json["maxMemory"]?.asJsonPrimitive)
+ permSize = parseJsonPrimitive(json["permSize"]?.asJsonPrimitive)
+ width = parseJsonPrimitive(json["width"]?.asJsonPrimitive)
+ height = parseJsonPrimitive(json["height"]?.asJsonPrimitive)
+ javaDir = json["javaDir"]?.asString ?: ""
+ precalledCommand = json["precalledCommand"]?.asString ?: ""
+ serverIp = json["serverIp"]?.asString ?: ""
+ java = json["java"]?.asString
+ wrapper = json["wrapper"]?.asString ?: ""
+ fullscreen = json["fullscreen"]?.asBoolean ?: false
+ noJVMArgs = json["noJVMArgs"]?.asBoolean ?: false
+ notCheckGame = json["notCheckGame"]?.asBoolean ?: false
+ launcherVisibility = LauncherVisibility.values()[json["launcherVisibility"]?.asInt ?: 1]
+ gameDirType = EnumGameDirectory.values()[json["gameDirType"]?.asInt ?: 0]
+ }
+ }
+
+ fun parseJsonPrimitive(primitive: JsonPrimitive?, defaultValue: Int = 0): Int {
+ if (primitive != null)
+ if (primitive.isNumber)
+ return primitive.asInt
+ else
+ return primitive.asString.toIntOrNull() ?: defaultValue
+ else
+ return defaultValue
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.kt
new file mode 100644
index 000000000..095fe61c9
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.kt
@@ -0,0 +1,63 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui
+
+import javafx.fxml.FXMLLoader
+import javafx.scene.Node
+import javafx.scene.Scene
+import javafx.scene.layout.Pane
+import javafx.scene.layout.StackPane
+import javafx.stage.Stage
+
+object Controllers {
+ lateinit var scene: Scene private set
+ lateinit var stage: Stage private set
+
+ lateinit var mainController: MainController
+ private val mainPane: Pane = loadPane("main")
+
+ lateinit var versionController: VersionController
+ val versionPane: Pane = loadPane("version")
+
+ fun initialize(stage: Stage) {
+ this.stage = stage
+
+ val decorator = Decorator(stage, mainPane, max = false)
+ // Let root pane fix window size.
+ (mainPane.parent as StackPane).run {
+ mainPane.prefWidthProperty().bind(widthProperty())
+ mainPane.prefHeightProperty().bind(heightProperty())
+ }
+ decorator.isCustomMaximize = false
+
+ scene = Scene(decorator, 800.0, 480.0)
+ scene.stylesheets.addAll(Controllers::class.java.getResource("/css/jfoenix-design.css").toExternalForm(),
+ Controllers::class.java.getResource("/assets/css/jfoenix-components.css").toExternalForm(),
+ Controllers::class.java.getResource("/assets/css/jfoenix-main-demo.css").toExternalForm())
+ stage.minWidth = 800.0
+ stage.maxWidth = 800.0
+ stage.maxHeight = 480.0
+ stage.minHeight = 480.0
+ }
+
+ fun navigate(node: Node?) {
+ mainController.setContentPage(node)
+ }
+
+ private fun loadPane(s: String): T = FXMLLoader(Controllers::class.java.getResource("/assets/fxml/$s.fxml")).load()
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Decorator.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Decorator.kt
new file mode 100644
index 000000000..002cddf88
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Decorator.kt
@@ -0,0 +1,384 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui
+
+import com.jfoenix.controls.JFXButton
+import com.jfoenix.svg.SVGGlyph
+import javafx.animation.*
+import javafx.application.Platform
+import javafx.beans.property.BooleanProperty
+import javafx.beans.property.ObjectProperty
+import javafx.beans.property.SimpleBooleanProperty
+import javafx.beans.property.SimpleObjectProperty
+import javafx.geometry.BoundingBox
+import javafx.geometry.Bounds
+import javafx.geometry.Insets
+import javafx.geometry.Pos
+import javafx.scene.Cursor
+import javafx.scene.Node
+import javafx.scene.control.Button
+import javafx.scene.control.Label
+import javafx.scene.control.Tooltip
+import javafx.scene.input.MouseEvent
+import javafx.scene.layout.*
+import javafx.scene.paint.Color
+import javafx.scene.shape.Rectangle
+import javafx.stage.Screen
+import javafx.stage.Stage
+import javafx.stage.StageStyle
+import java.util.ArrayList
+
+class Decorator @JvmOverloads constructor(private val primaryStage: Stage, node: Node, max: Boolean = true, min: Boolean = true) : VBox() {
+ private var xOffset: Double = 0.toDouble()
+ private var yOffset: Double = 0.toDouble()
+ private var newX: Double = 0.toDouble()
+ private var newY: Double = 0.toDouble()
+ private var initX: Double = 0.toDouble()
+ private var initY: Double = 0.toDouble()
+ private var allowMove: Boolean = false
+ private var isDragging: Boolean = false
+ private var windowDecoratorAnimation: Timeline? = null
+ private val contentPlaceHolder: StackPane
+ private val titleContainer: BorderPane
+ private val onCloseButtonAction: ObjectProperty
+ private val customMaximize: BooleanProperty
+ private var maximized: Boolean = false
+ private var originalBox: BoundingBox? = null
+ private var maximizedBox: BoundingBox? = null
+ private val btnMax: JFXButton
+
+ init {
+ this.xOffset = 0.0
+ this.yOffset = 0.0
+ this.allowMove = false
+ this.isDragging = false
+ this.contentPlaceHolder = StackPane()
+ this.onCloseButtonAction = SimpleObjectProperty(Runnable { this.primaryStage.close() })
+ this.customMaximize = SimpleBooleanProperty(false)
+ this.maximized = false
+ this.primaryStage.initStyle(StageStyle.UNDECORATED)
+ this.isPickOnBounds = false
+ this.styleClass.add("jfx-decorator")
+ val minus = SVGGlyph(0, "MINUS", "M804.571 420.571v109.714q0 22.857-16 38.857t-38.857 16h-694.857q-22.857 0-38.857-16t-16-38.857v-109.714q0-22.857 16-38.857t38.857-16h694.857q22.857 0 38.857 16t16 38.857z", Color.WHITE)
+ minus.setSize(12.0, 2.0)
+ minus.translateY = 4.0
+ val resizeMax = SVGGlyph(0, "RESIZE_MAX", "M726 810v-596h-428v596h428zM726 44q34 0 59 25t25 59v768q0 34-25 60t-59 26h-428q-34 0-59-26t-25-60v-768q0-34 25-60t59-26z", Color.WHITE)
+ resizeMax.setSize(12.0, 12.0)
+ val resizeMin = SVGGlyph(0, "RESIZE_MIN", "M80.842 943.158v-377.264h565.894v377.264h-565.894zM0 404.21v619.79h727.578v-619.79h-727.578zM377.264 161.684h565.894v377.264h-134.736v80.842h215.578v-619.79h-727.578v323.37h80.842v-161.686z", Color.WHITE)
+ resizeMin.setSize(12.0, 12.0)
+ val close = SVGGlyph(0, "CLOSE", "M810 274l-238 238 238 238-60 60-238-238-238 238-60-60 238-238-238-238 60-60 238 238 238-238z", Color.WHITE)
+ close.setSize(12.0, 12.0)
+ val btnClose = JFXButton()
+ btnClose.styleClass.add("jfx-decorator-button")
+ btnClose.cursor = Cursor.HAND
+ btnClose.setOnAction { action -> (this.onCloseButtonAction.get() as Runnable).run() }
+ btnClose.graphic = close
+ btnClose.ripplerFill = Color.WHITE
+ val btnMin = JFXButton()
+ btnMin.styleClass.add("jfx-decorator-button")
+ btnMin.cursor = Cursor.HAND
+ btnMin.setOnAction { action -> this.primaryStage.isIconified = true }
+ btnMin.graphic = minus
+ btnMin.ripplerFill = Color.WHITE
+ this.btnMax = JFXButton()
+ this.btnMax.styleClass.add("jfx-decorator-button")
+ this.btnMax.cursor = Cursor.HAND
+ this.btnMax.ripplerFill = Color.WHITE
+ this.btnMax.setOnAction { action ->
+ if (!this.isCustomMaximize) {
+ this.primaryStage.isMaximized = !this.primaryStage.isMaximized
+ this.maximized = this.primaryStage.isMaximized
+ if (this.primaryStage.isMaximized) {
+ this.btnMax.graphic = resizeMin
+ this.btnMax.tooltip = Tooltip("Restore Down")
+ } else {
+ this.btnMax.graphic = resizeMax
+ this.btnMax.tooltip = Tooltip("Maximize")
+ }
+ } else {
+ if (!this.maximized) {
+ this.originalBox = BoundingBox(primaryStage.x, primaryStage.y, primaryStage.width, primaryStage.height)
+ val screen = Screen.getScreensForRectangle(primaryStage.x, primaryStage.y, primaryStage.width, primaryStage.height)[0] as Screen
+ val bounds = screen.visualBounds
+ this.maximizedBox = BoundingBox(bounds.minX, bounds.minY, bounds.width, bounds.height)
+ primaryStage.x = this.maximizedBox!!.minX
+ primaryStage.y = this.maximizedBox!!.minY
+ primaryStage.width = this.maximizedBox!!.width
+ primaryStage.height = this.maximizedBox!!.height
+ this.btnMax.graphic = resizeMin
+ this.btnMax.tooltip = Tooltip("Restore Down")
+ } else {
+ primaryStage.x = this.originalBox!!.minX
+ primaryStage.y = this.originalBox!!.minY
+ primaryStage.width = this.originalBox!!.width
+ primaryStage.height = this.originalBox!!.height
+ this.originalBox = null
+ this.btnMax.graphic = resizeMax
+ this.btnMax.tooltip = Tooltip("Maximize")
+ }
+
+ this.maximized = !this.maximized
+ }
+
+ }
+ this.btnMax.graphic = resizeMax
+ titleContainer = BorderPane()
+ titleContainer.styleClass += "jfx-decorator-buttons-container"
+ titleContainer.isPickOnBounds = false
+ val titleWrapper = HBox()
+ titleWrapper.style += "-fx-padding: 15;"
+ titleWrapper.alignment = Pos.CENTER_LEFT
+ val title = Label("Hello Minecraft! Launcher")
+ title.alignment = Pos.CENTER_LEFT
+ title.style += "--fx-background-color: transparent; -fx-text-fill: white; -fx-font-size: 15px;"
+ title.isMouseTransparent = false
+ titleWrapper.children.setAll(title)
+ titleContainer.left = titleWrapper
+
+ val buttonsContainer = HBox()
+ buttonsContainer.styleClass.add("jfx-decorator-buttons-container")
+ buttonsContainer.background = Background(*arrayOf(BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY)))
+ buttonsContainer.padding = Insets(4.0)
+ buttonsContainer.alignment = Pos.CENTER_RIGHT
+ titleContainer.addEventHandler(MouseEvent.MOUSE_CLICKED) { mouseEvent ->
+ if (mouseEvent.clickCount == 2) {
+ this.btnMax.fire()
+ }
+
+ }
+ val btns = ArrayList()
+
+ if (min) {
+ btns.add(btnMin)
+ }
+
+ if (max) {
+ btns.add(this.btnMax)
+ }
+
+ btns.add(btnClose)
+ buttonsContainer.children.addAll(btns)
+ titleContainer.addEventHandler(MouseEvent.MOUSE_ENTERED) { enter -> this.allowMove = true }
+ titleContainer.addEventHandler(MouseEvent.MOUSE_EXITED) { enter ->
+ if (!this.isDragging) {
+ this.allowMove = false
+ }
+
+ }
+ buttonsContainer.minWidth = 180.0
+
+ titleContainer.right = buttonsContainer
+ this.contentPlaceHolder.styleClass.add("jfx-decorator-content-container")
+ this.contentPlaceHolder.setMinSize(0.0, 0.0)
+ this.contentPlaceHolder.children.add(node)
+ (node as Region).setMinSize(0.0, 0.0)
+ VBox.setVgrow(this.contentPlaceHolder, Priority.ALWAYS)
+ this.contentPlaceHolder.styleClass.add("resize-border")
+ this.contentPlaceHolder.border = Border(*arrayOf(BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths(0.0, 4.0, 4.0, 4.0))))
+ val clip = Rectangle()
+ clip.widthProperty().bind(node.widthProperty())
+ clip.heightProperty().bind(node.heightProperty())
+ node.setClip(clip)
+ this.children.addAll(*arrayOf(titleContainer, this.contentPlaceHolder))
+ this.setOnMouseMoved { mouseEvent ->
+ if (!this.primaryStage.isMaximized && !this.primaryStage.isFullScreen && !this.maximized) {
+ if (!this.primaryStage.isResizable) {
+ this.updateInitMouseValues(mouseEvent)
+ } else {
+ val x = mouseEvent.x
+ val y = mouseEvent.y
+ val boundsInParent = this.boundsInParent
+ if (this.contentPlaceHolder.border != null && this.contentPlaceHolder.border.strokes.size > 0) {
+ val borderWidth = this.contentPlaceHolder.snappedLeftInset()
+ if (this.isRightEdge(x, y, boundsInParent)) {
+ if (y < borderWidth) {
+ this.cursor = Cursor.NE_RESIZE
+ } else if (y > this.height - borderWidth) {
+ this.cursor = Cursor.SE_RESIZE
+ } else {
+ this.cursor = Cursor.E_RESIZE
+ }
+ } else if (this.isLeftEdge(x, y, boundsInParent)) {
+ if (y < borderWidth) {
+ this.cursor = Cursor.NW_RESIZE
+ } else if (y > this.height - borderWidth) {
+ this.cursor = Cursor.SW_RESIZE
+ } else {
+ this.cursor = Cursor.W_RESIZE
+ }
+ } else if (this.isTopEdge(x, y, boundsInParent)) {
+ this.cursor = Cursor.N_RESIZE
+ } else if (this.isBottomEdge(x, y, boundsInParent)) {
+ this.cursor = Cursor.S_RESIZE
+ } else {
+ this.cursor = Cursor.DEFAULT
+ }
+
+ this.updateInitMouseValues(mouseEvent)
+ }
+
+ }
+ } else {
+ this.cursor = Cursor.DEFAULT
+ }
+ }
+ this.setOnMouseReleased { mouseEvent -> this.isDragging = false }
+ this.setOnMouseDragged { mouseEvent ->
+ this.isDragging = true
+ if (mouseEvent.isPrimaryButtonDown && (this.xOffset != -1.0 || this.yOffset != -1.0)) {
+ if (!this.primaryStage.isFullScreen && !mouseEvent.isStillSincePress && !this.primaryStage.isMaximized && !this.maximized) {
+ this.newX = mouseEvent.screenX
+ this.newY = mouseEvent.screenY
+ val deltax = this.newX - this.initX
+ val deltay = this.newY - this.initY
+ val cursor = this.cursor
+ if (Cursor.E_RESIZE == cursor) {
+ this.setStageWidth(this.primaryStage.width + deltax)
+ mouseEvent.consume()
+ } else if (Cursor.NE_RESIZE == cursor) {
+ if (this.setStageHeight(this.primaryStage.height - deltay)) {
+ this.primaryStage.y = this.primaryStage.y + deltay
+ }
+
+ this.setStageWidth(this.primaryStage.width + deltax)
+ mouseEvent.consume()
+ } else if (Cursor.SE_RESIZE == cursor) {
+ this.setStageWidth(this.primaryStage.width + deltax)
+ this.setStageHeight(this.primaryStage.height + deltay)
+ mouseEvent.consume()
+ } else if (Cursor.S_RESIZE == cursor) {
+ this.setStageHeight(this.primaryStage.height + deltay)
+ mouseEvent.consume()
+ } else if (Cursor.W_RESIZE == cursor) {
+ if (this.setStageWidth(this.primaryStage.width - deltax)) {
+ this.primaryStage.x = this.primaryStage.x + deltax
+ }
+
+ mouseEvent.consume()
+ } else if (Cursor.SW_RESIZE == cursor) {
+ if (this.setStageWidth(this.primaryStage.width - deltax)) {
+ this.primaryStage.x = this.primaryStage.x + deltax
+ }
+
+ this.setStageHeight(this.primaryStage.height + deltay)
+ mouseEvent.consume()
+ } else if (Cursor.NW_RESIZE == cursor) {
+ if (this.setStageWidth(this.primaryStage.width - deltax)) {
+ this.primaryStage.x = this.primaryStage.x + deltax
+ }
+
+ if (this.setStageHeight(this.primaryStage.height - deltay)) {
+ this.primaryStage.y = this.primaryStage.y + deltay
+ }
+
+ mouseEvent.consume()
+ } else if (Cursor.N_RESIZE == cursor) {
+ if (this.setStageHeight(this.primaryStage.height - deltay)) {
+ this.primaryStage.y = this.primaryStage.y + deltay
+ }
+
+ mouseEvent.consume()
+ } else if (this.allowMove) {
+ this.primaryStage.x = mouseEvent.screenX - this.xOffset
+ this.primaryStage.y = mouseEvent.screenY - this.yOffset
+ mouseEvent.consume()
+ }
+
+ }
+ }
+ }
+ }
+
+ private fun updateInitMouseValues(mouseEvent: MouseEvent) {
+ this.initX = mouseEvent.screenX
+ this.initY = mouseEvent.screenY
+ this.xOffset = mouseEvent.sceneX
+ this.yOffset = mouseEvent.sceneY
+ }
+
+ private fun isRightEdge(x: Double, y: Double, boundsInParent: Bounds): Boolean {
+ return x < this.width && x > this.width - this.contentPlaceHolder.snappedLeftInset()
+ }
+
+ private fun isTopEdge(x: Double, y: Double, boundsInParent: Bounds): Boolean {
+ return y >= 0.0 && y < this.contentPlaceHolder.snappedLeftInset()
+ }
+
+ private fun isBottomEdge(x: Double, y: Double, boundsInParent: Bounds): Boolean {
+ return y < this.height && y > this.height - this.contentPlaceHolder.snappedLeftInset()
+ }
+
+ private fun isLeftEdge(x: Double, y: Double, boundsInParent: Bounds): Boolean {
+ return x >= 0.0 && x < this.contentPlaceHolder.snappedLeftInset()
+ }
+
+ internal fun setStageWidth(width: Double): Boolean {
+ var width = width
+ if (width >= this.primaryStage.minWidth && width >= this.titleContainer.minWidth) {
+ this.primaryStage.width = width
+ this.initX = this.newX
+ return true
+ } else {
+ if (width >= this.primaryStage.minWidth && width <= this.titleContainer.minWidth) {
+ width = this.titleContainer.minWidth
+ this.primaryStage.width = width
+ }
+
+ return false
+ }
+ }
+
+ internal fun setStageHeight(height: Double): Boolean {
+ var height = height
+ if (height >= this.primaryStage.minHeight && height >= this.titleContainer.height) {
+ this.primaryStage.height = height
+ this.initY = this.newY
+ return true
+ } else {
+ if (height >= this.primaryStage.minHeight && height <= this.titleContainer.height) {
+ height = this.titleContainer.height
+ this.primaryStage.height = height
+ }
+
+ return false
+ }
+ }
+
+ fun setOnCloseButtonAction(onCloseButtonAction: Runnable) {
+ this.onCloseButtonAction.set(onCloseButtonAction)
+ }
+
+ fun customMaximizeProperty(): BooleanProperty {
+ return this.customMaximize
+ }
+
+ var isCustomMaximize: Boolean
+ get() = this.customMaximizeProperty().get()
+ set(customMaximize) = this.customMaximizeProperty().set(customMaximize)
+
+ fun setMaximized(maximized: Boolean) {
+ if (this.maximized != maximized) {
+ Platform.runLater { this.btnMax.fire() }
+ }
+
+ }
+
+ fun setContent(content: Node) {
+ this.contentPlaceHolder.children.setAll(*arrayOf(content))
+ }
+}
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.kt
new file mode 100644
index 000000000..a42bd3e58
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.kt
@@ -0,0 +1,89 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui
+
+import com.jfoenix.concurrency.JFXUtilities
+import javafx.animation.Animation
+import javafx.animation.KeyFrame
+import javafx.animation.Timeline
+import javafx.event.ActionEvent
+import javafx.event.EventHandler
+import javafx.fxml.FXMLLoader
+import javafx.scene.Node
+import javafx.scene.control.ListView
+import javafx.scene.control.ScrollBar
+import javafx.scene.input.MouseEvent
+import javafx.scene.input.ScrollEvent
+import javafx.util.Duration
+
+fun Node.loadFXML(absolutePath: String) {
+ val fxmlLoader = FXMLLoader(this.javaClass.getResource(absolutePath))
+ fxmlLoader.setRoot(this)
+ fxmlLoader.setController(this)
+ fxmlLoader.load()
+}
+
+fun ListView<*>.smoothScrolling() {
+ skinProperty().addListener { _ ->
+ val bar = lookup(".scroll-bar") as ScrollBar
+ val virtualFlow = lookup(".virtual-flow")
+ val frictions = doubleArrayOf(0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003, 0.00001)
+ val pushes = doubleArrayOf(1.0)
+ val derivatives = DoubleArray(frictions.size)
+
+ val timeline = Timeline()
+ bar.addEventHandler(MouseEvent.DRAG_DETECTED) { timeline.stop() }
+
+ val scrollEventHandler = EventHandler { event ->
+ if (event.eventType == ScrollEvent.SCROLL) {
+ val direction = if (event.deltaY > 0) -1 else 1
+ for (i in pushes.indices) {
+ derivatives[i] += direction * pushes[i]
+ }
+ if (timeline.status == Animation.Status.STOPPED) {
+ timeline.play()
+ }
+ event.consume()
+ }
+ }
+
+ bar.addEventHandler(ScrollEvent.ANY, scrollEventHandler)
+ virtualFlow.onScroll = scrollEventHandler
+
+ timeline.keyFrames.add(KeyFrame(Duration.millis(3.0), EventHandler {
+ for (i in derivatives.indices) {
+ derivatives[i] *= frictions[i]
+ }
+ for (i in 1..derivatives.size - 1) {
+ derivatives[i] += derivatives[i - 1]
+ }
+ val dy = derivatives[derivatives.size - 1]
+ val height = layoutBounds.height
+ bar.value = Math.min(Math.max(bar.value + dy / height, 0.0), 1.0)
+ if (Math.abs(dy) < 0.001) {
+ timeline.stop()
+ }
+ requestLayout()
+ }))
+ timeline.cycleCount = Animation.INDEFINITE
+ }
+}
+
+fun runOnUiThread(runnable: () -> Unit) = {
+ JFXUtilities.runInFX(runnable)
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainController.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainController.kt
new file mode 100644
index 000000000..942164c1f
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainController.kt
@@ -0,0 +1,125 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui
+
+import com.jfoenix.controls.JFXButton
+import com.jfoenix.controls.JFXComboBox
+import com.jfoenix.controls.JFXListCell
+import com.jfoenix.controls.JFXListView
+import javafx.fxml.FXML
+import javafx.scene.Node
+import javafx.scene.layout.Pane
+import javafx.scene.layout.StackPane
+import org.jackhuang.hmcl.setting.VersionSetting
+import org.jackhuang.hmcl.ui.animation.ContainerAnimations
+import org.jackhuang.hmcl.ui.download.DownloadWizardProvider
+import org.jackhuang.hmcl.ui.animation.TransitionHandler
+import org.jackhuang.hmcl.ui.wizard.Wizard
+
+/**
+ * @see /assets/fxml/main.fxml
+ */
+class MainController {
+
+ /**
+ * A combo box that allows user to select Minecraft directory.
+ */
+ @FXML lateinit var comboProfiles: JFXComboBox // TODO: JFXComboBox
+
+ /**
+ * The button that is to launch the selected game version.
+ */
+ @FXML lateinit var buttonLaunch: JFXButton
+
+ /**
+ * A central pane that contains popups like (global) version settings, app settings, game installations and so on.
+ */
+ @FXML lateinit var page: StackPane
+
+ @FXML lateinit var listVersions: JFXListView // TODO: JFXListView including icon, title, game version(if equals to title, hidden)
+
+ lateinit var animationHandler: TransitionHandler
+
+ // TODO: implementing functions.
+ fun initialize() {
+ Controllers.mainController = this
+
+ animationHandler = TransitionHandler(page)
+
+ listVersions.items.add(VersionSetting("1"))
+ listVersions.items.add(VersionSetting("2"))
+ listVersions.items.add(VersionSetting("3"))
+ listVersions.items.add(VersionSetting("4"))
+ listVersions.items.add(VersionSetting("5"))
+ listVersions.items.add(VersionSetting("6"))
+ listVersions.items.add(VersionSetting("7"))
+ listVersions.items.add(VersionSetting("8"))
+ listVersions.items.add(VersionSetting("9"))
+ listVersions.items.add(VersionSetting("10"))
+ listVersions.items.add(VersionSetting("11"))
+ listVersions.items.add(VersionSetting("12"))
+
+ listVersions.setCellFactory {
+ object : JFXListCell() {
+ override fun updateItem(item: VersionSetting?, empty: Boolean) {
+ super.updateItem(item, empty)
+
+ if (item == null || empty) return
+ val g = VersionListItem(item, item.gameVersion)
+ g.onSettingsButtonClicked {
+ setContentPage(Controllers.versionPane)
+ Controllers.versionController.loadVersionSetting(g.setting)
+ }
+ graphic = g
+ }
+ }
+ }
+
+ listVersions.setOnMouseClicked {
+ if (it.clickCount == 2) {
+ setContentPage(Controllers.versionPane)
+ Controllers.versionController.loadVersionSetting(listVersions.selectionModel.selectedItem)
+ } else
+ it.consume()
+ }
+
+ comboProfiles.items.add("SA")
+ comboProfiles.items.add("SB")
+ comboProfiles.items.add("SC")
+ comboProfiles.items.add("SD")
+ comboProfiles.items.add("SE")
+ comboProfiles.items.add("SF")
+ comboProfiles.items.add("SG")
+ comboProfiles.items.add("SH")
+ comboProfiles.items.add("SI")
+ comboProfiles.items.add("SJ")
+ comboProfiles.items.add("SK")
+
+ listVersions.smoothScrolling()
+ }
+
+ private val empty = Pane()
+
+ fun setContentPage(node: Node?) {
+ animationHandler.setContent(node ?: empty, ContainerAnimations.FADE.animationProducer)
+ }
+
+ fun installNewVersion() {
+ setContentPage(Wizard.createWizard("Install New Game", DownloadWizardProvider()))
+ }
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.kt
new file mode 100644
index 000000000..687a4039c
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.kt
@@ -0,0 +1,57 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui
+
+import javafx.fxml.FXMLLoader
+import javafx.scene.Group
+import javafx.scene.Node
+import javafx.scene.shape.SVGPath
+import kotlin.coroutines.experimental.EmptyCoroutineContext.plus
+
+object SVG {
+ val svgNames = setOf("gear")
+ val svgs: Map
+
+ init {
+ val svgsImpl = HashMap()
+ for (svgName in svgNames) {
+ svgsImpl[svgName] = FXMLLoader(Controllers::class.java.getResource("/assets/svg/$svgName.fxml")).load()
+ }
+ svgs = svgsImpl
+ }
+
+ private fun createSVGPath(d: String, fill: String = "black", width: Double = 20.0, height: Double = 20.0): Node {
+ val path = SVGPath()
+ path.styleClass += "svg"
+ path.content = d
+ path.style = "-fx-fill: $fill;"
+
+ val svg = Group(path)
+ val scale = minOf(width / svg.boundsInParent.width, height / svg.boundsInParent.height)
+ svg.scaleX = scale
+ svg.scaleY = scale
+ svg.maxWidth(width)
+
+ return svg
+ }
+
+ fun gear(): Node = 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")
+ fun back(fill: String = "white"): Node = createSVGPath("M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z", fill)
+ fun close(fill: String = "white"): Node = createSVGPath("M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z", fill)
+ fun dotsVertical(fill: String = "white"): Node = createSVGPath("M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z", fill)
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionController.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionController.kt
new file mode 100644
index 000000000..8be42e113
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionController.kt
@@ -0,0 +1,73 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui
+
+import com.jfoenix.controls.JFXButton
+import com.jfoenix.controls.JFXScrollPane
+import com.jfoenix.controls.JFXTextField
+import javafx.fxml.FXML
+import javafx.scene.control.Label
+import javafx.scene.control.ScrollPane
+import javafx.scene.layout.ColumnConstraints
+import javafx.scene.layout.GridPane
+import javafx.scene.layout.Priority
+import javafx.scene.paint.Color
+import javafx.stage.DirectoryChooser
+import org.jackhuang.hmcl.setting.VersionSetting
+
+class VersionController {
+ @FXML
+ lateinit var titleLabel: Label
+ @FXML
+ lateinit var backButton: JFXButton
+ @FXML
+ lateinit var scroll: ScrollPane
+ @FXML
+ lateinit var settingsPane: GridPane
+ @FXML
+ lateinit var txtGameDir: JFXTextField
+
+ fun initialize() {
+ Controllers.versionController = this
+
+ settingsPane.columnConstraints.addAll(
+ ColumnConstraints(),
+ ColumnConstraints().apply { hgrow = Priority.ALWAYS },
+ ColumnConstraints()
+ )
+
+ backButton.ripplerFill = Color.WHITE
+ backButton.setOnMouseClicked {
+ Controllers.navigate(null)
+ }
+
+ JFXScrollPane.smoothScrolling(scroll)
+ }
+
+ fun loadVersionSetting(version: VersionSetting) {
+ titleLabel.text = version.name
+ }
+
+ fun onExploreJavaDir() {
+ val chooser = DirectoryChooser()
+ chooser.title = "Selecting Java Directory"
+ val selectedDir = chooser.showDialog(Controllers.stage)
+ if (selectedDir != null)
+ txtGameDir.text = selectedDir.absolutePath
+ }
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionListItem.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionListItem.kt
new file mode 100644
index 000000000..8b01f18e0
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionListItem.kt
@@ -0,0 +1,49 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui
+
+import javafx.fxml.FXML
+import javafx.scene.control.Label
+import javafx.scene.layout.BorderPane
+import org.jackhuang.hmcl.setting.VersionSetting
+
+class VersionListItem(val setting: VersionSetting, val gameVersion: String) : BorderPane() {
+
+ @FXML lateinit var lblVersionName: Label
+ @FXML lateinit var lblGameVersion: Label
+
+ private var handler: () -> Unit = {}
+
+ init {
+ loadFXML("/assets/fxml/version-list-item.fxml")
+ lblVersionName.text = setting.name
+ lblGameVersion.text = gameVersion
+ }
+
+ fun onSettings() {
+ handler()
+ }
+
+ fun onLaunch() {
+
+ }
+
+ fun onSettingsButtonClicked(handler: () -> Unit) {
+ this.handler = handler
+ }
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/AnimationHandler.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/AnimationHandler.kt
new file mode 100644
index 000000000..ba2ebac73
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/AnimationHandler.kt
@@ -0,0 +1,28 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui.animation
+
+import javafx.scene.Node
+import javafx.scene.layout.Pane
+import javafx.util.Duration
+
+interface AnimationHandler {
+ val snapshot: Node
+ val duration: Duration
+ val view: Pane
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/ContainerAnimations.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/ContainerAnimations.kt
new file mode 100644
index 000000000..a5ac3ca0c
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/ContainerAnimations.kt
@@ -0,0 +1,99 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui.animation
+
+import javafx.animation.Interpolator
+import javafx.animation.KeyFrame
+import javafx.animation.KeyValue
+import javafx.util.Duration
+
+enum class ContainerAnimations(val animationProducer: (AnimationHandler) -> List) {
+ /**
+ * None
+ */
+ NONE({
+ emptyList()
+ }),
+ /**
+ * A fade between the old and new view
+ */
+ FADE({ c ->
+ listOf(
+ KeyFrame(Duration.ZERO,
+ KeyValue(c.snapshot.opacityProperty(), 1.0, Interpolator.EASE_BOTH)),
+ KeyFrame(c.duration,
+ KeyValue(c.snapshot.opacityProperty(), 0.0, Interpolator.EASE_BOTH))
+ )
+ }),
+ /**
+ * A zoom effect
+ */
+ ZOOM_IN({ c ->
+ listOf(
+ KeyFrame(Duration.ZERO,
+ KeyValue(c.snapshot.scaleXProperty(), 1, Interpolator.EASE_BOTH),
+ KeyValue(c.snapshot.scaleYProperty(), 1, Interpolator.EASE_BOTH),
+ KeyValue(c.snapshot.opacityProperty(), 1.0, Interpolator.EASE_BOTH)),
+ KeyFrame(c.duration,
+ KeyValue(c.snapshot.scaleXProperty(), 4, Interpolator.EASE_BOTH),
+ KeyValue(c.snapshot.scaleYProperty(), 4, Interpolator.EASE_BOTH),
+ KeyValue(c.snapshot.opacityProperty(), 0, Interpolator.EASE_BOTH))
+ )
+ }),
+ /**
+ * A zoom effect
+ */
+ ZOOM_OUT({ c ->
+ listOf(
+ KeyFrame(Duration.ZERO,
+ KeyValue(c.snapshot.scaleXProperty(), 1, Interpolator.EASE_BOTH),
+ KeyValue(c.snapshot.scaleYProperty(), 1, Interpolator.EASE_BOTH),
+ KeyValue(c.snapshot.opacityProperty(), 1.0, Interpolator.EASE_BOTH)),
+ KeyFrame(c.duration,
+ KeyValue(c.snapshot.scaleXProperty(), 0, Interpolator.EASE_BOTH),
+ KeyValue(c.snapshot.scaleYProperty(), 0, Interpolator.EASE_BOTH),
+ KeyValue(c.snapshot.opacityProperty(), 0, Interpolator.EASE_BOTH))
+ )
+ }),
+ /**
+ * A swipe effect
+ */
+ SWIPE_LEFT({ c ->
+ listOf(
+ KeyFrame(Duration.ZERO,
+ KeyValue(c.view.translateXProperty(), c.view.getWidth(), Interpolator.EASE_BOTH),
+ KeyValue(c.snapshot.translateXProperty(), -c.view.getWidth(), Interpolator.EASE_BOTH)),
+ KeyFrame(c.duration,
+ KeyValue(c.view.translateXProperty(), 0, Interpolator.EASE_BOTH),
+ KeyValue(c.snapshot.translateXProperty(), -c.view.getWidth(), Interpolator.EASE_BOTH)
+ ))
+ }),
+ /**
+ * A swipe effect
+ */
+ SWIPE_RIGHT({ c ->
+ listOf(
+ KeyFrame(Duration.ZERO,
+ KeyValue(c.view.translateXProperty(), -c.view.getWidth(), Interpolator.EASE_BOTH),
+ KeyValue(c.snapshot.translateXProperty(), c.view.getWidth(), Interpolator.EASE_BOTH)),
+ KeyFrame(c.duration,
+ KeyValue(c.view.translateXProperty(), 0, Interpolator.EASE_BOTH),
+ KeyValue(c.snapshot.translateXProperty(), c.view.getWidth(), Interpolator.EASE_BOTH))
+ )
+ })
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/TransitionHandler.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/TransitionHandler.kt
new file mode 100644
index 000000000..294951242
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/TransitionHandler.kt
@@ -0,0 +1,81 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui.animation
+
+import javafx.animation.KeyFrame
+import javafx.animation.Timeline
+import javafx.event.EventHandler
+import javafx.scene.Node
+import javafx.scene.SnapshotParameters
+import javafx.scene.image.ImageView
+import javafx.scene.image.WritableImage
+import javafx.scene.layout.StackPane
+import javafx.util.Duration
+
+class TransitionHandler(override val view: StackPane): AnimationHandler {
+ private var animation: Timeline? = null
+
+ override val snapshot = ImageView().apply {
+ isPreserveRatio = true
+ isSmooth = true
+ }
+
+ override lateinit var duration: Duration
+ private set
+
+ fun setContent(newView: Node, transition: (TransitionHandler) -> List, duration: Duration = Duration.millis(320.0)) {
+ this.duration = duration
+
+ val prevAnimation = animation
+ if (prevAnimation != null)
+ prevAnimation.stop()
+
+ updateContent(newView)
+
+ val nowAnimation = Timeline().apply {
+ keyFrames.addAll(transition(this@TransitionHandler))
+ keyFrames.add(KeyFrame(duration, EventHandler {
+ snapshot.image = null
+ snapshot.x = 0.0
+ snapshot.y = 0.0
+ snapshot.isVisible = false
+ }))
+ }
+ nowAnimation.play()
+ animation = nowAnimation
+ }
+
+ private fun updateContent(newView: Node) {
+ if (view.width > 0 && view.height > 0) {
+ val image = view.snapshot(SnapshotParameters(), null)
+ val x = (image.width - view.width) / 2
+ val y = image.height - view.height
+ val newImage = WritableImage(image.pixelReader, x.toInt(), y.toInt(), view.width.toInt(), view.height.toInt())
+ snapshot.image = newImage
+ snapshot.fitWidth = newImage.width
+ snapshot.fitHeight = newImage.height
+ } else
+ snapshot.image = null
+
+ snapshot.isVisible = true
+ snapshot.opacity = 1.0
+ view.children.setAll(snapshot)
+ view.children.add(newView)
+ snapshot.toFront()
+ }
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadWizardProvider.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadWizardProvider.kt
new file mode 100644
index 000000000..011ac03af
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadWizardProvider.kt
@@ -0,0 +1,48 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui.download
+
+import javafx.scene.Node
+import javafx.scene.layout.Pane
+import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider
+import org.jackhuang.hmcl.ui.wizard.WizardController
+import org.jackhuang.hmcl.ui.wizard.WizardProvider
+
+class DownloadWizardProvider(): WizardProvider() {
+
+ override fun finish(settings: Map): Any? {
+ println(settings)
+ return null
+ }
+
+ override fun createPage(controller: WizardController, step: Int, settings: Map): Node {
+ return when (step) {
+ 0 -> InstallTypePage(controller)
+ 1 -> when (settings[InstallTypePage.INSTALL_TYPE]) {
+ 0 -> VersionsPage(controller, "", BMCLAPIDownloadProvider, "game", { controller.onNext(InstallersPage(controller, BMCLAPIDownloadProvider)) })
+ else -> Pane()
+ }
+ else -> throw IllegalStateException()
+ }
+ }
+
+ override fun cancel(): Boolean {
+ return true
+ }
+
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallTypePage.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallTypePage.kt
new file mode 100644
index 000000000..afc89a1af
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallTypePage.kt
@@ -0,0 +1,50 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui.download
+
+import com.jfoenix.controls.JFXListView
+import javafx.fxml.FXML
+import javafx.scene.layout.StackPane
+import org.jackhuang.hmcl.ui.loadFXML
+import org.jackhuang.hmcl.ui.wizard.WizardController
+import org.jackhuang.hmcl.ui.wizard.WizardPage
+
+class InstallTypePage(private val controller: WizardController): StackPane(), WizardPage {
+
+ @FXML lateinit var list: JFXListView
+
+ init {
+ loadFXML("/assets/fxml/download/dltype.fxml")
+
+ list.selectionModel.selectedIndexProperty().addListener { _, _, newValue ->
+ controller.settings[INSTALL_TYPE] = newValue
+ controller.onNext()
+ }
+ }
+
+ override fun cleanup(settings: MutableMap) {
+ settings.remove(INSTALL_TYPE)
+ }
+
+ override val title: String
+ get() = "Select an operation"
+
+ companion object {
+ const val INSTALL_TYPE: String = "INSTALL_TYPE"
+ }
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.kt
new file mode 100644
index 000000000..bd9870522
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.kt
@@ -0,0 +1,78 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui.download
+
+import com.jfoenix.controls.JFXListView
+import javafx.fxml.FXML
+import javafx.scene.control.Label
+import javafx.scene.layout.StackPane
+import org.jackhuang.hmcl.download.DownloadProvider
+import org.jackhuang.hmcl.ui.loadFXML
+import org.jackhuang.hmcl.ui.wizard.WizardController
+import org.jackhuang.hmcl.ui.wizard.WizardPage
+
+class InstallersPage(private val controller: WizardController, private val downloadProvider: DownloadProvider): StackPane(), WizardPage {
+
+ @FXML lateinit var list: JFXListView
+ @FXML lateinit var lblGameVersion: Label
+ @FXML lateinit var lblForge: Label
+ @FXML lateinit var lblLiteLoader: Label
+ @FXML lateinit var lblOptiFine: Label
+
+ init {
+ loadFXML("/assets/fxml/download/installers.fxml")
+ list.selectionModel.selectedIndexProperty().addListener { _, _, newValue ->
+ controller.settings[INSTALLER_TYPE] = newValue
+ controller.onNext(when (newValue){
+ 0 -> VersionsPage(controller, controller.settings["game"] as String, downloadProvider, "forge") { controller.onPrev(false) }
+ 1 -> VersionsPage(controller, controller.settings["game"] as String, downloadProvider, "liteloader") { controller.onPrev(false) }
+ 2 -> VersionsPage(controller, controller.settings["game"] as String, downloadProvider, "optifine") { controller.onPrev(false) }
+ else -> throw IllegalStateException()
+ })
+ }
+ }
+
+ override val title: String
+ get() = "Choose a game version"
+
+ override fun onNavigate(settings: MutableMap) {
+ lblGameVersion.text = "Current Game Version: ${controller.settings["game"]}"
+ if (controller.settings.containsKey("forge"))
+ lblForge.text = "Forge Versoin: ${controller.settings["forge"]}"
+ else
+ lblForge.text = "Forge not installed"
+
+ if (controller.settings.containsKey("liteloader"))
+ lblLiteLoader.text = "LiteLoader Versoin: ${controller.settings["liteloader"]}"
+ else
+ lblLiteLoader.text = "LiteLoader not installed"
+
+ if (controller.settings.containsKey("optifine"))
+ lblOptiFine.text = "OptiFine Versoin: ${controller.settings["optifine"]}"
+ else
+ lblOptiFine.text = "OptiFine not installed"
+ }
+
+ override fun cleanup(settings: MutableMap) {
+ settings.remove(INSTALLER_TYPE)
+ }
+
+ companion object {
+ val INSTALLER_TYPE = "INSTALLER_TYPE"
+ }
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.kt
new file mode 100644
index 000000000..f1ea0ff86
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.kt
@@ -0,0 +1,65 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui.download
+
+import com.jfoenix.controls.JFXListView
+import com.jfoenix.controls.JFXSpinner
+import javafx.fxml.FXML
+import javafx.scene.layout.StackPane
+import org.jackhuang.hmcl.download.RemoteVersion
+import org.jackhuang.hmcl.download.DownloadProvider
+import org.jackhuang.hmcl.task.Scheduler
+import org.jackhuang.hmcl.ui.animation.ContainerAnimations
+import org.jackhuang.hmcl.ui.animation.TransitionHandler
+import org.jackhuang.hmcl.ui.loadFXML
+import org.jackhuang.hmcl.ui.wizard.WizardController
+import org.jackhuang.hmcl.ui.wizard.WizardPage
+
+class VersionsPage(private val controller: WizardController, private val gameVersion: String, private val downloadProvider: DownloadProvider, private val libraryId: String, private val callback: () -> Unit): StackPane(), WizardPage {
+
+ @FXML lateinit var list: JFXListView
+ @FXML lateinit var spinner: JFXSpinner
+
+ val transitionHandler = TransitionHandler(this)
+ private val versionList = downloadProvider.getVersionListById(libraryId)
+
+ init {
+ loadFXML("/assets/fxml/download/versions.fxml")
+ children.setAll(spinner)
+ list.selectionModel.selectedItemProperty().addListener { _, _, newValue ->
+ controller.settings[libraryId] = newValue.remoteVersion.selfVersion
+ callback()
+ }
+ versionList.refreshAsync(downloadProvider).subscribe(Scheduler.JAVAFX) {
+ val versions = ArrayList(versionList.getVersions(gameVersion))
+ versions.sortWith(RemoteVersion)
+ for (version in versions) {
+ list.items.add(VersionsPageItem(version))
+ }
+
+ transitionHandler.setContent(list, ContainerAnimations.FADE.animationProducer)
+ }
+ }
+
+ override val title: String
+ get() = "Choose a game version"
+
+ override fun cleanup(settings: MutableMap) {
+ settings.remove(libraryId)
+ }
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPageItem.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPageItem.kt
new file mode 100644
index 000000000..f5e13b1dc
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPageItem.kt
@@ -0,0 +1,38 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui.download
+
+import javafx.fxml.FXML
+import javafx.scene.control.Label
+import javafx.scene.layout.BorderPane
+import org.jackhuang.hmcl.download.RemoteVersion
+import org.jackhuang.hmcl.ui.loadFXML
+
+class VersionsPageItem(val remoteVersion: RemoteVersion<*>) : BorderPane() {
+
+ @FXML lateinit var lblSelfVersion: Label
+ @FXML lateinit var lblGameVersion: Label
+
+ private var handler: () -> Unit = {}
+
+ init {
+ loadFXML("/assets/fxml/download/versions-list-item.fxml")
+ lblSelfVersion.text = remoteVersion.selfVersion
+ lblGameVersion.text = remoteVersion.gameVersion
+ }
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/DefaultWizardDisplayer.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/DefaultWizardDisplayer.kt
new file mode 100644
index 000000000..00cfb492b
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/DefaultWizardDisplayer.kt
@@ -0,0 +1,124 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui.wizard
+
+import com.jfoenix.concurrency.JFXUtilities
+import com.jfoenix.controls.JFXButton
+import com.jfoenix.controls.JFXProgressBar
+import com.jfoenix.controls.JFXToolbar
+import javafx.fxml.FXML
+import javafx.scene.Node
+import javafx.scene.control.Label
+import javafx.scene.layout.StackPane
+import javafx.scene.layout.VBox
+import org.jackhuang.hmcl.ui.Controllers
+import org.jackhuang.hmcl.ui.animation.TransitionHandler
+import org.jackhuang.hmcl.ui.loadFXML
+import kotlin.concurrent.thread
+
+internal class DefaultWizardDisplayer(private val prefix: String, wizardProvider: WizardProvider) : StackPane(), WizardDisplayer {
+
+ val wizardController = WizardController(this, wizardProvider)
+
+ lateinit var transitionHandler: TransitionHandler
+
+ @FXML lateinit var root: StackPane
+ @FXML lateinit var closeButton: JFXButton
+ @FXML lateinit var backButton: JFXButton
+ @FXML lateinit var toolbar: JFXToolbar
+ /**
+ * Only shown if it is needed in now step.
+ */
+ @FXML lateinit var refreshButton: JFXButton
+ @FXML lateinit var titleLabel: Label
+
+ init {
+ loadFXML("/assets/fxml/wizard.fxml")
+ toolbar.effect = null
+ }
+
+ fun initialize() {
+ transitionHandler = TransitionHandler(root)
+
+ wizardController.onStart()
+ }
+
+ fun back() {
+ wizardController.onPrev(true)
+ }
+
+ fun close() {
+ wizardController.onCancel()
+ Controllers.navigate(null)
+ }
+
+ override fun navigateTo(page: Node, nav: Navigation.NavigationDirection) {
+ backButton.isDisable = !wizardController.canPrev()
+ transitionHandler.setContent(page, nav.animation.animationProducer)
+ val title = if (prefix.isEmpty()) "" else "$prefix - "
+ if (page is WizardPage)
+ titleLabel.text = title + page.title
+ }
+
+ override fun handleDeferredWizardResult(settings: Map, deferredResult: DeferredWizardResult) {
+ val vbox = VBox()
+ val progressBar = JFXProgressBar()
+ val label = Label()
+ progressBar.maxHeight = 10.0
+ vbox.children += progressBar
+ vbox.children += label
+
+ root.children.setAll(progressBar)
+
+ thread {
+ deferredResult.start(settings, object : ResultProgressHandle {
+ private var running = true
+
+ override fun setProgress(currentStep: Int, totalSteps: Int) {
+ progressBar.progress = 1.0 * currentStep / totalSteps
+ }
+
+ override fun setProgress(description: String, currentStep: Int, totalSteps: Int) {
+ label.text = description
+ progressBar.progress = 1.0 * currentStep / totalSteps
+ }
+
+ override fun setBusy(description: String) {
+ progressBar.progress = JFXProgressBar.INDETERMINATE_PROGRESS
+ }
+
+ override fun finished(result: Any) {
+ running = false
+ }
+
+ override fun failed(message: String, canNavigateBack: Boolean) {
+ label.text = message
+ running = false
+ }
+
+ override val isRunning: Boolean
+ get() = running
+
+ })
+
+ JFXUtilities.runInFX {
+ navigateTo(Label("Successful"), Navigation.NavigationDirection.FINISH)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/DeferredWizardResult.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/DeferredWizardResult.kt
new file mode 100644
index 000000000..84a1aea20
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/DeferredWizardResult.kt
@@ -0,0 +1,24 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui.wizard
+
+abstract class DeferredWizardResult(val canAbort: Boolean = false) {
+
+ abstract fun start(settings: Map, progressHandle: ResultProgressHandle)
+ open fun abort() { }
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/Navigation.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/Navigation.kt
new file mode 100644
index 000000000..e9327513b
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/Navigation.kt
@@ -0,0 +1,38 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui.wizard
+
+import org.jackhuang.hmcl.ui.animation.ContainerAnimations
+
+interface Navigation {
+ fun onStart()
+ fun onNext()
+ fun onPrev(cleanUp: Boolean)
+ fun canPrev(): Boolean
+ fun onFinish()
+ fun onCancel()
+
+ enum class NavigationDirection(val animation: ContainerAnimations) {
+ START(ContainerAnimations.NONE),
+ PREVIOUS(ContainerAnimations.SWIPE_RIGHT),
+ NEXT(ContainerAnimations.SWIPE_LEFT),
+ FINISH(ContainerAnimations.SWIPE_LEFT),
+ IN(ContainerAnimations.ZOOM_IN),
+ OUT(ContainerAnimations.ZOOM_OUT)
+ }
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/ResultProgressHandle.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/ResultProgressHandle.kt
new file mode 100644
index 000000000..ffc6ce2b9
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/ResultProgressHandle.kt
@@ -0,0 +1,104 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui.wizard
+
+/**
+ * A controller for the progress bar shown in the user interface. Used in
+ * conjunction then `DeferredWizardResult` for cases where at
+ * the conclusion of the wizard, the work to create the final wizard result
+ * will take a while and needs to happen on a background thread.
+ * @author Tim Boudreau
+ */
+interface ResultProgressHandle {
+
+ /**
+ * Set the current position and total number of steps. Note it is
+ * inadvisable to be holding any locks when calling this method, as it
+ * may immediately update the GUI using
+ * `EventQueue.invokeAndWait()`.
+
+ * @param currentStep the current step in the progress of computing the
+ * * result.
+ * *
+ * @param totalSteps the total number of steps. Must be greater than
+ * * or equal to currentStep.
+ */
+ fun setProgress(currentStep: Int, totalSteps: Int)
+
+ /**
+ * Set the current position and total number of steps, and description
+ * of what the computation is doing. Note it is
+ * inadvisable to be holding any locks when calling this method, as it
+ * may immediately update the GUI using
+ * `EventQueue.invokeAndWait()`.
+ * @param description Text to describe what is being done, which can
+ * * be displayed in the UI.
+ * *
+ * @param currentStep the current step in the progress of computing the
+ * * result.
+ * *
+ * @param totalSteps the total number of steps. Must be greater than
+ * * or equal to currentStep.
+ */
+ fun setProgress(description: String, currentStep: Int, totalSteps: Int)
+
+ /**
+ * Set the status as "busy" - a rotating icon will be displayed instead
+ * of a percent complete progress bar.
+
+ * Note it is inadvisable to be holding any locks when calling this method, as it
+ * may immediately update the GUI using
+ * `EventQueue.invokeAndWait()`.
+ * @param description Text to describe what is being done, which can
+ * * be displayed in the UI.
+ */
+ fun setBusy(description: String)
+
+ /**
+ * Call this method when the computation is complete, and pass in the
+ * final result of the computation. The method doing the computation
+ * (`DeferredWizardResult.start()` or something it
+ * called) should exit immediately after calling this method. If the
+ * `failed()` method is called after this method has been
+ * called, a runtime exception may be thrown.
+ * @param result the Object which was computed, if any.
+ */
+ fun finished(result: Any)
+
+ /**
+ * Call this method if computation fails. The message may be some text
+ * describing what went wrong, or null if no description.
+ * @param message The text to display to the user. The method
+ * * doing the computation (`DeferredWizardResult.start()` or something it
+ * * called). If the `finished()` method is called after this
+ * * method has been called, a runtime exception may be thrown.
+ * * should exit immediately after calling this method.
+ * * It is A description of what went wrong, or null.
+ * *
+ * @param canNavigateBack whether or not the Prev button should be
+ * * enabled.
+ */
+ fun failed(message: String, canNavigateBack: Boolean)
+
+ /**
+ * Returns true if the computation is still running, i.e., if neither finished or failed have been called.
+
+ * @return true if there is no result yet.
+ */
+ val isRunning: Boolean
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/Summary.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/Summary.kt
new file mode 100644
index 000000000..d54a624eb
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/Summary.kt
@@ -0,0 +1,41 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui.wizard
+
+import com.jfoenix.controls.JFXListView
+import com.jfoenix.controls.JFXTextArea
+import javafx.scene.Node
+
+class Summary(
+ /**
+ * The component that will display the summary information
+ */
+ val component: Node,
+ /**
+ * The object that represents the actual result of whatever that Wizard
+ * that created this Summary object computes, or null.
+ */
+ val result: Any?) {
+ constructor(items: Array, result: Any?)
+ : this(JFXListView().apply { this.items.addAll(*items) }, result) {
+ }
+
+ constructor(text: String, result: Any?)
+ : this(JFXTextArea(text).apply { isEditable = false }, result) {
+ }
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/Wizard.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/Wizard.kt
new file mode 100644
index 000000000..f0f4ef09b
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/Wizard.kt
@@ -0,0 +1,24 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui.wizard
+
+import javafx.scene.Node
+
+object Wizard {
+ fun createWizard(namespace: String = "", provider: WizardProvider): Node = DefaultWizardDisplayer(namespace, provider)
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardController.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardController.kt
new file mode 100644
index 000000000..6162f51ec
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardController.kt
@@ -0,0 +1,76 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui.wizard
+
+import javafx.scene.Node
+import java.util.*
+
+class WizardController(protected val displayer: WizardDisplayer, protected val provider: WizardProvider) : Navigation {
+ val settings = mutableMapOf()
+ val pages = Stack()
+
+ override fun onStart() {
+ val page = navigatingTo(0)
+ pages.push(page)
+ displayer.navigateTo(page, Navigation.NavigationDirection.START)
+ }
+
+ override fun onNext() {
+ onNext(navigatingTo(pages.size))
+ }
+
+ fun onNext(page: Node) {
+ pages.push(page)
+
+ if (page is WizardPage)
+ page.onNavigate(settings)
+
+ displayer.navigateTo(page, Navigation.NavigationDirection.NEXT)
+ }
+
+ override fun onPrev(cleanUp: Boolean) {
+ val page = pages.pop()
+ if (cleanUp && page is WizardPage)
+ page.cleanup(settings)
+
+ val prevPage = pages.peek()
+ if (prevPage is WizardPage)
+ prevPage.onNavigate(settings)
+
+ displayer.navigateTo(prevPage, Navigation.NavigationDirection.PREVIOUS)
+ }
+
+ override fun canPrev() = pages.size > 1
+
+ override fun onFinish() {
+ val result = provider.finish(settings)
+ when (result) {
+ is DeferredWizardResult -> displayer.handleDeferredWizardResult(settings, result)
+ is Summary -> displayer.navigateTo(result.component, Navigation.NavigationDirection.NEXT)
+ }
+ }
+
+ override fun onCancel() {
+
+ }
+
+ fun navigatingTo(step: Int): Node {
+ return provider.createPage(this, step, settings)
+ }
+
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardDisplayer.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardDisplayer.kt
new file mode 100644
index 000000000..f30785508
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardDisplayer.kt
@@ -0,0 +1,26 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui.wizard
+
+import javafx.scene.Node
+
+interface WizardDisplayer {
+ fun navigateTo(page: Node, nav: Navigation.NavigationDirection)
+
+ fun handleDeferredWizardResult(settings: Map, deferredResult: DeferredWizardResult)
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardNavigationResult.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardNavigationResult.kt
new file mode 100644
index 000000000..5790c995d
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardNavigationResult.kt
@@ -0,0 +1,29 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui.wizard
+
+enum class WizardNavigationResult {
+ PROCEED {
+ override val deferredComputation = false
+ },
+ DENY {
+ override val deferredComputation = false
+ };
+
+ abstract val deferredComputation: Boolean
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardObserver.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardObserver.kt
new file mode 100644
index 000000000..84be351ed
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardObserver.kt
@@ -0,0 +1,23 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui.wizard
+
+interface WizardObserver {
+
+ fun stepsChanged(wizard: Wizard)
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardPage.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardPage.kt
new file mode 100644
index 000000000..8895c8fd8
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardPage.kt
@@ -0,0 +1,24 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui.wizard
+
+interface WizardPage {
+ fun onNavigate(settings: MutableMap) {}
+ fun cleanup(settings: MutableMap)
+ val title: String
+}
\ No newline at end of file
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardProvider.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardProvider.kt
new file mode 100644
index 000000000..c18f8606a
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardProvider.kt
@@ -0,0 +1,27 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.ui.wizard
+
+import javafx.scene.Node
+
+abstract class WizardProvider {
+
+ abstract fun finish(settings: Map): Any?
+ abstract fun createPage(controller: WizardController, step: Int, settings: Map): Node
+ abstract fun cancel(): Boolean
+}
\ No newline at end of file
diff --git a/HMCL/src/main/resources/assets/css/jfoenix-components.css b/HMCL/src/main/resources/assets/css/jfoenix-components.css
new file mode 100644
index 000000000..1c02f8720
--- /dev/null
+++ b/HMCL/src/main/resources/assets/css/jfoenix-components.css
@@ -0,0 +1,394 @@
+.root {
+ -fx-font-family: Roboto;
+ src: "/resources/roboto/Roboto-Regular.ttf";
+}
+
+/* Burgers Demo */
+
+.jfx-hamburger {
+ -fx-spacing: 5;
+ -fx-cursor: hand;
+}
+
+.jfx-hamburger StackPane {
+ -fx-pref-width: 40px;
+ -fx-pref-height: 7px;
+ -fx-background-color: #D63333;
+ -fx-background-radius: 5px;
+}
+
+/* Input Demo */
+
+.text-field {
+ -fx-max-width: 300;
+}
+
+.jfx-text-field, .jfx-password-field {
+ -fx-background-color: WHITE;
+ -fx-font-weight: BOLD;
+ -fx-prompt-text-fill: #808080;
+ -fx-alignment: top-left;
+ -jfx-focus-color: #4059A9;
+ -jfx-unfocus-color: #4d4d4d;
+ -fx-max-width: 300;
+}
+
+.jfx-decorator {
+ -fx-decorator-color: RED;
+}
+
+.jfx-decorator .jfx-decorator-buttons-container {
+ -fx-background-color: -fx-decorator-color;
+}
+
+.jfx-decorator .resize-border {
+ -fx-border-color: -fx-decorator-color;
+ -fx-border-width: 0 4 4 4;
+}
+
+.jfx-text-area, .text-area {
+ -fx-font-weight: BOLD;
+}
+
+.jfx-text-field:error, .jfx-password-field:error, .jfx-text-area:error {
+ -jfx-focus-color: #D34336;
+ -jfx-unfocus-color: #D34336;
+}
+
+.jfx-text-field .error-label, .jfx-password-field .error-label, .jfx-text-area .error-label {
+ -fx-text-fill: #D34336;
+ -fx-font-size: 0.75em;
+}
+
+.jfx-text-field .error-icon, .jfx-password-field .error-icon, .jfx-text-area .error-icon {
+ -fx-text-fill: #D34336;
+ -fx-font-size: 1em;
+}
+
+/* Progress Bar Demo */
+
+.progress-bar > .bar {
+ -fx-min-width: 500;
+}
+
+.jfx-progress-bar > .bar {
+ -fx-min-width: 500;
+}
+
+.jfx-progress-bar {
+ -fx-progress-color: #0F9D58;
+ -fx-stroke-width: 3;
+}
+
+/* Icons Demo */
+.icon {
+ -fx-text-fill: #FE774D;
+ -fx-padding: 10;
+ -fx-cursor: hand;
+}
+
+.icons-rippler {
+ -jfx-rippler-fill: BLUE;
+ -jfx-mask-type: CIRCLE;
+}
+
+.icons-rippler:hover {
+ -fx-cursor: hand;
+}
+
+.jfx-check-box {
+ -fx-font-weight: BOLD;
+}
+
+.custom-jfx-check-box {
+ -jfx-checked-color: RED;
+ -jfx-unchecked-color: BLACK;
+}
+
+/* Button */
+.button {
+ -fx-padding: 0.7em 0.57em;
+ -fx-font-size: 14px;
+}
+
+.button-raised {
+ -fx-padding: 0.7em 0.57em;
+ -fx-font-size: 14px;
+ -jfx-button-type: RAISED;
+ -fx-background-color: rgb(77, 102, 204);
+ -fx-pref-width: 200;
+ -fx-text-fill: WHITE;
+}
+
+/* The main scrollbar **track** CSS class */
+.mylistview .scroll-bar:horizontal .track,
+.mylistview .scroll-bar:vertical .track {
+ -fx-background-color: transparent;
+ -fx-border-color: transparent;
+ -fx-background-radius: 0em;
+ -fx-border-radius: 2em;
+}
+
+/* The increment and decrement button CSS class of scrollbar */
+.mylistview .scroll-bar:horizontal .increment-button,
+.mylistview .scroll-bar:horizontal .decrement-button {
+ -fx-background-color: transparent;
+ -fx-background-radius: 0em;
+ -fx-padding: 0 0 10 0;
+}
+
+/* The increment and decrement button CSS class of scrollbar */
+
+.mylistview .scroll-bar:vertical .increment-button,
+.mylistview .scroll-bar:vertical .decrement-button {
+ -fx-background-color: transparent;
+ -fx-background-radius: 0em;
+ -fx-padding: 0 10 0 0;
+
+}
+
+.mylistview .scroll-bar .increment-arrow,
+.mylistview .scroll-bar .decrement-arrow {
+ -fx-shape: " ";
+ -fx-padding: 0;
+}
+
+/* The main scrollbar **thumb** CSS class which we drag every time (movable) */
+.mylistview .scroll-bar:horizontal .thumb,
+.mylistview .scroll-bar:vertical .thumb {
+ -fx-background-color: derive(black, 90%);
+ -fx-background-insets: 2, 0, 0;
+ -fx-background-radius: 2em;
+}
+
+.jfx-list-cell-container {
+ -fx-alignment: center-left;
+}
+
+.jfx-list-cell-container > .label {
+ -fx-text-fill: BLACK;
+}
+
+.jfx-list-cell:odd:selected > .jfx-rippler > StackPane, .jfx-list-cell:even:selected > .jfx-rippler > StackPane {
+ -fx-background-color: rgba(0, 0, 255, 0.2);
+}
+
+.jfx-list-cell {
+ -fx-background-insets: 0.0;
+ -fx-text-fill: BLACK;
+}
+
+.jfx-list-cell:odd, .jfx-list-cell:even {
+ -fx-background-color: WHITE;
+}
+
+.jfx-list-cell:filled:hover {
+ -fx-text-fill: black;
+}
+
+.jfx-list-cell .jfx-rippler {
+ -jfx-rippler-fill: BLUE;
+}
+
+.jfx-list-view {
+ -fx-background-insets: 0;
+ -jfx-cell-horizontal-margin: 0.0;
+ -jfx-cell-vertical-margin: 5.0;
+ -jfx-vertical-gap: 10;
+ -jfx-expanded: false;
+ -fx-pref-width: 200;
+}
+
+.jfx-toggle-button {
+ -jfx-toggle-color: RED;
+}
+
+.jfx-tool-bar {
+ -fx-font-size: 15;
+ -fx-background-color: #5264AE;
+ -fx-pref-width: 100%;
+ -fx-pref-height: 64px;
+}
+
+.jfx-tool-bar HBox {
+ -fx-alignment: center;
+ -fx-spacing: 25;
+ -fx-padding: 0 10;
+}
+
+.jfx-tool-bar Label {
+ -fx-text-fill: WHITE;
+}
+
+.jfx-popup-container {
+ -fx-background-color: WHITE;
+}
+
+.jfx-snackbar-content {
+ -fx-background-color: #323232;
+ -fx-padding: 5;
+ -fx-spacing: 5;
+}
+
+.jfx-snackbar-toast {
+ -fx-text-fill: WHITE;
+}
+
+.jfx-snackbar-action {
+ -fx-text-fill: #ff4081;
+}
+
+.jfx-list-cell-content-container {
+ -fx-alignment: center-left;
+}
+
+.jfx-list-cell-container .label {
+ -fx-text-fill: BLACK;
+}
+
+.combo-box-popup .list-view .jfx-list-cell:odd:selected .jfx-list-cell-container,
+.combo-box-popup .list-view .jfx-list-cell:even:selected .jfx-list-cell-container {
+ -fx-background-color: rgba(0.0, 0.0, 255.0, 0.2);
+}
+
+.combo-box-popup .list-view .jfx-list-cell {
+ -fx-background-insets: 0.0;
+ -fx-text-fill: BLACK;
+}
+
+.combo-box-popup .list-view .jfx-list-cell:odd,
+.combo-box-popup .list-view .jfx-list-cell:even {
+ -fx-background-color: WHITE;
+}
+
+.combo-box-popup .list-view .jfx-list-cell:filled:hover {
+ -fx-text-fill: black;
+}
+
+.combo-box-popup .list-view .jfx-list-cell .jfx-rippler {
+ -fx-rippler-fill: #5264AE;
+}
+
+/*.combo-box .combo-box-button-container{
+ -fx-border-color:BLACK;-fx-border-width: 0 0 1 0;
+}
+.combo-box .combo-box-selected-value-container{
+ -fx-border-color:BLACK;
+} */
+
+/*
+ * TREE TABLE CSS
+ */
+
+.tree-table-view {
+ -fx-tree-table-color: rgba(255, 0, 0, 0.2);
+ -fx-tree-table-rippler-color: rgba(255, 0, 0, 0.4);
+}
+
+.tree-table-view:focused .tree-table-row-cell:selected {
+ -fx-background-color: -fx-tree-table-color;
+ -fx-table-cell-border-color: -fx-tree-table-color;
+ -fx-text-fill: BLACK;
+}
+
+.tree-table-view:focused .tree-table-row-cell:selected .tree-table-cell {
+ -fx-text-fill: BLACK;
+}
+
+.tree-table-view .jfx-rippler {
+ -jfx-rippler-fill: -fx-tree-table-rippler-color;
+}
+
+.tree-table-view .column-header,
+.tree-table-view .column-header-background,
+.tree-table-view .column-header-background .filler {
+ -fx-background-color: TRANSPARENT;
+}
+
+.tree-table-view .column-header {
+ -fx-border-width: 0 1 0 1;
+ -fx-border-color: #F3F3F3;
+}
+
+.tree-table-view .column-header .label {
+ -fx-text-fill: #949494;
+ -fx-padding: 16 0 16 0;
+}
+
+.tree-table-view .column-header .arrow, .tree-table-view .column-header .sort-order-dot {
+ -fx-background-color: #949494;
+}
+
+.tree-table-view .column-header:last-visible {
+ -fx-border-width: 0 2 0 1;
+}
+
+.tree-table-view .column-header-background {
+ -fx-border-width: 0 0.0 1 0;
+ -fx-border-color: #F3F3F3;
+}
+
+.tree-table-view .tree-table-cell {
+ -fx-border-width: 0 0 0 0;
+ -fx-padding: 16 0 16 0;
+}
+
+.tree-table-view .column-overlay {
+ -fx-background-color: -fx-tree-table-color;
+}
+
+.tree-table-view .column-resize-line, .tree-table-view .column-drag-header {
+ -fx-background-color: -fx-tree-table-rippler-color;
+}
+
+.tree-table-view:focused {
+ -fx-background-color: -fx-tree-table-color, -fx-box-border, -fx-control-inner-background;
+ -fx-background-insets: -1.4, 0, 1;
+ -fx-background-radius: 1.4, 0, 0;
+ /*....*/
+ -fx-padding: 1; /* 0.083333em; */
+}
+
+.tree-table-row-cell > .tree-disclosure-node > .arrow {
+ -fx-background-color: -fx-text-fill;
+ -fx-padding: 0.333333em 0.229em 0.333333em 0.229em; /* 4 */
+ -fx-shape: "M 0 -3.5 L 4 0 L 0 3.5 z";
+}
+
+.tree-table-row-cell .jfx-text-field {
+ -fx-focus-color: rgba(240, 40, 40);
+}
+
+.tree-table-row-group {
+ -fx-background-color: rgba(230, 230, 230);
+}
+
+.animated-option-button {
+ -fx-pref-width: 50px;
+ -fx-background-color: #44B449;
+ -fx-background-radius: 50px;
+ -fx-pref-height: 50px;
+ -fx-text-fill: white;
+ -fx-border-color: WHITE;
+ -fx-border-radius: 50px;
+ -fx-border-width: 4px;
+}
+
+.animated-option-sub-button {
+ -fx-background-color: #43609C;
+}
+
+.animated-option-sub-button2 {
+ -fx-background-color: rgb(203, 104, 96);
+}
+
+.tree-table-view .menu-item:focused {
+ -fx-background-color: -fx-tree-table-color;
+
+}
+
+.tree-table-view .menu-item .label {
+ -fx-padding: 5 0 5 0;
+}
+
+
diff --git a/HMCL/src/main/resources/assets/css/jfoenix-main-demo.css b/HMCL/src/main/resources/assets/css/jfoenix-main-demo.css
new file mode 100644
index 000000000..6c5c8d875
--- /dev/null
+++ b/HMCL/src/main/resources/assets/css/jfoenix-main-demo.css
@@ -0,0 +1,1033 @@
+/*
+ * Apply google Roboto font to all UI components
+ */
+
+.root {
+ -fx-font-family: "Roboto";
+}
+
+.side-menu {
+ -fx-padding: 20.0, 10.0;
+ -fx-font-size: 15.0;
+ -fx-font-weight: NORMAL;
+ -fx-text-fill: #444;
+}
+
+.title-label {
+ -fx-font-size: 16.0px;
+ -fx-padding: 14.0px;
+ -fx-text-fill: rgba(0.0, 0.0, 0.0, 0.87);
+}
+
+.radio-button-title-label {
+ -fx-font-size: 16.0px;
+ -fx-padding: 14.0 0.0 -20.0 0.0;
+ -fx-text-fill: rgba(0.0, 0.0, 0.0, 0.87);
+}
+
+/*******************************************************************************
+ * *
+ * JFX Drawer *
+ * *
+ ******************************************************************************/
+
+.jfx-drawer-overlay-pane {
+ -fx-background-color: rgba(0.0, 0.0, 0.0, 0.1)
+}
+
+.jfx-drawer-side-pane {
+ -fx-background-color: WHITE;
+}
+
+/*******************************************************************************
+ * *
+ * JFX Dialog Layout *
+ * *
+ ******************************************************************************/
+
+.jfx-dialog-overlay-pane {
+ -fx-background-color: rgba(0.0, 0.0, 0.0, 0.1);
+}
+
+.dialog-trigger {
+ -fx-background-color: WHITE;
+ -jfx-button-type: RAISED;
+ -fx-font-size: 14.0px;
+}
+
+.jfx-dialog-layout {
+ -fx-padding: 24.0px 24.0px 16.0px 24.0px;
+ -fx-text-fill: rgba(0.0, 0.0, 0.0, 0.87);
+}
+
+.jfx-layout-heading {
+ -fx-font-size: 20.0px;
+ -fx-font-weight: BOLD;
+ -fx-alignment: center-left;
+ -fx-padding: 5.0 0.0 5.0 0.0;
+}
+
+.jfx-layout-body .label {
+ -fx-font-size: 14.0px;
+ -fx-pref-width: 400.0px;
+ -fx-wrap-text: true;
+}
+
+.jfx-layout-actions {
+ -fx-padding: 10.0px 0.0 0.0 0.0;
+ -fx-alignment: center-right;
+}
+
+.dialog-accept {
+ -fx-text-fill: #03A9F4;
+ -fx-font-weight: BOLD;
+ -fx-padding: 0.7em 0.8em;
+}
+
+/*******************************************************************************
+ * *
+ * JFX Pop Up *
+ * *
+ ******************************************************************************/
+
+.jfx-popup-overlay-pane {
+ -fx-background-color: transparent;
+}
+
+.jfx-popup-container {
+ -fx-background-color: WHITE;
+ -fx-pref-width: 151.0px;
+}
+
+.popup-list-view {
+ -fx-pref-width: 150.0px;
+}
+
+.jfx-snackbar-content {
+ -fx-background-color: #323232;
+}
+
+.jfx-snackbar-toast {
+ -fx-text-fill: WHITE;
+}
+
+.jfx-snackbar-action {
+ -fx-text-fill: #ff4081;
+}
+
+/*******************************************************************************
+ * *
+ * JFX Icons *
+ * *
+ ******************************************************************************/
+
+.icon {
+ -fx-fill: #FE774D;
+ -fx-padding: 10.0;
+ -fx-cursor: hand;
+}
+
+.icons-rippler {
+ -jfx-rippler-fill: #FE774D;
+ -jfx-mask-type: CIRCLE;
+}
+
+.icons-rippler1 {
+ -jfx-rippler-fill: #4285F4;
+ -jfx-mask-type: CIRCLE;
+}
+
+.icons-rippler:hover {
+ -fx-cursor: hand;
+}
+
+.animated-burgers .jfx-hamburger StackPane {
+ -fx-background-color: #5264AE;
+ -fx-background-radius: 4px;
+}
+
+.animated-burgers .jfx-rippler {
+ -jfx-rippler-fill: #5264AE;
+}
+
+.icons-badge .badge-pane {
+ -fx-background-color: #ff4081;
+ -fx-background-radius: 23;
+ -fx-pref-width: 23;
+ -fx-pref-height: 23;
+ -fx-alignment: center;
+}
+
+.icons-badge Label {
+ -fx-font-weight: BOLD;
+ -fx-font-size: 14.0px;
+ -fx-text-fill: WHITE;
+}
+
+/*******************************************************************************
+* *
+* JFX Hamburger Icon *
+* *
+*******************************************************************************/
+
+.jfx-hamburger {
+ -fx-padding: 10.0;
+ -fx-spacing: 4px;
+ -fx-cursor: hand;
+}
+
+.jfx-hamburger StackPane {
+ -fx-pref-width: 30px;
+ -fx-pref-height: 4px;
+ -fx-background-color: #FFF;
+ -fx-background-radius: 5px;
+}
+
+/*******************************************************************************
+ * *
+ * JFX Tool Bar *
+ * *
+ ******************************************************************************/
+
+.jfx-tool-bar {
+ -fx-font-size: 13.0;
+ -fx-font-weight: BOLD;
+ -fx-background-color: #5264AE;
+ -fx-pref-width: 100.0%;
+ -fx-pref-height: 32.0px;
+}
+
+.jfx-tool-bar HBox {
+ -fx-alignment: center;
+ -fx-spacing: 25.0;
+ -fx-padding: 0.0 10.0;
+}
+
+.jfx-tool-bar Label {
+ -fx-text-fill: WHITE;
+}
+
+.jfx-tool-bar .jfx-options-burger {
+ -fx-padding: 22px;
+}
+
+.jfx-tool-bar .jfx-options-burger StackPane {
+ -fx-pref-width: 4px;
+}
+
+.jfx-tool-bar .jfx-rippler {
+ -jfx-rippler-fill: WHITE;
+}
+
+.option-list-view {
+ -fx-pref-width: 160.0px;
+ -fx-background-color: WHITE;
+}
+
+.option-jfx-list-view-icon {
+ -fx-fill: #5264AE;
+ -fx-padding: 0.0 10.0 0.0 5.0;
+ -fx-cursor: hand;
+}
+
+/*******************************************************************************
+ * *
+ * JFX Radio Button *
+ * *
+ ******************************************************************************/
+
+.jfx-radio-button .radio {
+ -fx-stroke-width: 2.0px;
+ -fx-fill: transparent;
+}
+
+.jfx-radio-button .dot {
+ -fx-fill: #0F9D58;
+}
+
+.custom-jfx-radio-button {
+ -fx-font-size: 16.0px;
+}
+
+.custom-jfx-radio-button .radio {
+ -fx-stroke-width: 2.0px;
+ -fx-fill: transparent;
+}
+
+.custom-jfx-radio-button-blue {
+ -fx-text-fill: #5264AE;
+ -jfx-selected-color: #5264AE;
+ -jfx-unselected-color: #212121;
+}
+
+.custom-jfx-radio-button-red {
+ -fx-text-fill: #f44336;
+ -jfx-selected-color: #f44336;
+ -jfx-unselected-color: #b71c1c;
+}
+
+.custom-jfx-radio-button-green {
+ -fx-text-fill: #4caf50;
+ -jfx-selected-color: #4caf50;
+ -jfx-unselected-color: #1b5e20;
+}
+
+/*******************************************************************************
+ * *
+ * JFX Slider *
+ * *
+ ******************************************************************************/
+
+.svg-slider .thumb {
+ -fx-stroke: black;
+ -fx-fill: black;
+}
+
+.jfx-slider:disabled {
+ -fx-opacity: 0.4;
+}
+
+/*******************************************************
+* *
+* For the main demo sliders *
+* *
+*******************************************************/
+
+.jfx-slider-style {
+ -jfx-indicator-position: right;
+}
+
+.jfx-slider-style > .thumb {
+ -fx-background-color: #03a9f4;
+}
+
+.jfx-slider-style .track {
+ -fx-background-color: #ff5252;
+ -fx-pref-height: 5px;
+ -fx-pref-width: 5px;
+}
+
+.jfx-slider-style .sliderValue {
+ -fx-stroke: WHITE;
+ -fx-font-size: 10.0;
+}
+
+/******************************************************/
+
+/*******************************************************************************
+* *
+* JFX Rippler *
+* *
+*******************************************************************************/
+
+/*.jfx-rippler {
+ -fx-rippler-fill: #5264AE;
+ -fx-mask-type: RECT;
+}*/
+.jfx-rippler:hover {
+ -fx-cursor: hand;
+}
+
+/*******************************************************************************
+* *
+* JFX Button *
+* *
+*******************************************************************************/
+
+.custom-jfx-button-raised .jfx-rippler {
+ -jfx-rippler-fill: YELLOW;
+}
+
+.custom-jfx-button-raised {
+ -fx-padding: 0.7em 0.57em;
+ -fx-font-size: 14.0px;
+ -jfx-button-type: RAISED;
+ -fx-background-color: rgb(102.0, 153.0, 102.0);
+ -fx-pref-width: 200.0;
+ -fx-text-fill: WHITE;
+}
+
+.circle-jfx-button-raised .jfx-rippler {
+ -jfx-rippler-fill: YELLOW;
+}
+
+.circle-jfx-button-raised {
+ -fx-padding: 0.7em 0.57em;
+ -fx-font-size: 14.0px;
+ -jfx-button-type: RAISED;
+ -fx-background-color: rgb(102.0, 153.0, 102.0);
+ -fx-pref-width: 200.0;
+ -fx-text-fill: WHITE;
+ -jfx-mask-type: CIRCLE;
+}
+
+/*******************************************************************************
+* *
+* JFX Check Box *
+* *
+*******************************************************************************/
+
+.jfx-check-box {
+ -fx-font-weight: BOLD;
+}
+
+.custom-jfx-check-box {
+ -jfx-checked-color: RED;
+ -jfx-unchecked-color: BLACK;
+}
+
+.custom-jfx-check-box-all-colored {
+ -jfx-checked-color: #5264AE;
+ -jfx-unchecked-color: #5264AE;
+ -fx-text-fill: #5264AE;
+}
+
+.custom-jfx-check-box-text-colored {
+ -fx-text-fill: rgb(153.0, 0.0, 0.0);
+}
+
+/*******************************************************************************
+* *
+* JFX Progress Bar *
+* *
+*******************************************************************************/
+
+.jfx-progress-bar {
+ -fx-pref-width: 500.0;
+}
+
+.jfx-progress-bar > .track, .jfx-progress-bar > .bar {
+ -fx-background-radius: 0;
+ -fx-background-insets: 0;
+}
+
+.jfx-progress-bar > .track {
+ -fx-background-color: #E0E0E0;
+}
+
+.jfx-progress-bar > .bar {
+ -fx-background-color: #0F9D58;
+}
+
+.custom-jfx-progress-bar > .bar {
+ -fx-background-color: rgb(255.0, 128.0, 0.0);
+}
+
+.custom-jfx-progress-bar-stroke > .bar {
+ -fx-background-color: #5264AE;
+ -fx-padding: 6;
+}
+
+/*******************************************************************************
+* *
+* JFX Textfield *
+* *
+*******************************************************************************/
+
+.jfx-text-field, .jfx-password-field, .jfx-text-area {
+ -fx-background-color: transparent;
+ -fx-font-weight: BOLD;
+ -fx-prompt-text-fill: #808080;
+ -fx-alignment: top-left;
+ -fx-max-width: 300.0;
+ -fx-pref-width: 300.0;
+ -jfx-focus-color: #4059A9;
+ -jfx-unfocus-color: #4d4d4d;
+}
+
+.jfx-text-area .viewport {
+ -fx-background-color: #F4F4F4;
+}
+
+/*******************************************************************************
+* *
+* JFX List View *
+* *
+*******************************************************************************/
+
+.jfx-list-cell {
+ -fx-background-insets: 0.0;
+ -fx-text-fill: BLACK;
+}
+
+.jfx-list-cell:odd,
+.jfx-list-cell:even {
+ -fx-background-color: WHITE;
+}
+
+.jfx-list-cell:filled:hover,
+.jfx-list-cell:selected .label {
+ -fx-text-fill: black;
+}
+
+.jfx-list-cell .jfx-rippler {
+ -jfx-rippler-fill: #5264AE;
+}
+
+.jfx-list-cell:odd:selected > .jfx-rippler > StackPane,
+.jfx-list-cell:even:selected > .jfx-rippler > StackPane {
+ -fx-background-color: rgba(0.0, 0.0, 255.0, 0.2);
+}
+
+.jfx-list-view {
+ -fx-background-insets: 0.0;
+ -jfx-cell-horizontal-margin: 0.0;
+ -jfx-cell-vertical-margin: 5.0;
+ -jfx-vertical-gap: 10.0;
+ -jfx-expanded: false;
+}
+
+.custom-jfx-list-view .jfx-list-cell:odd:selected > .jfx-rippler > StackPane,
+.custom-jfx-list-view .jfx-list-cell:even:selected > .jfx-rippler > StackPane {
+ -fx-background-color: rgba(255, 0, 0, 0.2);
+}
+
+.custom-jfx-list-view {
+ -fx-background-insets: 0.0;
+ -jfx-cell-horizontal-margin: 0.0;
+ -jfx-cell-vertical-margin: 5.0;
+ -jfx-expanded: false;
+ -fx-max-width: 200.0px;
+ /* important to hide the list change of height */
+ -fx-background-color: TRANSPARENT;
+}
+
+.custom-jfx-list-view .jfx-rippler {
+ -jfx-rippler-fill: RED;
+}
+
+.custom-jfx-list-view1 {
+ -jfx-vertical-gap: 10.0;
+ -fx-pref-width: 150px;
+ -fx-background-color: transparent;
+}
+
+.custom-jfx-list-view-icon,
+.jfx-list-cell:selected .label .custom-jfx-list-view-icon {
+ /*-fx-text-fill: #5264AE;*/
+ -fx-fill: #5264AE;
+ -fx-padding: 0.0 10.0 0.0 5.0;
+ -fx-cursor: hand;
+}
+
+.custom-jfx-list-view-icon-container {
+ -fx-pref-width: 40px;
+}
+
+.custom-jfx-list-view .sublist-item {
+ -fx-border-color: #e0e0e0;
+ -fx-border-width: 1 0 1 0;
+}
+
+.custom-jfx-list-view .jfx-list-cell .sublist-header > .drop-icon {
+ -fx-background-color: GRAY;
+}
+
+.custom-jfx-list-view .jfx-list-cell:filled:hover .sublist-header > .drop-icon {
+ -fx-background-color: BLACK;
+}
+
+/*******************************************************************************/
+/*******************************************************************************/
+/*******************************************************************************/
+/*******************************************************************************/
+/*******************************************************************************/
+/*******************************************************************************/
+/*******************************************************************************
+* *
+* JFX SUBLIST IMPORTANT *
+* *
+*******************************************************************************/
+.sublist .scroll-bar:vertical .increment-arrow,
+.sublist .scroll-bar:vertical .decrement-arrow,
+.sublist .scroll-bar:vertical .increment-button,
+.sublist .scroll-bar:vertical .decrement-button {
+ -fx-padding: 0;
+}
+
+.sublist .scroll-bar:vertical .track,
+.sublist .scroll-bar:vertical .thumb {
+ -fx-background-color: WHITE;
+}
+
+.sublist {
+ -fx-background-color: TRANSPARENT;
+}
+
+.sublist .jfx-list-cell:odd:selected > .jfx-rippler > StackPane,
+.sublist .jfx-list-cell:even:selected > .jfx-rippler > StackPane {
+ -fx-background-color: rgba(255, 0, 0, 0.2);
+}
+
+.sublist .jfx-list-cell {
+ -fx-background-insets: 0.0;
+ -fx-text-fill: BLACK;
+}
+
+.sublist-header {
+ -fx-alignment: center-left;
+}
+
+.sublist-header .sub-label {
+ -fx-padding: 0 0 0 12;
+}
+
+/*.custom-jfx-list-view .sublist-container {
+ -fx-padding : 0 0 5 0;
+}*/
+
+/*******************************************************************************
+* *
+* JFX Toggle Button *
+* *
+*******************************************************************************/
+
+.jfx-toggle-button {
+ -jfx-toggle-color: #0F9D58;
+}
+
+.custom-jfx-toggle-button {
+ -jfx-toggle-color: #4285F4;
+}
+
+.custom-jfx-toggle-button-red {
+ -jfx-toggle-color: red;
+}
+
+.toggle-label {
+ -fx-font-size: 14.0px;
+}
+
+.toggle-icon1 .icon {
+ -fx-fill: #4285F4;
+ -fx-padding: 20.0;
+}
+
+.toggle-icon1 {
+ -fx-pref-width: 80px;
+ -fx-background-radius: 80px;
+ -fx-pref-height: 80px;
+ -fx-background-color: transparent;
+ -jfx-toggle-color: rgba(66.0, 133.0, 244.0, 0.29885056614875793);
+ -jfx-untoggle-color: transparent;
+}
+
+.toggle-icon1 .jfx-rippler {
+ -jfx-rippler-fill: rgba(66.0, 133.0, 244.0, 0.29885056614875793);
+ -jfx-mask-type: CIRCLE;
+}
+
+.toggle-icon2, .toggle-icon3 {
+ -fx-pref-width: 50px;
+ -fx-background-radius: 50px;
+ -fx-pref-height: 50px;
+ -fx-background-color: transparent;
+}
+
+.toggle-icon2 .icon {
+ -fx-fill: RED;
+}
+
+.toggle-icon2 .jfx-rippler {
+ -jfx-rippler-fill: RED;
+ -jfx-mask-type: CIRCLE;
+}
+
+.toggle-icon3 {
+ -fx-pref-width: 40px;
+ -fx-background-radius: 50px;
+ -fx-pref-height: 30px;
+ -fx-background-color: transparent;
+ -jfx-toggle-color: rgba(128, 128, 255, 0.2);
+ -jfx-untoggle-color: transparent;
+}
+
+.toggle-icon3 .icon {
+ -fx-fill: rgb(204.0, 204.0, 51.0);
+ -fx-padding: 10.0;
+}
+
+.toggle-icon3 .jfx-rippler {
+ -jfx-rippler-fill: #0F9D58;
+ -jfx-mask-type: CIRCLE;
+}
+
+/*******************************************************************************
+* *
+* JFX Spinner *
+* *
+*******************************************************************************/
+
+.jfx-spinner > .arc {
+ -fx-stroke-width: 3.0;
+ -fx-fill: transparent;
+}
+
+.first-spinner {
+ -jfx-radius: 20;
+}
+
+.first-spinner > .arc {
+ -fx-stroke-width: 5.0;
+}
+
+.second-spinner {
+ -jfx-radius: 30;
+}
+
+.second-spinner > .arc {
+ -fx-stroke-width: 5.0;
+}
+
+.third-spinner {
+ -jfx-radius: 40;
+}
+
+.third-spinner > .arc {
+ -fx-stroke-width: 5.0;
+}
+
+.fourth-spinner {
+ -jfx-radius: 50;
+}
+
+.fourth-spinner > .arc {
+ -fx-stroke-width: 5.0;
+}
+
+.fifth-spinner {
+ -jfx-radius: 60;
+}
+
+.fifth-spinner > .arc {
+ -fx-stroke-width: 5.0;
+}
+
+.sixth-spinner {
+ -jfx-radius: 70;
+}
+
+.sixth-spinner > .arc {
+ -fx-stroke-width: 5.0;
+}
+
+.seventh-spinner {
+ -jfx-radius: 80;
+}
+
+.seventh-spinner > .arc {
+ -fx-stroke-width: 5.0;
+}
+
+.blue-spinner .arc {
+ -fx-stroke: #4285f4;
+}
+
+.red-spinner .arc {
+ -fx-stroke: #db4437;
+}
+
+.green-spinner .arc {
+ -fx-stroke: #f4b400;
+}
+
+.yellow-spinner .arc {
+ -fx-stroke: #0F9D58;
+}
+
+.materialDesign-purple .arc {
+ -fx-stroke: #ab47bc;
+}
+
+.materialDesign-blue .arc {
+ -fx-stroke: #2962ff;
+}
+
+.materialDesign-cyan .arc {
+ -fx-stroke: #00b8d4;
+}
+
+.materialDesign-green .arc {
+ -fx-stroke: #00c853;
+}
+
+.materialDesign-yellow .arc {
+ -fx-stroke: #ffd600;
+}
+
+.materialDesign-orange .arc {
+ -fx-stroke: #ff6d00;
+}
+
+.materialDesign-red .arc {
+ -fx-stroke: #d50000;
+}
+
+/*******************************************************************************
+* *
+* JFX Combo Box *
+* *
+*******************************************************************************/
+
+.combo-box-popup .list-view .jfx-list-cell .label,
+.combo-box-popup .list-view .jfx-list-cell:filled:hover .label {
+ -fx-text-fill: BLACK;
+}
+
+.combo-box-popup .list-view .jfx-list-cell .custom-jfx-list-view-icon,
+.combo-box-popup .list-view .jfx-list-cell:filled:hover .custom-jfx-list-view-icon,
+.combo-box-popup .list-view .jfx-list-cell:selected .custom-jfx-list-view-icon {
+ -fx-fill: #5264AE;
+}
+
+.combo-box-popup .list-view .jfx-list-cell:odd:selected > .jfx-rippler > StackPane,
+.combo-box-popup .list-view .jfx-list-cell:even:selected > .jfx-rippler > StackPane {
+ -fx-background-color: rgba(0.0, 0.0, 255.0, 0.2);
+}
+
+.combo-box-popup .list-view .jfx-list-cell {
+ -fx-background-insets: 0.0;
+}
+
+.combo-box-popup .list-view .jfx-list-cell:odd,
+.combo-box-popup .list-view .jfx-list-cell:even {
+ -fx-background-color: WHITE;
+}
+
+.combo-box-popup .list-view .jfx-list-cell .jfx-rippler {
+ -jfx-rippler-fill: #5264AE;
+}
+
+/*******************************************************************************
+* *
+* JFX Decorator *
+* *
+*******************************************************************************/
+
+.jfx-decorator {
+ -fx-decorator-color: derive(#5264AE, -20%);
+}
+
+.jfx-decorator .jfx-decorator-buttons-container {
+ -fx-background-color: -fx-decorator-color;
+}
+
+.jfx-decorator .resize-border {
+ -fx-border-color: -fx-decorator-color;
+ -fx-border-width: 0 4 4 4;
+}
+
+/*******************************************************************************
+* *
+* Tree Table View *
+* *
+*******************************************************************************/
+
+.tree-table-view {
+ -fx-tree-table-color: rgba(82, 100, 174, 0.4);
+ -fx-tree-table-rippler-color: rgba(82, 100, 174, 0.6);
+}
+
+.tree-table-view:focused .tree-table-row-cell:selected {
+ -fx-background-color: -fx-tree-table-color;
+ -fx-table-cell-border-color: -fx-tree-table-color;
+ -fx-text-fill: BLACK;
+}
+
+.tree-table-view:focused .tree-table-row-cell:selected .tree-table-cell {
+ -fx-text-fill: BLACK;
+}
+
+.tree-table-view .jfx-rippler {
+ -jfx-rippler-fill: -fx-tree-table-rippler-color;
+}
+
+.tree-table-view .column-header,
+.tree-table-view .column-header-background,
+.tree-table-view .column-header-background .filler {
+ -fx-background-color: TRANSPARENT;
+}
+
+.tree-table-view .column-header {
+ -fx-border-width: 0 1 0 1;
+ -fx-border-color: #F3F3F3;
+}
+
+.tree-table-view .column-header .label {
+ -fx-text-fill: #949494;
+ -fx-padding: 16 0 16 0;
+}
+
+.tree-table-view .column-header .arrow, .tree-table-view .column-header .sort-order-dot {
+ -fx-background-color: #949494;
+}
+
+.tree-table-view .column-header:last-visible {
+ -fx-border-width: 0 2 0 1;
+}
+
+.tree-table-view .column-header-background {
+ -fx-border-width: 0 0.0 1 0;
+ -fx-border-color: #F3F3F3;
+}
+
+.tree-table-view .tree-table-cell {
+ -fx-border-width: 0 0 0 0;
+ -fx-padding: 16 0 16 0;
+ -fx-alignment: top-center;
+}
+
+.tree-table-view .column-overlay {
+ -fx-background-color: -fx-tree-table-color;
+}
+
+.tree-table-view .column-resize-line, .tree-table-view .column-drag-header {
+ -fx-background-color: -fx-tree-table-rippler-color;
+}
+
+.tree-table-view:focused {
+ -fx-background-color: -fx-tree-table-color, -fx-box-border, -fx-control-inner-background;
+ -fx-background-insets: -1.4, 0, 1;
+ -fx-background-radius: 1.4, 0, 0;
+ /*....*/
+ -fx-padding: 1; /* 0.083333em; */
+}
+
+.tree-table-row-cell > .tree-disclosure-node > .arrow {
+ -fx-background-color: -fx-text-fill;
+ -fx-padding: 0.333333em 0.229em 0.333333em 0.229em; /* 4 */
+ -fx-shape: "M 0 -3.5 L 4 0 L 0 3.5 z";
+}
+
+.tree-table-row-cell .jfx-text-field {
+ -fx-focus-color: rgba(82, 100, 174);
+}
+
+.tree-table-row-cell .jfx-text-field:error {
+ -jfx-focus-color: #D34336;
+ -jfx-unfocus-color: #D34336;
+}
+
+.tree-table-row-cell .jfx-text-field .error-label {
+ -fx-text-fill: #D34336;
+ -fx-font-size: 0.75em;
+}
+
+.tree-table-row-cell .jfx-text-field .error-icon {
+ -fx-fill: #D34336;
+ -fx-font-size: 1.0em;
+}
+
+.tree-table-row-group {
+ -fx-background-color: rgba(230, 230, 230);
+}
+
+.tree-table-view .menu-item:focused {
+ -fx-background-color: -fx-tree-table-color;
+
+}
+
+.tree-table-view .menu-item .label {
+ -fx-padding: 5 0 5 0;
+}
+
+/*******************************************************************************
+ * *
+ * Scroll Bar *
+ * *
+ ******************************************************************************/
+
+.scroll-bar:vertical > .track-background, .scroll-bar:horizontal > .track-background {
+ -fx-background-color: #F1F1F1;
+ -fx-background-insets: 0.0;
+}
+
+.scroll-bar:vertical > .thumb, .scroll-bar:horizontal > .thumb {
+ -fx-background-color: #BCBCBC;
+ -fx-background-insets: 0.0;
+ -fx-background-radius: 1.0;
+}
+
+.scroll-bar > .increment-button, .scroll-bar > .decrement-button {
+ -fx-padding: 5 2 5 2;
+}
+
+.scroll-bar > .increment-button, .scroll-bar > .decrement-button, .scroll-bar:hover > .increment-button, .scroll-bar:hover > .decrement-button {
+ -fx-background-color: transparent;
+}
+
+.scroll-bar > .increment-button > .increment-arrow, .scroll-bar > .decrement-button > .decrement-arrow {
+ -fx-background-color: rgb(150.0, 150.0, 150.0);
+}
+
+.scroll-bar > .increment-button > .increment-arrow {
+ -fx-shape: "M298 426h428l-214 214z";
+}
+
+.scroll-bar > .decrement-button > .decrement-arrow {
+ -fx-shape: "M298 598l214-214 214 214h-428z";
+}
+
+/*******************************************************************************
+ * *
+ * Scroll Pane *
+ * *
+ ******************************************************************************/
+
+.scroll-pane {
+ -fx-background-insets: 0;
+ -fx-padding: 0;
+}
+
+.scroll-pane:focused {
+ -fx-background-insets: 0;
+}
+
+.scroll-pane .corner {
+ -fx-background-insets: 0;
+}
+
+/*******************************************************************************
+* *
+* Error Facade *
+* *
+*******************************************************************************/
+
+.error-label {
+ -fx-text-fill: #D34336;
+ -fx-font-size: 0.75em;
+ -fx-font-weight: bold;
+}
+
+.error-icon {
+ -fx-fill: #D34336;
+ -fx-font-size: 1.0em;
+}
+
+.jfx-text-field:error, .jfx-password-field:error, .jfx-text-area:error, .jfx-combo-box:error {
+ -jfx-focus-color: #D34336;
+ -jfx-unfocus-color: #D34336;
+}
+
+.jfx-text-field .error-label, .jfx-password-field .error-label, .jfx-text-area .error-label {
+ -fx-text-fill: #D34336;
+ -fx-font-size: 0.75em;
+}
+
+.jfx-text-field .error-icon, .jfx-password-field .error-icon, .jfx-text-area .error-icon {
+ -fx-fill: #D34336;
+ -fx-font-size: 1.0em;
+}
+
+.fit-width {
+ -fx-pref-width: 100%;
+}
+/*
+.jfx-scroll-pane .main-header {
+ -fx-background-image: url("../bg1.jpg");
+}
+
+.jfx-scroll-pane .condensed-header {
+ -fx-background-image: url("../bg4.jpg");
+}
+*/
\ No newline at end of file
diff --git a/HMCL/src/main/resources/assets/fxml/download/dltype.fxml b/HMCL/src/main/resources/assets/fxml/download/dltype.fxml
new file mode 100644
index 000000000..5da12c5d5
--- /dev/null
+++ b/HMCL/src/main/resources/assets/fxml/download/dltype.fxml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+ Install New Game
+
+
+
+
+
+
+
+ Install Modpack (CurseForge supported)
+
+
+
+
+
+
+
diff --git a/HMCL/src/main/resources/assets/fxml/download/installers.fxml b/HMCL/src/main/resources/assets/fxml/download/installers.fxml
new file mode 100644
index 000000000..e81963ce1
--- /dev/null
+++ b/HMCL/src/main/resources/assets/fxml/download/installers.fxml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Install Forge
+
+
+
+
+
+
+
+ Install LiteLoader
+
+
+
+
+
+
+
+ Install OptiFine
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HMCL/src/main/resources/assets/fxml/download/versions-list-item.fxml b/HMCL/src/main/resources/assets/fxml/download/versions-list-item.fxml
new file mode 100644
index 000000000..c39c1fd4a
--- /dev/null
+++ b/HMCL/src/main/resources/assets/fxml/download/versions-list-item.fxml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HMCL/src/main/resources/assets/fxml/download/versions.fxml b/HMCL/src/main/resources/assets/fxml/download/versions.fxml
new file mode 100644
index 000000000..ab7abbe50
--- /dev/null
+++ b/HMCL/src/main/resources/assets/fxml/download/versions.fxml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/HMCL/src/main/resources/assets/fxml/main.fxml b/HMCL/src/main/resources/assets/fxml/main.fxml
new file mode 100644
index 000000000..4cd42eb03
--- /dev/null
+++ b/HMCL/src/main/resources/assets/fxml/main.fxml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HMCL/src/main/resources/assets/fxml/version-list-item.fxml b/HMCL/src/main/resources/assets/fxml/version-list-item.fxml
new file mode 100644
index 000000000..682f54fa8
--- /dev/null
+++ b/HMCL/src/main/resources/assets/fxml/version-list-item.fxml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HMCL/src/main/resources/assets/fxml/version.fxml b/HMCL/src/main/resources/assets/fxml/version.fxml
new file mode 100644
index 000000000..c1eb0c969
--- /dev/null
+++ b/HMCL/src/main/resources/assets/fxml/version.fxml
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Java Directory
+ Max Memory
+ Launcher Visibility
+ Run Directory
+ Dimension
+
+
+
+
+
+
+
+
+ Physical Memory: 16000MB
+
+
+
+
+
+
+
+
+
+ Close
+ Hide
+ Keep
+ Hide and Reopen
+
+
+
+
+
+
+ Default(.minecraft/)
+ Divided(.minecraft/versions/<versionName>)
+
+
+
+
+
+
+
+ x
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HMCL/src/main/resources/assets/fxml/wizard.fxml b/HMCL/src/main/resources/assets/fxml/wizard.fxml
new file mode 100644
index 000000000..5bfc439c2
--- /dev/null
+++ b/HMCL/src/main/resources/assets/fxml/wizard.fxml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HMCL/src/main/resources/assets/svg/arrow-left.fxml b/HMCL/src/main/resources/assets/svg/arrow-left.fxml
new file mode 100644
index 000000000..008914a29
--- /dev/null
+++ b/HMCL/src/main/resources/assets/svg/arrow-left.fxml
@@ -0,0 +1,2 @@
+
+
diff --git a/HMCL/src/main/resources/assets/svg/arrow-right.fxml b/HMCL/src/main/resources/assets/svg/arrow-right.fxml
new file mode 100644
index 000000000..535557cc7
--- /dev/null
+++ b/HMCL/src/main/resources/assets/svg/arrow-right.fxml
@@ -0,0 +1,2 @@
+
+
diff --git a/HMCL/src/main/resources/assets/svg/close.fxml b/HMCL/src/main/resources/assets/svg/close.fxml
new file mode 100644
index 000000000..b045a2a2a
--- /dev/null
+++ b/HMCL/src/main/resources/assets/svg/close.fxml
@@ -0,0 +1,2 @@
+
+
diff --git a/HMCL/src/main/resources/assets/svg/dots-vertical.fxml b/HMCL/src/main/resources/assets/svg/dots-vertical.fxml
new file mode 100644
index 000000000..d156a7f9b
--- /dev/null
+++ b/HMCL/src/main/resources/assets/svg/dots-vertical.fxml
@@ -0,0 +1,2 @@
+
+
diff --git a/HMCL/src/main/resources/assets/svg/gear.fxml b/HMCL/src/main/resources/assets/svg/gear.fxml
new file mode 100644
index 000000000..e777fa296
--- /dev/null
+++ b/HMCL/src/main/resources/assets/svg/gear.fxml
@@ -0,0 +1,2 @@
+
+
diff --git a/HMCL/src/main/resources/assets/svg/plus.fxml b/HMCL/src/main/resources/assets/svg/plus.fxml
new file mode 100644
index 000000000..498e812f4
--- /dev/null
+++ b/HMCL/src/main/resources/assets/svg/plus.fxml
@@ -0,0 +1,2 @@
+
+
diff --git a/HMCL/src/main/resources/assets/svg/refresh.fxml b/HMCL/src/main/resources/assets/svg/refresh.fxml
new file mode 100644
index 000000000..f1c6b5ff7
--- /dev/null
+++ b/HMCL/src/main/resources/assets/svg/refresh.fxml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/HMCL/src/main/resources/assets/svg/rocket.fxml b/HMCL/src/main/resources/assets/svg/rocket.fxml
new file mode 100644
index 000000000..8558b678b
--- /dev/null
+++ b/HMCL/src/main/resources/assets/svg/rocket.fxml
@@ -0,0 +1,2 @@
+
+
diff --git a/HMCLCore/NVIDIA/GLCache/38d31357932bb5cfb5d9bd361c203e5c/888c7c7e4d65a61d/82bd674ac494837f.bin b/HMCLCore/NVIDIA/GLCache/38d31357932bb5cfb5d9bd361c203e5c/888c7c7e4d65a61d/82bd674ac494837f.bin
new file mode 100644
index 000000000..273eba0d2
Binary files /dev/null and b/HMCLCore/NVIDIA/GLCache/38d31357932bb5cfb5d9bd361c203e5c/888c7c7e4d65a61d/82bd674ac494837f.bin differ
diff --git a/HMCLCore/NVIDIA/GLCache/38d31357932bb5cfb5d9bd361c203e5c/888c7c7e4d65a61d/82bd674ac494837f.toc b/HMCLCore/NVIDIA/GLCache/38d31357932bb5cfb5d9bd361c203e5c/888c7c7e4d65a61d/82bd674ac494837f.toc
new file mode 100644
index 000000000..f48bf45fb
Binary files /dev/null and b/HMCLCore/NVIDIA/GLCache/38d31357932bb5cfb5d9bd361c203e5c/888c7c7e4d65a61d/82bd674ac494837f.toc differ
diff --git a/HMCLCore/gradle/wrapper/gradle-wrapper.jar b/HMCLCore/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..375c0788f
Binary files /dev/null and b/HMCLCore/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/HMCLCore/gradle/wrapper/gradle-wrapper.properties b/HMCLCore/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..d147633c0
--- /dev/null
+++ b/HMCLCore/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Jul 19 18:12:48 CST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-rc-2-bin.zip
diff --git a/HMCLCore/gradlew b/HMCLCore/gradlew
new file mode 100644
index 000000000..d9116bf59
--- /dev/null
+++ b/HMCLCore/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save ( ) {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when execute from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/HMCLCore/gradlew.bat b/HMCLCore/gradlew.bat
new file mode 100644
index 000000000..f74b6cd90
--- /dev/null
+++ b/HMCLCore/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables then windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables then windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/HMCLCore/hmcl.log.1 b/HMCLCore/hmcl.log.1
new file mode 100644
index 000000000..8a384687d
--- /dev/null
+++ b/HMCLCore/hmcl.log.1
@@ -0,0 +1,8 @@
+[13:06:53] [org.jackhuang.hmcl.util.task.Subscription.executeTask/FINER] Executing task: class org.jackhuang.hmcl.util.task.ParallelTask
+[13:06:53] [org.jackhuang.hmcl.util.task.Subscription.executeTask/FINER] Executing task: class org.jackhuang.hmcl.download.game.GameAssetDownloadTask
+[13:06:53] [org.jackhuang.hmcl.util.task.Subscription.executeTask/FINER] Executing task: class org.jackhuang.hmcl.download.game.GameLibrariesTask
+[13:06:53] [org.jackhuang.hmcl.util.task.Subscription.executeTask/FINER] Executing task: class org.jackhuang.hmcl.download.game.GameLoggingDownloadTask
+[13:06:53] [org.jackhuang.hmcl.util.task.Subscription.executeTask/FINEST] Task finished: class org.jackhuang.hmcl.download.game.GameAssetDownloadTask
+[13:06:53] [org.jackhuang.hmcl.util.task.Subscription.executeTask/FINEST] Task finished: class org.jackhuang.hmcl.download.game.GameLoggingDownloadTask
+[13:06:53] [org.jackhuang.hmcl.util.task.Subscription.executeTask/FINEST] Task finished: class org.jackhuang.hmcl.download.game.GameLibrariesTask
+[13:06:53] [org.jackhuang.hmcl.util.task.Subscription.executeTask/FINEST] Task finished: class org.jackhuang.hmcl.util.task.ParallelTask
diff --git a/HMCLCore/out/production/classes/META-INF/HMCLCore_main.kotlin_module b/HMCLCore/out/production/classes/META-INF/HMCLCore_main.kotlin_module
new file mode 100644
index 000000000..80a42d156
Binary files /dev/null and b/HMCLCore/out/production/classes/META-INF/HMCLCore_main.kotlin_module differ
diff --git a/HMCLCore/settings.gradle b/HMCLCore/settings.gradle
new file mode 100644
index 000000000..394154c39
--- /dev/null
+++ b/HMCLCore/settings.gradle
@@ -0,0 +1,2 @@
+rootProject.name = 'HMCLCore'
+
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.kt
new file mode 100644
index 000000000..3d6a7f484
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.kt
@@ -0,0 +1,28 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.auth
+
+import java.net.Proxy
+
+abstract class Account() {
+ abstract val username: String
+ @Throws(AuthenticationException::class)
+ abstract fun logIn(proxy: Proxy = Proxy.NO_PROXY): AuthInfo
+ abstract fun logOut()
+ abstract fun toStorage(): Map
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/AccountFactory.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/AccountFactory.kt
new file mode 100644
index 000000000..c2ab52f9c
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/AccountFactory.kt
@@ -0,0 +1,23 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.auth
+
+interface AccountFactory {
+ fun fromUsername(username: String, password: String = ""): T
+ fun fromStorage(storage: Map): T
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/AuthInfo.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/AuthInfo.kt
new file mode 100644
index 000000000..e859ecde7
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/AuthInfo.kt
@@ -0,0 +1,30 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.auth
+
+import org.jackhuang.hmcl.util.Immutable
+
+@Immutable
+data class AuthInfo(
+ val username: String,
+ val userId: String,
+ val authToken: String,
+ val userType: UserType = UserType.LEGACY,
+ val userProperties: String = "{}",
+ val userPropertyMap: String = "{}"
+)
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/AuthenticationException.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/AuthenticationException.kt
new file mode 100644
index 000000000..6ce2b0257
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/AuthenticationException.kt
@@ -0,0 +1,24 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.auth
+
+class AuthenticationException : Exception {
+ constructor() : super() {}
+ constructor(message: String) : super(message) {}
+ constructor(message: String, cause: Throwable) : super(message, cause) {}
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OfflineAccount.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OfflineAccount.kt
new file mode 100644
index 000000000..f17688ebd
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OfflineAccount.kt
@@ -0,0 +1,66 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.auth
+
+import org.jackhuang.hmcl.util.DigestUtils
+import java.net.Proxy
+
+class OfflineAccount private constructor(val uuid: String, override val username: String): Account() {
+ override fun logIn(proxy: Proxy): AuthInfo {
+ if (username.isBlank() || uuid.isBlank())
+ throw AuthenticationException("Username cannot be empty")
+ return AuthInfo(
+ username = username,
+ userId = uuid,
+ authToken = uuid
+ )
+ }
+
+ override fun logOut() {
+ // Offline account need not log out.
+ }
+
+ override fun toStorage(): Map {
+ return mapOf(
+ "uuid" to uuid,
+ "username" to username
+ )
+ }
+
+ companion object OfflineAccountFactory : AccountFactory {
+
+ override fun fromUsername(username: String, password: String): OfflineAccount {
+ return OfflineAccount(
+ username = username,
+ uuid = getUUIDFromUserName(username)
+ )
+ }
+
+ override fun fromStorage(storage: Map): OfflineAccount {
+ val username = storage["username"] as? String
+ ?: throw IllegalStateException("Configuration is malformed.")
+ val obj = storage["uuid"]
+ return OfflineAccount(
+ username = username,
+ uuid = obj as? String ?: getUUIDFromUserName(username)
+ )
+ }
+
+ private fun getUUIDFromUserName(username: String) = DigestUtils.md5Hex(username)
+ }
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/UserType.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/UserType.kt
new file mode 100644
index 000000000..ea7d72764
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/UserType.kt
@@ -0,0 +1,34 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.auth
+
+enum class UserType() {
+ LEGACY,
+ MOJANG;
+
+ companion object {
+
+ fun fromName(name: String) = BY_NAME[name.toLowerCase()]
+ fun fromLegacy(isLegacy: Boolean) = if (isLegacy) LEGACY else MOJANG
+
+ private val BY_NAME = HashMap().apply {
+ for (type in values())
+ this[type.name.toLowerCase()] = type
+ }
+ }
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthenticationRequest.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthenticationRequest.kt
new file mode 100644
index 000000000..45a2d91bc
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthenticationRequest.kt
@@ -0,0 +1,30 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.auth.yggdrasil
+
+data class AuthenticationRequest(
+ val username: String,
+ val password: String,
+ val clientToken: String,
+ val agent: Map = mapOf(
+ "name" to "Minecraft",
+ "version" to 1
+ ),
+ val requestUser: Boolean = true
+) {
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/GameProfile.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/GameProfile.kt
new file mode 100644
index 000000000..c5836b594
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/GameProfile.kt
@@ -0,0 +1,51 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.auth.yggdrasil
+
+import com.google.gson.*
+import java.lang.reflect.Type
+import com.google.gson.JsonObject
+import java.util.UUID
+import com.google.gson.JsonParseException
+
+data class GameProfile(
+ val id: UUID? = null,
+ val name: String? = null,
+ val properties: PropertyMap = PropertyMap(),
+ val legacy: Boolean = false
+) {
+ companion object GameProfileSerializer: JsonSerializer, JsonDeserializer {
+ override fun serialize(src: GameProfile, typeOfSrc: Type?, context: JsonSerializationContext): JsonElement {
+ val result = JsonObject()
+ if (src.id != null)
+ result.add("id", context.serialize(src.id))
+ if (src.name != null)
+ result.addProperty("name", src.name)
+ return result
+ }
+
+ override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext): GameProfile {
+ if (json !is JsonObject)
+ throw JsonParseException("The json element is not a JsonObject.")
+ val id = if (json.has("id")) context.deserialize(json.get("id"), UUID::class.java) else null
+ val name = if (json.has("name")) json.getAsJsonPrimitive("name").asString else null
+ return GameProfile(id, name)
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/Property.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/Property.kt
new file mode 100644
index 000000000..ebdbeb777
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/Property.kt
@@ -0,0 +1,20 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.auth.yggdrasil
+
+data class Property(val name: String, val value: String)
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/PropertyMap.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/PropertyMap.kt
new file mode 100644
index 000000000..32194005f
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/PropertyMap.kt
@@ -0,0 +1,90 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.auth.yggdrasil
+
+import com.google.gson.*
+import java.lang.reflect.Type
+import java.util.*
+import com.google.gson.JsonObject
+import com.google.gson.JsonArray
+import com.google.gson.JsonElement
+import kotlin.collections.HashMap
+import com.google.gson.JsonPrimitive
+import com.google.gson.JsonSerializationContext
+
+
+
+class PropertyMap: HashMap() {
+
+ fun toList(): List> {
+ val properties = LinkedList>()
+ values.forEach { (name, value) ->
+ properties += mapOf(
+ "name" to name,
+ "value" to value
+ )
+ }
+ return properties
+ }
+
+ /**
+ * Load property map from list.
+ * @param list Right type is List>. Using List<*> here because of fault tolerance
+ */
+ fun fromList(list: List<*>) {
+ list.forEach { propertyMap ->
+ if (propertyMap is Map<*, *>) {
+ val name = propertyMap["name"] as? String
+ val value = propertyMap["value"] as? String
+ if (name != null && value != null)
+ put(name, Property(name, value))
+ }
+ }
+ }
+
+ companion object Serializer : JsonSerializer, JsonDeserializer {
+ override fun serialize(src: PropertyMap, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
+ val result = JsonArray()
+ for ((name, value) in src.values)
+ result.add(JsonObject().apply {
+ addProperty("name", name)
+ addProperty("value", value)
+ })
+
+ return result
+ }
+
+ override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): PropertyMap {
+ val result = PropertyMap()
+ if (json is JsonObject) {
+ for ((key, value) in json.entrySet())
+ if (value is JsonArray)
+ for (element in value)
+ result.put(key, Property(key, element.asString))
+ } else if (json is JsonArray)
+ for (element in json)
+ if (element is JsonObject) {
+ val name = element.getAsJsonPrimitive("name").asString
+ val value = element.getAsJsonPrimitive("value").asString
+ result.put(name, Property(name, value))
+ }
+ return result
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/RefreshRequest.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/RefreshRequest.kt
new file mode 100644
index 000000000..a8a749a6c
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/RefreshRequest.kt
@@ -0,0 +1,25 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.auth.yggdrasil
+
+data class RefreshRequest(
+ val clientToken: String,
+ val accessToken: String,
+ val selectedProfile: GameProfile?,
+ val requestUser: Boolean = true
+)
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/Response.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/Response.kt
new file mode 100644
index 000000000..b845ae6d0
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/Response.kt
@@ -0,0 +1,29 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.auth.yggdrasil
+
+class Response (
+ val accessToken: String? = null,
+ val clientToken: String? = null,
+ val selectedProfile: GameProfile? = null,
+ val availableProfiles: Array? = null,
+ val user: User? = null,
+ val error: String? = null,
+ val errorMessage: String? = null,
+ val cause: String? = null
+)
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/User.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/User.kt
new file mode 100644
index 000000000..9e0aeed03
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/User.kt
@@ -0,0 +1,31 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.auth.yggdrasil
+
+import com.google.gson.JsonParseException
+import org.jackhuang.hmcl.util.Validation
+
+class User (
+ val id: String = "",
+ val properties: PropertyMap? = null
+) : Validation {
+ override fun validate() {
+ if (id.isBlank())
+ throw JsonParseException("User id cannot be empty")
+ }
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/ValidateRequest.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/ValidateRequest.kt
new file mode 100644
index 000000000..ae04fe801
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/ValidateRequest.kt
@@ -0,0 +1,24 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.auth.yggdrasil
+
+data class ValidateRequest (
+ val clientToken: String = "",
+ val accessToken: String = ""
+) {
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.kt
new file mode 100644
index 000000000..ee611f139
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.kt
@@ -0,0 +1,242 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.auth.yggdrasil
+
+import com.google.gson.GsonBuilder
+import com.google.gson.JsonParseException
+import org.jackhuang.hmcl.auth.*
+import org.jackhuang.hmcl.util.*
+import org.jackhuang.hmcl.util.doGet
+import org.jackhuang.hmcl.util.doPost
+import org.jackhuang.hmcl.util.toURL
+import java.io.IOException
+import java.net.Proxy
+import java.net.URL
+import java.util.*
+
+class YggdrasilAccount private constructor(override val username: String): Account() {
+ private var password: String? = null
+ private var userId: String? = null
+ private var accessToken: String? = null
+ private var clientToken: String = UUID.randomUUID().toString()
+ private var isOnline: Boolean = false
+ private var userProperties = PropertyMap()
+ private var selectedProfile: GameProfile? = null
+ private var profiles: Array? = null
+ private var userType: UserType = UserType.LEGACY
+
+ init {
+ if (username.isBlank())
+ throw IllegalArgumentException("Username cannot be blank")
+ if (!username.contains("@"))
+ throw IllegalArgumentException("Yggdrasil account user name must be email")
+ }
+
+ val isLoggedIn: Boolean
+ get() = isNotBlank(accessToken)
+
+ val canPlayOnline: Boolean
+ get() = isLoggedIn && selectedProfile != null && isOnline
+
+ val canLogIn: Boolean
+ get() = !canPlayOnline && username.isNotBlank() && (isNotBlank(password) || isNotBlank((accessToken)))
+
+ override fun logIn(proxy: Proxy): AuthInfo {
+ if (canPlayOnline)
+ return AuthInfo(
+ username = selectedProfile!!.name!!,
+ userId = UUIDTypeAdapter.fromUUID(selectedProfile!!.id!!),
+ authToken = accessToken!!,
+ userType = userType,
+ userProperties = GSON.toJson(userProperties)
+ )
+ else {
+ logIn0(proxy)
+ if (!isLoggedIn)
+ throw AuthenticationException("Wrong password for account $username")
+ if (selectedProfile == null) {
+ // TODO: multi-available-profiles support
+ throw UnsupportedOperationException("Do not support multi-available-profiles account yet.")
+ } else {
+ return AuthInfo(
+ username = selectedProfile!!.name!!,
+ userId = UUIDTypeAdapter.fromUUID(selectedProfile!!.id!!),
+ authToken = accessToken!!,
+ userType = userType,
+ userProperties = GSON.toJson(userProperties)
+ )
+ }
+ }
+ }
+
+ private fun logIn0(proxy: Proxy) {
+ if (isNotBlank(accessToken)) {
+ if (isBlank(userId))
+ if (isNotBlank(username))
+ userId = username
+ else
+ throw AuthenticationException("Invalid uuid and username")
+ if (checkTokenValidity(proxy)) {
+ isOnline = true
+ return
+ }
+ logIn1(ROUTE_REFRESH, RefreshRequest(accessToken!!, clientToken, selectedProfile), proxy)
+ } else if (isNotBlank(password)) {
+ logIn1(ROUTE_AUTHENTICATE, AuthenticationRequest(username, password!!, clientToken), proxy)
+ } else
+ throw AuthenticationException("Password cannot be blank")
+ }
+
+ private fun logIn1(url: URL, input: Any, proxy: Proxy) {
+ val response = makeRequest(url, input, proxy)
+
+ if (clientToken != response?.clientToken)
+ throw AuthenticationException("Client token changed")
+
+ if (response.selectedProfile != null)
+ userType = UserType.fromLegacy(response.selectedProfile.legacy)
+ else if (response.availableProfiles?.getOrNull(0) != null)
+ userType = UserType.fromLegacy(response.availableProfiles[0].legacy)
+
+ val user = response.user
+ userId = user?.id ?: username
+ isOnline = true
+ profiles = response.availableProfiles
+ selectedProfile = response.selectedProfile
+ userProperties.clear()
+ accessToken = response.accessToken
+
+ if (user != null && user.properties != null)
+ userProperties.putAll(user.properties)
+ }
+
+ override fun logOut() {
+ password = null
+ userId = null
+ accessToken = null
+ isOnline = false
+ userProperties.clear()
+ profiles = null
+ selectedProfile = null
+ }
+
+ override fun toStorage(): Map {
+ val result = HashMap()
+
+ result[STORAGE_KEY_USER_NAME] = username
+ if (userId != null)
+ result[STORAGE_KEY_USER_ID] = userId!!
+ if (!userProperties.isEmpty())
+ result[STORAGE_KEY_USER_PROPERTIES] = userProperties.toList()
+ val profile = selectedProfile
+ if (profile != null && profile.name != null && profile.id != null) {
+ result[STORAGE_KEY_PROFILE_NAME] = profile.name
+ result[STORAGE_KEY_PROFILE_ID] = profile.id
+ if (!profile.properties.isEmpty())
+ result[STORAGE_KEY_PROFILE_PROPERTIES] = profile.properties.toList()
+ }
+ if (accessToken?.isNotBlank() ?: false)
+ result[STORAGE_KEY_ACCESS_TOKEN] = accessToken!!
+
+ return result
+ }
+
+ private fun makeRequest(url: URL, input: Any?, proxy: Proxy): Response? {
+ try {
+ val jsonResult =
+ if (input == null) url.doGet(proxy)
+ else url.doPost(GSON.toJson(input), "application/json", proxy)
+
+ val response = GSON.fromJson(jsonResult) ?: return null
+
+ if (response.error?.isNotBlank() ?: false) {
+ LOG.severe("Failed to log in, the auth server returned an error: " + response.error + ", message: " + response.errorMessage + ", cause: " + response.cause)
+ throw AuthenticationException("Request error: ${response.errorMessage}")
+ }
+
+ return response
+ } catch (e: IOException) {
+ throw AuthenticationException("Unable to connect to authentication server", e)
+ } catch (e: JsonParseException) {
+ throw AuthenticationException("Unable to parse server response", e)
+ }
+ }
+
+ private fun checkTokenValidity(proxy: Proxy): Boolean {
+ val access = accessToken
+ try {
+ if (access == null)
+ return false
+ makeRequest(ROUTE_VALIDATE, ValidateRequest(clientToken, access), proxy)
+ return true
+ } catch (e: AuthenticationException) {
+ return false
+ }
+ }
+
+ companion object YggdrasilAccountFactory : AccountFactory {
+ private val GSON = GsonBuilder()
+ .registerTypeAdapter(GameProfile::class.java, GameProfile)
+ .registerTypeAdapter(PropertyMap::class.java, PropertyMap)
+ .registerTypeAdapter(UUID::class.java, UUIDTypeAdapter)
+ .create()
+
+ private val BASE_URL = "https://authserver.mojang.com/"
+ private val ROUTE_AUTHENTICATE = (BASE_URL + "authenticate").toURL()
+ private val ROUTE_REFRESH = (BASE_URL + "refresh").toURL()
+ private val ROUTE_VALIDATE = (BASE_URL + "validate").toURL()
+
+ private val STORAGE_KEY_ACCESS_TOKEN = "accessToken"
+ private val STORAGE_KEY_PROFILE_NAME = "displayName"
+ private val STORAGE_KEY_PROFILE_ID = "uuid"
+ private val STORAGE_KEY_PROFILE_PROPERTIES = "profileProperties"
+ private val STORAGE_KEY_USER_NAME = "username"
+ private val STORAGE_KEY_USER_ID = "userid"
+ private val STORAGE_KEY_USER_PROPERTIES = "userProperties"
+
+ override fun fromUsername(username: String, password: String): YggdrasilAccount {
+ val account = YggdrasilAccount(username)
+ account.password = password
+ return account
+ }
+
+ override fun fromStorage(storage: Map): YggdrasilAccount {
+ val username = storage[STORAGE_KEY_USER_NAME] as? String ?: throw IllegalArgumentException("storage does not have key $STORAGE_KEY_USER_NAME")
+ val account = YggdrasilAccount(username)
+ account.userId = storage[STORAGE_KEY_USER_ID] as? String ?: username
+ account.accessToken = storage[STORAGE_KEY_ACCESS_TOKEN] as? String
+ val userProperties = storage[STORAGE_KEY_USER_PROPERTIES] as? List<*>
+ if (userProperties != null)
+ account.userProperties.fromList(userProperties)
+ val profileId = storage[STORAGE_KEY_PROFILE_ID] as? String
+ val profileName = storage[STORAGE_KEY_PROFILE_NAME] as? String
+ val profile: GameProfile?
+ if (profileId != null && profileName != null) {
+ profile = GameProfile(UUIDTypeAdapter.fromString(profileId), profileName)
+ val profileProperties = storage[STORAGE_KEY_PROFILE_PROPERTIES] as? List<*>
+ if (profileProperties != null)
+ profile.properties.fromList(profileProperties)
+ } else
+ profile = null
+ account.selectedProfile = profile
+
+ return account
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/AbstractDependencyManager.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/AbstractDependencyManager.kt
new file mode 100644
index 000000000..bcbdf4e3a
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/AbstractDependencyManager.kt
@@ -0,0 +1,33 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import org.jackhuang.hmcl.game.*
+
+abstract class AbstractDependencyManager(repository: GameRepository)
+ : DependencyManager(repository) {
+ abstract val downloadProvider: DownloadProvider
+
+ fun getVersions(id: String, selfVersion: String) =
+ downloadProvider.getVersionListById(id).getVersions(selfVersion)
+
+ override fun getVersionList(id: String): VersionList<*> {
+ return downloadProvider.getVersionListById(id)
+ }
+
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.kt
new file mode 100644
index 000000000..28059ed0c
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.kt
@@ -0,0 +1,49 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import org.jackhuang.hmcl.download.forge.ForgeVersionList
+import org.jackhuang.hmcl.download.game.GameVersionList
+import org.jackhuang.hmcl.download.liteloader.LiteLoaderVersionList
+import org.jackhuang.hmcl.download.optifine.OptiFineBMCLVersionList
+
+object BMCLAPIDownloadProvider : DownloadProvider() {
+ override val libraryBaseURL: String = "http://bmclapi2.bangbang93.com/libraries/"
+ override val versionListURL: String = "http://bmclapi2.bangbang93.com/mc/game/version_manifest.json"
+ override val versionBaseURL: String = "http://bmclapi2.bangbang93.com/versions/"
+ override val assetIndexBaseURL: String = "http://bmclapi2.bangbang93.com/indexes/"
+ override val assetBaseURL: String = "http://bmclapi2.bangbang93.com/assets/"
+
+ override fun getVersionListById(id: String): VersionList<*> {
+ return when(id) {
+ "game" -> GameVersionList
+ "forge" -> ForgeVersionList
+ "liteloader" -> LiteLoaderVersionList
+ "optifine" -> OptiFineBMCLVersionList
+ else -> throw IllegalArgumentException("Unrecognized version list id: $id")
+ }
+ }
+
+ override fun injectURL(baseURL: String): String = baseURL
+ .replace("https://launchermeta.mojang.com", "http://bmclapi2.bangbang93.com")
+ .replace("https://launcher.mojang.com", "http://bmclapi2.bangbang93.com")
+ .replace("https://libraries.minecraft.net", "http://bmclapi2.bangbang93.com/libraries")
+ .replace("http://files.minecraftforge.net/maven", "http://bmclapi2.bangbang93.com/maven")
+ .replace("http://dl.liteloader.com/versions/versions.json", "http://bmclapi2.bangbang93.com/maven/com/mumfrey/liteloader/versions.json")
+ .replace("http://dl.liteloader.com/versions", "http://bmclapi2.bangbang93.com/maven")
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.kt
new file mode 100644
index 000000000..4aeee51f4
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.kt
@@ -0,0 +1,65 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import org.jackhuang.hmcl.download.game.GameAssetDownloadTask
+import org.jackhuang.hmcl.download.game.GameLibrariesTask
+import org.jackhuang.hmcl.download.game.GameLoggingDownloadTask
+import org.jackhuang.hmcl.download.game.VersionJSONSaveTask
+import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask
+import org.jackhuang.hmcl.game.DefaultGameRepository
+import org.jackhuang.hmcl.game.Version
+import org.jackhuang.hmcl.task.ParallelTask
+import org.jackhuang.hmcl.task.Task
+import org.jackhuang.hmcl.task.then
+import java.net.Proxy
+
+class DefaultDependencyManager(override val repository: DefaultGameRepository, override val downloadProvider: DownloadProvider, val proxy: Proxy = Proxy.NO_PROXY)
+ : AbstractDependencyManager(repository) {
+
+ override fun gameBuilder(): GameBuilder = DefaultGameBuilder(this)
+
+ override fun checkGameCompletionAsync(version: Version): Task {
+ val tasks: Array = arrayOf(
+ GameAssetDownloadTask(this, version),
+ GameLoggingDownloadTask(this, version),
+ GameLibrariesTask(this, version)
+ )
+ return ParallelTask(*tasks)
+ }
+
+ override fun installLibraryAsync(version: Version, libraryId: String, libraryVersion: String): Task {
+ if (libraryId == "forge")
+ return ForgeInstallTask(this, version, libraryVersion) then { task ->
+ val newVersion = task.result!!
+ VersionJSONSaveTask(this@DefaultDependencyManager, newVersion)
+ }
+ else if (libraryId == "liteloader")
+ return LiteLoaderInstallTask(this, version, libraryVersion) then { task ->
+ val newVersion = task.result!!
+ VersionJSONSaveTask(this@DefaultDependencyManager, newVersion)
+ }
+ else if (libraryId == "optifine")
+ return OptiFineInstallTask(this, version, libraryVersion) then { task ->
+ val newVersion = task.result!!
+ VersionJSONSaveTask(this@DefaultDependencyManager, newVersion)
+ }
+ else
+ throw IllegalArgumentException("Library id $libraryId is unrecognized.")
+ }
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.kt
new file mode 100644
index 000000000..c068efc6b
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.kt
@@ -0,0 +1,96 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import org.jackhuang.hmcl.download.game.GameAssetDownloadTask
+import org.jackhuang.hmcl.download.game.GameLibrariesTask
+import org.jackhuang.hmcl.download.game.GameLoggingDownloadTask
+import org.jackhuang.hmcl.download.game.VersionJSONSaveTask
+import org.jackhuang.hmcl.game.*
+import org.jackhuang.hmcl.task.*
+import org.jackhuang.hmcl.util.*
+import java.util.*
+
+class DefaultGameBuilder(val dependencyManager: DefaultDependencyManager): GameBuilder() {
+ val repository = dependencyManager.repository
+ val downloadProvider = dependencyManager.downloadProvider
+
+ override fun buildAsync(): Task {
+ return VersionJSONDownloadTask(gameVersion = gameVersion) then { task ->
+ var version = GSON.fromJson(task.result!!)
+ version = version.copy(jar = version.id, id = name)
+ var result = ParallelTask(
+ GameAssetDownloadTask(dependencyManager, version),
+ GameLoggingDownloadTask(dependencyManager, version),
+ GameDownloadTask(version),
+ GameLibrariesTask(dependencyManager, version) // Game libraries will be downloaded for multiple times partly, this time is for vanilla libraries.
+ ) then VersionJSONSaveTask(dependencyManager, version)
+
+ if (toolVersions.containsKey("forge"))
+ result = result then libraryTaskHelper(version, "forge")
+ if (toolVersions.containsKey("liteloader"))
+ result = result then libraryTaskHelper(version, "liteloader")
+ if (toolVersions.containsKey("optifine"))
+ result = result then libraryTaskHelper(version, "optifine")
+ result
+ }
+ }
+
+ private fun libraryTaskHelper(version: Version, libraryId: String): Task.(Task) -> Task = { prev ->
+ var thisVersion = version
+ if (prev is TaskResult<*> && prev.result is Version) {
+ thisVersion = prev.result as Version
+ }
+ dependencyManager.installLibraryAsync(thisVersion, libraryId, toolVersions[libraryId]!!)
+ }
+
+ inner class VersionJSONDownloadTask(val gameVersion: String): Task() {
+ override val dependents: MutableCollection = LinkedList()
+ override val dependencies: MutableCollection = LinkedList()
+ var httpTask: GetTask? = null
+ val result: String? get() = httpTask?.result
+
+ val gameVersionList: VersionList<*> = dependencyManager.getVersionList("game")
+ init {
+ if (!gameVersionList.loaded)
+ dependents += gameVersionList.refreshAsync(downloadProvider)
+ }
+
+ override fun execute() {
+ val remoteVersion = gameVersionList.getVersions(gameVersion).firstOrNull()
+ ?: throw Error("Cannot find specific version $gameVersion in remote repository")
+
+ val jsonURL = downloadProvider.injectURL(remoteVersion.url)
+ httpTask = GetTask(jsonURL.toURL(), proxy = dependencyManager.proxy)
+ dependencies += httpTask!!
+ }
+ }
+
+ inner class GameDownloadTask(var version: Version) : Task() {
+ override val dependencies: MutableCollection = LinkedList()
+ override fun execute() {
+ val jar = repository.getVersionJar(version)
+
+ dependencies += FileDownloadTask(
+ url = downloadProvider.injectURL(version.download.url).toURL(),
+ file = jar,
+ hash = version.download.sha1,
+ proxy = dependencyManager.proxy)
+ }
+ }
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.kt
new file mode 100644
index 000000000..f48cd1a1b
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.kt
@@ -0,0 +1,47 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import org.jackhuang.hmcl.game.GameRepository
+import org.jackhuang.hmcl.game.Version
+import org.jackhuang.hmcl.task.Task
+
+abstract class DependencyManager(open val repository: GameRepository) {
+
+ /**
+ * Check if the game is complete.
+ * Check libraries, assets, logging files and so on.
+ *
+ * @return
+ */
+ abstract fun checkGameCompletionAsync(version: Version): Task
+
+ /**
+ * The builder to build a brand new game then libraries such as Forge, LiteLoader and OptiFine.
+ */
+ abstract fun gameBuilder(): GameBuilder
+
+ abstract fun installLibraryAsync(version: Version, libraryId: String, libraryVersion: String): Task
+
+ /**
+ * Get registered version list.
+ * @param id the id of version list. i.e. game, forge, liteloader, optifine
+ * @throws IllegalArgumentException if the version list of specific id is not found.
+ */
+ abstract fun getVersionList(id: String): VersionList<*>
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DownloadProvider.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DownloadProvider.kt
new file mode 100644
index 000000000..dfeb348ba
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DownloadProvider.kt
@@ -0,0 +1,30 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import org.jackhuang.hmcl.download.VersionList
+
+abstract class DownloadProvider {
+ abstract val libraryBaseURL: String
+ abstract val versionListURL: String
+ abstract val versionBaseURL: String
+ abstract val assetIndexBaseURL: String
+ abstract val assetBaseURL: String
+ abstract fun injectURL(baseURL: String): String
+ abstract fun getVersionListById(id: String): VersionList<*>
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/ForgeInstallTask.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/ForgeInstallTask.kt
new file mode 100644
index 000000000..61933bc58
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/ForgeInstallTask.kt
@@ -0,0 +1,82 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import org.jackhuang.hmcl.download.game.GameLibrariesTask
+import org.jackhuang.hmcl.game.Library
+import org.jackhuang.hmcl.game.SimpleVersionProvider
+import org.jackhuang.hmcl.game.Version
+import org.jackhuang.hmcl.task.FileDownloadTask
+import org.jackhuang.hmcl.task.Task
+import org.jackhuang.hmcl.task.TaskResult
+import org.jackhuang.hmcl.task.then
+import org.jackhuang.hmcl.util.*
+import java.io.File
+import java.io.IOException
+import java.util.zip.ZipFile
+
+class ForgeInstallTask(private val dependencyManager: DefaultDependencyManager,
+ private val version: Version,
+ private val remoteVersion: String) : TaskResult() {
+ private val forgeVersionList = dependencyManager.getVersionList("forge")
+ private val installer: File = File("forge-installer.jar").absoluteFile
+ lateinit var remote: RemoteVersion<*>
+ override val dependents: MutableCollection = mutableListOf()
+ override val dependencies: MutableCollection = mutableListOf()
+
+ init {
+ if (version.jar == null)
+ throw IllegalArgumentException()
+ if (!forgeVersionList.loaded)
+ dependents += forgeVersionList.refreshAsync(dependencyManager.downloadProvider) then {
+ remote = forgeVersionList.getVersion(version.jar, remoteVersion) ?: throw IllegalArgumentException("Remote forge version ${version.jar}, $remoteVersion not found")
+ FileDownloadTask(remote.url.toURL(), installer)
+ }
+ else {
+ remote = forgeVersionList.getVersion(version.jar, remoteVersion) ?: throw IllegalArgumentException("Remote forge version ${version.jar}, $remoteVersion not found")
+ dependents += FileDownloadTask(remote.url.toURL(), installer)
+ }
+ }
+
+ override fun execute() {
+ ZipFile(installer).use { zipFile ->
+ val installProfile = GSON.fromJson(zipFile.getInputStream(zipFile.getEntry("install_profile.json")).readFullyAsString())
+
+ // unpack the universal jar in the installer file.
+
+ val forgeLibrary = Library.fromName(installProfile.install!!.path!!)
+ val forgeFile = dependencyManager.repository.getLibraryFile(version, forgeLibrary)
+ if (!forgeFile.makeFile())
+ throw IOException("Cannot make directory ${forgeFile.parentFile}")
+
+ val forgeEntry = zipFile.getEntry(installProfile.install.filePath)
+ zipFile.getInputStream(forgeEntry).copyToAndClose(forgeFile.outputStream())
+
+ // resolve the version
+ val versionProvider = SimpleVersionProvider()
+ versionProvider.addVersion(version)
+
+ result = installProfile.versionInfo!!.copy(inheritsFrom = version.id).resolve(versionProvider).copy(id = version.id)
+ dependencies += GameLibrariesTask(dependencyManager, installProfile.versionInfo)
+ }
+
+ check(installer.delete(), { "Unable to delete installer file $installer" })
+ }
+
+
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/ForgeRemote.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/ForgeRemote.kt
new file mode 100644
index 000000000..66d747d98
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/ForgeRemote.kt
@@ -0,0 +1,84 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import com.google.gson.annotations.SerializedName
+import org.jackhuang.hmcl.game.Version
+import org.jackhuang.hmcl.util.Immutable
+import org.jackhuang.hmcl.util.Validation
+
+@Immutable
+internal class ForgeVersion (
+ val branch: String? = null,
+ val mcversion: String? = null,
+ val jobver: String? = null,
+ val version: String? = null,
+ val build: Int = 0,
+ val modified: Double = 0.0,
+ val files: Array>? = null
+) : Validation {
+ override fun validate() {
+ check(files != null, { "ForgeVersion files cannot be null" })
+ check(version != null, { "ForgeVersion version cannot be null" })
+ check(mcversion != null, { "ForgeVersion mcversion cannot be null" })
+ }
+}
+
+@Immutable
+internal class ForgeVersionRoot (
+ val artifact: String? = null,
+ val webpath: String? = null,
+ val adfly: String? = null,
+ val homepage: String? = null,
+ val name: String? = null,
+ val branches: Map>? = null,
+ val mcversion: Map>? = null,
+ val promos: Map? = null,
+ val number: Map? = null
+) : Validation {
+ override fun validate() {
+ check(number != null, { "ForgeVersionRoot number cannot be null" })
+ check(mcversion != null, { "ForgeVersionRoot mcversion cannot be null" })
+ }
+}
+
+@Immutable
+internal data class Install (
+ val profileName: String? = null,
+ val target: String? = null,
+ val path: String? = null,
+ val version: String? = null,
+ val filePath: String? = null,
+ val welcome: String? = null,
+ val minecraft: String? = null,
+ val mirrorList: String? = null,
+ val logo: String? = null
+)
+
+@Immutable
+internal data class InstallProfile (
+ @SerializedName("install")
+ val install: Install? = null,
+ @SerializedName("versionInfo")
+ val versionInfo: Version? = null
+) : Validation {
+ override fun validate() {
+ check(install != null, { "InstallProfile install cannot be null" })
+ check(versionInfo != null, { "InstallProfile versionInfo cannot be null" })
+ }
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/ForgeVersionList.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/ForgeVersionList.kt
new file mode 100644
index 000000000..6fc6fa86b
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/ForgeVersionList.kt
@@ -0,0 +1,69 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import org.jackhuang.hmcl.task.GetTask
+import org.jackhuang.hmcl.task.Task
+import org.jackhuang.hmcl.util.*
+
+object ForgeVersionList : VersionList() {
+ @JvmField
+ val FORGE_LIST = "http://files.minecraftforge.net/maven/net/minecraftforge/forge/json"
+
+ override fun refreshAsync(downloadProvider: DownloadProvider): Task {
+ return RefreshTask(downloadProvider)
+ }
+
+ private class RefreshTask(val downloadProvider: DownloadProvider): Task() {
+ val task = GetTask(downloadProvider.injectURL(FORGE_LIST).toURL())
+ override val dependents: Collection = listOf(task)
+ override fun execute() {
+ val root = GSON.fromJson(task.result!!) ?: return
+ versions.clear()
+
+ for ((x, versions) in root.mcversion!!.entries) {
+ val gameVersion = x.asVersion() ?: continue
+ for (v in versions) {
+ val version = root.number!!.get(v) ?: continue
+ var jar: String? = null
+ for (file in version.files!!)
+ if (file.getOrNull(1) == "installer") {
+ val classifier = "${version.mcversion}-${version.version}" + (
+ if (isNotBlank(version.branch))
+ "-${version.branch}"
+ else
+ ""
+ )
+ val fileName = "${root.artifact}-$classifier-${file[1]}.${file[0]}"
+ jar = downloadProvider.injectURL("${root.webpath}$classifier/$fileName")
+ }
+
+ if (jar == null) continue
+ val remoteVersion = RemoteVersion(
+ gameVersion = version.mcversion!!,
+ selfVersion = version.version!!,
+ url = jar,
+ tag = Unit
+ )
+ ForgeVersionList.versions.put(gameVersion, remoteVersion)
+ }
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.kt
new file mode 100644
index 000000000..eb406b16f
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.kt
@@ -0,0 +1,44 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import org.jackhuang.hmcl.task.Task
+
+
+abstract class GameBuilder {
+ var name: String = ""
+ protected var gameVersion: String = ""
+ protected var toolVersions = HashMap()
+
+ fun name(name: String): GameBuilder {
+ this.name = name
+ return this
+ }
+
+ fun gameVersion(version: String): GameBuilder {
+ gameVersion = version
+ return this
+ }
+
+ fun version(id: String, version: String): GameBuilder {
+ toolVersions[id] = version
+ return this
+ }
+
+ abstract fun buildAsync(): Task
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameDownloadTasks.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameDownloadTasks.kt
new file mode 100644
index 000000000..58f72e1ef
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameDownloadTasks.kt
@@ -0,0 +1,145 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import org.jackhuang.hmcl.game.AssetIndex
+import org.jackhuang.hmcl.game.AssetObject
+import org.jackhuang.hmcl.game.DownloadType
+import org.jackhuang.hmcl.game.Version
+import org.jackhuang.hmcl.task.FileDownloadTask
+import org.jackhuang.hmcl.task.Task
+import org.jackhuang.hmcl.task.TaskResult
+import org.jackhuang.hmcl.util.*
+import java.io.File
+import java.io.IOException
+import java.util.*
+import java.util.logging.Level
+
+
+/**
+ * This task is to download game libraries.
+ * This task should be executed last(especially after game downloading, Forge, LiteLoader and OptiFine install task)
+ * @param resolvedVersion the resolved version
+ */
+class GameLibrariesTask(private val dependencyManager: DefaultDependencyManager, private val resolvedVersion: Version): Task() {
+ override val dependencies: MutableCollection = LinkedList()
+ override fun execute() {
+ for (library in resolvedVersion.libraries)
+ if (library.appliesToCurrentEnvironment) {
+ val file = dependencyManager.repository.getLibraryFile(resolvedVersion, library)
+ if (!file.exists())
+ dependencies += FileDownloadTask(dependencyManager.downloadProvider.injectURL(library.download.url).toURL(), file, library.download.sha1, proxy = dependencyManager.proxy)
+ }
+ }
+
+}
+
+class GameLoggingDownloadTask(private val dependencyManager: DefaultDependencyManager, private val version: Version) : Task() {
+ override val dependencies: MutableCollection = LinkedList()
+ override fun execute() {
+ val logging = version.logging?.get(DownloadType.CLIENT) ?: return
+ val file = dependencyManager.repository.getLoggingObject(version.actualAssetIndex.id, logging)
+ if (!file.exists())
+ dependencies += FileDownloadTask(logging.file.url.toURL(), file, proxy = dependencyManager.proxy)
+ }
+}
+
+class GameAssetIndexDownloadTask(private val dependencyManager: DefaultDependencyManager, private val version: Version) : Task() {
+ override val dependencies: MutableCollection = LinkedList()
+ override fun execute() {
+ val assetIndexInfo = version.actualAssetIndex
+ val assetDir = dependencyManager.repository.getAssetDirectory(assetIndexInfo.id)
+ if (!assetDir.makeDirectory())
+ throw IOException("Cannot create directory: $assetDir")
+
+ val assetIndexFile = dependencyManager.repository.getIndexFile(assetIndexInfo.id)
+ dependencies += FileDownloadTask(dependencyManager.downloadProvider.injectURL(assetIndexInfo.url).toURL(), assetIndexFile, proxy = dependencyManager.proxy)
+ }
+}
+
+class GameAssetRefreshTask(private val dependencyManager: DefaultDependencyManager, private val version: Version) : TaskResult>>() {
+ private val assetIndexTask = GameAssetIndexDownloadTask(dependencyManager, version)
+ private val assetIndexInfo = version.actualAssetIndex
+ private val assetIndexFile = dependencyManager.repository.getIndexFile(assetIndexInfo.id)
+ override val dependents: MutableCollection = LinkedList()
+
+ init {
+ if (!assetIndexFile.exists())
+ dependents += assetIndexTask
+ }
+
+ override fun execute() {
+ val index = GSON.fromJson(assetIndexFile.readText())
+ val res = LinkedList>()
+ var progress = 0
+ index?.objects?.entries?.forEach { (_, assetObject) ->
+ res += Pair(dependencyManager.repository.getAssetObject(assetIndexInfo.id, assetObject), assetObject)
+ updateProgress(++progress, index.objects.size)
+ }
+ result = res
+ }
+}
+
+class GameAssetDownloadTask(private val dependencyManager: DefaultDependencyManager, private val version: Version) : Task() {
+ private val refreshTask = GameAssetRefreshTask(dependencyManager, version)
+ override val dependents: Collection = listOf(refreshTask)
+ override val dependencies: MutableCollection = LinkedList()
+ override fun execute() {
+ val size = refreshTask.result?.size ?: 0
+ refreshTask.result?.forEach single@{ (file, assetObject) ->
+ val url = dependencyManager.downloadProvider.assetBaseURL + assetObject.location
+ if (!file.absoluteFile.parentFile.makeDirectory()) {
+ LOG.severe("Unable to create new file $file, because parent directory cannot be created")
+ return@single
+ }
+ if (file.isDirectory)
+ return@single
+
+ var flag = true
+ var downloaded = 0
+ try {
+ // check the checksum of file to ensure that the file is not need to re-download.
+ if (file.exists()) {
+ val sha1 = DigestUtils.sha1Hex(file.readBytes())
+ if (assetObject.hash == sha1) {
+ ++downloaded
+ LOG.finest("File $file has been downloaded successfully, skipped downloading")
+ updateProgress(downloaded, size)
+ return@single
+ }
+ }
+ } catch (e: IOException) {
+ LOG.log(Level.WARNING, "Unable to get hash code of file $file", e)
+ flag = !file.exists()
+ }
+ if (flag)
+ dependencies += FileDownloadTask(url.toURL(), file, assetObject.hash, proxy = dependencyManager.proxy).apply {
+ title = assetObject.hash
+ }
+ }
+ }
+}
+
+class VersionJSONSaveTask(private val dependencyManager: DefaultDependencyManager, private val version: Version): Task() {
+ override fun execute() {
+ val json = dependencyManager.repository.getVersionJson(version.id).absoluteFile
+ if (!json.makeFile())
+ throw IOException("Cannot create file $json")
+ json.writeText(GSON.toJson(version))
+ }
+}
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameRemoteVersions.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameRemoteVersions.kt
new file mode 100644
index 000000000..934b321ba
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameRemoteVersions.kt
@@ -0,0 +1,69 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import com.google.gson.annotations.SerializedName
+import org.jackhuang.hmcl.game.ReleaseType
+import org.jackhuang.hmcl.util.DEFAULT_LIBRARY_URL
+import org.jackhuang.hmcl.util.Immutable
+import org.jackhuang.hmcl.util.Validation
+import java.util.*
+
+@Immutable
+internal class GameRemoteLatestVersions(
+ @SerializedName("snapshot")
+ val snapshot: String,
+ @SerializedName("release")
+ val release: String
+)
+
+@Immutable
+internal class GameRemoteVersion(
+ @SerializedName("id")
+ val gameVersion: String = "",
+ @SerializedName("time")
+ val time: Date = Date(),
+ @SerializedName("releaseTime")
+ val releaseTime: Date = Date(),
+ @SerializedName("type")
+ val type: ReleaseType = ReleaseType.UNKNOWN,
+ @SerializedName("url")
+ val url: String = "$DEFAULT_LIBRARY_URL$gameVersion/$gameVersion.json"
+) : Validation {
+ override fun validate() {
+ if (gameVersion.isBlank())
+ throw IllegalArgumentException("GameRemoteVersion id cannot be blank")
+ if (url.isBlank())
+ throw IllegalArgumentException("GameRemoteVersion url cannot be blank")
+ }
+}
+
+@Immutable
+internal class GameRemoteVersions(
+ @SerializedName("versions")
+ val versions: List = emptyList(),
+
+ @SerializedName("latest")
+ val latest: GameRemoteLatestVersions? = null
+)
+
+@Immutable
+data class GameRemoteVersionTag (
+ val type: ReleaseType,
+ val time: Date
+)
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameVersionList.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameVersionList.kt
new file mode 100644
index 000000000..735b50744
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameVersionList.kt
@@ -0,0 +1,58 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import org.jackhuang.hmcl.util.GSON
+import org.jackhuang.hmcl.util.asVersion
+import org.jackhuang.hmcl.util.fromJson
+import org.jackhuang.hmcl.task.GetTask
+import org.jackhuang.hmcl.task.Task
+import org.jackhuang.hmcl.util.toURL
+
+
+object GameVersionList : VersionList() {
+
+ override fun refreshAsync(downloadProvider: DownloadProvider): Task {
+ return RefreshTask(downloadProvider)
+ }
+
+ private class RefreshTask(provider: DownloadProvider) : Task() {
+ val task = GetTask(provider.versionListURL.toURL())
+ override val dependents: Collection = listOf(task)
+ override fun execute() {
+ versionMap.clear()
+ versions.clear()
+
+ val root = GSON.fromJson(task.result!!) ?: return
+ for (remoteVersion in root.versions) {
+ val gg = remoteVersion.gameVersion.asVersion() ?: continue
+ val x = RemoteVersion(
+ gameVersion = remoteVersion.gameVersion,
+ selfVersion = remoteVersion.gameVersion,
+ url = remoteVersion.url,
+ tag = GameRemoteVersionTag(
+ type = remoteVersion.type,
+ time = remoteVersion.releaseTime
+ )
+ )
+ versions.add(x)
+ versionMap[gg] = listOf(x)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LiteLoaderInstallTask.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LiteLoaderInstallTask.kt
new file mode 100644
index 000000000..5924d132a
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LiteLoaderInstallTask.kt
@@ -0,0 +1,75 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import org.jackhuang.hmcl.download.game.GameLibrariesTask
+import org.jackhuang.hmcl.game.LibrariesDownloadInfo
+import org.jackhuang.hmcl.game.Library
+import org.jackhuang.hmcl.game.LibraryDownloadInfo
+import org.jackhuang.hmcl.game.Version
+import org.jackhuang.hmcl.task.Task
+import org.jackhuang.hmcl.task.TaskResult
+import org.jackhuang.hmcl.task.then
+import org.jackhuang.hmcl.util.merge
+
+/**
+ * LiteLoader must be installed after Forge.
+ */
+class LiteLoaderInstallTask(private val dependencyManager: DefaultDependencyManager,
+ private val version: Version,
+ private val remoteVersion: String): TaskResult() {
+ private val liteLoaderVersionList = dependencyManager.getVersionList("liteloader") as LiteLoaderVersionList
+ lateinit var remote: RemoteVersion
+ override val dependents: MutableCollection = mutableListOf()
+ override val dependencies: MutableCollection = mutableListOf()
+
+ init {
+ if (version.jar == null)
+ throw IllegalArgumentException()
+ if (!liteLoaderVersionList.loaded)
+ dependents += LiteLoaderVersionList.refreshAsync(dependencyManager.downloadProvider) then {
+ remote = liteLoaderVersionList.getVersion(version.jar, remoteVersion) ?: throw IllegalArgumentException("Remote LiteLoader version ${version.jar}, $remoteVersion not found")
+ null
+ }
+ else {
+ remote = liteLoaderVersionList.getVersion(version.jar, remoteVersion) ?: throw IllegalArgumentException("Remote LiteLoader version ${version.jar}, $remoteVersion not found")
+ }
+ }
+
+ override fun execute() {
+ val library = Library(
+ groupId = "com.mumfrey",
+ artifactId = "liteloader",
+ version = remote.selfVersion,
+ url = "http://dl.liteloader.com/versions/",
+ downloads = LibrariesDownloadInfo(
+ artifact = LibraryDownloadInfo(
+ url = remote.url
+ )
+ )
+ )
+ val tempVersion = version.copy(libraries = merge(remote.tag.libraries, listOf(library)))
+ result = version.copy(
+ mainClass = "net.minecraft.launchwrapper.Launch",
+ minecraftArguments = version.minecraftArguments + " --tweakClass " + remote.tag.tweakClass,
+ libraries = merge(tempVersion.libraries, version.libraries)
+ )
+ dependencies += GameLibrariesTask(dependencyManager, tempVersion)
+ }
+
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LiteLoaderRemote.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LiteLoaderRemote.kt
new file mode 100644
index 000000000..40ad1213e
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LiteLoaderRemote.kt
@@ -0,0 +1,94 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import com.google.gson.annotations.SerializedName
+import org.jackhuang.hmcl.game.Library
+import org.jackhuang.hmcl.util.Immutable
+
+@Immutable
+internal data class LiteLoaderVersionsMeta (
+ @SerializedName("description")
+ val description: String = "",
+ @SerializedName("authors")
+ val authors: String = "",
+ @SerializedName("url")
+ val url: String = ""
+)
+
+@Immutable
+internal data class LiteLoaderRepository (
+ @SerializedName("stream")
+ val stream: String = "",
+ @SerializedName("type")
+ val type: String = "",
+ @SerializedName("url")
+ val url: String = "",
+ @SerializedName("classifier")
+ val classifier: String = ""
+)
+
+@Immutable
+internal class LiteLoaderVersion (
+ @SerializedName("tweakClass")
+ val tweakClass: String = "",
+ @SerializedName("file")
+ val file: String = "",
+ @SerializedName("version")
+ val version: String = "",
+ @SerializedName("md5")
+ val md5: String = "",
+ @SerializedName("timestamp")
+ val timestamp: String = "",
+ @SerializedName("lastSuccessfulBuild")
+ val lastSuccessfulBuild: Int = 0,
+ @SerializedName("libraries")
+ val libraries: Collection = emptyList()
+)
+
+@Immutable
+internal class LiteLoaderBranch (
+ @SerializedName("libraries")
+ val libraries: Collection = emptyList(),
+ @SerializedName("com.mumfrey:liteloader")
+ val liteLoader: Map = emptyMap()
+)
+
+@Immutable
+internal class LiteLoaderGameVersions (
+ @SerializedName("repo")
+ val repo: LiteLoaderRepository? = null,
+ @SerializedName("artefacts")
+ val artifacts: LiteLoaderBranch? = null,
+ @SerializedName("snapshots")
+ val snapshots: LiteLoaderBranch? = null
+)
+
+@Immutable
+internal class LiteLoaderVersionsRoot (
+ @SerializedName("versions")
+ val versions: Map = emptyMap(),
+ @SerializedName("meta")
+ val meta: LiteLoaderVersionsMeta? = null
+)
+
+@Immutable
+data class LiteLoaderRemoteVersionTag (
+ val tweakClass: String,
+ val libraries: Collection
+)
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LiteLoaderVersionList.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LiteLoaderVersionList.kt
new file mode 100644
index 000000000..ff762e4ba
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LiteLoaderVersionList.kt
@@ -0,0 +1,70 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import org.jackhuang.hmcl.util.GSON
+import org.jackhuang.hmcl.util.asVersion
+import org.jackhuang.hmcl.util.fromJson
+import org.jackhuang.hmcl.task.GetTask
+import org.jackhuang.hmcl.task.Task
+import org.jackhuang.hmcl.util.toURL
+
+object LiteLoaderVersionList : VersionList() {
+ @JvmField
+ val LITELOADER_LIST = "http://dl.liteloader.com/versions/versions.json"
+
+ override fun refreshAsync(downloadProvider: DownloadProvider): Task {
+ return RefreshTask(downloadProvider)
+ }
+
+ internal class RefreshTask(val downloadProvider: DownloadProvider) : Task() {
+ val task = GetTask(downloadProvider.injectURL(LITELOADER_LIST).toURL())
+ override val dependents: Collection = listOf(task)
+ override fun execute() {
+ val root = GSON.fromJson(task.result!!) ?: return
+ versions.clear()
+
+ for ((gameVersion, liteLoader) in root.versions.entries) {
+ val gg = gameVersion.asVersion() ?: continue
+ doBranch(gg, gameVersion, liteLoader.repo, liteLoader.artifacts, false)
+ doBranch(gg, gameVersion, liteLoader.repo, liteLoader.snapshots, true)
+ }
+ }
+
+ private fun doBranch(key: String, gameVersion: String, repository: LiteLoaderRepository?, branch: LiteLoaderBranch?, snapshot: Boolean) {
+ if (branch == null || repository == null)
+ return
+ for ((branchName, v) in branch.liteLoader.entries) {
+ if ("latest" == branchName)
+ continue
+ val iv = RemoteVersion(
+ selfVersion = v.version.replace("SNAPSHOT", "SNAPSHOT-" + v.lastSuccessfulBuild),
+ gameVersion = gameVersion,
+ url = if (snapshot)
+ "http://jenkins.liteloader.com/view/$gameVersion/job/LiteLoader $gameVersion/lastSuccessfulBuild/artifact/build/libs/liteloader-${v.version}-release.jar"
+ else
+ downloadProvider.injectURL(repository.url + "com/mumfrey/liteloader/" + gameVersion + "/" + v.file),
+ tag = LiteLoaderRemoteVersionTag(tweakClass = v.tweakClass, libraries = v.libraries)
+ )
+
+ versions.put(key, iv)
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MojangDownloadProvider.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MojangDownloadProvider.kt
new file mode 100644
index 000000000..c75df41a2
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MojangDownloadProvider.kt
@@ -0,0 +1,59 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import org.jackhuang.hmcl.download.forge.ForgeVersionList
+import org.jackhuang.hmcl.download.game.GameVersionList
+import org.jackhuang.hmcl.download.liteloader.LiteLoaderVersionList
+import java.util.*
+
+object MojangDownloadProvider : DownloadProvider() {
+ override val libraryBaseURL: String = "https://libraries.minecraft.net/"
+ override val versionBaseURL: String = "http://s3.amazonaws.com/Minecraft.Download/versions/"
+ override val versionListURL: String = "https://launchermeta.mojang.com/mc/game/version_manifest.json"
+ override val assetIndexBaseURL: String = "http://s3.amazonaws.com/Minecraft.Download/indexes/"
+ override val assetBaseURL: String = "http://resources.download.minecraft.net/"
+
+ override fun getVersionListById(id: String): VersionList<*> {
+ return when(id) {
+ "game" -> GameVersionList
+ "forge" -> ForgeVersionList
+ "liteloader" -> LiteLoaderVersionList
+ "optifine" -> OptiFineVersionList
+ else -> throw IllegalArgumentException("Unrecognized version list id: $id")
+ }
+ }
+
+ override fun injectURL(baseURL: String): String {
+ /**if (baseURL.contains("scala-swing") || baseURL.contains("scala-xml") || baseURL.contains("scala-parser-combinators"))
+ return baseURL.replace("http://files.minecraftforge.net/maven", "http://ftb.cursecdn.com/FTB2/maven/");
+ else if (baseURL.contains("typesafe") || baseURL.contains("scala"))
+ if (Locale.getDefault() == Locale.CHINA)
+ return baseURL.replace("http://files.minecraftforge.net/maven", "http://maven.aliyun.com/nexus/content/groups/public");
+ else
+ return baseURL.replace("http://files.minecraftforge.net/maven", "http://repo1.maven.org/maven2");
+ else
+ return baseURL; */
+ if (baseURL.endsWith("net/minecraftforge/forge/json"))
+ return baseURL
+ else if (Locale.getDefault() == Locale.CHINA)
+ return baseURL.replace("http://files.minecraftforge.net/maven", "http://maven.aliyun.com/nexus/content/groups/public");
+ else
+ return baseURL.replace("http://files.minecraftforge.net/maven", "http://ftb.cursecdn.com/FTB2/maven")
+ }
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/OptiFineBMCLVersionList.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/OptiFineBMCLVersionList.kt
new file mode 100644
index 000000000..29f495256
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/OptiFineBMCLVersionList.kt
@@ -0,0 +1,66 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import org.jackhuang.hmcl.task.GetTask
+import org.jackhuang.hmcl.task.Task
+import org.jackhuang.hmcl.util.GSON
+import org.jackhuang.hmcl.util.asVersion
+import org.jackhuang.hmcl.util.toURL
+import org.jackhuang.hmcl.util.typeOf
+import java.util.TreeSet
+
+object OptiFineBMCLVersionList : VersionList() {
+ override fun refreshAsync(downloadProvider: DownloadProvider): Task {
+ return RefreshTask(downloadProvider)
+ }
+
+ private class RefreshTask(val downloadProvider: DownloadProvider): Task() {
+ val task = GetTask("http://bmclapi.bangbang93.com/optifine/versionlist".toURL())
+ override val dependents: Collection = listOf(task)
+ override fun execute() {
+ versionMap.clear()
+ versions.clear()
+
+ val duplicates = mutableSetOf()
+ val root = GSON.fromJson>(task.result!!, typeOf>())
+ for (element in root) {
+ val version = element.type ?: continue
+ val mirror = "http://bmclapi2.bangbang93.com/optifine/${element.gameVersion}/${element.type}/${element.patch}"
+ if (duplicates.contains(mirror))
+ continue
+ else
+ duplicates += mirror
+
+ val gameVersion = element.gameVersion?.asVersion() ?: continue
+ val remoteVersion = RemoteVersion(
+ gameVersion = gameVersion,
+ selfVersion = version,
+ url = mirror,
+ tag = Unit
+ )
+
+ val set = versionMap.getOrPut(gameVersion, { TreeSet>() }) as MutableCollection>
+ set.add(remoteVersion)
+ versions.add(remoteVersion)
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/OptiFineDownloadFormatter.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/OptiFineDownloadFormatter.kt
new file mode 100644
index 000000000..fd3d2fa12
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/OptiFineDownloadFormatter.kt
@@ -0,0 +1,21 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+
+object Opt
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/OptiFineInstallTask.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/OptiFineInstallTask.kt
new file mode 100644
index 000000000..b722afa08
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/OptiFineInstallTask.kt
@@ -0,0 +1,78 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import org.jackhuang.hmcl.download.game.GameLibrariesTask
+import org.jackhuang.hmcl.game.LibrariesDownloadInfo
+import org.jackhuang.hmcl.game.Library
+import org.jackhuang.hmcl.game.LibraryDownloadInfo
+import org.jackhuang.hmcl.game.Version
+import org.jackhuang.hmcl.task.Task
+import org.jackhuang.hmcl.task.TaskResult
+import org.jackhuang.hmcl.task.then
+import org.jackhuang.hmcl.util.merge
+
+class OptiFineInstallTask(private val dependencyManager: DefaultDependencyManager,
+ private val version: Version,
+ private val remoteVersion: String): TaskResult() {
+ private val optiFineVersionList = dependencyManager.getVersionList("optifine")
+ lateinit var remote: RemoteVersion<*>
+ override val dependents: MutableCollection = mutableListOf()
+ override val dependencies: MutableCollection = mutableListOf()
+
+ init {
+ if (version.jar == null)
+ throw IllegalArgumentException()
+ if (!optiFineVersionList.loaded)
+ dependents += optiFineVersionList.refreshAsync(dependencyManager.downloadProvider) then {
+ remote = optiFineVersionList.getVersion(version.jar, remoteVersion) ?: throw IllegalArgumentException("Remote LiteLoader version $remoteVersion not found")
+ null
+ }
+ else {
+ remote = optiFineVersionList.getVersion(version.jar, remoteVersion) ?: throw IllegalArgumentException("Remote LiteLoader version $remoteVersion not found")
+ }
+ }
+ override fun execute() {
+ val library = Library(
+ groupId = "net.optifine",
+ artifactId = "optifine",
+ version = remoteVersion,
+ lateload = true,
+ downloads = LibrariesDownloadInfo(
+ artifact = LibraryDownloadInfo(
+ path = "net/optifine/optifine/$remoteVersion/optifine-$remoteVersion.jar",
+ url = remote.url
+ )
+ ))
+ val libraries = mutableListOf(library)
+ var arg = version.minecraftArguments!!
+ if (!arg.contains("FMLTweaker"))
+ arg += " --tweakClass optifine.OptiFineTweaker"
+ var mainClass = version.mainClass
+ if (mainClass == null || !mainClass.startsWith("net.minecraft.launchwrapper.")) {
+ mainClass = "net.minecraft.launchwrapper.Launch"
+ libraries.add(0, Library(
+ groupId = "net.minecraft",
+ artifactId = "launchwrapper",
+ version = "1.12"
+ ))
+ }
+ result = version.copy(libraries = merge(version.libraries, libraries), mainClass = mainClass, minecraftArguments = arg)
+ dependencies += GameLibrariesTask(dependencyManager, version.copy(libraries = libraries))
+ }
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/OptiFineRemotes.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/OptiFineRemotes.kt
new file mode 100644
index 000000000..98dcc2784
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/OptiFineRemotes.kt
@@ -0,0 +1,37 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import com.google.gson.annotations.SerializedName
+
+data class OptiFineVersion (
+ @SerializedName("dl")
+ val downloadLink: String? = null,
+ @SerializedName("ver")
+ val version: String? = null,
+ @SerializedName("date")
+ val date: String? = null,
+ @SerializedName("type")
+ val type: String? = null,
+ @SerializedName("patch")
+ val patch: String? = null,
+ @SerializedName("mirror")
+ val mirror: String? = null,
+ @SerializedName("mcversion")
+ val gameVersion: String? = null
+)
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/OptiFineVersionList.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/OptiFineVersionList.kt
new file mode 100644
index 000000000..e8cb524e6
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/OptiFineVersionList.kt
@@ -0,0 +1,82 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import org.jackhuang.hmcl.task.GetTask
+import org.jackhuang.hmcl.task.Task
+import org.jackhuang.hmcl.util.toURL
+import org.w3c.dom.Element
+import java.io.ByteArrayInputStream
+import javax.xml.parsers.DocumentBuilderFactory
+import java.util.regex.Pattern
+
+object OptiFineVersionList : VersionList() {
+ private val pattern = Pattern.compile("OptiFine (.*?) ")
+
+ override fun refreshAsync(downloadProvider: DownloadProvider): Task {
+ return RefreshTask(downloadProvider)
+ }
+
+ private class RefreshTask(val downloadProvider: DownloadProvider) : Task() {
+ val task = GetTask("http://optifine.net/downloads".toURL())
+ override val dependents: Collection = listOf(task)
+ override fun execute() {
+ versions.clear()
+
+ val html = task.result!!.replace(" ", " ").replace(">", ">").replace("<", "<").replace(" ", " ")
+
+ val factory = DocumentBuilderFactory.newInstance()
+ val db = factory.newDocumentBuilder()
+ val doc = db.parse(ByteArrayInputStream(html.toByteArray()))
+ val r = doc.documentElement
+ val tables = r.getElementsByTagName("table")
+ for (i in 0..tables.length - 1) {
+ val e = tables.item(i) as Element
+ if ("downloadTable" == e.getAttribute("class")) {
+ val tr = e.getElementsByTagName("tr")
+ for (k in 0..tr.length - 1) {
+ val downloadLine = (tr.item(k) as Element).getElementsByTagName("td")
+ var url: String? = null
+ var version: String? = null
+ for (j in 0..downloadLine.length - 1) {
+ val td = downloadLine.item(j) as Element
+ if (td.getAttribute("class")?.startsWith("downloadLineMirror") ?: false)
+ url = (td.getElementsByTagName("a").item(0) as Element).getAttribute("href")
+ if (td.getAttribute("class")?.startsWith("downloadLineFile") ?: false)
+ version = td.textContent
+ }
+ val matcher = pattern.matcher(version)
+ var gameVersion: String? = null
+ while (matcher.find())
+ gameVersion = matcher.group(1)
+ if (gameVersion == null || version == null || url == null) continue
+ val remoteVersion = RemoteVersion(
+ gameVersion = gameVersion,
+ selfVersion = version,
+ url = url,
+ tag = Unit
+ )
+ versions.put(gameVersion, remoteVersion)
+ }
+ }
+ }
+
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.kt
new file mode 100644
index 000000000..be0391629
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.kt
@@ -0,0 +1,50 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import org.jackhuang.hmcl.util.VersionNumber
+import java.util.*
+import kotlin.Comparator
+
+data class RemoteVersion (
+ val gameVersion: String,
+ val selfVersion: String,
+ /**
+ * The file of remote version, may be an installer or an universal jar.
+ */
+ val url: String,
+ val tag: T
+): Comparable> {
+ override fun hashCode(): Int {
+ return selfVersion.hashCode()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is RemoteVersion<*> && Objects.equals(this.selfVersion, other.selfVersion)
+ }
+
+ override fun compareTo(other: RemoteVersion): Int {
+ return -selfVersion.compareTo(other.selfVersion)
+ }
+
+ companion object RemoteVersionComparator: Comparator> {
+ override fun compare(o1: RemoteVersion<*>, o2: RemoteVersion<*>): Int {
+ return -VersionNumber.asVersion(o1.selfVersion).compareTo(VersionNumber.asVersion(o2.selfVersion))
+ }
+ }
+}
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/VersionList.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/VersionList.kt
new file mode 100644
index 000000000..e013dbadd
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/VersionList.kt
@@ -0,0 +1,54 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.download
+
+import org.jackhuang.hmcl.task.Task
+import org.jackhuang.hmcl.util.SimpleMultimap
+import java.util.*
+import kotlin.collections.HashMap
+
+abstract class VersionList {
+ @JvmField
+ protected val versions = SimpleMultimap>(::HashMap, ::TreeSet)
+
+ val loaded = versions.isNotEmpty
+
+ abstract fun refreshAsync(downloadProvider: DownloadProvider): Task
+
+ protected open fun getVersionsImpl(gameVersion: String): Collection> {
+ return versions[gameVersion] ?: versions.values
+ }
+
+ /**
+ * @param gameVersion the Minecraft version that remote versions belong to
+ * @return the collection of specific remote versions
+ */
+ fun getVersions(gameVersion: String): Collection> {
+ return Collections.unmodifiableCollection(getVersionsImpl(gameVersion))
+ }
+
+ fun getVersion(gameVersion: String, remoteVersion: String): RemoteVersion? {
+ var result : RemoteVersion? = null
+ versions[gameVersion]?.forEach {
+ if (it.selfVersion == remoteVersion)
+ result = it
+ }
+ return result
+ }
+
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/Event.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/Event.kt
new file mode 100644
index 000000000..c8e1a751e
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/Event.kt
@@ -0,0 +1,60 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.event
+
+import java.util.*
+
+open class Event(source: Any) : EventObject(source) {
+ /**
+ * true if this event is canceled.
+ *
+ * @throws UnsupportedOperationException if trying to cancel a non-cancelable event.
+ */
+ var isCanceled = false
+ set(value) {
+ if (!isCancelable)
+ throw UnsupportedOperationException("Attempted to cancel a non-cancelable event: ${this.javaClass}")
+ field = value
+ }
+
+ /**
+ * Retutns the value set as the result of this event
+ */
+ var result = Result.DEFAULT
+ set(value) {
+ if (!hasResult)
+ throw UnsupportedOperationException("Attempted to set result on a no result event: ${this.javaClass} of type.")
+ field = value
+ }
+
+ /**
+ * true if this Event this cancelable.
+ */
+ open val isCancelable = false
+
+ /**
+ * true if this event has a significant result.
+ */
+ open val hasResult = false
+
+ enum class Result {
+ DENY,
+ DEFAULT,
+ ALLOW
+ }
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventBus.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventBus.kt
new file mode 100644
index 000000000..e8c21a085
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventBus.kt
@@ -0,0 +1,38 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.event
+
+import java.util.*
+
+class EventBus {
+ val events = HashMap, EventManager<*>>()
+
+ @Suppress("UNCHECKED_CAST")
+ fun channel(classOfT: Class): EventManager {
+ if (!events.containsKey(classOfT))
+ events.put(classOfT, EventManager())
+ return events[classOfT] as EventManager
+ }
+
+ inline fun channel() = channel(T::class.java)
+
+ fun fireEvent(obj: EventObject) {
+ channel(obj.javaClass).fireEvent(obj)
+ }
+
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventManager.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventManager.kt
new file mode 100644
index 000000000..f0612b4ff
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventManager.kt
@@ -0,0 +1,46 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.event
+
+import java.util.*
+
+class EventManager {
+ private val handlers = EnumMap Unit>>(EventPriority::class.java).apply {
+ for (value in EventPriority.values())
+ put(value, LinkedList<(T) -> Unit>())
+ }
+
+ fun register(func: (T) -> Unit, priority: EventPriority = EventPriority.NORMAL) {
+ if (!handlers[priority]!!.contains(func))
+ handlers[priority]!!.add(func)
+ }
+
+ fun unregister(func: (T) -> Unit) {
+ EventPriority.values().forEach { handlers[it]!!.remove(func) }
+ }
+
+ fun fireEvent(event: T) {
+ for (priority in EventPriority.values())
+ for (handler in handlers[priority]!!)
+ handler(event)
+ }
+
+ operator fun plusAssign(func: (T) -> Unit) = register(func)
+ operator fun minusAssign(func: (T) -> Unit) = unregister(func)
+ operator fun invoke(event: T) = fireEvent(event)
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventPriority.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventPriority.kt
new file mode 100644
index 000000000..c6beb68d9
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventPriority.kt
@@ -0,0 +1,26 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.event
+
+enum class EventPriority {
+ HIGHEST,
+ HIGH,
+ NORMAL,
+ LOW,
+ LOWEST
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/ResultEvent.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/ResultEvent.kt
new file mode 100644
index 000000000..87a8e58c8
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/ResultEvent.kt
@@ -0,0 +1,21 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.event
+
+open class FailedEvent(source: Any, val failedTime: Int, var newResult: T) : Event(source) {
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/AssetIndex.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/AssetIndex.kt
new file mode 100644
index 000000000..6cc40de8c
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/AssetIndex.kt
@@ -0,0 +1,33 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.game
+
+import com.google.gson.annotations.SerializedName
+import java.util.*
+
+class AssetIndex(
+ @SerializedName("virtual")
+ val virtual: Boolean = false,
+ objects: Map = emptyMap()
+) {
+ val objects: Map
+ get() = Collections.unmodifiableMap(objectsImpl)
+
+ @SerializedName("objects")
+ private val objectsImpl: MutableMap = HashMap(objects)
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/AssetIndexInfo.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/AssetIndexInfo.kt
new file mode 100644
index 000000000..35878df33
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/AssetIndexInfo.kt
@@ -0,0 +1,30 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.game
+
+import jdk.nashorn.internal.ir.annotations.Immutable
+import java.net.URL
+
+@Immutable
+class AssetIndexInfo(
+ url: String = "",
+ sha1: String? = null,
+ size: Int = 0,
+ id: String = "",
+ val totalSize: Long = 0
+) : IdDownloadInfo(url, sha1, size, id)
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/AssetObject.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/AssetObject.kt
new file mode 100644
index 000000000..cf550bc08
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/AssetObject.kt
@@ -0,0 +1,32 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.game
+
+import org.jackhuang.hmcl.util.Validation
+
+data class AssetObject(
+ val hash: String = "",
+ val size: Long = 0
+): Validation {
+ val location: String
+ get() = hash.substring(0, 2) + "/" + hash
+
+ override fun validate() {
+ check(hash.isNotBlank(), { "AssetObject hash cannot be blank." })
+ }
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/CircleDependencyException.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/CircleDependencyException.kt
new file mode 100644
index 000000000..5ae2b45aa
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/CircleDependencyException.kt
@@ -0,0 +1,28 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.game
+
+/**
+ * What's circle dependency?
+ * When C inherits from B, and B inherits from something else, and finally inherits from C again.
+ */
+class CircleDependencyException : Exception {
+ constructor() : super() {}
+ constructor(message: String) : super(message) {}
+ constructor(message: String, cause: Throwable) : super(message, cause) {}
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/ClassicVersion.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/ClassicVersion.kt
new file mode 100644
index 000000000..284802208
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/ClassicVersion.kt
@@ -0,0 +1,53 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.game
+
+import org.jackhuang.hmcl.util.Immutable
+import java.io.File
+
+@Immutable
+class ClassicVersion : Version(
+ mainClass = "net.minecraft.client.Minecraft",
+ id = "Classic",
+ type = ReleaseType.UNKNOWN,
+ minecraftArguments = "\${auth_player_name} \${auth_session} --workDir \${game_directory}",
+ libraries = listOf(
+ ClassicLibrary("lwjgl"),
+ ClassicLibrary("jinput"),
+ ClassicLibrary("lwjgl_util")
+ )
+) {
+ @Immutable
+ private class ClassicLibrary(name: String) :
+ Library(groupId = "", artifactId = "", version = "",
+ downloads = LibrariesDownloadInfo(
+ artifact = LibraryDownloadInfo(path = "bin/$name.jar")
+ )
+ )
+
+ companion object {
+ fun hasClassicVersion(baseDirectory: File): Boolean {
+ val file = File(baseDirectory, "bin")
+ if (!file.exists()) return false
+ if (!File(file, "lwjgl.jar").exists()) return false
+ if (!File(file, "jinput.jar").exists()) return false
+ if (!File(file, "lwjgl_util.jar").exists()) return false
+ return true
+ }
+ }
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/CompatibilityRule.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/CompatibilityRule.kt
new file mode 100644
index 000000000..46f930496
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/CompatibilityRule.kt
@@ -0,0 +1,77 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.game
+
+import com.google.gson.annotations.SerializedName
+import org.jackhuang.hmcl.util.Immutable
+import org.jackhuang.hmcl.util.OS
+import org.jackhuang.hmcl.util.ignoreThrowable
+import java.util.regex.Pattern
+
+@Immutable
+data class CompatibilityRule(
+ val action: Action = CompatibilityRule.Action.ALLOW,
+ val os: OSRestriction? = null
+) {
+
+ val appliedAction: Action? get() = if (os != null && !os.allow()) null else action
+
+ companion object {
+ fun appliesToCurrentEnvironment(rules: Collection?): Boolean {
+ if (rules == null)
+ return true
+ var action = CompatibilityRule.Action.DISALLOW
+ for (rule in rules) {
+ val thisAction = rule.appliedAction
+ if (thisAction != null) action = thisAction
+ }
+ return action == CompatibilityRule.Action.ALLOW
+ }
+ }
+
+ enum class Action {
+ @SerializedName("allow")
+ ALLOW,
+ @SerializedName("disallow")
+ DISALLOW
+ }
+
+ @Immutable
+ data class OSRestriction(
+ val name: OS = OS.UNKNOWN,
+ val version: String? = null,
+ val arch: String? = null
+ ) {
+ fun allow(): Boolean {
+ if (name != OS.UNKNOWN && name != OS.CURRENT_OS)
+ return false
+ if (version != null) {
+ ignoreThrowable {
+ if (!Pattern.compile(version).matcher(OS.SYSTEM_VERSION).matches())
+ return false
+ }
+ }
+ if (arch != null)
+ ignoreThrowable {
+ if (!Pattern.compile(arch).matcher(OS.SYSTEM_ARCH).matches())
+ return false
+ }
+ return true
+ }
+ }
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.kt
new file mode 100644
index 000000000..ceadcd463
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.kt
@@ -0,0 +1,203 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.game
+
+import com.google.gson.JsonSyntaxException
+import org.jackhuang.hmcl.util.GSON
+import org.jackhuang.hmcl.util.LOG
+import org.jackhuang.hmcl.util.fromJson
+import org.jackhuang.hmcl.util.listFilesByExtension
+import java.io.File
+import java.io.IOException
+import java.util.*
+import java.util.logging.Level
+
+open class DefaultGameRepository(val baseDirectory: File): GameRepository {
+ protected val versions: MutableMap = TreeMap()
+
+ override fun hasVersion(id: String) = versions.containsKey(id)
+ override fun getVersion(id: String): Version {
+ return versions[id] ?: throw VersionNotFoundException("Version '$id' does not exist.")
+ }
+ override fun getVersionCount() = versions.size
+ override fun getVersions() = versions.values
+ override fun getLibraryFile(id: Version, lib: Library) = File(baseDirectory, "libraries/${lib.path}")
+ override fun getRunDirectory(id: String) = baseDirectory
+ override fun getVersionJar(version: Version): File {
+ val v = version.resolve(this)
+ val id = v.id
+ return getVersionRoot(id).resolve("$id.jar")
+ }
+ override fun getNativeDirectory(id: String) = File(getVersionRoot(id), "$id-natives")
+ open fun getVersionRoot(id: String) = File(baseDirectory, "versions/$id")
+ open fun getVersionJson(id: String) = File(getVersionRoot(id), "$id.json")
+ open fun readVersionJson(id: String): Version? = readVersionJson(getVersionJson(id))
+ @Throws(IOException::class, JsonSyntaxException::class, VersionNotFoundException::class)
+ open fun readVersionJson(file: File): Version? = GSON.fromJson(file.readText())
+ override fun renameVersion(from: String, to: String): Boolean {
+ try {
+ val fromVersion = getVersion(from)
+ val fromDir = getVersionRoot(from)
+ val toDir = getVersionRoot(to)
+ if (!fromDir.renameTo(toDir))
+ return false
+ val toJson = File(toDir, "$to.json")
+ val toJar = File(toDir, "$to.jar")
+ if (!File(toDir, "$from.json").renameTo(toJson) ||
+ !File(toDir, "$from.jar").renameTo(toJar)) {
+ toDir.renameTo(fromDir) // simple recovery
+ return false
+ }
+ toJson.writeText(GSON.toJson(fromVersion.copy(id = to)))
+ return true
+ } catch (e: IOException) {
+ return false
+ } catch (e2: JsonSyntaxException) {
+ return false
+ } catch (e: VersionNotFoundException) {
+ return false
+ }
+ }
+ open fun removeVersionFromDisk(name: String): Boolean {
+ val file = getVersionRoot(name)
+ if (!file.exists())
+ return true
+ versions.remove(name)
+ return file.deleteRecursively()
+ }
+
+
+ @Synchronized
+ override fun refreshVersions() {
+ versions.clear()
+
+ if (ClassicVersion.hasClassicVersion(baseDirectory)) {
+ val version = ClassicVersion()
+ versions[version.id] = version
+ }
+
+ baseDirectory.resolve("versions").listFiles()?.filter { it.isDirectory }?.forEach tryVersion@{ dir ->
+ val id = dir.name
+ val json = dir.resolve("$id.json")
+
+ // If user renamed the json file by mistake or created the json file in a wrong name,
+ // we will find the only json and rename it to correct name.
+ if (!json.exists()) {
+ val jsons = dir.listFilesByExtension("json")
+ if (jsons.size == 1)
+ jsons.single().renameTo(json)
+ }
+
+ var version: Version
+ try {
+ version = readVersionJson(json)!!
+ } catch(e: Exception) { // JsonSyntaxException or IOException or NullPointerException(!!)
+ // TODO: auto making up for the missing json
+ // TODO: and even asking for removing the redundant version folder.
+ return@tryVersion
+ }
+
+ if (id != version.id) {
+ version = version.copy(id = id)
+ try {
+ json.writeText(GSON.toJson(version))
+ } catch(e: Exception) {
+ LOG.warning("Ignoring version $id because wrong id ${version.id} is set and cannot correct it.")
+ return@tryVersion
+ }
+ }
+
+ versions[id] = version
+ }
+ }
+
+ override fun getAssetIndex(assetId: String): AssetIndex {
+ return GSON.fromJson(getIndexFile(assetId).readText())
+ }
+
+ override fun getActualAssetDirectory(assetId: String): File {
+ try {
+ return reconstructAssets(assetId)
+ } catch (e: IOException) {
+ LOG.log(Level.SEVERE, "Unable to reconstruct asset directory", e)
+ return getAssetDirectory(assetId)
+ }
+ }
+
+ override fun getAssetDirectory(assetId: String): File =
+ baseDirectory.resolve("assets")
+
+ @Throws(IOException::class)
+ override fun getAssetObject(assetId: String, name: String): File {
+ try {
+ return getAssetObject(assetId, getAssetIndex(assetId).objects["name"]!!)
+ } catch (e: Exception) {
+ throw IOException("Asset index file malformed", e)
+ }
+ }
+
+ override fun getAssetObject(assetId: String, obj: AssetObject): File =
+ getAssetObject(getAssetDirectory(assetId), obj)
+
+ open fun getAssetObject(assetDir: File, obj: AssetObject): File {
+ return assetDir.resolve("objects/${obj.location}")
+ }
+
+ open fun getIndexFile(assetId: String): File =
+ getAssetDirectory(assetId).resolve("indexes/$assetId.json")
+
+ override fun getLoggingObject(assetId: String, loggingInfo: LoggingInfo): File =
+ getAssetDirectory(assetId).resolve("log_configs/${loggingInfo.file.id}")
+
+ @Throws(IOException::class, JsonSyntaxException::class)
+ protected open fun reconstructAssets(assetId: String): File {
+ val assetsDir = getAssetDirectory(assetId)
+ val assetVersion = assetId
+ val indexFile: File = getIndexFile(assetVersion)
+ val virtualRoot = assetsDir.resolve("virtual").resolve(assetVersion)
+
+ if (!indexFile.isFile) {
+ return assetsDir
+ }
+
+ val assetIndexContent = indexFile.readText()
+ val index = GSON.fromJson(assetIndexContent, AssetIndex::class.java) ?: return assetsDir
+
+ if (index.virtual) {
+ var cnt = 0
+ LOG.info("Reconstructing virtual assets folder at " + virtualRoot)
+ val tot = index.objects.entries.size
+ for ((location, assetObject) in index.objects.entries) {
+ val target = File(virtualRoot, location)
+ val original = getAssetObject(assetsDir, assetObject)
+ if (original.exists()) {
+ cnt++
+ if (!target.isFile)
+ original.copyTo(target)
+ }
+ }
+ // If the scale new format existent file is lower then 0.1, use the old format.
+ if (cnt * 10 < tot)
+ return assetsDir
+ else
+ return virtualRoot
+ }
+
+ return assetsDir
+ }
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DownloadInfo.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DownloadInfo.kt
new file mode 100644
index 000000000..1542fb253
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DownloadInfo.kt
@@ -0,0 +1,54 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.game
+
+import com.google.gson.JsonSyntaxException
+import com.google.gson.annotations.SerializedName
+import org.jackhuang.hmcl.util.Immutable
+import org.jackhuang.hmcl.util.Validation
+
+@Immutable
+open class DownloadInfo(
+ @SerializedName("url")
+ val url: String = "",
+ @SerializedName("sha1")
+ val sha1: String? = null,
+ @SerializedName("size")
+ val size: Int = 0
+): Validation {
+ override fun validate() {
+ if (url.isBlank())
+ throw JsonSyntaxException("DownloadInfo url can not be null")
+ }
+}
+
+@Immutable
+open class IdDownloadInfo(
+ url: String = "",
+ sha1: String? = null,
+ size: Int = 0,
+ @SerializedName("id")
+ val id: String = ""
+): DownloadInfo(url, sha1, size) {
+ override fun validate() {
+ super.validate()
+
+ if (id.isBlank())
+ throw JsonSyntaxException("IdDownloadInfo id can not be null")
+ }
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DownloadType.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DownloadType.kt
new file mode 100644
index 000000000..6c475f361
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DownloadType.kt
@@ -0,0 +1,29 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.game
+
+import com.google.gson.annotations.SerializedName
+
+enum class DownloadType {
+ @SerializedName("client")
+ CLIENT,
+ @SerializedName("server")
+ SERVER,
+ @SerializedName("windows-server")
+ WINDOWS_SERVER
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/ExtractRules.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/ExtractRules.kt
new file mode 100644
index 000000000..bbeaaadfc
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/ExtractRules.kt
@@ -0,0 +1,32 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.game
+
+import com.google.gson.annotations.SerializedName
+import java.util.*
+
+class ExtractRules(exclude: List = emptyList()) {
+ val exclude: List get() = Collections.unmodifiableList(excludeImpl)
+
+ @SerializedName("exclude")
+ private val excludeImpl: MutableList = LinkedList(exclude)
+
+ fun shouldExtract(path: String): Boolean =
+ exclude.filter { path.startsWith(it) }.isEmpty()
+
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameException.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameException.kt
new file mode 100644
index 000000000..fd4b104e7
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameException.kt
@@ -0,0 +1,24 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.game
+
+class GameException : Exception {
+ constructor() : super() {}
+ constructor(message: String) : super(message) {}
+ constructor(message: String, cause: Throwable) : super(message, cause) {}
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.kt
new file mode 100644
index 000000000..0cca97320
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.kt
@@ -0,0 +1,160 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.game
+
+import java.io.File
+
+/**
+ * Supports operations on versioning.
+ *
+ * Note that game repository will not do any tasks that connect to Internet.
+ */
+interface GameRepository : VersionProvider {
+ /**
+ * Does the version of id exist?
+ * @param id the id of version
+ * @return true if the version exists
+ */
+ override fun hasVersion(id: String): Boolean
+
+ /**
+ * Get the version
+ * @param id the id of version
+ * @return the version you want
+ */
+ override fun getVersion(id: String): Version
+
+ /**
+ * How many version are there?
+ */
+ fun getVersionCount(): Int
+
+ /**
+ * Gets the collection of versions
+ * @return the collection of versions
+ */
+ fun getVersions(): Collection
+
+ /**
+ * Load version list.
+ *
+ * This method should be called before launching a version.
+ * A time-costly operation.
+ * You'd better execute this method in a new thread.
+ */
+ fun refreshVersions()
+
+ /**
+ * Gets the current running directory of the given version for game.
+ * @param id the version id
+ */
+ fun getRunDirectory(id: String): File
+
+ /**
+ * Get the library file in disk.
+ * This method allows versions and libraries that are not loaded by this game repository.
+ *
+ * @param id version id
+ * @param lib the library, [Version.libraries]
+ * @return the library file
+ */
+ fun getLibraryFile(id: Version, lib: Library): File
+
+ /**
+ * Get the directory that native libraries will be unzipped to.
+ *
+ * You'd better return a unique directory.
+ * Or if it returns a temporary directory, [org.jackhuang.hmcl.launch.Launcher.makeLaunchScript] will fail.
+ * If you do want to return a temporary directory, make [org.jackhuang.hmcl.launch.Launcher.makeLaunchScript] always fail([UnsupportedOperationException]) and not to use it.
+ *
+ * @param id version id
+ * @return the native directory
+ */
+ fun getNativeDirectory(id: String): File
+
+ /**
+ * Get minecraft jar
+ *
+ * @param version resolvedVersion
+ * @return the minecraft jar
+ */
+ fun getVersionJar(version: Version): File
+
+ /**
+ * Rename given version to new name.
+ *
+ * @param from The id of original version
+ * @param to The new id of the version
+ * @throws UnsupportedOperationException if this game repository does not support renaming a version
+ * @throws java.io.IOException if I/O operation fails.
+ * @return true if the operation is done successfully.
+ */
+ fun renameVersion(from: String, to: String): Boolean
+
+ /**
+ * Get actual asset directory.
+ * Will reconstruct assets or do some blocking tasks if necessary.
+ * You'd better create a new thread to invoke this method.
+ *
+ * @param assetId the asset id, [AssetIndexInfo.id] [Version.assets]
+ * @throws java.io.IOException if I/O operation fails.
+ * @return the actual asset directory
+ */
+ fun getActualAssetDirectory(assetId: String): File
+
+ /**
+ * Get the asset directory according to the asset id.
+ */
+ fun getAssetDirectory(assetId: String): File
+
+ /**
+ * Get the file that given asset object refers to
+ *
+ * @param assetId the asset id, [AssetIndexInfo.id] [Version.assets]
+ * @param name the asset object name, [AssetIndex.objects.key]
+ * @throws java.io.IOException if I/O operation fails.
+ * @return the file that given asset object refers to
+ */
+ fun getAssetObject(assetId: String, name: String): File
+
+ /**
+ * Get the file that given asset object refers to
+ *
+ * @param assetId the asset id, [AssetIndexInfo.id] [Version.assets]
+ * @param obj the asset object, [AssetIndex.objects]
+ * @return the file that given asset object refers to
+ */
+ fun getAssetObject(assetId: String, obj: AssetObject): File
+
+ /**
+ * Get asset index that assetId represents
+ *
+ * @param assetId the asset id, [AssetIndexInfo.id] [Version.assets]
+ * @return the asset index
+ */
+ fun getAssetIndex(assetId: String): AssetIndex
+
+ /**
+ * Get logging object
+ *
+ * @param assetId the asset id, [AssetIndexInfo.id] [Version.assets]
+ * @param loggingInfo the logging info
+ * @return the file that loggingInfo refers to
+ */
+ fun getLoggingObject(assetId: String, loggingInfo: LoggingInfo): File
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchOptions.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchOptions.kt
new file mode 100644
index 000000000..97df046f3
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchOptions.kt
@@ -0,0 +1,128 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.game
+
+import org.jackhuang.hmcl.util.JavaVersion
+import java.io.File
+import java.io.Serializable
+
+data class LaunchOptions(
+ /**
+ * The game directory
+ */
+ val gameDir: File,
+
+ /**
+ * The Java Environment that Minecraft runs on.
+ */
+ val java: JavaVersion = JavaVersion.fromCurrentEnvironment(),
+
+ /**
+ * Will shown in the left bottom corner of the main menu of Minecraft.
+ * null if use the id of launch version.
+ */
+ val versionName: String? = null,
+
+ /**
+ * Don't know what the hell this is.
+ */
+ var profileName: String? = null,
+
+ /**
+ * User custom additional minecraft command line arguments.
+ */
+ val minecraftArgs: String? = null,
+
+ /**
+ * User custom additional java virtual machine command line arguments.
+ */
+ val javaArgs: String? = null,
+
+ /**
+ * The minimum memory that the JVM can allocate.
+ */
+ val minMemory: Int? = null,
+
+ /**
+ * The maximum memory that the JVM can allocate.
+ */
+ val maxMemory: Int? = null,
+
+ /**
+ * The maximum metaspace memory that the JVM can allocate.
+ * For Java 7 -XX:PermSize and Java 8 -XX:MetaspaceSize
+ * Containing class instances.
+ */
+ val metaspace: Int? = null,
+
+ /**
+ * The initial game window width
+ */
+ val width: Int? = null,
+
+ /**
+ * The initial game window height
+ */
+ val height: Int? = null,
+
+ /**
+ * Is inital game window fullscreen.
+ */
+ val fullscreen: Boolean = false,
+
+ /**
+ * The server ip that will connect to when enter game main menu.
+ */
+ val serverIp: String? = null,
+
+ /**
+ * i.e. optirun
+ */
+ val wrapper: String? = null,
+
+ /**
+ * The host of the proxy address
+ */
+ val proxyHost: String? = null,
+
+ /**
+ * the port of the proxy address.
+ */
+ val proxyPort: String? = null,
+
+ /**
+ * The user name of the proxy, optional.
+ */
+ val proxyUser: String? = null,
+
+ /**
+ * The password of the proxy, optional
+ */
+ val proxyPass: String? = null,
+
+ /**
+ * Prevent game launcher from generating default JVM arguments like max memory.
+ */
+ val noGeneratedJVMArgs: Boolean = false,
+
+ /**
+ * Called command line before launching the game.
+ */
+ val precalledCommand: String? = null
+
+) : Serializable
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LibrariesDownloadInfo.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LibrariesDownloadInfo.kt
new file mode 100644
index 000000000..0ee688ec0
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LibrariesDownloadInfo.kt
@@ -0,0 +1,38 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.game
+
+import com.google.gson.annotations.SerializedName
+import org.jackhuang.hmcl.util.Immutable
+import java.util.*
+import kotlin.collections.HashMap
+
+@Immutable
+class LibrariesDownloadInfo(
+ @SerializedName("artifact")
+ val artifact: LibraryDownloadInfo? = null,
+ classifier_: Map? = null
+) {
+ val classifiers: Map?
+ get() = Collections.unmodifiableMap(classifiersImpl)
+
+ @SerializedName("classifiers")
+ private var classifiersImpl: MutableMap? =
+ if (classifier_ == null || classifier_.isEmpty()) null
+ else HashMap(classifier_)
+}
\ No newline at end of file
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.kt
new file mode 100644
index 000000000..5a4cf2aa8
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.kt
@@ -0,0 +1,114 @@
+/*
+ * Hello Minecraft! Launcher.
+ * Copyright (C) 2017 huangyuhui
+ *
+ * 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 {http://www.gnu.org/licenses/}.
+ */
+package org.jackhuang.hmcl.game
+
+import com.google.gson.*
+import com.google.gson.annotations.SerializedName
+import org.jackhuang.hmcl.util.*
+import java.lang.reflect.Type
+
+/**
+ * A class that describes a Minecraft dependency.
+ *
+ * @see LibraryDeserializer
+ */
+@Immutable
+open class Library(
+ val groupId: String,
+ val artifactId: String,
+ val version: String,
+ classifier_: String? = null,
+ @SerializedName("url")
+ private val url: String? = null,
+ @SerializedName("downloads")
+ private val downloads: LibrariesDownloadInfo? = null,
+ @SerializedName("extract")
+ val extract: ExtractRules? = null,
+ @SerializedName("lateload")
+ val lateload: Boolean = false,
+ private val natives: Map? = null,
+ private val rules: List? = null
+) {
+
+ val appliesToCurrentEnvironment: Boolean = CompatibilityRule.appliesToCurrentEnvironment(rules)
+ val isNative: Boolean = natives != null && appliesToCurrentEnvironment
+ val download: LibraryDownloadInfo
+ val classifier: String? = classifier_ ?: natives?.get(OS.CURRENT_OS)?.replace("\${arch}", Platform.PLATFORM.bit)
+ val path: String
+
+ init {
+
+ var temp: LibraryDownloadInfo? = null
+ if (downloads != null) {
+ if (isNative)
+ temp = downloads.classifiers?.get(classifier)
+ else
+ temp = downloads.artifact
+ }
+ path = temp?.path ?: "${groupId.replace(".", "/")}/$artifactId/$version/$artifactId-$version" + (if (classifier == null) "" else "-$classifier") + ".jar"
+ download = LibraryDownloadInfo(
+ url = temp?.url ?: (url ?: DEFAULT_LIBRARY_URL) + path,
+ path = path,
+ size = temp?.size ?: 0,
+ sha1 = temp?.sha1
+ )
+ }
+
+ override fun toString() = "Library[$groupId:$artifactId:$version]"
+
+ companion object LibrarySerializer : JsonDeserializer, JsonSerializer {
+ fun fromName(name: String, url: String? = null, downloads: LibrariesDownloadInfo? = null, extract: ExtractRules? = null, natives: Map? = null, rules: List