Able to launch game now

This commit is contained in:
huangyuhui
2017-08-08 23:57:17 +08:00
parent f1ffa5ca03
commit da66102bc0
62 changed files with 1790 additions and 744 deletions

View File

@@ -19,6 +19,7 @@ package org.jackhuang.hmcl
import javafx.application.Application
import javafx.stage.Stage
import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.ui.Controllers
import org.jackhuang.hmcl.util.DEFAULT_USER_AGENT
import org.jackhuang.hmcl.util.OS
@@ -27,6 +28,7 @@ import java.io.File
class MainApplication : Application() {
override fun start(stage: Stage) {
PRIMARY_STAGE = stage
Controllers.initialize(stage)
stage.isResizable = false
@@ -36,6 +38,10 @@ class MainApplication : Application() {
companion object {
val VERSION = "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@"
val TITLE = "HMCL $VERSION"
lateinit var PRIMARY_STAGE: Stage
@JvmStatic
fun main(args: Array<String>) {
DEFAULT_USER_AGENT = "Hello Minecraft! Launcher"
@@ -57,5 +63,10 @@ class MainApplication : Application() {
}
fun getMinecraftDirectory(): File = getWorkingDirectory("minecraft")
fun stop() {
PRIMARY_STAGE.close()
Scheduler.shutdown()
}
}
}

View File

@@ -0,0 +1,41 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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.launch.DefaultLauncher
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Scheduler
object LauncherHelper {
fun launch() {
val profile = Settings.selectedProfile
val repository = profile.repository
val account = Settings.selectedAccount ?: throw IllegalStateException("No account here")
val version = repository.getVersion(profile.selectedVersion)
val launcher = DefaultLauncher(
repository = repository,
versionId = profile.selectedVersion,
options = profile.getVersionSetting(profile.selectedVersion).toLaunchOptions(profile.gameDir),
account = account.logIn(Settings.PROXY)
)
profile.dependency.checkGameCompletionAsync(version)
.then(launcher.launchAsync())
.subscribe(Scheduler.JAVAFX) { println("lalala") }
}
}

View File

@@ -25,7 +25,7 @@ import java.util.TreeMap
class Config {
@SerializedName("last")
var last: String = ""
var selectedProfile: String = ""
set(value) {
field = value
Settings.save()
@@ -97,7 +97,13 @@ class Config {
Settings.save()
}
@SerializedName("accounts")
var accounts: MutableMap<String, MutableMap<*, *>> = TreeMap()
var accounts: MutableMap<String, MutableMap<Any, Any>> = TreeMap()
set(value) {
field = value
Settings.save()
}
@SerializedName("selectedAccount")
var selectedAccount: String = ""
set(value) {
field = value
Settings.save()

View File

@@ -20,6 +20,9 @@ package org.jackhuang.hmcl.setting
import com.google.gson.*
import javafx.beans.InvalidationListener
import javafx.beans.property.*
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider
import org.jackhuang.hmcl.download.DefaultDependencyManager
import org.jackhuang.hmcl.download.DependencyManager
import org.jackhuang.hmcl.game.HMCLGameRepository
import org.jackhuang.hmcl.util.*
import java.io.File
@@ -39,6 +42,7 @@ class Profile(var name: String = "Default", gameDir: File = File(".minecraft"))
var noCommon: Boolean by noCommonProperty
var repository = HMCLGameRepository(gameDir)
var dependency = DefaultDependencyManager(repository, BMCLAPIDownloadProvider)
init {
gameDirProperty.addListener { _, _, newValue ->

View File

@@ -30,8 +30,15 @@ import java.io.File
import java.util.logging.Level
import org.jackhuang.hmcl.ProfileLoadingEvent
import org.jackhuang.hmcl.ProfileChangedEvent
import org.jackhuang.hmcl.auth.Account
import org.jackhuang.hmcl.auth.Accounts
import org.jackhuang.hmcl.auth.OfflineAccount
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount
import org.jackhuang.hmcl.event.EVENT_BUS
import org.jackhuang.hmcl.util.FileTypeAdapter
import org.jackhuang.hmcl.util.ignoreException
import java.net.Proxy
import java.util.*
object Settings {
@@ -48,8 +55,31 @@ object Settings {
val SETTINGS: Config
private val ACCOUNTS = mutableMapOf<String, Account>()
init {
SETTINGS = initSettings();
loop@for ((name, settings) in SETTINGS.accounts) {
val factory = when(settings["type"]) {
"yggdrasil" -> YggdrasilAccount
"offline" -> OfflineAccount
else -> {
SETTINGS.accounts.remove(name)
continue@loop
}
}
val account = factory.fromStorage(settings)
if (account.username != name) {
SETTINGS.accounts.remove(name)
continue
}
ACCOUNTS[name] = account
}
save()
if (!getProfiles().containsKey(DEFAULT_PROFILE))
@@ -59,6 +89,10 @@ object Settings {
profile.name = name
profile.addPropertyChangedListener(InvalidationListener { save() })
}
ignoreException {
Runtime.getRuntime().addShutdownHook(Thread(this::save))
}
}
fun getDownloadProvider(): DownloadProvider = when (SETTINGS.downloadtype) {
@@ -93,16 +127,62 @@ object Settings {
fun save() {
try {
SETTINGS.accounts.clear()
for ((name, account) in ACCOUNTS) {
val storage = account.toStorage()
storage["type"] = when(account) {
is OfflineAccount -> "offline"
is YggdrasilAccount -> "yggdrasil"
else -> ""
}
SETTINGS.accounts[name] = storage
}
SETTINGS_FILE.writeText(GSON.toJson(SETTINGS))
} catch (ex: IOException) {
LOG.log(Level.SEVERE, "Failed to save config", ex)
}
}
fun getLastProfile(): Profile {
if (!hasProfile(SETTINGS.last))
SETTINGS.last = DEFAULT_PROFILE
return getProfile(SETTINGS.last)
val selectedProfile: Profile
get() {
if (!hasProfile(SETTINGS.selectedProfile))
SETTINGS.selectedProfile = DEFAULT_PROFILE
return getProfile(SETTINGS.selectedProfile)
}
val selectedAccount: Account?
get() {
val a = getAccount(SETTINGS.selectedAccount)
if (a == null && ACCOUNTS.isNotEmpty()) {
val (key, acc) = ACCOUNTS.entries.first()
SETTINGS.selectedAccount = key
return acc
}
return a
}
fun setSelectedAccount(name: String) {
if (ACCOUNTS.containsKey(name))
SETTINGS.selectedAccount = name
}
val PROXY: Proxy = Proxy.NO_PROXY
fun addAccount(account: Account) {
ACCOUNTS[account.username] = account
}
fun getAccount(name: String): Account? {
return ACCOUNTS[name]
}
fun getAccounts(): Map<String, Account> {
return Collections.unmodifiableMap(ACCOUNTS)
}
fun deleteAccount(name: String) {
ACCOUNTS.remove(name)
}
fun getProfile(name: String?): Profile {
@@ -136,16 +216,16 @@ object Settings {
return true
}
fun delProfile(ver: Profile): Boolean {
return delProfile(ver.name)
fun deleteProfile(ver: Profile): Boolean {
return deleteProfile(ver.name)
}
fun delProfile(ver: String): Boolean {
fun deleteProfile(ver: String): Boolean {
if (DEFAULT_PROFILE == ver) {
return false
}
var notify = false
if (getLastProfile().name == ver)
if (selectedProfile.name == ver)
notify = true
val flag = getProfiles().remove(ver) != null
if (notify && flag)
@@ -154,9 +234,8 @@ object Settings {
}
internal fun onProfileChanged() {
val p = getLastProfile()
EVENT_BUS.fireEvent(ProfileChangedEvent(SETTINGS, p))
p.repository.refreshVersions()
EVENT_BUS.fireEvent(ProfileChangedEvent(SETTINGS, selectedProfile))
selectedProfile.repository.refreshVersions()
}
/**

View File

@@ -20,7 +20,11 @@ package org.jackhuang.hmcl.setting
import com.google.gson.*
import javafx.beans.InvalidationListener
import javafx.beans.property.*
import org.jackhuang.hmcl.MainApplication
import org.jackhuang.hmcl.game.LaunchOptions
import org.jackhuang.hmcl.util.*
import java.io.File
import java.io.IOException
import java.lang.reflect.Type
class VersionSetting() {
@@ -61,14 +65,14 @@ class VersionSetting() {
/**
* The permanent generation size of JVM garbage collection.
*/
val permSizeProperty = SimpleIntegerProperty(this, "permSize", 0)
var permSize: Int by permSizeProperty
val permSizeProperty = SimpleStringProperty(this, "permSize", "")
var permSize: String by permSizeProperty
/**
* The maximum memory that JVM can allocate.
* The size of JVM heap.
*/
val maxMemoryProperty = SimpleIntegerProperty(this, "maxMemory", 0)
val maxMemoryProperty = SimpleIntegerProperty(this, "maxMemory", OS.SUGGESTED_MEMORY)
var maxMemory: Int by maxMemoryProperty
/**
@@ -127,7 +131,7 @@ class VersionSetting() {
* 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)
val widthProperty = SimpleIntegerProperty(this, "width", 854)
var width: Int by widthProperty
@@ -138,7 +142,7 @@ class VersionSetting() {
* 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)
val heightProperty = SimpleIntegerProperty(this, "height", 480)
var height: Int by heightProperty
@@ -179,6 +183,32 @@ class VersionSetting() {
launcherVisibilityProperty.addListener(listener)
}
@Throws(IOException::class)
fun toLaunchOptions(gameDir: File): LaunchOptions {
return LaunchOptions(
gameDir = gameDir,
java = if (java == null) JavaVersion.fromCurrentEnvironment()
else JavaVersion.fromExecutable(File(java)),
versionName = MainApplication.TITLE,
profileName = MainApplication.TITLE,
minecraftArgs = minecraftArgs,
javaArgs = javaArgs,
maxMemory = maxMemory,
metaspace = permSize.toIntOrNull(),
width = width,
height = height,
fullscreen = fullscreen,
serverIp = serverIp,
wrapper = wrapper,
proxyHost = Settings.SETTINGS.proxyHost,
proxyPort = Settings.SETTINGS.proxyPort,
proxyUser = Settings.SETTINGS.proxyUserName,
proxyPass = Settings.SETTINGS.proxyPassword,
precalledCommand = precalledCommand,
noGeneratedJVMArgs = noJVMArgs
)
}
companion object Serializer: JsonSerializer<VersionSetting>, JsonDeserializer<VersionSetting> {
override fun serialize(src: VersionSetting?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
if (src == null) return JsonNull.INSTANCE
@@ -214,7 +244,7 @@ class VersionSetting() {
javaArgs = json["javaArgs"]?.asString ?: ""
minecraftArgs = json["minecraftArgs"]?.asString ?: ""
maxMemory = parseJsonPrimitive(json["maxMemory"]?.asJsonPrimitive)
permSize = parseJsonPrimitive(json["permSize"]?.asJsonPrimitive)
permSize = json["permSize"]?.asString ?: ""
width = parseJsonPrimitive(json["width"]?.asJsonPrimitive)
height = parseJsonPrimitive(json["height"]?.asJsonPrimitive)
javaDir = json["javaDir"]?.asString ?: ""

View File

@@ -0,0 +1,83 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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.effects.JFXDepthManager
import javafx.beans.binding.Bindings
import javafx.fxml.FXML
import javafx.scene.control.Label
import javafx.scene.layout.Pane
import javafx.scene.layout.StackPane
import javafx.scene.layout.VBox
import java.util.concurrent.Callable
class AccountItem(i: Int, width: Double, height: Double) : StackPane() {
@FXML lateinit var icon: Pane
@FXML lateinit var content: VBox
@FXML lateinit var header: StackPane
@FXML lateinit var body: StackPane
@FXML lateinit var btnDelete: JFXButton
@FXML lateinit var btnEdit: JFXButton
@FXML lateinit var lblUser: Label
@FXML lateinit var lblType: Label
init {
loadFXML("/assets/fxml/account-item.fxml")
minWidth = width
maxWidth = width
prefWidth = width
minHeight = height
maxHeight = height
prefHeight = height
JFXDepthManager.setDepth(this, 1)
// create content
val headerColor = getDefaultColor(i % 12)
header.style = "-fx-background-radius: 5 5 0 0; -fx-background-color: " + headerColor
body.minHeight = Math.random() * 20 + 50
// create image view
icon.translateYProperty().bind(Bindings.createDoubleBinding(Callable { header.boundsInParent.height - icon.height / 2 }, header.boundsInParentProperty(), icon.heightProperty()))
}
private fun getDefaultColor(i: Int): String {
var color = "#FFFFFF"
when (i) {
0 -> color = "#8F3F7E"
1 -> color = "#B5305F"
2 -> color = "#CE584A"
3 -> color = "#DB8D5C"
4 -> color = "#DA854E"
5 -> color = "#E9AB44"
6 -> color = "#FEE435"
7 -> color = "#99C286"
8 -> color = "#01A05E"
9 -> color = "#4A8895"
10 -> color = "#16669B"
11 -> color = "#2F65A5"
12 -> color = "#4E6A9C"
else -> {
}
}
return color
}
}

View File

@@ -0,0 +1,152 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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.*
import javafx.application.Platform
import javafx.fxml.FXML
import javafx.scene.Node
import javafx.scene.control.ScrollPane
import javafx.scene.layout.StackPane
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
import javafx.scene.control.Label
import org.jackhuang.hmcl.auth.Account
import org.jackhuang.hmcl.auth.OfflineAccount
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.task.with
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.ui.wizard.HasTitle
import java.util.concurrent.Callable
class AccountsPage : StackPane(), HasTitle {
override val titleProperty: StringProperty = SimpleStringProperty(this, "title", "Accounts")
@FXML lateinit var scrollPane: ScrollPane
@FXML lateinit var masonryPane: JFXMasonryPane
@FXML lateinit var dialog: JFXDialog
@FXML lateinit var txtUsername: JFXTextField
@FXML lateinit var txtPassword: JFXPasswordField
@FXML lateinit var lblPassword: Label
@FXML lateinit var lblCreationWarning: Label
@FXML lateinit var cboType: JFXComboBox<String>
init {
loadFXML("/assets/fxml/account.fxml")
children.remove(dialog)
dialog.dialogContainer = this
JFXScrollPane.smoothScrolling(scrollPane)
cboType.selectionModel.selectedIndexProperty().addListener { _, _, newValue ->
val visible = newValue != 0
lblPassword.isVisible = visible
txtPassword.isVisible = visible
}
cboType.selectionModel.select(0)
loadAccounts()
}
fun loadAccounts() {
val children = mutableListOf<Node>()
var i = 0
for ((_, account) in Settings.getAccounts()) {
children += buildNode(++i, account)
}
masonryPane.children.setAll(children)
Platform.runLater { scrollPane.requestLayout() }
}
private fun buildNode(i: Int, account: Account): Node {
return AccountItem(i, Math.random() * 100 + 100, Math.random() * 100 + 100).apply {
lblUser.text = account.username
lblType.text = when(account) {
is OfflineAccount -> "Offline Account"
is YggdrasilAccount -> "Yggdrasil Account"
else -> throw Error("Unsupported account: $account")
}
btnDelete.setOnMouseClicked {
Settings.deleteAccount(account.username)
Platform.runLater(this@AccountsPage::loadAccounts)
}
}
}
private fun getDefaultColor(i: Int): String {
var color = "#FFFFFF"
when (i) {
0 -> color = "#8F3F7E"
1 -> color = "#B5305F"
2 -> color = "#CE584A"
3 -> color = "#DB8D5C"
4 -> color = "#DA854E"
5 -> color = "#E9AB44"
6 -> color = "#FEE435"
7 -> color = "#99C286"
8 -> color = "#01A05E"
9 -> color = "#4A8895"
10 -> color = "#16669B"
11 -> color = "#2F65A5"
12 -> color = "#4E6A9C"
else -> {
}
}
return color
}
fun addNewAccount() {
dialog.show()
}
fun onCreationAccept() {
val type = cboType.selectionModel.selectedIndex
val username = txtUsername.text
val password = txtPassword.text
val task = Task.of(Callable {
try {
val account = when (type) {
0 -> OfflineAccount.fromUsername(username)
1 -> YggdrasilAccount.fromUsername(username, password)
else -> throw UnsupportedOperationException()
}
account.logIn(Settings.PROXY)
account
} catch (e: Exception) {
e
}
})
task.subscribe(Scheduler.JAVAFX) {
val account = task.result
if (account is Account) {
Settings.addAccount(account)
dialog.close()
loadAccounts()
} else if (account is Exception) {
lblCreationWarning.text = account.localizedMessage
}
}
}
fun onCreationCancel() {
dialog.close()
}
}

View File

@@ -0,0 +1,40 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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.scene.layout.StackPane
import javafx.scene.layout.VBox
import javafx.scene.paint.Color
import javafx.scene.shape.Rectangle
import javafx.scene.text.Text
class ClassTitle(val text: String) : StackPane() {
init {
val vbox = VBox()
vbox.children += Text(text).apply {
}
vbox.children += Rectangle().apply {
widthProperty().bind(vbox.widthProperty())
height = 1.0
fill = Color.GRAY
}
children.setAll(vbox)
styleClass += "class-title"
}
}

View File

@@ -20,7 +20,6 @@ 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
@@ -28,18 +27,22 @@ object Controllers {
lateinit var scene: Scene private set
lateinit var stage: Stage private set
lateinit var mainController: MainController
private val mainPane: Pane = loadPane("main")
val mainPane = MainPage()
lateinit var versionController: VersionController
val versionPane: Pane = loadPane("version")
val versionPane = VersionPage()
lateinit var leftPaneController: LeftPaneController
lateinit var decorator: Decorator
fun initialize(stage: Stage) {
this.stage = stage
val decorator = Decorator(stage, mainPane, max = false)
decorator = Decorator(stage, max = false)
decorator.mainPage = mainPane
decorator.showPage(null)
leftPaneController = LeftPaneController(decorator.leftPane)
// Let root pane fix window size.
with(mainPane.parent as StackPane) {
mainPane.prefWidthProperty().bind(widthProperty())
@@ -56,7 +59,7 @@ object Controllers {
}
fun navigate(node: Node?) {
//mainController.setContentPage(node)
decorator.showPage(node)
}
private fun <T> loadPane(s: String): T = FXMLLoader(Controllers::class.java.getResource("/assets/fxml/$s.fxml")).load()

View File

@@ -43,9 +43,18 @@ import javafx.stage.Stage
import javafx.stage.StageStyle
import javafx.scene.layout.BorderStrokeStyle
import javafx.scene.layout.BorderStroke
import org.jackhuang.hmcl.MainApplication
import org.jackhuang.hmcl.ui.animation.AnimationProducer
import org.jackhuang.hmcl.ui.animation.ContainerAnimations
import org.jackhuang.hmcl.ui.animation.TransitionHandler
import org.jackhuang.hmcl.ui.wizard.*
import org.jackhuang.hmcl.util.*
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
class Decorator @JvmOverloads constructor(private val primaryStage: Stage, private val max: Boolean = true, min: Boolean = true) : GridPane(), AbstractWizardDisplayer {
override val wizardController: WizardController = WizardController(this)
class Decorator @JvmOverloads constructor(private val primaryStage: Stage, node: Node, private val max: Boolean = true, min: Boolean = true) : GridPane() {
private var xOffset: Double = 0.0
private var yOffset: Double = 0.0
private var newX: Double = 0.0
@@ -66,7 +75,7 @@ class Decorator @JvmOverloads constructor(private val primaryStage: Stage, node:
@FXML lateinit var titleLabel: Label
@FXML lateinit var leftPane: VBox
private val onCloseButtonActionProperty: ObjectProperty<Runnable> = SimpleObjectProperty(Runnable { this.primaryStage.close() })
private val onCloseButtonActionProperty: ObjectProperty<Runnable> = SimpleObjectProperty(Runnable { MainApplication.stop() })
@JvmName("onCloseButtonActionProperty") get
var onCloseButtonAction: Runnable by onCloseButtonActionProperty
@@ -89,6 +98,9 @@ class Decorator @JvmOverloads constructor(private val primaryStage: Stage, node:
private 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)
.apply { setPrefSize(12.0, 12.0); setSize(12.0, 12.0) }
val animationHandler: TransitionHandler
override val cancelQueue: Queue<Any> = ConcurrentLinkedQueue<Any>()
init {
loadFXML("/assets/fxml/decorator.fxml")
@@ -111,13 +123,9 @@ class Decorator @JvmOverloads constructor(private val primaryStage: Stage, node:
titleContainer.addEventHandler(MouseEvent.MOUSE_ENTERED) { this.allowMove = true }
titleContainer.addEventHandler(MouseEvent.MOUSE_EXITED) { if (!this.isDragging) this.allowMove = false }
this.contentPlaceHolder.children.add(node)
(node as Region).setMinSize(0.0, 0.0)
this.border = Border(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)
animationHandler = TransitionHandler(contentPlaceHolder)
setOverflowHidden(lookup("#contentPlaceHolderRoot") as Pane)
}
fun onMouseMoved(mouseEvent: MouseEvent) {
@@ -285,18 +293,22 @@ class Decorator @JvmOverloads constructor(private val primaryStage: Stage, node:
this.yOffset = mouseEvent.sceneY
}
@Suppress("UNUSED_PARAMETER")
private fun isRightEdge(x: Double, y: Double, boundsInParent: Bounds): Boolean {
return x < this.width && x > this.width - this.contentPlaceHolder.snappedLeftInset()
}
@Suppress("UNUSED_PARAMETER")
private fun isTopEdge(x: Double, y: Double, boundsInParent: Bounds): Boolean {
return y >= 0.0 && y < this.contentPlaceHolder.snappedLeftInset()
}
@Suppress("UNUSED_PARAMETER")
private fun isBottomEdge(x: Double, y: Double, boundsInParent: Bounds): Boolean {
return y < this.height && y > this.height - this.contentPlaceHolder.snappedLeftInset()
}
@Suppress("UNUSED_PARAMETER")
private fun isLeftEdge(x: Double, y: Double, boundsInParent: Bounds): Boolean {
return x >= 0.0 && x < this.contentPlaceHolder.snappedLeftInset()
}
@@ -335,7 +347,74 @@ class Decorator @JvmOverloads constructor(private val primaryStage: Stage, node:
}
}
fun setContent(content: Node) {
this.contentPlaceHolder.children.setAll(content)
private fun setContent(content: Node, animation: AnimationProducer) {
animationHandler.setContent(content, animation)
if (content is Region) {
content.setMinSize(0.0, 0.0)
setOverflowHidden(content)
}
backNavButton.isDisable = !wizardController.canPrev()
if (content is Refreshable)
refreshNavButton.isVisible = true
if (content != mainPage)
closeNavButton.isVisible = true
val prefix = if (category == null) "" else category + " - "
titleLabel.textProperty().unbind()
if (content is WizardPage)
titleLabel.text = prefix + content.title
if (content is HasTitle)
titleLabel.textProperty().bind(content.titleProperty)
}
lateinit var mainPage: Node
var category: String? = null
fun showPage(content: Node?) {
onEnd()
setContent(content ?: mainPage, ContainerAnimations.FADE.animationProducer)
}
fun startWizard(wizardProvider: WizardProvider, category: String? = null) {
this.category = category
wizardController.provider = wizardProvider
wizardController.onStart()
}
override fun onStart() {
backNavButton.isVisible = true
backNavButton.isDisable = false
closeNavButton.isVisible = true
refreshNavButton.isVisible = false
}
override fun onEnd() {
backNavButton.isVisible = false
closeNavButton.isVisible = false
refreshNavButton.isVisible = false
}
override fun navigateTo(page: Node, nav: Navigation.NavigationDirection) {
setContent(page, nav.animation.animationProducer)
}
fun onRefresh() {
(contentPlaceHolder.children.single() as Refreshable).refresh()
}
fun onCloseNav() {
wizardController.onCancel()
showPage(null)
}
fun onBack() {
wizardController.onPrev(true)
}
}

View File

@@ -33,6 +33,7 @@ import javafx.scene.image.WritableImage
import javafx.scene.input.MouseEvent
import javafx.scene.input.ScrollEvent
import javafx.scene.layout.Pane
import javafx.scene.layout.Region
import javafx.scene.shape.Rectangle
import javafx.util.Duration
@@ -99,7 +100,7 @@ fun takeSnapshot(node: Parent, width: Double, height: Double): WritableImage {
return scene.snapshot(null)
}
fun setOverflowHidden(node: Pane) {
fun setOverflowHidden(node: Region) {
val rectangle = Rectangle()
rectangle.widthProperty().bind(node.widthProperty())
rectangle.heightProperty().bind(node.heightProperty())
@@ -109,5 +110,5 @@ fun setOverflowHidden(node: Pane) {
val stylesheets = arrayOf(
Controllers::class.java.getResource("/css/jfoenix-fonts.css").toExternalForm(),
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-components.css").toExternalForm(),
Controllers::class.java.getResource("/assets/css/jfoenix-main-demo.css").toExternalForm())

View File

@@ -0,0 +1,29 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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.geometry.Pos
import javafx.scene.Node
import javafx.scene.control.Label
import javafx.scene.layout.HBox
class IconedItem(val icon: Node, val text: String)
: RipplerContainer(HBox().apply {
children += icon.apply { isMouseTransparent = true }
children += Label(text).apply { alignment = Pos.CENTER; isMouseTransparent = true }
})

View File

@@ -0,0 +1,120 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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.JFXComboBox
import javafx.scene.Node
import javafx.scene.layout.*
import javafx.scene.paint.Paint
import org.jackhuang.hmcl.ProfileChangedEvent
import org.jackhuang.hmcl.ProfileLoadingEvent
import org.jackhuang.hmcl.event.EVENT_BUS
import org.jackhuang.hmcl.event.RefreshedVersionsEvent
import org.jackhuang.hmcl.game.LauncherHelper
import org.jackhuang.hmcl.game.minecraftVersion
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.ui.download.DownloadWizardProvider
class LeftPaneController(val leftPane: VBox) {
val versionsPane = VBox()
val cboProfiles = JFXComboBox<String>().apply { items.add("Default"); prefWidthProperty().bind(leftPane.widthProperty()) }
val accountItem = VersionListItem("mojang@mojang.com", "Yggdrasil")
init {
addChildren(ClassTitle("ACCOUNTS"))
addChildren(RipplerContainer(accountItem).apply {
accountItem.onSettingsButtonClicked {
Controllers.navigate(AccountsPage())
}
})
addChildren(ClassTitle("LAUNCHER"))
addChildren(IconedItem(SVG.gear("black"), "Settings").apply { prefWidthProperty().bind(leftPane.widthProperty()) })
addChildren(ClassTitle("PROFILES"))
addChildren(cboProfiles)
addChildren(ClassTitle("VERSIONS"))
addChildren(versionsPane)
EVENT_BUS.channel<RefreshedVersionsEvent>() += this::loadVersions
EVENT_BUS.channel<ProfileLoadingEvent>() += this::onProfilesLoading
EVENT_BUS.channel<ProfileChangedEvent>() += this::onProfileChanged
Settings.onProfileLoading()
Controllers.decorator.addMenuButton.setOnMouseClicked {
Controllers.decorator.startWizard(DownloadWizardProvider(), "Install New Game")
}
Controllers.decorator.refreshMenuButton.setOnMouseClicked {
Settings.selectedProfile.repository.refreshVersions()
}
Controllers.mainPane.buttonLaunch.setOnMouseClicked { LauncherHelper.launch() }
}
private fun addChildren(content: Node) {
if (content is Pane) {
leftPane.children += content
} else {
val pane = StackPane()
pane.styleClass += "left-pane-item"
pane.children.setAll(content)
leftPane.children += pane
}
}
fun onProfilesLoading() {
// TODO: Profiles
}
fun onProfileChanged(event: ProfileChangedEvent) {
val profile = event.value
profile.selectedVersionProperty.addListener { _, _, newValue ->
versionChanged(newValue)
}
}
private fun loadAccounts() {
}
private fun loadVersions() {
val profile = Settings.selectedProfile
versionsPane.children.clear()
profile.repository.getVersions().forEach { version ->
val item = VersionListItem(version.id, minecraftVersion(profile.repository.getVersionJar(version.id)) ?: "Unknown")
val ripplerContainer = RipplerContainer(item)
item.onSettingsButtonClicked {
Controllers.decorator.showPage(Controllers.versionPane)
Controllers.versionPane.loadVersionSetting(item.versionName, profile.getVersionSetting(item.versionName))
}
ripplerContainer.ripplerFill = Paint.valueOf("#89E1F9")
ripplerContainer.setOnMouseClicked {
// clean selected property
versionsPane.children.forEach { if (it is RipplerContainer) it.selected = false }
ripplerContainer.selected = true
profile.selectedVersion = version.id
}
ripplerContainer.userData = version.id to item
versionsPane.children += ripplerContainer
}
}
fun versionChanged(selectedVersion: String) {
versionsPane.children
.filter { it is RipplerContainer && it.userData is Pair<*, *> }
.forEach { (it as RipplerContainer).selected = (it.userData as Pair<String, VersionListItem>).first == selectedVersion }
}
}

View File

@@ -21,9 +21,12 @@ import com.jfoenix.controls.JFXButton
import com.jfoenix.controls.JFXComboBox
import com.jfoenix.controls.JFXListCell
import com.jfoenix.controls.JFXListView
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
import javafx.collections.FXCollections
import javafx.fxml.FXML
import javafx.scene.Node
import javafx.scene.layout.BorderPane
import javafx.scene.layout.Pane
import javafx.scene.layout.StackPane
import org.jackhuang.hmcl.ProfileChangedEvent
@@ -36,11 +39,19 @@ 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.HasTitle
import org.jackhuang.hmcl.ui.wizard.Wizard
/**
* @see /assets/fxml/main.fxml
*/
class MainController {
class MainPage : BorderPane(), HasTitle {
override val titleProperty: StringProperty = SimpleStringProperty(this, "title", "Main Page")
@FXML lateinit var buttonLaunch: JFXButton
init {
loadFXML("/assets/fxml/main.fxml")
}
}

View File

@@ -0,0 +1,46 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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.validation.base.ValidatorBase
import javafx.scene.control.TextInputControl
class NumberValidator @JvmOverloads constructor(val nullable: Boolean = false) : ValidatorBase() {
override fun eval() {
if (this.srcControl.get() is TextInputControl) {
this.evalTextInputField()
}
}
private fun evalTextInputField() {
val textField = this.srcControl.get() as TextInputControl
if (textField.text.isBlank())
hasErrors.set(false)
else
try {
Integer.parseInt(textField.text)
this.hasErrors.set(false)
} catch (var3: Exception) {
this.hasErrors.set(true)
}
}
}

View File

@@ -0,0 +1,230 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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.JFXRippler
import javafx.animation.Transition
import javafx.beans.DefaultProperty
import javafx.beans.NamedArg
import javafx.beans.Observable
import javafx.beans.binding.Bindings
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleObjectProperty
import javafx.geometry.Insets
import javafx.geometry.Pos
import javafx.scene.Node
import javafx.scene.control.Label
import javafx.scene.layout.*
import javafx.scene.paint.Color
import javafx.scene.paint.Paint
import javafx.scene.shape.Rectangle
import javafx.scene.shape.Shape
import org.jackhuang.hmcl.util.getValue
import org.jackhuang.hmcl.util.setValue
import java.util.concurrent.Callable
@DefaultProperty("container")
open class RipplerContainer(@NamedArg("container") container: Node): StackPane() {
val containerProperty = SimpleObjectProperty<Node>(this, "container", null)
@JvmName("containerProperty") get
var container: Node by containerProperty
val ripplerFillProperty = SimpleObjectProperty<Paint>(this, "ripplerFill", null)
@JvmName("ripplerFillProperty") get
var ripplerFill: Paint? by ripplerFillProperty
val selectedProperty = SimpleBooleanProperty(this, "selected", false)
@JvmName("selectedProperty") get
var selected: Boolean by selectedProperty
private val buttonContainer = StackPane()
private val buttonRippler = object : JFXRippler(StackPane()) {
override fun getMask(): Node {
val mask = StackPane()
mask.shapeProperty().bind(buttonContainer.shapeProperty())
mask.backgroundProperty().bind(Bindings.createObjectBinding<Background>(Callable<Background> { Background(BackgroundFill(Color.WHITE, if (buttonContainer.backgroundProperty().get() != null && buttonContainer.getBackground().getFills().size > 0) buttonContainer.background.fills[0].radii else defaultRadii, if (buttonContainer.backgroundProperty().get() != null && buttonContainer.background.fills.size > 0) buttonContainer.background.fills[0].insets else Insets.EMPTY)) }, buttonContainer.backgroundProperty()))
mask.resize(buttonContainer.width - buttonContainer.snappedRightInset() - buttonContainer.snappedLeftInset(), buttonContainer.height - buttonContainer.snappedBottomInset() - buttonContainer.snappedTopInset())
return mask
}
override fun initListeners() {
this.ripplerPane.setOnMousePressed { event ->
if (releaseManualRippler != null) {
releaseManualRippler!!.run()
}
releaseManualRippler = null
this.createRipple(event.x, event.y)
}
}
}
private var clickedAnimation: Transition? = null
private val defaultRadii = CornerRadii(3.0)
private var invalid = true
private var releaseManualRippler: Runnable? = null
init {
styleClass += "rippler-container"
this.container = container
/*
armedProperty().addListener { o, oldVal, newVal ->
if (newVal!!.booleanValue()) {
this.releaseManualRippler = this.buttonRippler.createManualRipple()
if (this.clickedAnimation != null) {
this.clickedAnimation!!.rate = 1.0
this.clickedAnimation!!.play()
}
} else {
if (this.releaseManualRippler != null) {
this.releaseManualRippler!!.run()
}
if (this.clickedAnimation != null) {
this.clickedAnimation!!.rate = -1.0
this.clickedAnimation!!.play()
}
}
}*/
this.buttonContainer.children.add(this.buttonRippler)
setOnMousePressed { e ->
if (this.clickedAnimation != null) {
this.clickedAnimation!!.rate = 1.0
this.clickedAnimation!!.play()
}
}
setOnMouseReleased { e ->
if (this.clickedAnimation != null) {
this.clickedAnimation!!.rate = -1.0
this.clickedAnimation!!.play()
}
}
focusedProperty().addListener { o, oldVal, newVal ->
if (newVal) {
if (!isPressed) {
this.buttonRippler.showOverlay()
}
} else {
this.buttonRippler.hideOverlay()
}
}
pressedProperty().addListener { _, _, _ -> this.buttonRippler.hideOverlay() }
isPickOnBounds = false
this.buttonContainer.isPickOnBounds = false
this.buttonContainer.shapeProperty().bind(shapeProperty())
this.buttonContainer.borderProperty().bind(borderProperty())
this.buttonContainer.backgroundProperty().bind(Bindings.createObjectBinding<Background>(Callable<Background> {
if (background == null || this.isJavaDefaultBackground(background) || this.isJavaDefaultClickedBackground(background)) {
background = Background(BackgroundFill(Color.TRANSPARENT, defaultRadii, null))
}
try {
return@Callable(
if (background != null && (background.fills[0] as BackgroundFill).insets == Insets(-0.2, -0.2, -0.2, -0.2))
Background(BackgroundFill((if (background != null) (background.fills[0] as BackgroundFill).fill else Color.TRANSPARENT) as Paint,
if (backgroundProperty().get() != null) background.fills[0].radii else defaultRadii, Insets.EMPTY))
else
Background(BackgroundFill((if (background != null) background.fills[0].fill else Color.TRANSPARENT) as Paint,
if (background != null) background.fills[0].radii else defaultRadii, Insets.EMPTY))
)
} catch (var3: Exception) {
return@Callable background
}
}, backgroundProperty()))
ripplerFillProperty.addListener { o, oldVal, newVal -> this.buttonRippler.ripplerFill = newVal }
if (background == null || this.isJavaDefaultBackground(background)) {
background = Background(BackgroundFill(Color.TRANSPARENT, this.defaultRadii, null))
}
this.updateChildren()
containerProperty.addListener { _ -> updateChildren() }
selectedProperty.addListener { _, _, newValue ->
if (newValue) background = Background(BackgroundFill(ripplerFill, defaultRadii, null))
else background = Background(BackgroundFill(Color.TRANSPARENT, defaultRadii, null))
}
shape = Rectangle().apply {
widthProperty().bind(this@RipplerContainer.widthProperty())
heightProperty().bind(this@RipplerContainer.heightProperty())
}
}
protected fun updateChildren() {
children.add(container)
if (this.buttonContainer != null) {
this.children.add(0, this.buttonContainer)
}
for (i in 1..this.children.size - 1) {
this.children[i].isPickOnBounds = false
}
}
fun layoutChildren(x: Double, y: Double, w: Double, h: Double) {
if (this.invalid) {
if (ripplerFill == null) {
for (i in this.children.size - 1 downTo 1) {
if (this.children[i] is Shape) {
this.buttonRippler.ripplerFill = (this.children[i] as Shape).fill
(this.children[i] as Shape).fillProperty().addListener { o, oldVal, newVal -> this.buttonRippler.ripplerFill = newVal }
break
}
if (this.children[i] is Label) {
this.buttonRippler.ripplerFill = (this.children[i] as Label).textFill
(this.children[i] as Label).textFillProperty().addListener { o, oldVal, newVal -> this.buttonRippler.ripplerFill = newVal }
break
}
}
} else {
this.buttonRippler.ripplerFill = ripplerFill
}
this.invalid = false
}
val shift = 1.0
this.buttonContainer.resizeRelocate(layoutBounds.minX - shift, layoutBounds.minY - shift, width + 2.0 * shift, height + 2.0 * shift)
//this.layoutLabelInArea(x, y, w, h)
}
private fun isJavaDefaultBackground(background: Background): Boolean {
try {
val firstFill = (background.fills[0] as BackgroundFill).fill.toString()
return "0xffffffba" == firstFill || "0xffffffbf" == firstFill || "0xffffffbd" == firstFill
} catch (var3: Exception) {
return false
}
}
private fun isJavaDefaultClickedBackground(background: Background): Boolean {
try {
return "0x039ed3ff" == (background.fills[0] as BackgroundFill).fill.toString()
} catch (var3: Exception) {
return false
}
}
}

View File

@@ -50,8 +50,10 @@ object SVG {
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)
fun gear(fill: String = "white", width: Double = 20.0, height: Double = 20.0): 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", fill, width, height)
fun back(fill: String = "white", width: Double = 20.0, height: Double = 20.0): Node = createSVGPath("M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z", fill, width, height)
fun close(fill: String = "white", width: Double = 20.0, height: Double = 20.0): 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, width, height)
fun dotsVertical(fill: String = "white", width: Double = 20.0, height: Double = 20.0): 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, width, height)
fun delete(fill: String = "white", width: Double = 20.0, height: Double = 20.0): Node = createSVGPath("M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z", fill, width, height)
fun accountEdit(fill: String = "white", width: Double = 20.0, height: Double = 20.0) = createSVGPath("M21.7,13.35L20.7,14.35L18.65,12.3L19.65,11.3C19.86,11.09 20.21,11.09 20.42,11.3L21.7,12.58C21.91,12.79 21.91,13.14 21.7,13.35M12,18.94L18.06,12.88L20.11,14.93L14.06,21H12V18.94M12,14C7.58,14 4,15.79 4,18V20H10V18.11L14,14.11C13.34,14.03 12.67,14 12,14M12,4A4,4 0 0,0 8,8A4,4 0 0,0 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4Z", fill, width, height)
}

View File

@@ -0,0 +1,28 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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.util.StringConverter
class SafeIntStringConverter : StringConverter<Int>() {
/** {@inheritDoc} */
override fun fromString(value: String?) = value?.toIntOrNull()
/** {@inheritDoc} */
override fun toString(value: Int?) = value?.toString() ?: ""
}

View File

@@ -1,73 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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(id: String, version: VersionSetting) {
titleLabel.text = id
}
fun onExploreJavaDir() {
val chooser = DirectoryChooser()
chooser.title = "Selecting Java Directory"
val selectedDir = chooser.showDialog(Controllers.stage)
if (selectedDir != null)
txtGameDir.text = selectedDir.absolutePath
}
}

View File

@@ -39,10 +39,6 @@ class VersionListItem(val versionName: String, val gameVersion: String) : Border
handler()
}
fun onLaunch() {
}
fun onSettingsButtonClicked(handler: () -> Unit) {
this.handler = handler
}

View File

@@ -0,0 +1,123 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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.JFXScrollPane
import com.jfoenix.controls.JFXTextField
import javafx.beans.InvalidationListener
import javafx.beans.property.Property
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
import javafx.fxml.FXML
import javafx.scene.control.ScrollPane
import javafx.scene.layout.*
import javafx.stage.DirectoryChooser
import org.jackhuang.hmcl.setting.VersionSetting
import org.jackhuang.hmcl.ui.wizard.HasTitle
class VersionPage : StackPane(), HasTitle {
override val titleProperty: StringProperty = SimpleStringProperty(this, "title", null)
var lastVersionSetting: VersionSetting? = null
@FXML lateinit var rootPane: VBox
@FXML lateinit var scroll: ScrollPane
@FXML lateinit var settingsPane: GridPane
@FXML lateinit var txtWidth: JFXTextField
@FXML lateinit var txtHeight: JFXTextField
@FXML lateinit var txtMaxMemory: JFXTextField
@FXML lateinit var txtJVMArgs: JFXTextField
@FXML lateinit var txtGameArgs: JFXTextField
@FXML lateinit var txtMetaspace: JFXTextField
@FXML lateinit var txtWrapper: JFXTextField
@FXML lateinit var txtPrecallingCommand: JFXTextField
@FXML lateinit var txtServerIP: JFXTextField
@FXML lateinit var txtGameDir: JFXTextField
@FXML lateinit var advancedSettingsPane: VBox
init {
loadFXML("/assets/fxml/version.fxml")
}
fun initialize() {
JFXScrollPane.smoothScrolling(scroll)
fun validation(field: JFXTextField) = InvalidationListener { field.validate() }
fun validator(nullable: Boolean = false) = NumberValidator(nullable).apply { message = "Must be a number." }
txtWidth.setValidators(validator())
txtWidth.textProperty().addListener(validation(txtWidth))
txtHeight.setValidators(validator())
txtHeight.textProperty().addListener(validation(txtHeight))
txtMaxMemory.setValidators(validator())
txtMaxMemory.textProperty().addListener(validation(txtMaxMemory))
txtMetaspace.setValidators(validator(true))
txtMetaspace.textProperty().addListener(validation(txtMetaspace))
}
fun loadVersionSetting(id: String, version: VersionSetting) {
titleProperty.set("Version settings - " + id)
rootPane.children -= advancedSettingsPane
lastVersionSetting?.apply {
widthProperty.unbind()
heightProperty.unbind()
maxMemoryProperty.unbind()
javaArgsProperty.unbind()
minecraftArgsProperty.unbind()
permSizeProperty.unbind()
wrapperProperty.unbind()
precalledCommandProperty.unbind()
serverIpProperty.unbind()
}
bindInt(txtWidth, version.widthProperty)
bindInt(txtHeight, version.heightProperty)
bindInt(txtMaxMemory, version.maxMemoryProperty)
bindString(txtJVMArgs, version.javaArgsProperty)
bindString(txtGameArgs, version.minecraftArgsProperty)
bindString(txtMetaspace, version.permSizeProperty)
bindString(txtWrapper, version.wrapperProperty)
bindString(txtPrecallingCommand, version.precalledCommandProperty)
bindString(txtServerIP, version.serverIpProperty)
lastVersionSetting = version
}
private fun bindInt(textField: JFXTextField, property: Property<*>) {
textField.textProperty().unbind()
textField.textProperty().bindBidirectional(property as Property<Int>, SafeIntStringConverter())
}
private fun bindString(textField: JFXTextField, property: Property<String>) {
textField.textProperty().unbind()
textField.textProperty().bindBidirectional(property)
}
fun onShowAdvanced() {
if (!rootPane.children.contains(advancedSettingsPane))
rootPane.children += advancedSettingsPane
}
fun onExploreJavaDir() {
val chooser = DirectoryChooser()
chooser.title = "Selecting Java Directory"
val selectedDir = chooser.showDialog(Controllers.stage)
if (selectedDir != null)
txtGameDir.text = selectedDir.absolutePath
}
}

View File

@@ -22,7 +22,9 @@ import javafx.animation.KeyFrame
import javafx.animation.KeyValue
import javafx.util.Duration
enum class ContainerAnimations(val animationProducer: (AnimationHandler) -> List<KeyFrame>) {
typealias AnimationProducer = (AnimationHandler) -> List<KeyFrame>
enum class ContainerAnimations(val animationProducer: AnimationProducer) {
/**
* None
*/

View File

@@ -26,7 +26,9 @@ import javafx.scene.SnapshotParameters
import javafx.scene.image.ImageView
import javafx.scene.image.WritableImage
import javafx.scene.layout.StackPane
import javafx.scene.shape.Rectangle
import javafx.util.Duration
import org.jackhuang.hmcl.ui.setOverflowHidden
import org.jackhuang.hmcl.ui.takeSnapshot
/**
@@ -43,7 +45,7 @@ class TransitionHandler(override val view: StackPane): AnimationHandler {
override lateinit var duration: Duration
private set
fun setContent(newView: Node, transition: (TransitionHandler) -> List<KeyFrame>, duration: Duration = Duration.millis(320.0)) {
fun setContent(newView: Node, transition: AnimationProducer, duration: Duration = Duration.millis(320.0)) {
this.duration = duration
val prevAnimation = animation
@@ -82,8 +84,7 @@ class TransitionHandler(override val view: StackPane): AnimationHandler {
snapshot.isVisible = true
snapshot.opacity = 1.0
view.children.setAll(snapshot)
view.children.add(newView)
view.children.setAll(snapshot, newView)
snapshot.toFront()
}
}

View File

@@ -20,14 +20,28 @@ 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.setting.Settings
import org.jackhuang.hmcl.ui.wizard.WizardController
import org.jackhuang.hmcl.ui.wizard.WizardProvider
class DownloadWizardProvider(): WizardProvider() {
override fun finish(settings: Map<String, Any>): Any? {
println(settings)
return null
val builder = Settings.selectedProfile.dependency.gameBuilder()
builder.name(settings["name"] as String)
builder.gameVersion(settings["game"] as String)
if (settings.containsKey("forge"))
builder.version("forge", settings["forge"] as String)
if (settings.containsKey("liteloader"))
builder.version("liteloader", settings["liteloader"] as String)
if (settings.containsKey("optifine"))
builder.version("optifine", settings["optifine"] as String)
return builder.buildAsync()
}
override fun createPage(controller: WizardController, step: Int, settings: Map<String, Any>): Node {

View File

@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui.download
import com.jfoenix.controls.JFXListView
import com.jfoenix.controls.JFXTextField
import javafx.fxml.FXML
import javafx.scene.control.Label
import javafx.scene.layout.StackPane
@@ -33,15 +34,20 @@ class InstallersPage(private val controller: WizardController, private val downl
@FXML lateinit var lblForge: Label
@FXML lateinit var lblLiteLoader: Label
@FXML lateinit var lblOptiFine: Label
@FXML lateinit var txtName: JFXTextField
init {
loadFXML("/assets/fxml/download/installers.fxml")
val gameVersion = controller.settings["game"] as String
txtName.text = gameVersion
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) }
0 -> VersionsPage(controller, gameVersion, downloadProvider, "forge") { controller.onPrev(false) }
1 -> VersionsPage(controller, gameVersion, downloadProvider, "liteloader") { controller.onPrev(false) }
2 -> VersionsPage(controller, gameVersion, downloadProvider, "optifine") { controller.onPrev(false) }
else -> throw IllegalStateException()
})
}
@@ -72,6 +78,11 @@ class InstallersPage(private val controller: WizardController, private val downl
settings.remove(INSTALLER_TYPE)
}
fun onInstall() {
controller.settings["name"] = txtName.text
controller.onFinish()
}
companion object {
val INSTALLER_TYPE = "INSTALLER_TYPE"
}

View File

@@ -24,6 +24,7 @@ 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.task.TaskExecutor
import org.jackhuang.hmcl.ui.animation.ContainerAnimations
import org.jackhuang.hmcl.ui.animation.TransitionHandler
import org.jackhuang.hmcl.ui.loadFXML
@@ -37,6 +38,7 @@ class VersionsPage(private val controller: WizardController, private val gameVer
val transitionHandler = TransitionHandler(this)
private val versionList = downloadProvider.getVersionListById(libraryId)
private var executor: TaskExecutor? = null
init {
loadFXML("/assets/fxml/download/versions.fxml")
@@ -45,7 +47,7 @@ class VersionsPage(private val controller: WizardController, private val gameVer
controller.settings[libraryId] = newValue.remoteVersion.selfVersion
callback()
}
versionList.refreshAsync(downloadProvider).subscribe(Scheduler.JAVAFX) {
executor = versionList.refreshAsync(downloadProvider).subscribe(Scheduler.JAVAFX) {
val versions = ArrayList(versionList.getVersions(gameVersion))
versions.sortWith(RemoteVersion)
for (version in versions) {
@@ -61,5 +63,6 @@ class VersionsPage(private val controller: WizardController, private val gameVer
override fun cleanup(settings: MutableMap<String, Any>) {
settings.remove(libraryId)
executor?.cancel()
}
}

View File

@@ -20,10 +20,11 @@ package org.jackhuang.hmcl.ui.download
import javafx.fxml.FXML
import javafx.scene.control.Label
import javafx.scene.layout.BorderPane
import javafx.scene.layout.StackPane
import org.jackhuang.hmcl.download.RemoteVersion
import org.jackhuang.hmcl.ui.loadFXML
class VersionsPageItem(val remoteVersion: RemoteVersion<*>) : BorderPane() {
class VersionsPageItem(val remoteVersion: RemoteVersion<*>) : StackPane() {
@FXML lateinit var lblSelfVersion: Label
@FXML lateinit var lblGameVersion: Label

View File

@@ -0,0 +1,143 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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.JFXProgressBar
import javafx.application.Platform
import javafx.scene.Node
import javafx.scene.control.Label
import javafx.scene.layout.StackPane
import javafx.scene.layout.VBox
import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.task.TaskExecutor
import org.jackhuang.hmcl.task.TaskListener
import java.util.*
import kotlin.concurrent.thread
interface AbstractWizardDisplayer : WizardDisplayer {
val wizardController: WizardController
val cancelQueue: Queue<Any>
override fun handleDeferredWizardResult(settings: Map<String, Any>, deferredResult: DeferredWizardResult) {
val vbox = VBox()
val progressBar = JFXProgressBar()
val label = Label()
progressBar.maxHeight = 10.0
vbox.children += progressBar
vbox.children += label
navigateTo(StackPane().apply { children += vbox }, Navigation.NavigationDirection.FINISH)
cancelQueue.add(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
})
if (!Thread.currentThread().isInterrupted)
JFXUtilities.runInFX {
navigateTo(Label("Successful"), Navigation.NavigationDirection.FINISH)
}
})
}
override fun handleTask(settings: Map<String, Any>, task: Task) {
val vbox = VBox()
val tasksBar = JFXProgressBar()
val label = Label()
tasksBar.maxHeight = 10.0
vbox.children += tasksBar
vbox.children += label
var finishedTasks = 0
navigateTo(StackPane().apply { children += vbox }, Navigation.NavigationDirection.FINISH)
task.executor().let { executor ->
executor.taskListener = object : TaskListener {
override fun onReady(task: Task) {
Platform.runLater { tasksBar.progressProperty().set(finishedTasks * 1.0 / executor.totTask.get()) }
}
override fun onFinished(task: Task) {
Platform.runLater {
label.text = task.title
++finishedTasks
tasksBar.progressProperty().set(finishedTasks * 1.0 / executor.totTask.get())
}
}
override fun onFailed(task: Task) {
Platform.runLater {
label.text = task.title
++finishedTasks
tasksBar.progressProperty().set(finishedTasks * 1.0 / executor.totTask.get())
}
}
override fun onTerminate() {
Platform.runLater { navigateTo(Label("Successful"), Navigation.NavigationDirection.FINISH) }
}
}
cancelQueue.add(executor)
executor.submit(Task.of(Scheduler.JAVAFX) {
navigateTo(Label("Successful"), Navigation.NavigationDirection.FINISH)
})
}.start()
}
override fun onCancel() {
while (cancelQueue.isNotEmpty()) {
val x = cancelQueue.poll()
when (x) {
is TaskExecutor -> x.cancel()
is Thread -> x.interrupt()
}
}
}
}

View File

@@ -17,29 +17,26 @@
*/
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 com.jfoenix.effects.JFXDepthManager
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
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
internal class DefaultWizardDisplayer(private val prefix: String, wizardProvider: WizardProvider) : StackPane(), WizardDisplayer {
internal class DefaultWizardDisplayer(private val prefix: String, wizardProvider: WizardProvider) : StackPane(), AbstractWizardDisplayer {
val wizardController = WizardController(this, wizardProvider)
override val wizardController = WizardController(this).apply { provider = wizardProvider }
override val cancelQueue: Queue<Any> = ConcurrentLinkedQueue<Any>()
lateinit var transitionHandler: TransitionHandler
@FXML lateinit var root: StackPane
@FXML lateinit var closeButton: JFXButton
@FXML lateinit var backButton: JFXButton
@FXML lateinit var toolbar: JFXToolbar
/**
@@ -48,6 +45,8 @@ internal class DefaultWizardDisplayer(private val prefix: String, wizardProvider
@FXML lateinit var refreshButton: JFXButton
@FXML lateinit var titleLabel: Label
lateinit var nowPage: Node
init {
loadFXML("/assets/fxml/wizard.fxml")
toolbar.effect = null
@@ -59,6 +58,16 @@ internal class DefaultWizardDisplayer(private val prefix: String, wizardProvider
wizardController.onStart()
}
override fun onStart() {
}
override fun onEnd() {
}
override fun onCancel() {
}
fun back() {
wizardController.onPrev(true)
}
@@ -68,58 +77,17 @@ internal class DefaultWizardDisplayer(private val prefix: String, wizardProvider
Controllers.navigate(null)
}
fun refresh() {
(nowPage as Refreshable).refresh()
}
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<String, Any>, 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)
}
}
refreshButton.isVisible = page is Refreshable
nowPage = page
}
}

View File

@@ -0,0 +1,24 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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.beans.property.StringProperty
interface HasTitle {
val titleProperty: StringProperty
}

View File

@@ -25,6 +25,7 @@ interface Navigation {
fun onPrev(cleanUp: Boolean)
fun canPrev(): Boolean
fun onFinish()
fun onEnd()
fun onCancel()
enum class NavigationDirection(val animation: ContainerAnimations) {

View File

@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.wizard
interface WizardObserver {
fun stepsChanged(wizard: Wizard)
interface Refreshable {
fun refresh()
}

View File

@@ -18,15 +18,20 @@
package org.jackhuang.hmcl.ui.wizard
import javafx.scene.Node
import org.jackhuang.hmcl.task.Task
import java.util.*
class WizardController(protected val displayer: WizardDisplayer, protected val provider: WizardProvider) : Navigation {
class WizardController(protected val displayer: WizardDisplayer) : Navigation {
lateinit var provider: WizardProvider
val settings = mutableMapOf<String, Any>()
val pages = Stack<Node>()
override fun onStart() {
settings.clear()
pages.clear()
val page = navigatingTo(0)
pages.push(page)
displayer.onStart()
displayer.navigateTo(page, Navigation.NavigationDirection.START)
}
@@ -62,11 +67,19 @@ class WizardController(protected val displayer: WizardDisplayer, protected val p
when (result) {
is DeferredWizardResult -> displayer.handleDeferredWizardResult(settings, result)
is Summary -> displayer.navigateTo(result.component, Navigation.NavigationDirection.NEXT)
is Task -> displayer.handleTask(settings, result)
}
}
override fun onEnd() {
settings.clear()
pages.clear()
displayer.onEnd()
}
override fun onCancel() {
displayer.onCancel()
onEnd()
}
fun navigatingTo(step: Int): Node {

View File

@@ -18,9 +18,16 @@
package org.jackhuang.hmcl.ui.wizard
import javafx.scene.Node
import org.jackhuang.hmcl.task.Task
interface WizardDisplayer {
fun onStart()
fun onEnd()
fun onCancel()
fun navigateTo(page: Node, nav: Navigation.NavigationDirection)
fun handleDeferredWizardResult(settings: Map<String, Any>, deferredResult: DeferredWizardResult)
fun handleTask(settings: Map<String, Any>, task: Task)
}

View File

@@ -1,394 +0,0 @@
.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 1 1 1;
}
.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;
}

View File

@@ -25,6 +25,25 @@
-fx-text-fill: rgba(0.0, 0.0, 0.0, 0.87);
}
.class-title {
-fx-font-size: 12px;
-fx-padding: 0 16 0 16;
}
.rippler-container HBox {
-fx-font-size: 14px;
-fx-padding: 10 16 10 16;
-fx-spacing: 10;
}
.left-pane-item {
-fx-padding: 10 16 10 16;
}
.jfx-decorator-left-pane {
-fx-padding: 20 0 20 0;
}
/*******************************************************************************
* *
* JFX Drawer *
@@ -84,6 +103,11 @@
-fx-padding: 0.7em 0.8em;
}
.dialog-cancel {
-fx-font-weight: BOLD;
-fx-padding: 0.7em 0.8em;
}
/*******************************************************************************
* *
* JFX Pop Up *
@@ -453,7 +477,6 @@
-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;
@@ -658,9 +681,9 @@
}
.toggle-icon3 {
-fx-pref-width: 40px;
-fx-pref-width: 5px;
-fx-background-radius: 50px;
-fx-pref-height: 30px;
-fx-pref-height: 5px;
-fx-background-color: transparent;
-jfx-toggle-color: rgba(128, 128, 255, 0.2);
-jfx-untoggle-color: transparent;
@@ -672,10 +695,33 @@
}
.toggle-icon3 .jfx-rippler {
-jfx-rippler-fill: white;
-jfx-mask-type: CIRCLE;
}
.toggle-icon4 {
-fx-pref-width: 5px;
-fx-background-radius: 50px;
-fx-pref-height: 5px;
-fx-background-color: transparent;
-jfx-toggle-color: rgba(128, 128, 255, 0.2);
-jfx-untoggle-color: transparent;
}
.toggle-icon4 .icon {
-fx-fill: rgb(204.0, 204.0, 51.0);
-fx-padding: 10.0;
}
.toggle-icon4 .jfx-rippler {
-jfx-rippler-fill: #0F9D58;
-jfx-mask-type: CIRCLE;
}
.transparent {
-fx-background-color: null;
}
/*******************************************************************************
* *
* JFX Spinner *

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>
<?import com.jfoenix.controls.JFXButton?>
<?import javafx.scene.shape.SVGPath?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.image.Image?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
<VBox fx:id="content">
<StackPane fx:id="header" VBox.vgrow="ALWAYS">
<BorderPane>
<top>
<HBox alignment="CENTER_RIGHT">
<JFXButton fx:id="btnDelete" styleClass="toggle-icon3" />
</HBox>
</top>
<center>
<VBox style="-fx-padding: 0 0 0 20;">
<Label fx:id="lblUser" style="-fx-font-size: 20;" />
<Label fx:id="lblType" style="-fx-font-size: 10;" />
</VBox>
</center>
</BorderPane>
</StackPane>
<StackPane fx:id="body" style="-fx-background-radius: 0 0 5 5; -fx-background-color: rgb(255,255,255,0.87);" />
</VBox>
<StackPane fx:id="icon" StackPane.alignment="TOP_RIGHT">
<ImageView StackPane.alignment="CENTER_RIGHT">
<StackPane.margin>
<Insets right="12" />
</StackPane.margin>
<Image url="/assets/img/icon.png" />
</ImageView>
</StackPane>
</fx:root>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import com.jfoenix.controls.JFXMasonryPane?>
<?import com.jfoenix.controls.JFXButton?>
<?import com.jfoenix.controls.JFXDialogLayout?>
<?import com.jfoenix.controls.JFXDialog?>
<?import com.jfoenix.controls.JFXPasswordField?>
<?import com.jfoenix.controls.JFXTextField?>
<?import com.jfoenix.controls.JFXComboBox?>
<?import javafx.collections.FXCollections?>
<?import java.lang.String?>
<?import com.jfoenix.validation.RequiredFieldValidator?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
<ScrollPane fitToHeight="true" fitToWidth="true" fx:id="scrollPane" hbarPolicy="NEVER">
<JFXMasonryPane fx:id="masonryPane">
</JFXMasonryPane>
</ScrollPane>
<AnchorPane pickOnBounds="false">
<JFXButton onMouseClicked="#addNewAccount" AnchorPane.bottomAnchor="16" AnchorPane.rightAnchor="16" buttonType="RAISED" prefWidth="40" prefHeight="40" style="-fx-text-fill:WHITE;-fx-background-color:#5264AE;-fx-font-size:14px;-fx-background-radius: 80px;">
<graphic>
<fx:include source="/assets/svg/plus.fxml" />
</graphic>
</JFXButton>
</AnchorPane>
<JFXDialog fx:id="dialog" transitionType="CENTER">
<JFXDialogLayout>
<heading>
<Label>Create a new account</Label>
</heading>
<body>
<GridPane vgap="30" hgap="30">
<columnConstraints>
<ColumnConstraints />
<ColumnConstraints hgrow="ALWAYS" />
</columnConstraints>
<Label text="Type" GridPane.halignment="RIGHT" GridPane.columnIndex="0" GridPane.rowIndex="0" />
<Label text="Username" GridPane.halignment="RIGHT" GridPane.columnIndex="0" GridPane.rowIndex="1" />
<Label fx:id="lblPassword" text="Password" GridPane.halignment="RIGHT" GridPane.columnIndex="0" GridPane.rowIndex="2" />
<JFXComboBox fx:id="cboType" GridPane.columnIndex="1" GridPane.rowIndex="0">
<items>
<FXCollections fx:factory="observableArrayList">
<String fx:value="Offline Account"/>
<String fx:value="Online Account" />
</FXCollections>
</items>
</JFXComboBox>
<JFXTextField fx:id="txtUsername" promptText="Username" labelFloat="true" GridPane.columnIndex="1" GridPane.rowIndex="1">
<validators>
<RequiredFieldValidator message="Input Required!">
</RequiredFieldValidator>
</validators>
</JFXTextField>
<JFXPasswordField fx:id="txtPassword" promptText="Password" labelFloat="true" GridPane.columnIndex="1" GridPane.rowIndex="2">
<validators>
<RequiredFieldValidator message="Input Required!">
</RequiredFieldValidator>
</validators>
</JFXPasswordField>
</GridPane>
</body>
<actions>
<Label fx:id="lblCreationWarning" />
<JFXButton onMouseClicked="#onCreationAccept" text="Accept" styleClass="dialog-accept" />
<JFXButton onMouseClicked="#onCreationCancel" text="Cancel" styleClass="dialog-cancel" />
</actions>
</JFXDialogLayout>
</JFXDialog>
</fx:root>

View File

@@ -7,9 +7,6 @@
<?import javafx.geometry.Insets?>
<?import java.lang.String?>
<?import javafx.scene.shape.Rectangle?>
<?import com.jfoenix.controls.JFXComboBox?>
<?import com.jfoenix.controls.JFXListView?>
<?import com.jfoenix.controls.JFXListCell?>
<fx:root xmlns="http://javafx.com/javafx"
type="GridPane"
xmlns:fx="http://javafx.com/fxml"
@@ -22,7 +19,7 @@
<String fx:value="resize-border" />
</styleClass>
<columnConstraints>
<ColumnConstraints prefWidth="200" />
<ColumnConstraints minWidth="200" maxWidth="200" />
<ColumnConstraints hgrow="ALWAYS" />
</columnConstraints>
<rowConstraints>
@@ -30,10 +27,12 @@
<RowConstraints vgrow="ALWAYS" />
</rowConstraints>
<StackPane GridPane.rowIndex="1" GridPane.columnIndex="0" VBox.vgrow="ALWAYS" styleClass="jfx-decorator-content-container">
<BorderPane prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER">
<BorderPane fx:id="leftRootPane">
<center>
<BorderPane>
<center>
<ScrollPane fitToHeight="true" fitToWidth="true">
<VBox fx:id="leftPane">
<VBox fx:id="leftPane" styleClass="jfx-decorator-left-pane" spacing="5">
</VBox>
</ScrollPane>
</center>
@@ -54,25 +53,36 @@
</BorderPane>
</bottom>
</BorderPane>
</center>
<right>
<Rectangle height="${leftRootPane.height}" width="1" fill="gray" />
</right>
</BorderPane>
</StackPane>
<StackPane GridPane.rowIndex="1" GridPane.columnIndex="1" fx:id="contentPlaceHolder" styleClass="jfx-decorator-content-container" minWidth="0" minHeight="0" VBox.vgrow="ALWAYS">
<StackPane fx:id="contentPlaceHolderRoot" GridPane.rowIndex="1" GridPane.columnIndex="1" styleClass="jfx-decorator-content-container" VBox.vgrow="ALWAYS">
<StackPane fx:id="contentPlaceHolder" styleClass="jfx-decorator-content-container">
<styleClass>
<String fx:value="jfx-decorator-content-container" />
</styleClass>
<!-- Node -->
</StackPane>
<BorderPane GridPane.rowIndex="0" GridPane.columnIndex="0" GridPane.columnSpan="2" fx:id="titleContainer" styleClass="jfx-tool-bar" pickOnBounds="false">
</StackPane>
<BorderPane GridPane.rowIndex="0" GridPane.columnIndex="0" GridPane.columnSpan="2" fx:id="titleContainer" minHeight="30" styleClass="jfx-tool-bar" pickOnBounds="false">
<left>
<HBox minWidth="200" fx:id="titleWrapper" alignment="CENTER_LEFT">
<Label text="Hello Minecraft! Launcher" mouseTransparent="false" style="-fx-background-color: transparent; -fx-text-fill: white; -fx-font-size: 15px;" />
</HBox>
<BorderPane minWidth="200" maxWidth="200" fx:id="titleWrapper">
<center>
<Label text="Hello Minecraft! Launcher" BorderPane.alignment="CENTER" mouseTransparent="true" style="-fx-background-color: transparent; -fx-text-fill: white; -fx-font-size: 15px;" />
</center>
<right>
<Rectangle height="${navBar.height}" width="1" fill="gray" />
</right>
</BorderPane>
</left>
<center>
<BorderPane fx:id="navBar">
<left>
<HBox fx:id="navLeft" alignment="CENTER_LEFT" style="-fx-padding: 0;">
<Rectangle height="${navBar.height}" width="1" fill="gray" />
<JFXButton fx:id="backNavButton" maxHeight="20" styleClass="toggle-icon3">
<JFXButton fx:id="backNavButton" onMouseClicked="#onBack" maxHeight="20" styleClass="toggle-icon3">
<graphic>
<fx:include source="/assets/svg/arrow-left.fxml"/>
</graphic>
@@ -82,7 +92,7 @@
</left>
<right>
<HBox fx:id="navRight" alignment="CENTER_LEFT">
<JFXButton fx:id="refreshNavButton" maxHeight="20" styleClass="toggle-icon3" disable="true">
<JFXButton fx:id="refreshNavButton" onMouseClicked="#onRefresh" maxHeight="20" styleClass="toggle-icon3" disable="true">
<graphic>
<fx:include source="/assets/svg/refresh.fxml"/>
</graphic>
@@ -90,7 +100,7 @@
<Insets left="20"/>
</StackPane.margin>
</JFXButton>
<JFXButton fx:id="closeNavButton" maxHeight="20" styleClass="toggle-icon3">
<JFXButton fx:id="closeNavButton" onMouseClicked="#onCloseNav" maxHeight="20" styleClass="toggle-icon3">
<graphic>
<fx:include source="/assets/svg/close.fxml"/>
</graphic>

View File

@@ -7,14 +7,16 @@
<?import javafx.scene.layout.BorderPane?>
<?import com.jfoenix.controls.JFXButton?>
<?import javafx.scene.layout.HBox?>
<?import com.jfoenix.controls.JFXTextField?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
<BorderPane>
<top>
<HBox alignment="CENTER" style="-fx-padding: 40px;">
<VBox alignment="CENTER" style="-fx-padding: 40px;" spacing="20">
<Label fx:id="lblGameVersion" alignment="CENTER" />
</HBox>
<JFXTextField fx:id="txtName" labelFloat="true" promptText="Enter the name of this new version" maxWidth="300" />
</VBox>
</top>
<center>
<JFXListView fx:id="list" styleClass="jfx-list-view" maxHeight="150" maxWidth="300">
@@ -46,7 +48,7 @@
</center>
<bottom>
<HBox alignment="CENTER">
<JFXButton fx:id="buttonLaunch" prefWidth="100" prefHeight="40" buttonType="RAISED" text="Install"
<JFXButton onMouseClicked="#onInstall" prefWidth="100" prefHeight="40" buttonType="RAISED" text="Install"
style="-fx-text-fill:WHITE;-fx-background-color:#5264AE;-fx-font-size:14px;"/>
</HBox>
</bottom>

View File

@@ -5,18 +5,18 @@
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="BorderPane" pickOnBounds="false">
type="StackPane" mouseTransparent="true">
<VBox alignment="CENTER">
<Label alignment="CENTER" fx:id="lblGameVersion" style="-fx-color: gray;" />
</VBox>
<BorderPane>
<left>
<VBox alignment="CENTER_LEFT" mouseTransparent="true">
<VBox alignment="CENTER_LEFT">
<Label fx:id="lblSelfVersion" style="-fx-font-size: 15;" />
</VBox>
</left>
<center>
<VBox alignment="CENTER" mouseTransparent="true">
<Label alignment="CENTER" fx:id="lblGameVersion" style="-fx-color: gray;" />
</VBox>
</center>
<right>
<fx:include source="/assets/svg/arrow-right.fxml" />
</right>
</BorderPane>
</fx:root>

View File

@@ -2,10 +2,9 @@
<?import javafx.scene.layout.*?>
<?import com.jfoenix.controls.*?>
<BorderPane
<fx:root
maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity"
fx:controller="org.jackhuang.hmcl.ui.MainController"
style="-fx-background-color: white;"
style="-fx-background-color: white;" type="BorderPane"
xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1">
<center>
<StackPane fx:id="page" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER" />
@@ -23,4 +22,4 @@
</right>
</BorderPane>
</bottom>
</BorderPane>
</fx:root>

View File

@@ -3,29 +3,31 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.image.Image?>
<?import com.jfoenix.controls.JFXButton?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
style="-fx-background-color: transparent; "
styleClass="transparent"
type="BorderPane" pickOnBounds="false">
<left>
<VBox alignment="CENTER_LEFT" mouseTransparent="true">
<HBox alignment="CENTER" mouseTransparent="true">
<ImageView>
<Image url="/assets/img/icon.png" requestedWidth="25" requestedHeight="25" />
</ImageView>
<VBox alignment="CENTER_LEFT">
<Label fx:id="lblVersionName" style="-fx-font-size: 15;" />
<Label fx:id="lblGameVersion" />
<Label fx:id="lblGameVersion" style="-fx-font-size: 10;" />
</VBox>
</HBox>
</left>
<right>
<HBox>
<JFXButton onMouseClicked="#onSettings" styleClass="toggle-icon3">
<HBox alignment="CENTER" pickOnBounds="false">
<JFXButton styleClass="toggle-icon4" onMouseClicked="#onSettings">
<graphic>
<fx:include source="/assets/svg/gear.fxml"/>
</graphic>
</JFXButton>
<JFXButton onMouseClicked="#onLaunch" styleClass="toggle-icon3">
<graphic>
<fx:include source="/assets/svg/rocket.fxml"/>
</graphic>
</JFXButton>
</HBox>
</right>
</fx:root>

View File

@@ -5,16 +5,19 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<StackPane xmlns="http://javafx.com/javafx"
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.jackhuang.hmcl.ui.VersionController">
<BorderPane>
<center>
type="StackPane">
<ScrollPane fx:id="scroll"
style="-fx-background-color: white; -fx-font-size: 14; -fx-pref-width: 100%; "
style="-fx-font-size: 14; -fx-pref-width: 100%; "
fitToHeight="true" fitToWidth="true">
<VBox>
<GridPane fx:id="settingsPane" style="-fx-margin-left: 10; -fx-background-color: white; " hgap="5" vgap="10">
<VBox fx:id="rootPane" style="-fx-padding: 20;">
<GridPane fx:id="settingsPane" hgap="5" vgap="10">
<columnConstraints>
<ColumnConstraints />
<ColumnConstraints hgrow="ALWAYS" />
<ColumnConstraints />
</columnConstraints>
<Label GridPane.rowIndex="0" GridPane.columnIndex="0">Java Directory</Label>
<Label GridPane.rowIndex="1" GridPane.columnIndex="0">Max Memory</Label>
<Label GridPane.rowIndex="2" GridPane.columnIndex="0">Launcher Visibility</Label>
@@ -75,36 +78,17 @@
<JFXButton GridPane.rowIndex="0" GridPane.columnIndex="2" text="Explore" onMouseClicked="#onExploreJavaDir" />
</GridPane>
<VBox fx:id="advancedSettingsPane" style="-fx-padding-bottom: 10; -fx-background-color: white; " spacing="30">
<JFXTextField labelFloat="true" promptText="JVM Args" styleClass="fit-width" fx:id="txtJVMArgs" maxWidth="Infinity"/>
<JFXTextField labelFloat="true" promptText="Game Args" styleClass="fit-width" fx:id="txtGameArgs" maxWidth="Infinity"/>
<JFXTextField labelFloat="true" promptText="Metaspace" styleClass="fit-width" fx:id="txtMetaspace" maxWidth="Infinity"/>
<JFXTextField labelFloat="true" promptText="Wrapper Launcher(like optirun)" styleClass="fit-width" fx:id="txtWrapper" maxWidth="Infinity"/>
<JFXTextField labelFloat="true" promptText="Pre-calling command" styleClass="fit-width" fx:id="txtPrecallingCommand" maxWidth="Infinity"/>
<JFXTextField labelFloat="true" promptText="Server IP" styleClass="fit-width" fx:id="txtServerIP" maxWidth="Infinity"/>
<HBox alignment="CENTER">
<JFXButton text="Show advanced settings" onMouseClicked="#onShowAdvanced" />
</HBox>
<VBox fx:id="advancedSettingsPane" spacing="30">
<JFXTextField labelFloat="true" promptText="JVM Args" styleClass="fit-width" fx:id="txtJVMArgs" prefWidth="${advancedSettingsPane.width}" />
<JFXTextField labelFloat="true" promptText="Game Args" styleClass="fit-width" fx:id="txtGameArgs" prefWidth="${advancedSettingsPane.width}" />
<JFXTextField labelFloat="true" promptText="Metaspace" styleClass="fit-width" fx:id="txtMetaspace" prefWidth="${advancedSettingsPane.width}" />
<JFXTextField labelFloat="true" promptText="Wrapper Launcher(like optirun)" styleClass="fit-width" fx:id="txtWrapper" prefWidth="${advancedSettingsPane.width}" />
<JFXTextField labelFloat="true" promptText="Pre-calling command" styleClass="fit-width" fx:id="txtPrecallingCommand" prefWidth="${advancedSettingsPane.width}" />
<JFXTextField labelFloat="true" promptText="Server IP" styleClass="fit-width" fx:id="txtServerIP" prefWidth="${advancedSettingsPane.width}" />
</VBox>
</VBox>
</ScrollPane>
</center>
<top>
<JFXToolbar maxHeight="20" styleClass="jfx-tool-bar">
<leftItems>
<JFXButton fx:id="backButton" maxHeight="20" styleClass="toggle-icon3"
StackPane.alignment="CENTER_LEFT">
<graphic>
<fx:include source="/assets/svg/arrow-left.fxml" />
</graphic>
<StackPane.margin>
<Insets left="20" />
</StackPane.margin>
</JFXButton>
<Label fx:id="titleLabel" style="-fx-text-fill:WHITE; -fx-font-size: 20;"
StackPane.alignment="CENTER_LEFT"/>
</leftItems>
<rightItems>
</rightItems>
</JFXToolbar>
</top>
</BorderPane>
</StackPane>
</fx:root>

View File

@@ -11,7 +11,7 @@
<VBox>
<JFXToolbar fx:id="toolbar" styleClass="jfx-tool-bar">
<leftItems>
<JFXButton fx:id="closeButton" maxHeight="20" styleClass="toggle-icon3"
<JFXButton maxHeight="20" styleClass="toggle-icon3"
StackPane.alignment="CENTER_RIGHT" onMouseClicked="#close">
<graphic>
<fx:include source="/assets/svg/close.fxml"/>
@@ -31,7 +31,7 @@
</leftItems>
<rightItems>
<JFXButton fx:id="refreshButton" maxHeight="20" styleClass="toggle-icon3" disable="true"
StackPane.alignment="CENTER_RIGHT">
StackPane.alignment="CENTER_RIGHT" onMouseClicked="#refrseh">
<graphic>
<fx:include source="/assets/svg/refresh.fxml"/>
</graphic>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -24,5 +24,5 @@ abstract class Account() {
@Throws(AuthenticationException::class)
abstract fun logIn(proxy: Proxy = Proxy.NO_PROXY): AuthInfo
abstract fun logOut()
abstract fun toStorage(): Map<out Any, Any>
abstract fun toStorage(): MutableMap<Any, Any>
}

View File

@@ -0,0 +1,27 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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.auth.yggdrasil.YggdrasilAccount
object Accounts {
val ACCOUNTS = mapOf(
"offline" to OfflineAccount,
"yggdrasil" to YggdrasilAccount
)
}

View File

@@ -35,13 +35,15 @@ class OfflineAccount private constructor(val uuid: String, override val username
// Offline account need not log out.
}
override fun toStorage(): Map<Any, Any> {
return mapOf(
override fun toStorage(): MutableMap<Any, Any> {
return mutableMapOf(
"uuid" to uuid,
"username" to username
)
}
override fun toString() = "OfflineAccount[username=$username,uuid=$uuid]"
companion object OfflineAccountFactory : AccountFactory<OfflineAccount> {
override fun fromUsername(username: String, password: String): OfflineAccount {

View File

@@ -135,8 +135,8 @@ class YggdrasilAccount private constructor(override val username: String): Accou
selectedProfile = null
}
override fun toStorage(): Map<out Any, Any> {
val result = HashMap<String, Any>()
override fun toStorage(): MutableMap<Any, Any> {
val result = HashMap<Any, Any>()
result[STORAGE_KEY_USER_NAME] = username
if (userId != null)
@@ -189,6 +189,8 @@ class YggdrasilAccount private constructor(override val username: String): Accou
}
}
override fun toString() = "YggdrasilAccount[username=$username]"
companion object YggdrasilAccountFactory : AccountFactory<YggdrasilAccount> {
private val GSON = GsonBuilder()
.registerTypeAdapter(GameProfile::class.java, GameProfile)

View File

@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.launch
import org.jackhuang.hmcl.auth.AuthInfo
import org.jackhuang.hmcl.game.*
import org.jackhuang.hmcl.task.TaskResult
import org.jackhuang.hmcl.util.*
import java.io.File
import java.io.IOException
@@ -26,12 +27,12 @@ import java.util.*
import kotlin.concurrent.thread
/**
* @param version A resolved version(calling [Version.resolve])
* @param versionId The version to be launched.
* @param account The user account
* @param options The launching configuration
*/
open class DefaultLauncher(repository: GameRepository, version: Version, account: AuthInfo, options: LaunchOptions, listener: ProcessListener? = null, isDaemon: Boolean = true)
: Launcher(repository, version, account, options, listener, isDaemon) {
open class DefaultLauncher(repository: GameRepository, versionId: String, account: AuthInfo, options: LaunchOptions, listener: ProcessListener? = null, isDaemon: Boolean = true)
: Launcher(repository, versionId, account, options, listener, isDaemon) {
protected val native: File by lazy { repository.getNativeDirectory(version.id) }
@@ -234,7 +235,7 @@ open class DefaultLauncher(repository: GameRepository, version: Version, account
}
builder.directory(repository.getRunDirectory(version.id))
.environment().put("APPDATA", options.gameDir.parent)
.environment().put("APPDATA", options.gameDir.absoluteFile.parent)
val p = JavaProcess(builder.start(), rawCommandLine)
if (listener == null)
startMonitors(p)
@@ -243,6 +244,14 @@ open class DefaultLauncher(repository: GameRepository, version: Version, account
return p
}
fun launchAsync(): TaskResult<JavaProcess> {
return object : TaskResult<JavaProcess>() {
override fun execute() {
result = launch()
}
}
}
override fun makeLaunchScript(file: String): File {
val isWindows = OS.WINDOWS == OS.CURRENT_OS
val scriptFile = File(file + (if (isWindows) ".bat" else ".sh"))

View File

@@ -26,12 +26,13 @@ import java.io.File
abstract class Launcher(
protected val repository: GameRepository,
protected val version: Version,
protected val versionId: String,
protected val account: AuthInfo,
protected val options: LaunchOptions,
protected val listener: ProcessListener? = null,
protected val isDaemon: Boolean = true) {
val version: Version = repository.getVersion(versionId).resolve(repository)
abstract val rawCommandLine: List<String>
abstract fun launch(): JavaProcess

View File

@@ -17,7 +17,7 @@
*/
package org.jackhuang.hmcl.task
private class CoupleTask<P: Task>(private val pred: P, private val succ: Task.(P) -> Task?, override val reliant: Boolean) : Task() {
internal class CoupleTask<P: Task>(private val pred: P, private val succ: Task.(P) -> Task?, override val reliant: Boolean) : Task() {
override val hidden: Boolean = true
override val dependents: Collection<Task> = listOf(pred)
@@ -30,11 +30,12 @@ private class CoupleTask<P: Task>(private val pred: P, private val succ: Task.(P
}
}
infix fun Task.then(b: Task): Task = CoupleTask(this, { b }, true)
/**
* @param b A runnable that decides what to do next, You can also do something here.
*/
infix fun <T: Task> T.then(b: Task.(T) -> Task?): Task = CoupleTask(this, b, true)
infix fun Task.with(b: Task): Task = CoupleTask(this, { b }, false)
/**
* @param b A runnable that decides what to do next, You can also do something here.
*/
infix fun <T: Task> T.with(b: Task.(T) -> Task?): Task = CoupleTask(this, b, false)

View File

@@ -23,16 +23,17 @@ import java.util.concurrent.atomic.AtomicReference
import javax.swing.SwingUtilities
interface Scheduler {
fun schedule(block: () -> Unit): Future<*>? = schedule(Callable { block() })
fun schedule(block: Callable<Unit>): Future<*>?
companion object Schedulers {
val JAVAFX: Scheduler = SchedulerImpl(Platform::runLater)
val SWING: Scheduler = SchedulerImpl(SwingUtilities::invokeLater)
private class SchedulerImpl(val executor: (() -> Unit) -> Unit) : Scheduler {
private class SchedulerImpl(val executor: (Runnable) -> Unit) : Scheduler {
override fun schedule(block: Callable<Unit>): Future<*>? {
val latch = CountDownLatch(1)
val wrapper = AtomicReference<Exception>()
executor {
executor.invoke(Runnable {
try {
block.call()
} catch (e: Exception) {
@@ -40,7 +41,7 @@ interface Scheduler {
} finally {
latch.countDown()
}
}
})
return object : Future<Unit> {
override fun get(timeout: Long, unit: TimeUnit) {
latch.await(timeout, unit)
@@ -66,7 +67,9 @@ interface Scheduler {
}
val DEFAULT = NEW_THREAD
private val CACHED_EXECUTOR: ExecutorService by lazy {
Executors.newCachedThreadPool()
ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
SynchronousQueue<Runnable>());
}
private val IO_EXECUTOR: ExecutorService by lazy {

View File

@@ -57,6 +57,8 @@ abstract class Task {
abstract fun execute()
infix fun parallel(couple: Task): Task = ParallelTask(this, couple)
infix fun then(b: Task): Task = CoupleTask(this, { b }, true)
infix fun with(b: Task): Task = CoupleTask(this, { b }, false)
/**
* The collection of sub-tasks that should execute before this task running.
@@ -112,18 +114,18 @@ abstract class Task {
fun executor() = TaskExecutor().submit(this)
fun subscribe(subscriber: Task) {
executor().submit(subscriber).start()
fun subscribe(subscriber: Task) = executor().apply {
submit(subscriber).start()
}
fun subscribe(scheduler: Scheduler = Scheduler.DEFAULT, closure: () -> Unit) = subscribe(Task.of(closure, scheduler))
fun subscribe(scheduler: Scheduler = Scheduler.DEFAULT, closure: () -> Unit) = subscribe(Task.of(scheduler, closure))
override fun toString(): String {
return title
}
companion object {
fun of(closure: () -> Unit, scheduler: Scheduler = Scheduler.DEFAULT): Task = SimpleTask(closure, scheduler)
fun of(scheduler: Scheduler = Scheduler.DEFAULT, closure: () -> Unit): Task = SimpleTask(closure, scheduler)
fun <V> of(callable: Callable<V>): TaskResult<V> = TaskCallable(callable)
}
}

View File

@@ -29,7 +29,7 @@ class TaskExecutor() {
var canceled = false
private set
private val totTask = AtomicInteger(0)
val totTask = AtomicInteger(0)
private val taskQueue = ConcurrentLinkedQueue<Task>()
private val workerQueue = ConcurrentLinkedQueue<Future<*>>()
@@ -47,9 +47,10 @@ class TaskExecutor() {
* Start the subscription and run all registered tasks asynchronously.
*/
fun start() {
thread {
workerQueue.add(Scheduler.Schedulers.NEW_THREAD.schedule(Callable {
totTask.addAndGet(taskQueue.size)
while (!taskQueue.isEmpty() && !canceled) {
while (!taskQueue.isEmpty()) {
if (canceled) break
val task = taskQueue.poll()
if (task != null) {
val future = task.scheduler.schedule(Callable { executeTask(task); Unit })
@@ -61,9 +62,9 @@ class TaskExecutor() {
}
}
}
if (!canceled)
if (canceled || Thread.interrupted())
taskListener?.onTerminate()
}
}))
}
/**
@@ -91,9 +92,11 @@ class TaskExecutor() {
if (future != null)
workerQueue.add(future)
}
if (canceled)
return false
try {
counter.await()
return success.get()
return success.get() && !canceled
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
// Once interrupted, we are aborting the subscription.
@@ -110,9 +113,10 @@ class TaskExecutor() {
LOG.fine("Executing task: ${t.title}")
taskListener?.onReady(t)
val doDependentsSucceeded = executeTasks(t.dependents)
var flag = false
try {
if (!doDependentsSucceeded && t.reliant)
if (!doDependentsSucceeded && t.reliant || canceled)
throw SilentException()
t.execute()

View File

@@ -52,6 +52,7 @@ data class JavaVersion internal constructor(
}
}
@Throws(IOException::class)
fun fromExecutable(file: File): JavaVersion {
var platform = Platform.BIT_32
var version: String? = null
@@ -66,6 +67,7 @@ data class JavaVersion internal constructor(
platform = Platform.BIT_64
}
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
throw IOException("Java process is interrupted", e)
}
val thisVersion = version ?: throw IOException("Java version not matched")

View File

@@ -49,9 +49,9 @@ enum class OS {
ReflectionHelper.get<Long>(ManagementFactory.getOperatingSystemMXBean(), "getTotalPhysicalMemorySize") ?: 1024L
}
val SUGGESTED_MEMORY: Long by lazy {
val SUGGESTED_MEMORY: Int by lazy {
val memory = TOTAL_MEMORY / 1024 / 1024 / 4
Math.round(1.0 * memory / 128.0) * 128
(Math.round(1.0 * memory / 128.0) * 128).toInt()
}
val PATH_SEPARATOR: String = File.pathSeparator

View File

@@ -19,7 +19,7 @@ package org.jackhuang.hmcl
import org.jackhuang.hmcl.auth.OfflineAccount
import org.jackhuang.hmcl.download.DefaultDependencyManager
import org.jackhuang.hmcl.download.liteloader.LiteLoaderVersionList
import org.jackhuang.hmcl.download.LiteLoaderVersionList
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider
import org.jackhuang.hmcl.download.MojangDownloadProvider
import org.jackhuang.hmcl.game.DefaultGameRepository
@@ -50,7 +50,7 @@ class Test {
fun launch() {
val launcher = DefaultLauncher(
repository = repository,
version = repository.getVersion("test"),
versionId = "test",
account = OfflineAccount.fromUsername("player007").logIn(),
options = LaunchOptions(gameDir = repository.baseDirectory),
listener = object : ProcessListener {

View File

@@ -576,8 +576,8 @@ Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
If the Program specifies that a PROXY can decide which future
versions of the GNU General Public License can be used, that PROXY's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

Binary file not shown.