mirror of
https://github.com/packwiz/packwiz-installer.git
synced 2025-04-18 20:46:30 +02:00
Add support for mode field, with CurseForge metadata lookup
Now always asks the user before proceeding past the point where optional mods could be selected and configured When updating files, the hash is checked so an update isn't redownloaded if it already exists Added DevMain file for running in a dev environment
This commit is contained in:
parent
92d6f68f1d
commit
c6e304bc7f
5
src/main/kotlin/link/infra/packwiz/installer/DevMain.kt
Normal file
5
src/main/kotlin/link/infra/packwiz/installer/DevMain.kt
Normal file
@ -0,0 +1,5 @@
|
||||
package link.infra.packwiz.installer
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
Main(args)
|
||||
}
|
@ -10,14 +10,14 @@ import link.infra.packwiz.installer.target.Side
|
||||
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
||||
import link.infra.packwiz.installer.ui.data.IOptionDetails
|
||||
import link.infra.packwiz.installer.util.Log
|
||||
import okio.Buffer
|
||||
import okio.HashingSink
|
||||
import okio.buffer
|
||||
import okio.*
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import java.io.IOException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.util.*
|
||||
import kotlin.io.use
|
||||
|
||||
internal class DownloadTask private constructor(val metadata: IndexFile.File, defaultFormat: String, private val downloadSide: Side) : IOptionDetails {
|
||||
var cachedFile: ManifestFile.File? = null
|
||||
@ -27,7 +27,7 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
|
||||
|
||||
fun failed() = err != null
|
||||
|
||||
private var alreadyUpToDate = false
|
||||
var alreadyUpToDate = false
|
||||
private var metadataRequired = true
|
||||
private var invalidated = false
|
||||
// If file is new or isOptional changed to true, the option needs to be presented again
|
||||
@ -124,11 +124,63 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the file in the destination location is already valid
|
||||
* Must be done after metadata retrieval
|
||||
*/
|
||||
fun validateExistingFile(packFolder: String) {
|
||||
if (!alreadyUpToDate) {
|
||||
try {
|
||||
// TODO: only do this for files that didn't exist before or have been modified since last full update?
|
||||
val destPath = Paths.get(packFolder, metadata.destURI.toString())
|
||||
FileSystem.SYSTEM.source(destPath.toOkioPath()).buffer().use { src ->
|
||||
val hash: Hash
|
||||
val fileHashFormat: String
|
||||
val linkedFile = metadata.linkedFile
|
||||
|
||||
if (linkedFile != null) {
|
||||
hash = linkedFile.hash
|
||||
fileHashFormat = linkedFile.download!!.hashFormat!!
|
||||
} else {
|
||||
hash = metadata.getHashObj()
|
||||
fileHashFormat = metadata.hashFormat!!
|
||||
}
|
||||
|
||||
val fileSource = getHasher(fileHashFormat).getHashingSource(src)
|
||||
fileSource.buffer().readAll(blackholeSink())
|
||||
if (fileSource.hashIsEqual(hash)) {
|
||||
alreadyUpToDate = true
|
||||
|
||||
// Update the manifest file
|
||||
cachedFile = (cachedFile ?: ManifestFile.File()).also {
|
||||
try {
|
||||
it.hash = metadata.getHashObj()
|
||||
} catch (e: Exception) {
|
||||
err = e
|
||||
return
|
||||
}
|
||||
it.isOptional = isOptional
|
||||
it.cachedLocation = metadata.destURI.toString()
|
||||
metadata.linkedFile?.let { linked ->
|
||||
try {
|
||||
it.linkedFileHash = linked.hash
|
||||
} catch (e: Exception) {
|
||||
err = e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
// Ignore exceptions; if the file doesn't exist we'll be downloading it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun download(packFolder: String, indexUri: SpaceSafeURI) {
|
||||
if (err != null) return
|
||||
|
||||
// TODO: is this necessary if we overwrite?
|
||||
// Ensure it is removed
|
||||
// Ensure wrong-side or optional false files are removed
|
||||
cachedFile?.let {
|
||||
if (!it.optionValue || !correctSide()) {
|
||||
if (it.cachedLocation == null) return
|
||||
@ -143,8 +195,6 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
|
||||
}
|
||||
if (alreadyUpToDate) return
|
||||
|
||||
// TODO: should I be validating JSON properly, or this fine!!!!!!!??
|
||||
assert(metadata.destURI != null)
|
||||
val destPath = Paths.get(packFolder, metadata.destURI.toString())
|
||||
|
||||
// Don't update files marked with preserve if they already exist on disk
|
||||
@ -164,10 +214,10 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
|
||||
|
||||
if (linkedFile != null) {
|
||||
hash = linkedFile.hash
|
||||
fileHashFormat = Objects.requireNonNull(Objects.requireNonNull(linkedFile.download)!!.hashFormat)!!
|
||||
fileHashFormat = linkedFile.download!!.hashFormat!!
|
||||
} else {
|
||||
hash = metadata.getHashObj()
|
||||
fileHashFormat = Objects.requireNonNull(metadata.hashFormat)!!
|
||||
fileHashFormat = metadata.hashFormat!!
|
||||
}
|
||||
|
||||
val src = metadata.getSource(indexUri)
|
||||
@ -197,7 +247,7 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
|
||||
println("Calculated: " + fileSource.hash)
|
||||
println("Expected: $hash")
|
||||
// Attempt to get the SHA256 hash
|
||||
val sha256 = HashingSink.sha256(okio.blackholeSink())
|
||||
val sha256 = HashingSink.sha256(blackholeSink())
|
||||
data.readAll(sha256)
|
||||
println("SHA256 hash value: " + sha256.hash)
|
||||
err = Exception("Hash invalid!")
|
||||
|
@ -9,6 +9,7 @@ import link.infra.packwiz.installer.metadata.IndexFile
|
||||
import link.infra.packwiz.installer.metadata.ManifestFile
|
||||
import link.infra.packwiz.installer.metadata.PackFile
|
||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
||||
import link.infra.packwiz.installer.metadata.curseforge.resolveCfMetadata
|
||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
|
||||
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHasher
|
||||
@ -125,8 +126,9 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
||||
}
|
||||
|
||||
if (manifest.packFileHash?.let { packFileSource.hashIsEqual(it) } == true && invalidatedUris.isEmpty()) {
|
||||
Log.info("Modpack is already up to date!")
|
||||
// todo: --force?
|
||||
ui.submitProgress(InstallProgress("Modpack is already up to date!", 1, 1))
|
||||
ui.awaitOptionalButton(false)
|
||||
if (!ui.optionsButtonPressed) {
|
||||
return
|
||||
}
|
||||
@ -183,10 +185,15 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
||||
|
||||
private fun processIndex(indexUri: SpaceSafeURI, indexHash: Hash, hashFormat: String, manifest: ManifestFile, invalidatedUris: List<SpaceSafeURI>) {
|
||||
if (manifest.indexFileHash == indexHash && invalidatedUris.isEmpty()) {
|
||||
Log.info("Modpack files are already up to date!")
|
||||
ui.submitProgress(InstallProgress("Modpack files are already up to date!", 1, 1))
|
||||
ui.awaitOptionalButton(false)
|
||||
if (!ui.optionsButtonPressed) {
|
||||
return
|
||||
}
|
||||
if (ui.cancelButtonPressed) {
|
||||
showCancellationDialog()
|
||||
return
|
||||
}
|
||||
}
|
||||
manifest.indexFileHash = indexHash
|
||||
|
||||
@ -305,8 +312,20 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
||||
// TODO: task failed function?
|
||||
val nonFailedFirstTasks = tasks.filter { t -> !t.failed() }.toList()
|
||||
val optionTasks = nonFailedFirstTasks.filter(DownloadTask::correctSide).filter(DownloadTask::isOptional).toList()
|
||||
val optionsChanged = optionTasks.any(DownloadTask::isNewOptional)
|
||||
if (optionTasks.isNotEmpty() && !optionsChanged) {
|
||||
if (!ui.optionsButtonPressed) {
|
||||
// TODO: this is so ugly
|
||||
ui.submitProgress(InstallProgress("Reconfigure optional mods?", 0,1))
|
||||
ui.awaitOptionalButton(true)
|
||||
if (ui.cancelButtonPressed) {
|
||||
showCancellationDialog()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// If options changed, present all options again
|
||||
if (ui.optionsButtonPressed || optionTasks.any(DownloadTask::isNewOptional)) {
|
||||
if (ui.optionsButtonPressed || optionsChanged) {
|
||||
// new ArrayList is required so it's an IOptionDetails rather than a DownloadTask list
|
||||
if (ui.showOptions(ArrayList(optionTasks))) {
|
||||
cancelled = true
|
||||
@ -316,6 +335,38 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
||||
// TODO: keep this enabled? then apply changes after download process?
|
||||
ui.disableOptionsButton(optionTasks.isNotEmpty())
|
||||
|
||||
ui.submitProgress(InstallProgress("Validating existing files..."))
|
||||
|
||||
// Validate existing files
|
||||
for (downloadTask in nonFailedFirstTasks.filter(DownloadTask::correctSide)) {
|
||||
downloadTask.validateExistingFile(opts.packFolder)
|
||||
}
|
||||
|
||||
// Resolve CurseForge metadata
|
||||
val cfFiles = nonFailedFirstTasks.asSequence().filter { !it.alreadyUpToDate }
|
||||
.filter(DownloadTask::correctSide)
|
||||
.map { it.metadata }
|
||||
.filter { it.linkedFile != null }
|
||||
.filter { it.linkedFile?.download?.mode == "metadata:curseforge" }.toList()
|
||||
if (cfFiles.isNotEmpty()) {
|
||||
ui.submitProgress(InstallProgress("Resolving CurseForge metadata..."))
|
||||
val resolveFailures = resolveCfMetadata(cfFiles)
|
||||
if (resolveFailures.isNotEmpty()) {
|
||||
errorsOccurred = true
|
||||
when (ui.showExceptions(resolveFailures, cfFiles.size, true)) {
|
||||
ExceptionListResult.CONTINUE -> {}
|
||||
ExceptionListResult.CANCEL -> {
|
||||
cancelled = true
|
||||
return
|
||||
}
|
||||
ExceptionListResult.IGNORE -> {
|
||||
cancelledStartGame = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: different thread pool type?
|
||||
val threadPool = Executors.newFixedThreadPool(10)
|
||||
val completionService: CompletionService<DownloadTask> = ExecutorCompletionService(threadPool)
|
||||
|
@ -1,6 +1,9 @@
|
||||
package link.infra.packwiz.installer.metadata
|
||||
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import link.infra.packwiz.installer.metadata.curseforge.UpdateData
|
||||
import link.infra.packwiz.installer.metadata.curseforge.UpdateDeserializer
|
||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
|
||||
import link.infra.packwiz.installer.request.HandlerManager.getFileSource
|
||||
@ -19,11 +22,16 @@ class ModFile {
|
||||
@SerializedName("hash-format")
|
||||
var hashFormat: String? = null
|
||||
var hash: String? = null
|
||||
var mode: String? = null
|
||||
}
|
||||
|
||||
var update: Map<String, Any>? = null
|
||||
@JsonAdapter(UpdateDeserializer::class)
|
||||
var update: Map<String, UpdateData>? = null
|
||||
var option: Option? = null
|
||||
|
||||
@Transient
|
||||
val resolvedUpdateData = mutableMapOf<String, SpaceSafeURI>()
|
||||
|
||||
class Option {
|
||||
var optional = false
|
||||
var description: String? = null
|
||||
@ -34,11 +42,20 @@ class ModFile {
|
||||
@Throws(Exception::class)
|
||||
fun getSource(baseLoc: SpaceSafeURI?): Source {
|
||||
download?.let {
|
||||
if (it.url == null) {
|
||||
throw Exception("Metadata file doesn't have a download URI")
|
||||
if (it.mode == null || it.mode == "" || it.mode == "url") {
|
||||
if (it.url == null) {
|
||||
throw Exception("Metadata file doesn't have a download URI")
|
||||
}
|
||||
val newLoc = getNewLoc(baseLoc, it.url) ?: throw Exception("Metadata file URI is invalid")
|
||||
return getFileSource(newLoc)
|
||||
} else if (it.mode == "metadata:curseforge") {
|
||||
if (!resolvedUpdateData.contains("curseforge")) {
|
||||
throw Exception("Metadata file specifies CurseForge mode, but is missing metadata")
|
||||
}
|
||||
return getFileSource(resolvedUpdateData["curseforge"]!!)
|
||||
} else {
|
||||
throw Exception("Unsupported download mode " + it.mode)
|
||||
}
|
||||
val newLoc = getNewLoc(baseLoc, it.url) ?: throw Exception("Metadata file URI is invalid")
|
||||
return getFileSource(newLoc)
|
||||
} ?: throw Exception("Metadata file doesn't have download")
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,132 @@
|
||||
package link.infra.packwiz.installer.metadata.curseforge
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonIOException
|
||||
import com.google.gson.JsonSyntaxException
|
||||
import link.infra.packwiz.installer.metadata.IndexFile
|
||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
||||
import link.infra.packwiz.installer.target.ClientHolder
|
||||
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.internal.closeQuietly
|
||||
import okio.ByteString.Companion.decodeBase64
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
private class GetFilesRequest(val fileIds: List<Int>)
|
||||
private class GetModsRequest(val modIds: List<Int>)
|
||||
|
||||
private class GetFilesResponse {
|
||||
class CfFile {
|
||||
var id = 0
|
||||
var modId = 0
|
||||
var downloadUrl: SpaceSafeURI? = null
|
||||
}
|
||||
val data = mutableListOf<CfFile>()
|
||||
}
|
||||
|
||||
private class GetModsResponse {
|
||||
class CfMod {
|
||||
var id = 0
|
||||
var name = ""
|
||||
var links: CfLinks? = null
|
||||
}
|
||||
class CfLinks {
|
||||
var websiteUrl = ""
|
||||
}
|
||||
val data = mutableListOf<CfMod>()
|
||||
}
|
||||
|
||||
private const val APIServer = "api.curseforge.com"
|
||||
// If you fork/derive from packwiz, I request that you obtain your own API key.
|
||||
private val APIKey = "JDJhJDEwJHNBWVhqblU1N0EzSmpzcmJYM3JVdk92UWk2NHBLS3BnQ2VpbGc1TUM1UGNKL0RYTmlGWWxh".decodeBase64()!!
|
||||
.string(StandardCharsets.UTF_8)
|
||||
|
||||
private val clientHolder = ClientHolder()
|
||||
|
||||
// TODO: switch to PackwizPath stuff and OkHttp in old code
|
||||
|
||||
@Throws(JsonSyntaxException::class, JsonIOException::class)
|
||||
fun resolveCfMetadata(mods: List<IndexFile.File>): List<ExceptionDetails> {
|
||||
val failures = mutableListOf<ExceptionDetails>()
|
||||
val fileIdMap = mutableMapOf<Int, IndexFile.File>()
|
||||
|
||||
for (mod in mods) {
|
||||
if (mod.linkedFile!!.update == null) {
|
||||
failures.add(ExceptionDetails(mod.linkedFile!!.name ?: mod.linkedFile!!.filename!!, Exception("Failed to resolve CurseForge metadata: no update section")))
|
||||
continue
|
||||
}
|
||||
if (!mod.linkedFile!!.update!!.contains("curseforge")) {
|
||||
failures.add(ExceptionDetails(mod.linkedFile!!.name ?: mod.linkedFile!!.filename!!, Exception("Failed to resolve CurseForge metadata: no CurseForge update section")))
|
||||
continue
|
||||
}
|
||||
fileIdMap[(mod.linkedFile!!.update!!["curseforge"] as CurseForgeUpdateData).fileId] = mod
|
||||
}
|
||||
|
||||
val reqData = GetFilesRequest(fileIdMap.keys.toList())
|
||||
val req = Request.Builder()
|
||||
.url("https://${APIServer}/v1/mods/files")
|
||||
.header("Accept", "application/json")
|
||||
.header("User-Agent", "packwiz-installer")
|
||||
.header("X-API-Key", APIKey)
|
||||
.post(Gson().toJson(reqData, GetFilesRequest::class.java).toRequestBody("application/json".toMediaType()))
|
||||
.build()
|
||||
val res = clientHolder.okHttpClient.newCall(req).execute()
|
||||
if (!res.isSuccessful || res.body == null) {
|
||||
res.closeQuietly()
|
||||
failures.add(ExceptionDetails("Other", Exception("Failed to resolve CurseForge metadata for file data: error code ${res.code}")))
|
||||
return failures
|
||||
}
|
||||
|
||||
val resData = Gson().fromJson(res.body!!.charStream(), GetFilesResponse::class.java)
|
||||
res.closeQuietly()
|
||||
|
||||
val manualDownloadMods = mutableMapOf<Int, Pair<IndexFile.File, Int>>()
|
||||
for (file in resData.data) {
|
||||
if (!fileIdMap.contains(file.id)) {
|
||||
failures.add(ExceptionDetails(file.id.toString(),
|
||||
Exception("Failed to find file from result: ID ${file.id}, Project ID ${file.modId}")))
|
||||
continue
|
||||
}
|
||||
if (file.downloadUrl == null) {
|
||||
manualDownloadMods[file.modId] = Pair(fileIdMap[file.id]!!, file.id)
|
||||
continue
|
||||
}
|
||||
fileIdMap[file.id]!!.linkedFile!!.resolvedUpdateData["curseforge"] = file.downloadUrl!!
|
||||
}
|
||||
|
||||
if (manualDownloadMods.isNotEmpty()) {
|
||||
val reqModsData = GetModsRequest(manualDownloadMods.keys.toList())
|
||||
val reqMods = Request.Builder()
|
||||
.url("https://${APIServer}/v1/mods")
|
||||
.header("Accept", "application/json")
|
||||
.header("User-Agent", "packwiz-installer")
|
||||
.header("X-API-Key", APIKey)
|
||||
.post(Gson().toJson(reqModsData, GetModsRequest::class.java).toRequestBody("application/json".toMediaType()))
|
||||
.build()
|
||||
val resMods = clientHolder.okHttpClient.newCall(reqMods).execute()
|
||||
if (!resMods.isSuccessful || resMods.body == null) {
|
||||
resMods.closeQuietly()
|
||||
failures.add(ExceptionDetails("Other", Exception("Failed to resolve CurseForge metadata for mod data: error code ${resMods.code}")))
|
||||
return failures
|
||||
}
|
||||
|
||||
val resModsData = Gson().fromJson(resMods.body!!.charStream(), GetModsResponse::class.java)
|
||||
resMods.closeQuietly()
|
||||
|
||||
for (mod in resModsData.data) {
|
||||
if (!manualDownloadMods.contains(mod.id)) {
|
||||
failures.add(ExceptionDetails(mod.name,
|
||||
Exception("Failed to find project from result: ID ${mod.id}")))
|
||||
continue
|
||||
}
|
||||
|
||||
val modFile = manualDownloadMods[mod.id]!!
|
||||
failures.add(ExceptionDetails(mod.name, Exception("This mod is excluded from the CurseForge API and must be downloaded manually.\n" +
|
||||
"Please go to ${mod.links?.websiteUrl}/files/${modFile.second} and save this file to ${modFile.first.destURI}")))
|
||||
}
|
||||
}
|
||||
|
||||
return failures
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package link.infra.packwiz.installer.metadata.curseforge
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
class CurseForgeUpdateData: UpdateData {
|
||||
@SerializedName("file-id")
|
||||
var fileId = 0
|
||||
@SerializedName("project-id")
|
||||
var projectId = 0
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package link.infra.packwiz.installer.metadata.curseforge
|
||||
|
||||
interface UpdateData
|
@ -0,0 +1,22 @@
|
||||
package link.infra.packwiz.installer.metadata.curseforge
|
||||
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class UpdateDeserializer: JsonDeserializer<Map<String, UpdateData>> {
|
||||
override fun deserialize(
|
||||
json: JsonElement?,
|
||||
typeOfT: Type?,
|
||||
context: JsonDeserializationContext?
|
||||
): Map<String, UpdateData> {
|
||||
val out = mutableMapOf<String, UpdateData>()
|
||||
for ((k, v) in json!!.asJsonObject.entrySet()) {
|
||||
if (k == "curseforge") {
|
||||
out[k] = context!!.deserialize(v, CurseForgeUpdateData::class.java)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
@ -23,6 +23,8 @@ interface IUserInterface {
|
||||
|
||||
fun showCancellationDialog(): CancellationResult = CancellationResult.QUIT
|
||||
|
||||
fun awaitOptionalButton(showCancel: Boolean)
|
||||
|
||||
enum class ExceptionListResult {
|
||||
CONTINUE, CANCEL, IGNORE
|
||||
}
|
||||
|
@ -59,4 +59,8 @@ class CLIHandler : IUserInterface {
|
||||
}
|
||||
return ExceptionListResult.CANCEL
|
||||
}
|
||||
|
||||
override fun awaitOptionalButton(showCancel: Boolean) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ import link.infra.packwiz.installer.ui.data.InstallProgress
|
||||
import link.infra.packwiz.installer.util.Log
|
||||
import java.awt.EventQueue
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.UIManager
|
||||
@ -18,8 +19,21 @@ class GUIHandler : IUserInterface {
|
||||
|
||||
@Volatile
|
||||
override var optionsButtonPressed = false
|
||||
set(value) {
|
||||
optionalSelectedLatch.countDown()
|
||||
field = value
|
||||
}
|
||||
@Volatile
|
||||
override var cancelButtonPressed = false
|
||||
set(value) {
|
||||
optionalSelectedLatch.countDown()
|
||||
field = value
|
||||
}
|
||||
var okButtonPressed = false
|
||||
set(value) {
|
||||
optionalSelectedLatch.countDown()
|
||||
field = value
|
||||
}
|
||||
@Volatile
|
||||
override var firstInstall = false
|
||||
|
||||
@ -42,8 +56,12 @@ class GUIHandler : IUserInterface {
|
||||
}
|
||||
}
|
||||
|
||||
private val visibleCountdownLatch = CountDownLatch(1)
|
||||
private val optionalSelectedLatch = CountDownLatch(1)
|
||||
|
||||
override fun show() = EventQueue.invokeLater {
|
||||
frmPackwizlauncher.isVisible = true
|
||||
visibleCountdownLatch.countDown()
|
||||
}
|
||||
|
||||
override fun dispose() = EventQueue.invokeAndWait {
|
||||
@ -147,4 +165,15 @@ class GUIHandler : IUserInterface {
|
||||
}
|
||||
return future.get()
|
||||
}
|
||||
|
||||
override fun awaitOptionalButton(showCancel: Boolean) {
|
||||
EventQueue.invokeAndWait {
|
||||
frmPackwizlauncher.showOk(!showCancel)
|
||||
}
|
||||
visibleCountdownLatch.await()
|
||||
optionalSelectedLatch.await()
|
||||
EventQueue.invokeLater {
|
||||
frmPackwizlauncher.hideOk()
|
||||
}
|
||||
}
|
||||
}
|
@ -12,6 +12,9 @@ class InstallWindow(private val handler: GUIHandler) : JFrame() {
|
||||
private var lblProgresslabel: JLabel
|
||||
private var progressBar: JProgressBar
|
||||
private var btnOptions: JButton
|
||||
private val btnCancel: JButton
|
||||
private val btnOk: JButton
|
||||
private val buttonsPanel: JPanel
|
||||
|
||||
init {
|
||||
setBounds(100, 100, 493, 95)
|
||||
@ -35,7 +38,7 @@ class InstallWindow(private val handler: GUIHandler) : JFrame() {
|
||||
}, BorderLayout.CENTER)
|
||||
|
||||
// Buttons
|
||||
add(JPanel().apply {
|
||||
buttonsPanel = JPanel().apply {
|
||||
border = EmptyBorder(0, 5, 0, 5)
|
||||
layout = GridBagLayout()
|
||||
|
||||
@ -49,20 +52,28 @@ class InstallWindow(private val handler: GUIHandler) : JFrame() {
|
||||
}
|
||||
}
|
||||
add(btnOptions, GridBagConstraints().apply {
|
||||
gridx = 0
|
||||
gridx = 1
|
||||
gridy = 0
|
||||
})
|
||||
|
||||
add(JButton("Cancel").apply {
|
||||
btnCancel = JButton("Cancel").apply {
|
||||
addActionListener {
|
||||
isEnabled = false
|
||||
handler.cancelButtonPressed = true
|
||||
}
|
||||
}, GridBagConstraints().apply {
|
||||
gridx = 0
|
||||
}
|
||||
add(btnCancel, GridBagConstraints().apply {
|
||||
gridx = 1
|
||||
gridy = 1
|
||||
})
|
||||
}, BorderLayout.EAST)
|
||||
}
|
||||
|
||||
btnOk = JButton("Continue").apply {
|
||||
addActionListener {
|
||||
handler.okButtonPressed = true
|
||||
}
|
||||
}
|
||||
add(buttonsPanel, BorderLayout.EAST)
|
||||
}
|
||||
|
||||
fun displayProgress(progress: InstallProgress) {
|
||||
@ -83,4 +94,31 @@ class InstallWindow(private val handler: GUIHandler) : JFrame() {
|
||||
isEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
fun showOk(hideCancel: Boolean) {
|
||||
if (hideCancel) {
|
||||
buttonsPanel.add(btnOk, GridBagConstraints().apply {
|
||||
gridx = 1
|
||||
gridy = 1
|
||||
})
|
||||
buttonsPanel.remove(btnCancel)
|
||||
} else {
|
||||
buttonsPanel.add(btnOk, GridBagConstraints().apply {
|
||||
gridx = 0
|
||||
gridy = 1
|
||||
})
|
||||
}
|
||||
buttonsPanel.revalidate()
|
||||
}
|
||||
|
||||
fun hideOk() {
|
||||
buttonsPanel.remove(btnOk)
|
||||
if (!buttonsPanel.components.contains(btnCancel)) {
|
||||
buttonsPanel.add(btnCancel, GridBagConstraints().apply {
|
||||
gridx = 1
|
||||
gridy = 1
|
||||
})
|
||||
}
|
||||
buttonsPanel.revalidate()
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user