Launching progress

This commit is contained in:
huangyuhui
2017-08-19 21:51:46 +08:00
parent 27939c6b61
commit 383453f9af
28 changed files with 413 additions and 185 deletions

View File

@@ -17,27 +17,86 @@
*/ */
package org.jackhuang.hmcl.game package org.jackhuang.hmcl.game
import org.jackhuang.hmcl.auth.AuthenticationException
import org.jackhuang.hmcl.launch.DefaultLauncher
import org.jackhuang.hmcl.mod.CurseForgeModpackCompletionTask import org.jackhuang.hmcl.mod.CurseForgeModpackCompletionTask
import org.jackhuang.hmcl.setting.Settings import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Scheduler import org.jackhuang.hmcl.task.*
import org.jackhuang.hmcl.ui.Controllers
import org.jackhuang.hmcl.ui.DialogController
import org.jackhuang.hmcl.ui.LaunchingStepsPane
import org.jackhuang.hmcl.ui.runOnUiThread
object LauncherHelper { object LauncherHelper {
val launchingStepsPane = LaunchingStepsPane()
fun launch() { fun launch() {
val profile = Settings.selectedProfile val profile = Settings.selectedProfile
val repository = profile.repository val repository = profile.repository
val dependency = profile.dependency val dependency = profile.dependency
val account = Settings.selectedAccount ?: throw IllegalStateException("No account here") val account = Settings.selectedAccount ?: throw IllegalStateException("No account here")
val version = repository.getVersion(profile.selectedVersion) val version = repository.getVersion(profile.selectedVersion)
val launcher = HMCLGameLauncher( var finished = 0
repository = repository,
versionId = profile.selectedVersion,
options = profile.getVersionSetting(profile.selectedVersion).toLaunchOptions(profile.gameDir),
account = account.logIn(Settings.proxy)
)
dependency.checkGameCompletionAsync(version) Controllers.dialog(launchingStepsPane)
task(Scheduler.JAVAFX) { emitStatus(LoadingState.DEPENDENCIES) }
.then(dependency.checkGameCompletionAsync(version))
.then(task(Scheduler.JAVAFX) { emitStatus(LoadingState.MODS) })
.then(CurseForgeModpackCompletionTask(dependency, profile.selectedVersion)) .then(CurseForgeModpackCompletionTask(dependency, profile.selectedVersion))
.then(launcher.launchAsync())
.subscribe(Scheduler.JAVAFX) { println("lalala") } .then(task(Scheduler.JAVAFX) { emitStatus(LoadingState.LOGIN) })
.then(task {
try {
it["account"] = account.logIn(Settings.proxy)
} catch (e: AuthenticationException) {
it["account"] = DialogController.logIn(account)
runOnUiThread { Controllers.dialog(launchingStepsPane) }
}
})
.then(task(Scheduler.JAVAFX) { emitStatus(LoadingState.LAUNCHING) })
.then(task {
it["launcher"] = HMCLGameLauncher(
repository = repository,
versionId = profile.selectedVersion,
options = profile.getVersionSetting(profile.selectedVersion).toLaunchOptions(profile.gameDir),
account = it["account"]
)
})
.then { it.get<DefaultLauncher>("launcher").launchAsync() }
.then(task(Scheduler.JAVAFX) { emitStatus(LoadingState.DONE) })
.executor()
.apply {
taskListener = object : TaskListener {
override fun onFinished(task: Task) {
++finished
runOnUiThread { launchingStepsPane.pgsTasks.progress = 1.0 * finished / totTask.get() }
}
override fun onTerminate() {
runOnUiThread { Controllers.closeDialog() }
}
override fun end() {
runOnUiThread { Controllers.closeDialog() }
}
}
}.start()
}
fun emitStatus(state: LoadingState) {
launchingStepsPane.lblCurrentState.text = state.toString()
launchingStepsPane.lblSteps.text = "${state.ordinal + 1} / ${LoadingState.values().size}"
}
enum class LoadingState {
DEPENDENCIES,
MODS,
LOGIN,
LAUNCHING,
DONE
} }
} }

View File

@@ -23,10 +23,11 @@ import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount
import org.jackhuang.hmcl.task.FileDownloadTask import org.jackhuang.hmcl.task.FileDownloadTask
import org.jackhuang.hmcl.task.Scheduler import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.task.Task import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.ui.DialogController
import org.jackhuang.hmcl.util.toURL import org.jackhuang.hmcl.util.toURL
import java.net.Proxy import java.net.Proxy
object AccountSkin { object AccountHelper {
val SKIN_DIR = Main.APPDATA.resolve("skins") val SKIN_DIR = Main.APPDATA.resolve("skins")
fun loadSkins(proxy: Proxy = Settings.proxy) { fun loadSkins(proxy: Proxy = Settings.proxy) {
@@ -49,7 +50,7 @@ object AccountSkin {
override fun execute() { override fun execute() {
if (account.canLogIn && (account.selectedProfile == null || refresh)) if (account.canLogIn && (account.selectedProfile == null || refresh))
account.logIn(proxy) DialogController.logIn(account)
val profile = account.selectedProfile ?: return val profile = account.selectedProfile ?: return
val name = profile.name ?: return val name = profile.name ?: return
val url = "http://skins.minecraft.net/MinecraftSkins/$name.png" val url = "http://skins.minecraft.net/MinecraftSkins/$name.png"

View File

@@ -18,10 +18,8 @@
package org.jackhuang.hmcl.ui package org.jackhuang.hmcl.ui
import com.jfoenix.controls.JFXButton import com.jfoenix.controls.JFXButton
import com.jfoenix.controls.JFXCheckBox
import com.jfoenix.controls.JFXProgressBar import com.jfoenix.controls.JFXProgressBar
import com.jfoenix.controls.JFXRadioButton import com.jfoenix.controls.JFXRadioButton
import com.jfoenix.effects.JFXDepthManager
import javafx.beans.binding.Bindings import javafx.beans.binding.Bindings
import javafx.fxml.FXML import javafx.fxml.FXML
import javafx.geometry.Rectangle2D import javafx.geometry.Rectangle2D
@@ -30,13 +28,15 @@ import javafx.scene.control.ToggleGroup
import javafx.scene.effect.BlurType import javafx.scene.effect.BlurType
import javafx.scene.effect.DropShadow import javafx.scene.effect.DropShadow
import javafx.scene.image.ImageView import javafx.scene.image.ImageView
import javafx.scene.layout.HBox
import javafx.scene.layout.Pane import javafx.scene.layout.Pane
import javafx.scene.layout.StackPane import javafx.scene.layout.StackPane
import javafx.scene.layout.VBox import javafx.scene.layout.VBox
import javafx.scene.paint.Color import javafx.scene.paint.Color
import org.jackhuang.hmcl.auth.Account import org.jackhuang.hmcl.auth.Account
import org.jackhuang.hmcl.auth.OfflineAccount
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount
import org.jackhuang.hmcl.setting.AccountSkin import org.jackhuang.hmcl.setting.AccountHelper
import org.jackhuang.hmcl.setting.Settings import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Scheduler import org.jackhuang.hmcl.task.Scheduler
import java.util.concurrent.Callable import java.util.concurrent.Callable
@@ -53,6 +53,7 @@ class AccountItem(i: Int, val account: Account, group: ToggleGroup) : StackPane(
@FXML lateinit var lblType: Label @FXML lateinit var lblType: Label
@FXML lateinit var pgsSkin: JFXProgressBar @FXML lateinit var pgsSkin: JFXProgressBar
@FXML lateinit var portraitView: ImageView @FXML lateinit var portraitView: ImageView
@FXML lateinit var buttonPane: HBox
init { init {
loadFXML("/assets/fxml/account-item.fxml") loadFXML("/assets/fxml/account-item.fxml")
@@ -78,11 +79,19 @@ class AccountItem(i: Int, val account: Account, group: ToggleGroup) : StackPane(
lblUser.text = account.username lblUser.text = account.username
lblType.text = accountType(account) lblType.text = accountType(account)
if (account is YggdrasilAccount) if (account is YggdrasilAccount) {
btnRefresh.setOnMouseClicked { btnRefresh.setOnMouseClicked {
pgsSkin.isVisible = true pgsSkin.isVisible = true
AccountSkin.refreshSkinAsync(account).subscribe(Scheduler.JAVAFX) { loadSkin() } AccountHelper.refreshSkinAsync(account)
.subscribe(Scheduler.JAVAFX) { loadSkin() }
} }
AccountHelper.loadSkinAsync(account)
.subscribe(Scheduler.JAVAFX) { loadSkin() }
}
if (account is OfflineAccount) { // Offline Account cannot be refreshed,
buttonPane.children -= btnRefresh
}
} }
fun loadSkin() { fun loadSkin() {
@@ -91,7 +100,7 @@ class AccountItem(i: Int, val account: Account, group: ToggleGroup) : StackPane(
pgsSkin.isVisible = false pgsSkin.isVisible = false
val size = 8.0 * 4 val size = 8.0 * 4
portraitView.viewport = Rectangle2D(size, size, size, size) portraitView.viewport = Rectangle2D(size, size, size, size)
portraitView.image = AccountSkin.getSkin(account, 4.0) portraitView.image = AccountHelper.getSkin(account, 4.0)
portraitView.fitHeight = 32.0 portraitView.fitHeight = 32.0
portraitView.fitWidth = 32.0 portraitView.fitWidth = 32.0
} }

View File

@@ -32,7 +32,6 @@ import org.jackhuang.hmcl.auth.Account
import org.jackhuang.hmcl.auth.OfflineAccount import org.jackhuang.hmcl.auth.OfflineAccount
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount
import org.jackhuang.hmcl.i18n import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.setting.AccountSkin
import org.jackhuang.hmcl.setting.Settings import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Scheduler import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.task.taskResult import org.jackhuang.hmcl.task.taskResult
@@ -117,11 +116,6 @@ class AccountsPage() : StackPane(), DecoratorPage {
Settings.deleteAccount(account.username) Settings.deleteAccount(account.username)
Platform.runLater(this@AccountsPage::loadAccounts) Platform.runLater(this@AccountsPage::loadAccounts)
} }
if (account is YggdrasilAccount)
AccountSkin.loadSkinAsync(account).subscribe(Scheduler.JAVAFX) {
loadSkin()
}
} }
} }
@@ -136,6 +130,7 @@ class AccountsPage() : StackPane(), DecoratorPage {
val username = txtUsername.text val username = txtUsername.text
val password = txtPassword.text val password = txtPassword.text
progressBar.isVisible = true progressBar.isVisible = true
lblCreationWarning.text = ""
taskResult("create_account") { taskResult("create_account") {
try { try {
val account = when (type) { val account = when (type) {

View File

@@ -17,9 +17,11 @@
*/ */
package org.jackhuang.hmcl.ui package org.jackhuang.hmcl.ui
import com.jfoenix.controls.JFXDialog
import javafx.fxml.FXMLLoader import javafx.fxml.FXMLLoader
import javafx.scene.Node import javafx.scene.Node
import javafx.scene.Scene import javafx.scene.Scene
import javafx.scene.layout.Region
import javafx.stage.Stage import javafx.stage.Stage
import org.jackhuang.hmcl.Main import org.jackhuang.hmcl.Main
@@ -32,7 +34,6 @@ object Controllers {
val versionPane = VersionPage() val versionPane = VersionPage()
lateinit var leftPaneController: LeftPaneController lateinit var leftPaneController: LeftPaneController
lateinit var sidePaneController: SidePaneController
lateinit var decorator: Decorator lateinit var decorator: Decorator
@@ -42,7 +43,6 @@ object Controllers {
decorator = Decorator(stage, mainPane, Main.TITLE, max = false) decorator = Decorator(stage, mainPane, Main.TITLE, max = false)
decorator.showPage(null) decorator.showPage(null)
leftPaneController = LeftPaneController(decorator.leftPane) leftPaneController = LeftPaneController(decorator.leftPane)
sidePaneController = SidePaneController(decorator.sidePane, decorator.drawer)
decorator.isCustomMaximize = false decorator.isCustomMaximize = false
@@ -54,6 +54,14 @@ object Controllers {
stage.minHeight = 480.0 stage.minHeight = 480.0
} }
fun dialog(content: Region): JFXDialog {
return decorator.showDialog(content)
}
fun closeDialog() {
decorator.dialog.close()
}
fun navigate(node: Node?) { fun navigate(node: Node?) {
decorator.showPage(node) decorator.showPage(node)
} }

View File

@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui package org.jackhuang.hmcl.ui
import com.jfoenix.controls.JFXButton import com.jfoenix.controls.JFXButton
import com.jfoenix.controls.JFXDialog
import com.jfoenix.controls.JFXDrawer import com.jfoenix.controls.JFXDrawer
import com.jfoenix.controls.JFXHamburger import com.jfoenix.controls.JFXHamburger
import com.jfoenix.effects.JFXDepthManager import com.jfoenix.effects.JFXDepthManager
@@ -64,6 +65,7 @@ class Decorator @JvmOverloads constructor(private val primaryStage: Stage, priva
private var allowMove: Boolean = false private var allowMove: Boolean = false
private var isDragging: Boolean = false private var isDragging: Boolean = false
private var windowDecoratorAnimation: Timeline? = null private var windowDecoratorAnimation: Timeline? = null
private var dialogShown = false
@FXML lateinit var contentPlaceHolder: StackPane @FXML lateinit var contentPlaceHolder: StackPane
@FXML lateinit var drawerWrapper: StackPane @FXML lateinit var drawerWrapper: StackPane
@FXML lateinit var titleContainer: BorderPane @FXML lateinit var titleContainer: BorderPane
@@ -78,9 +80,9 @@ class Decorator @JvmOverloads constructor(private val primaryStage: Stage, priva
@FXML lateinit var lblTitle: Label @FXML lateinit var lblTitle: Label
@FXML lateinit var leftPane: AdvancedListBox @FXML lateinit var leftPane: AdvancedListBox
@FXML lateinit var drawer: JFXDrawer @FXML lateinit var drawer: JFXDrawer
@FXML lateinit var sidePane: AdvancedListBox
@FXML lateinit var titleBurgerContainer: StackPane @FXML lateinit var titleBurgerContainer: StackPane
@FXML lateinit var titleBurger: JFXHamburger @FXML lateinit var titleBurger: JFXHamburger
@FXML lateinit var dialog: JFXDialog
private val onCloseButtonActionProperty: ObjectProperty<Runnable> = SimpleObjectProperty(Runnable { Main.stop() }) private val onCloseButtonActionProperty: ObjectProperty<Runnable> = SimpleObjectProperty(Runnable { Main.stop() })
@JvmName("onCloseButtonActionProperty") get @JvmName("onCloseButtonActionProperty") get
@@ -125,6 +127,11 @@ class Decorator @JvmOverloads constructor(private val primaryStage: Stage, priva
} }
} }
drawerWrapper.children -= dialog
dialog.dialogContainer = drawerWrapper
dialog.setOnDialogClosed { dialogShown = false }
dialog.setOnDialogOpened { dialogShown = true }
if (!min) buttonsContainer.children.remove(btnMin) if (!min) buttonsContainer.children.remove(btnMin)
if (!max) buttonsContainer.children.remove(btnMax) if (!max) buttonsContainer.children.remove(btnMax)
@@ -136,25 +143,6 @@ class Decorator @JvmOverloads constructor(private val primaryStage: Stage, priva
setOverflowHidden(lookup("#contentPlaceHolderRoot") as Pane) setOverflowHidden(lookup("#contentPlaceHolderRoot") as Pane)
setOverflowHidden(drawerWrapper) setOverflowHidden(drawerWrapper)
// init the title hamburger icon
drawer.setOnDrawerOpening {
val animation = titleBurger.getAnimation()
animation.setRate(1.0)
animation.play()
}
drawer.setOnDrawerClosing {
val animation = titleBurger.getAnimation()
animation.setRate(-1.0)
animation.play()
}
titleBurgerContainer.setOnMouseClicked({
if (drawer.isHidden || drawer.isHiding) {
drawer.open()
} else {
drawer.close()
}
})
} }
fun onMouseMoved(mouseEvent: MouseEvent) { fun onMouseMoved(mouseEvent: MouseEvent) {
@@ -423,6 +411,13 @@ class Decorator @JvmOverloads constructor(private val primaryStage: Stage, priva
} }
} }
fun showDialog(content: Region): JFXDialog {
dialog.content = content
if (!dialogShown)
dialog.show()
return dialog
}
fun startWizard(wizardProvider: WizardProvider, category: String? = null) { fun startWizard(wizardProvider: WizardProvider, category: String? = null) {
this.category = category this.category = category
wizardController.provider = wizardProvider wizardController.provider = wizardProvider

View File

@@ -0,0 +1,52 @@
/*
* 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 org.jackhuang.hmcl.auth.Account
import org.jackhuang.hmcl.auth.AuthInfo
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount
import org.jackhuang.hmcl.task.SilentException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicReference
object DialogController {
fun logIn(account: Account): AuthInfo? {
if (account is YggdrasilAccount) {
val latch = CountDownLatch(1)
val res = AtomicReference<AuthInfo>(null)
runOnUiThread {
val pane = YggdrasilAccountLoginPane(account, success = {
res.set(it)
latch.countDown()
Controllers.closeDialog()
}, failed = {
latch.countDown()
Controllers.closeDialog()
})
pane.dialog = Controllers.dialog(pane)
}
latch.await()
if (res.get() == null)
throw SilentException()
else
return res.get()
}
return null
}
}

View File

@@ -98,9 +98,7 @@ fun ListView<*>.smoothScrolling() {
fun ScrollPane.smoothScrolling() = JFXScrollPane.smoothScrolling(this) fun ScrollPane.smoothScrolling() = JFXScrollPane.smoothScrolling(this)
fun runOnUiThread(runnable: () -> Unit) = { fun runOnUiThread(runnable: () -> Unit) = JFXUtilities.runInFX(runnable)
JFXUtilities.runInFX(runnable)
}
fun takeSnapshot(node: Parent, width: Double, height: Double): WritableImage { fun takeSnapshot(node: Parent, width: Double, height: Double): WritableImage {
val scene = Scene(node, width, height) val scene = Scene(node, width, height)

View File

@@ -17,9 +17,20 @@
*/ */
package org.jackhuang.hmcl.ui package org.jackhuang.hmcl.ui
import com.jfoenix.controls.JFXDrawer import com.jfoenix.controls.JFXProgressBar
import javafx.fxml.FXML
import javafx.scene.control.Label
import javafx.scene.layout.StackPane
class SidePaneController(sidePane: AdvancedListBox, drawer: JFXDrawer) { class LaunchingStepsPane(): StackPane() {
@FXML lateinit var pgsTasks: JFXProgressBar
@FXML lateinit var lblCurrentState: Label
@FXML lateinit var lblSteps: Label
init { init {
loadFXML("/assets/fxml/launching-steps.fxml")
limitHeight(200.0)
limitWidth(400.0)
} }
} }

View File

@@ -0,0 +1,80 @@
/*
* 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.JFXDialog
import com.jfoenix.controls.JFXPasswordField
import com.jfoenix.controls.JFXProgressBar
import javafx.fxml.FXML
import javafx.scene.control.Label
import javafx.scene.layout.StackPane
import org.jackhuang.hmcl.auth.AuthInfo
import org.jackhuang.hmcl.auth.yggdrasil.InvalidCredentialsException
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount
import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.task.taskResult
class YggdrasilAccountLoginPane(private val oldAccount: YggdrasilAccount, private val success: (AuthInfo) -> Unit, private val failed: () -> Unit) : StackPane() {
@FXML lateinit var lblUsername: Label
@FXML lateinit var txtPassword: JFXPasswordField
@FXML lateinit var lblCreationWarning: Label
@FXML lateinit var progressBar: JFXProgressBar
lateinit var dialog: JFXDialog
init {
loadFXML("/assets/fxml/yggdrasil-account-login.fxml")
lblUsername.text = oldAccount.username
txtPassword.setOnAction {
onAccept()
}
}
fun onAccept() {
val username = oldAccount.username
val password = txtPassword.text
progressBar.isVisible = true
lblCreationWarning.text = ""
taskResult("login") {
try {
val account = YggdrasilAccount.fromUsername(username, password)
account.logIn(Settings.proxy)
} catch (e: Exception) {
e
}
}.subscribe(Scheduler.JAVAFX) {
val account: Any = it["login"]
if (account is AuthInfo) {
success(account)
dialog.close()
} else if (account is InvalidCredentialsException) {
lblCreationWarning.text = i18n("login.wrong_password")
} else if (account is Exception) {
lblCreationWarning.text = account.javaClass.toString() + ": " + account.localizedMessage
}
progressBar.isVisible = false
}
}
fun onCancel() {
failed()
dialog.close()
}
}

View File

@@ -31,7 +31,7 @@
<StackPane fx:id="body" style="-fx-background-radius: 0 0 2 2; -fx-background-color: rgb(255,255,255,0.87); -fx-padding: 8;" minHeight="40"> <StackPane fx:id="body" style="-fx-background-radius: 0 0 2 2; -fx-background-color: rgb(255,255,255,0.87); -fx-padding: 8;" minHeight="40">
<BorderPane> <BorderPane>
<left> <left>
<HBox spacing="8"> <HBox fx:id="buttonPane" spacing="8">
<JFXButton fx:id="btnRefresh" styleClass="toggle-icon4" maxWidth="30" maxHeight="30" minWidth="30" minHeight="30" prefWidth="30" prefHeight="30" /> <JFXButton fx:id="btnRefresh" styleClass="toggle-icon4" maxWidth="30" maxHeight="30" minWidth="30" minHeight="30" prefWidth="30" prefHeight="30" />
<JFXButton fx:id="btnDelete" styleClass="toggle-icon4" maxWidth="30" maxHeight="30" minWidth="30" minHeight="30" prefWidth="30" prefHeight="30" /> <JFXButton fx:id="btnDelete" styleClass="toggle-icon4" maxWidth="30" maxHeight="30" minWidth="30" minHeight="30" prefWidth="30" prefHeight="30" />
</HBox> </HBox>

View File

@@ -29,7 +29,7 @@
</JFXButton> </JFXButton>
</AnchorPane> </AnchorPane>
<JFXDialog fx:id="dialog" transitionType="CENTER"> <JFXDialog fx:id="dialog">
<StackPane> <StackPane>
<JFXDialogLayout> <JFXDialogLayout>
<heading> <heading>

View File

@@ -6,8 +6,8 @@
<?import javafx.scene.control.Label?> <?import javafx.scene.control.Label?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<?import javafx.scene.shape.Rectangle?> <?import javafx.scene.shape.Rectangle?>
<?import java.lang.String?>
<?import org.jackhuang.hmcl.ui.AdvancedListBox?> <?import org.jackhuang.hmcl.ui.AdvancedListBox?>
<?import java.lang.String?>
<fx:root xmlns="http://javafx.com/javafx" <fx:root xmlns="http://javafx.com/javafx"
type="StackPane" type="StackPane"
xmlns:fx="http://javafx.com/fxml"> xmlns:fx="http://javafx.com/fxml">
@@ -18,42 +18,36 @@
<BorderPane> <BorderPane>
<center> <center>
<StackPane fx:id="drawerWrapper"> <StackPane fx:id="drawerWrapper">
<JFXDrawer fx:id="drawer" defaultDrawerSize="200" direction="LEFT"> <JFXDialog fx:id="dialog" overlayClose="false" />
<styleClass> <BorderPane>
<String fx:value="body"/> <left>
</styleClass> <StackPane minWidth="200" maxWidth="200" styleClass="jfx-decorator-content-container">
<sidePane> <BorderPane fx:id="leftRootPane">
<AdvancedListBox fx:id="sidePane" /> <center>
</sidePane> <BorderPane>
<BorderPane> <center>
<left> <AdvancedListBox fx:id="leftPane"/>
<StackPane minWidth="200" maxWidth="200" styleClass="jfx-decorator-content-container"> </center>
<BorderPane fx:id="leftRootPane"> </BorderPane>
<center> </center>
<BorderPane> <right>
<center> <Rectangle height="${leftRootPane.height}" width="1" fill="gray"/>
<AdvancedListBox fx:id="leftPane" /> </right>
</center> </BorderPane>
</BorderPane> </StackPane>
</center> </left>
<right> <center>
<Rectangle height="${leftRootPane.height}" width="1" fill="gray"/> <StackPane fx:id="contentPlaceHolderRoot" styleClass="jfx-decorator-content-container"
</right> VBox.vgrow="ALWAYS">
</BorderPane> <StackPane fx:id="contentPlaceHolder" styleClass="jfx-decorator-content-container">
<styleClass>
<String fx:value="jfx-decorator-content-container"/>
</styleClass>
<!-- Node -->
</StackPane> </StackPane>
</left> </StackPane>
<center> </center>
<StackPane fx:id="contentPlaceHolderRoot" styleClass="jfx-decorator-content-container" VBox.vgrow="ALWAYS"> </BorderPane>
<StackPane fx:id="contentPlaceHolder" styleClass="jfx-decorator-content-container">
<styleClass>
<String fx:value="jfx-decorator-content-container"/>
</styleClass>
<!-- Node -->
</StackPane>
</StackPane>
</center>
</BorderPane>
</JFXDrawer>
</StackPane> </StackPane>
</center> </center>
<top> <top>
@@ -65,18 +59,8 @@
<left> <left>
<BorderPane minWidth="200" maxWidth="200" fx:id="titleWrapper"> <BorderPane minWidth="200" maxWidth="200" fx:id="titleWrapper">
<center> <center>
<HBox> <Label fx:id="lblTitle" BorderPane.alignment="CENTER" mouseTransparent="true"
<JFXRippler maskType="CIRCLE" style="-fx-ripple-color:WHITE;"> style="-fx-background-color: transparent; -fx-text-fill: white; -fx-font-size: 15px;"/>
<StackPane fx:id="titleBurgerContainer" maxWidth="30" maxHeight="30" minWidth="30" minHeight="30" prefWidth="30" prefHeight="30">
<JFXHamburger fx:id="titleBurger">
<HamburgerBackArrowBasicTransition/>
</JFXHamburger>
</StackPane>
</JFXRippler>
<Label fx:id="lblTitle" BorderPane.alignment="CENTER"
mouseTransparent="true"
style="-fx-background-color: transparent; -fx-text-fill: white; -fx-font-size: 15px;"/>
</HBox>
</center> </center>
<right> <right>
<Rectangle height="${navBar.height}" width="1" fill="gray"/> <Rectangle height="${navBar.height}" width="1" fill="gray"/>
@@ -87,7 +71,8 @@
<BorderPane fx:id="navBar"> <BorderPane fx:id="navBar">
<left> <left>
<HBox fx:id="navLeft" alignment="CENTER_LEFT" style="-fx-padding: 0 5 0 5;"> <HBox fx:id="navLeft" alignment="CENTER_LEFT" style="-fx-padding: 0 5 0 5;">
<JFXButton fx:id="backNavButton" onMouseClicked="#onBack" styleClass="jfx-decorator-button" ripplerFill="white"> <JFXButton fx:id="backNavButton" onMouseClicked="#onBack"
styleClass="jfx-decorator-button" ripplerFill="white">
<graphic> <graphic>
<fx:include source="/assets/svg/arrow-left.fxml"/> <fx:include source="/assets/svg/arrow-left.fxml"/>
</graphic> </graphic>
@@ -97,12 +82,14 @@
</left> </left>
<right> <right>
<HBox fx:id="navRight" alignment="CENTER_LEFT"> <HBox fx:id="navRight" alignment="CENTER_LEFT">
<JFXButton fx:id="refreshNavButton" onMouseClicked="#onRefresh" styleClass="jfx-decorator-button" ripplerFill="white"> <JFXButton fx:id="refreshNavButton" onMouseClicked="#onRefresh"
styleClass="jfx-decorator-button" ripplerFill="white">
<graphic> <graphic>
<fx:include source="/assets/svg/refresh.fxml"/> <fx:include source="/assets/svg/refresh.fxml"/>
</graphic> </graphic>
</JFXButton> </JFXButton>
<JFXButton fx:id="closeNavButton" onMouseClicked="#onCloseNav" styleClass="jfx-decorator-button" ripplerFill="white"> <JFXButton fx:id="closeNavButton" onMouseClicked="#onCloseNav"
styleClass="jfx-decorator-button" ripplerFill="white">
<graphic> <graphic>
<fx:include source="/assets/svg/close.fxml"/> <fx:include source="/assets/svg/close.fxml"/>
</graphic> </graphic>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>
<?import com.jfoenix.controls.JFXProgressBar?>
<?import javafx.scene.control.Label?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
<JFXProgressBar fx:id="pgsTasks" StackPane.alignment="TOP_CENTER" />
<VBox alignment="CENTER">
<Label fx:id="lblCurrentState" style="-fx-font-size: 20px;" />
<Label fx:id="lblSteps" style="-fx-font-size: 14px;" />
</VBox>
</fx:root>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.jfoenix.controls.*?>
<?import com.jfoenix.validation.RequiredFieldValidator?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.*?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
<JFXDialogLayout>
<heading>
<Label text="%ui.message.enter_password" />
</heading>
<body>
<VBox spacing="15" style="-fx-padding: 15 0 0 0;">
<Label fx:id="lblUsername" />
<JFXPasswordField fx:id="txtPassword" promptText="%ui.label.password" labelFloat="true">
<validators>
<RequiredFieldValidator message="Input Required!">
</RequiredFieldValidator>
</validators>
</JFXPasswordField>
</VBox>
</body>
<actions>
<Label fx:id="lblCreationWarning" />
<JFXButton onMouseClicked="#onAccept" text="%button.ok" styleClass="dialog-accept"/>
<JFXButton onMouseClicked="#onCancel" text="%button.cancel" styleClass="dialog-cancel"/>
</actions>
</JFXDialogLayout>
<JFXProgressBar fx:id="progressBar" visible="false" StackPane.alignment="TOP_CENTER"/>
</fx:root>

View File

@@ -167,8 +167,8 @@ ui.label.version=版本
ui.label.password=密碼 ui.label.password=密碼
ui.label.profile=配置 ui.label.profile=配置
ui.message.first_load=在左邊輸入您的帳號 ui.message.first_load=請輸入您的帳號
ui.message.enter_password=在左邊輸入您的密碼 ui.message.enter_password=請輸入您的密碼
ui.message.launching=啟動中 ui.message.launching=啟動中
ui.message.making=生成中 ui.message.making=生成中
ui.message.sure_remove=真的要刪除配置%s嗎 ui.message.sure_remove=真的要刪除配置%s嗎

View File

@@ -167,8 +167,8 @@ ui.label.version=版本
ui.label.password=密码 ui.label.password=密码
ui.label.profile=配置 ui.label.profile=配置
ui.message.first_load=在左边输入您的账号 ui.message.first_load=请输入您的账号
ui.message.enter_password=在左边输入您的密码 ui.message.enter_password=请输入您的密码
ui.message.launching=启动中 ui.message.launching=启动中
ui.message.making=生成中 ui.message.making=生成中
ui.message.sure_remove=真的要删除配置%s吗 ui.message.sure_remove=真的要删除配置%s吗

View File

@@ -43,20 +43,14 @@ class DefaultDependencyManager(override val repository: DefaultGameRepository, o
override fun installLibraryAsync(gameVersion: String, version: Version, libraryId: String, libraryVersion: String): Task { override fun installLibraryAsync(gameVersion: String, version: Version, libraryId: String, libraryVersion: String): Task {
if (libraryId == "forge") if (libraryId == "forge")
return ForgeInstallTask(this, gameVersion, version, libraryVersion) then { task -> return ForgeInstallTask(this, gameVersion, version, libraryVersion)
val newVersion = task.result!! .then { VersionJSONSaveTask(this, it["version"]) }
VersionJSONSaveTask(this@DefaultDependencyManager, newVersion)
}
else if (libraryId == "liteloader") else if (libraryId == "liteloader")
return LiteLoaderInstallTask(this, gameVersion, version, libraryVersion) then { task -> return LiteLoaderInstallTask(this, gameVersion, version, libraryVersion)
val newVersion = task.result!! .then { VersionJSONSaveTask(this, it["version"]) }
VersionJSONSaveTask(this@DefaultDependencyManager, newVersion)
}
else if (libraryId == "optifine") else if (libraryId == "optifine")
return OptiFineInstallTask(this, gameVersion, version, libraryVersion) then { task -> return OptiFineInstallTask(this, gameVersion, version, libraryVersion)
val newVersion = task.result!! .then { VersionJSONSaveTask(this, it["version"]) }
VersionJSONSaveTask(this@DefaultDependencyManager, newVersion)
}
else else
throw IllegalArgumentException("Library id $libraryId is unrecognized.") throw IllegalArgumentException("Library id $libraryId is unrecognized.")
} }

View File

@@ -28,53 +28,48 @@ class DefaultGameBuilder(val dependencyManager: DefaultDependencyManager): GameB
override fun buildAsync(): Task { override fun buildAsync(): Task {
val gameVersion = gameVersion val gameVersion = gameVersion
return VersionJSONDownloadTask(gameVersion = gameVersion) then a@{ task -> return VersionJSONDownloadTask(gameVersion, dependencyManager, "raw_version_json")
var version = GSON.fromJson<Version>(task.result!!) ?: return@a null .then {
version = version.copy(id = name, jar = null) var version = GSON.fromJson<Version>(it["raw_version_json"])!!
var result = ParallelTask( it["version"] = version
GameAssetDownloadTask(dependencyManager, version), version = version.copy(id = name, jar = null)
GameLoggingDownloadTask(dependencyManager, version), var result = ParallelTask(
GameDownloadTask(version), GameAssetDownloadTask(dependencyManager, version),
GameLibrariesTask(dependencyManager, version) // Game libraries will be downloaded for multiple times partly, this time is for vanilla libraries. GameLoggingDownloadTask(dependencyManager, version),
) then VersionJSONSaveTask(dependencyManager, version) GameDownloadTask(version),
GameLibrariesTask(dependencyManager, version) // Game libraries will be downloaded for multiple times partly, this time is for vanilla libraries.
) then VersionJSONSaveTask(dependencyManager, version)
if (toolVersions.containsKey("forge")) if (toolVersions.containsKey("forge"))
result = result then libraryTaskHelper(gameVersion, version, "forge") result = result then libraryTaskHelper(gameVersion, "forge")
if (toolVersions.containsKey("liteloader")) if (toolVersions.containsKey("liteloader"))
result = result then libraryTaskHelper(gameVersion, version, "liteloader") result = result then libraryTaskHelper(gameVersion, "liteloader")
if (toolVersions.containsKey("optifine")) if (toolVersions.containsKey("optifine"))
result = result then libraryTaskHelper(gameVersion, version, "optifine") result = result then libraryTaskHelper(gameVersion, "optifine")
result result
} }
} }
private fun libraryTaskHelper(gameVersion: String, version: Version, libraryId: String): Task.(Task) -> Task = { prev -> private fun libraryTaskHelper(gameVersion: String, libraryId: String): (AutoTypingMap<String>) -> Task = {
var thisVersion = version dependencyManager.installLibraryAsync(gameVersion, it["version"], libraryId, toolVersions[libraryId]!!)
if (prev is TaskResult<*> && prev.result is Version) {
thisVersion = prev.result as Version
}
dependencyManager.installLibraryAsync(gameVersion, thisVersion, libraryId, toolVersions[libraryId]!!)
} }
inner class VersionJSONDownloadTask(val gameVersion: String): Task() { private class VersionJSONDownloadTask(val gameVersion: String, val dependencyManager: DefaultDependencyManager, val id: String): Task() {
override val dependents: MutableCollection<Task> = LinkedList() override val dependents: MutableCollection<Task> = LinkedList()
override val dependencies: MutableCollection<Task> = LinkedList() override val dependencies: MutableCollection<Task> = LinkedList()
var httpTask: GetTask? = null
val result: String? get() = httpTask?.result
val gameVersionList: VersionList<*> = dependencyManager.getVersionList("game") private val gameVersionList: VersionList<*> = dependencyManager.getVersionList("game")
init { init {
if (!gameVersionList.loaded) if (!gameVersionList.loaded)
dependents += gameVersionList.refreshAsync(downloadProvider) dependents += gameVersionList.refreshAsync(dependencyManager.downloadProvider)
} }
override fun execute() { override fun execute() {
val remoteVersion = gameVersionList.getVersions(gameVersion).firstOrNull() val remoteVersion = gameVersionList.getVersions(gameVersion).firstOrNull()
?: throw Error("Cannot find specific version $gameVersion in remote repository") ?: throw Error("Cannot find specific version $gameVersion in remote repository")
val jsonURL = downloadProvider.injectURL(remoteVersion.url) val jsonURL = dependencyManager.downloadProvider.injectURL(remoteVersion.url)
httpTask = GetTask(jsonURL.toURL(), proxy = dependencyManager.proxy) dependencies += GetTask(jsonURL.toURL(), proxy = dependencyManager.proxy, id = id)
dependencies += httpTask!!
} }
} }

View File

@@ -38,11 +38,11 @@ class ForgeInstallTask(private val dependencyManager: DefaultDependencyManager,
lateinit var remote: RemoteVersion<*> lateinit var remote: RemoteVersion<*>
override val dependents = mutableListOf<Task>() override val dependents = mutableListOf<Task>()
override val dependencies = mutableListOf<Task>() override val dependencies = mutableListOf<Task>()
override val id = ID override val id = "version"
init { init {
if (!forgeVersionList.loaded) if (!forgeVersionList.loaded)
dependents += forgeVersionList.refreshAsync(dependencyManager.downloadProvider) then { dependents += forgeVersionList.refreshAsync(dependencyManager.downloadProvider).then {
remote = forgeVersionList.getVersion(gameVersion, remoteVersion) ?: throw IllegalArgumentException("Remote forge version $gameVersion, $remoteVersion not found") remote = forgeVersionList.getVersion(gameVersion, remoteVersion) ?: throw IllegalArgumentException("Remote forge version $gameVersion, $remoteVersion not found")
FileDownloadTask(remote.url.toURL(), installer) FileDownloadTask(remote.url.toURL(), installer)
} }
@@ -76,8 +76,4 @@ class ForgeInstallTask(private val dependencyManager: DefaultDependencyManager,
check(installer.delete(), { "Unable to delete installer file $installer" }) check(installer.delete(), { "Unable to delete installer file $installer" })
} }
companion object {
const val ID = "forge_install_task"
}
} }

View File

@@ -37,11 +37,11 @@ class LiteLoaderInstallTask(private val dependencyManager: DefaultDependencyMana
lateinit var remote: RemoteVersion<LiteLoaderRemoteVersionTag> lateinit var remote: RemoteVersion<LiteLoaderRemoteVersionTag>
override val dependents = mutableListOf<Task>() override val dependents = mutableListOf<Task>()
override val dependencies = mutableListOf<Task>() override val dependencies = mutableListOf<Task>()
override val id = ID override val id = "version"
init { init {
if (!liteLoaderVersionList.loaded) if (!liteLoaderVersionList.loaded)
dependents += LiteLoaderVersionList.refreshAsync(dependencyManager.downloadProvider) then { dependents += LiteLoaderVersionList.refreshAsync(dependencyManager.downloadProvider).then {
remote = liteLoaderVersionList.getVersion(gameVersion, remoteVersion) ?: throw IllegalArgumentException("Remote LiteLoader version $gameVersion, $remoteVersion not found") remote = liteLoaderVersionList.getVersion(gameVersion, remoteVersion) ?: throw IllegalArgumentException("Remote LiteLoader version $gameVersion, $remoteVersion not found")
null null
} }
@@ -70,8 +70,4 @@ class LiteLoaderInstallTask(private val dependencyManager: DefaultDependencyMana
) )
dependencies += GameLibrariesTask(dependencyManager, tempVersion) dependencies += GameLibrariesTask(dependencyManager, tempVersion)
} }
companion object {
const val ID = "lite_loader_install_task"
}
} }

View File

@@ -34,7 +34,7 @@ class OptiFineInstallTask(private val dependencyManager: DefaultDependencyManage
lateinit var remote: RemoteVersion<*> lateinit var remote: RemoteVersion<*>
override val dependents = mutableListOf<Task>() override val dependents = mutableListOf<Task>()
override val dependencies = mutableListOf<Task>() override val dependencies = mutableListOf<Task>()
override val id = ID override val id = "version"
init { init {
if (!optiFineVersionList.loaded) if (!optiFineVersionList.loaded)
@@ -75,8 +75,4 @@ class OptiFineInstallTask(private val dependencyManager: DefaultDependencyManage
result = version.copy(libraries = merge(version.libraries, libraries), mainClass = mainClass, minecraftArguments = arg) result = version.copy(libraries = merge(version.libraries, libraries), mainClass = mainClass, minecraftArguments = arg)
dependencies += GameLibrariesTask(dependencyManager, version.copy(libraries = libraries)) dependencies += GameLibrariesTask(dependencyManager, version.copy(libraries = libraries))
} }
companion object {
const val ID = "optifine_install_task"
}
} }

View File

@@ -17,14 +17,16 @@
*/ */
package org.jackhuang.hmcl.task package org.jackhuang.hmcl.task
internal class CoupleTask<P: Task>(private val pred: P, private val succ: Task.(P) -> Task?, override val reliant: Boolean) : Task() { import org.jackhuang.hmcl.util.AutoTypingMap
internal class CoupleTask<P: Task>(pred: P, private val succ: (AutoTypingMap<String>) -> Task?, override val reliant: Boolean) : Task() {
override val hidden: Boolean = true override val hidden: Boolean = true
override val dependents: Collection<Task> = listOf(pred) override val dependents: Collection<Task> = listOf(pred)
override val dependencies: MutableCollection<Task> = mutableListOf() override val dependencies: MutableCollection<Task> = mutableListOf()
override fun execute() { override fun execute() {
val task = this.succ(pred) val task = this.succ(variables!!)
if (task != null) if (task != null)
dependencies += task dependencies += task
} }
@@ -33,9 +35,9 @@ internal class CoupleTask<P: Task>(private val pred: P, private val succ: Task.(
/** /**
* @param b A runnable that decides what to do next, You can also do something here. * @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 <T: Task> T.then(b: (AutoTypingMap<String>) -> Task?): Task = CoupleTask(this, b, true)
/** /**
* @param b A runnable that decides what to do next, You can also do something here. * @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) infix fun <T: Task> T.with(b: (AutoTypingMap<String>) -> Task?): Task = CoupleTask(this, b, false)

View File

@@ -25,9 +25,8 @@ import java.net.Proxy
import java.net.URL import java.net.URL
import java.nio.charset.Charset import java.nio.charset.Charset
class GetTask @JvmOverloads constructor(val url: URL, val encoding: Charset = Charsets.UTF_8, private val retry: Int = 5, private val proxy: Proxy = Proxy.NO_PROXY): TaskResult<String>() { class GetTask @JvmOverloads constructor(val url: URL, val encoding: Charset = Charsets.UTF_8, private val retry: Int = 5, private val proxy: Proxy = Proxy.NO_PROXY, override val id: String = ID): TaskResult<String>() {
override val scheduler: Scheduler = Scheduler.IO override val scheduler: Scheduler = Scheduler.IO
override val id = ID
override fun execute() { override fun execute() {
var exception: IOException? = null var exception: IOException? = null

View File

@@ -81,11 +81,15 @@ abstract class Task {
protected fun updateProgress(progress: Double) { protected fun updateProgress(progress: Double) {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
if (now - lastTime >= progressInterval) { if (now - lastTime >= progressInterval) {
progressPropertyImpl.updateAsync(progress, progressUpdate) updateProgressImmediately(progress)
lastTime = now lastTime = now
} }
} }
protected fun updateProgressImmediately(progress: Double) {
progressPropertyImpl.updateAsync(progress, progressUpdate)
}
private val messageUpdate = AtomicReference<String>() private val messageUpdate = AtomicReference<String>()
private val messagePropertyImpl = ReadOnlyStringWrapper(this, "message", null) private val messagePropertyImpl = ReadOnlyStringWrapper(this, "message", null)
val messageProperty: ReadOnlyStringProperty = messagePropertyImpl.readOnlyProperty val messageProperty: ReadOnlyStringProperty = messagePropertyImpl.readOnlyProperty

View File

@@ -49,13 +49,13 @@ class TaskExecutor() {
* Start the subscription and run all registered tasks asynchronously. * Start the subscription and run all registered tasks asynchronously.
*/ */
fun start() { fun start() {
workerQueue.add(Scheduler.Schedulers.NEW_THREAD.schedule(Callable { workerQueue.add(Scheduler.NEW_THREAD.schedule {
totTask.addAndGet(taskQueue.size) totTask.addAndGet(taskQueue.size)
while (!taskQueue.isEmpty()) { while (!taskQueue.isEmpty()) {
if (canceled) break if (canceled) break
val task = taskQueue.poll() val task = taskQueue.poll()
if (task != null) { if (task != null) {
val future = task.scheduler.schedule(Callable { executeTask(task); Unit }) val future = task.scheduler.schedule { executeTask(task) }
try { try {
future?.get() future?.get()
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
@@ -66,7 +66,9 @@ class TaskExecutor() {
} }
if (canceled || Thread.interrupted()) if (canceled || Thread.interrupted())
taskListener?.onTerminate() taskListener?.onTerminate()
})) else
taskListener?.end()
})
} }
/** /**

View File

@@ -20,8 +20,9 @@ package org.jackhuang.hmcl.task
import java.util.* import java.util.*
interface TaskListener : EventListener { interface TaskListener : EventListener {
fun onReady(task: Task) fun onReady(task: Task) {}
fun onFinished(task: Task) fun onFinished(task: Task) {}
fun onFailed(task: Task) fun onFailed(task: Task) {}
fun onTerminate() fun onTerminate() {}
fun end() {}
} }

View File

@@ -26,14 +26,14 @@ import java.util.logging.*
import java.util.logging.Formatter import java.util.logging.Formatter
val LOG = Logger.getLogger("HMCL").apply { val LOG = Logger.getLogger("HMCL").apply {
level = Level.FINEST level = Level.FINER
useParentHandlers = false useParentHandlers = false
addHandler(FileHandler("hmcl.log").apply { addHandler(FileHandler("hmcl.log").apply {
level = Level.FINEST level = Level.FINER
formatter = DefaultFormatter formatter = DefaultFormatter
}) })
addHandler(ConsoleHandler().apply { addHandler(ConsoleHandler().apply {
level = Level.FINEST level = Level.FINER
formatter = DefaultFormatter formatter = DefaultFormatter
}) })
} }