From c6e304bc7f086e16e4852ca6b107320be85c9ea3 Mon Sep 17 00:00:00 2001 From: comp500 Date: Sun, 22 May 2022 21:20:52 +0100 Subject: [PATCH] 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 --- .../link/infra/packwiz/installer/DevMain.kt | 5 + .../infra/packwiz/installer/DownloadTask.kt | 72 ++++++++-- .../infra/packwiz/installer/UpdateManager.kt | 57 +++++++- .../packwiz/installer/metadata/ModFile.kt | 27 +++- .../metadata/curseforge/CurseForgeSourcer.kt | 132 ++++++++++++++++++ .../curseforge/CurseForgeUpdateData.kt | 10 ++ .../metadata/curseforge/UpdateData.kt | 3 + .../metadata/curseforge/UpdateDeserializer.kt | 22 +++ .../packwiz/installer/ui/IUserInterface.kt | 2 + .../packwiz/installer/ui/cli/CLIHandler.kt | 4 + .../packwiz/installer/ui/gui/GUIHandler.kt | 29 ++++ .../packwiz/installer/ui/gui/InstallWindow.kt | 50 ++++++- 12 files changed, 388 insertions(+), 25 deletions(-) create mode 100644 src/main/kotlin/link/infra/packwiz/installer/DevMain.kt create mode 100644 src/main/kotlin/link/infra/packwiz/installer/metadata/curseforge/CurseForgeSourcer.kt create mode 100644 src/main/kotlin/link/infra/packwiz/installer/metadata/curseforge/CurseForgeUpdateData.kt create mode 100644 src/main/kotlin/link/infra/packwiz/installer/metadata/curseforge/UpdateData.kt create mode 100644 src/main/kotlin/link/infra/packwiz/installer/metadata/curseforge/UpdateDeserializer.kt diff --git a/src/main/kotlin/link/infra/packwiz/installer/DevMain.kt b/src/main/kotlin/link/infra/packwiz/installer/DevMain.kt new file mode 100644 index 0000000..5b21694 --- /dev/null +++ b/src/main/kotlin/link/infra/packwiz/installer/DevMain.kt @@ -0,0 +1,5 @@ +package link.infra.packwiz.installer + +fun main(args: Array) { + Main(args) +} \ No newline at end of file diff --git a/src/main/kotlin/link/infra/packwiz/installer/DownloadTask.kt b/src/main/kotlin/link/infra/packwiz/installer/DownloadTask.kt index d383596..38b36e6 100644 --- a/src/main/kotlin/link/infra/packwiz/installer/DownloadTask.kt +++ b/src/main/kotlin/link/infra/packwiz/installer/DownloadTask.kt @@ -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!") diff --git a/src/main/kotlin/link/infra/packwiz/installer/UpdateManager.kt b/src/main/kotlin/link/infra/packwiz/installer/UpdateManager.kt index 6c2bb58..ff5deff 100644 --- a/src/main/kotlin/link/infra/packwiz/installer/UpdateManager.kt +++ b/src/main/kotlin/link/infra/packwiz/installer/UpdateManager.kt @@ -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) { 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 = ExecutorCompletionService(threadPool) diff --git a/src/main/kotlin/link/infra/packwiz/installer/metadata/ModFile.kt b/src/main/kotlin/link/infra/packwiz/installer/metadata/ModFile.kt index 7767e72..0a02be2 100644 --- a/src/main/kotlin/link/infra/packwiz/installer/metadata/ModFile.kt +++ b/src/main/kotlin/link/infra/packwiz/installer/metadata/ModFile.kt @@ -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? = null + @JsonAdapter(UpdateDeserializer::class) + var update: Map? = null var option: Option? = null + @Transient + val resolvedUpdateData = mutableMapOf() + 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") } diff --git a/src/main/kotlin/link/infra/packwiz/installer/metadata/curseforge/CurseForgeSourcer.kt b/src/main/kotlin/link/infra/packwiz/installer/metadata/curseforge/CurseForgeSourcer.kt new file mode 100644 index 0000000..594cb1f --- /dev/null +++ b/src/main/kotlin/link/infra/packwiz/installer/metadata/curseforge/CurseForgeSourcer.kt @@ -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) +private class GetModsRequest(val modIds: List) + +private class GetFilesResponse { + class CfFile { + var id = 0 + var modId = 0 + var downloadUrl: SpaceSafeURI? = null + } + val data = mutableListOf() +} + +private class GetModsResponse { + class CfMod { + var id = 0 + var name = "" + var links: CfLinks? = null + } + class CfLinks { + var websiteUrl = "" + } + val data = mutableListOf() +} + +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): List { + val failures = mutableListOf() + val fileIdMap = mutableMapOf() + + 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>() + 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 +} \ No newline at end of file diff --git a/src/main/kotlin/link/infra/packwiz/installer/metadata/curseforge/CurseForgeUpdateData.kt b/src/main/kotlin/link/infra/packwiz/installer/metadata/curseforge/CurseForgeUpdateData.kt new file mode 100644 index 0000000..72f4660 --- /dev/null +++ b/src/main/kotlin/link/infra/packwiz/installer/metadata/curseforge/CurseForgeUpdateData.kt @@ -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 +} \ No newline at end of file diff --git a/src/main/kotlin/link/infra/packwiz/installer/metadata/curseforge/UpdateData.kt b/src/main/kotlin/link/infra/packwiz/installer/metadata/curseforge/UpdateData.kt new file mode 100644 index 0000000..612140b --- /dev/null +++ b/src/main/kotlin/link/infra/packwiz/installer/metadata/curseforge/UpdateData.kt @@ -0,0 +1,3 @@ +package link.infra.packwiz.installer.metadata.curseforge + +interface UpdateData \ No newline at end of file diff --git a/src/main/kotlin/link/infra/packwiz/installer/metadata/curseforge/UpdateDeserializer.kt b/src/main/kotlin/link/infra/packwiz/installer/metadata/curseforge/UpdateDeserializer.kt new file mode 100644 index 0000000..a525b95 --- /dev/null +++ b/src/main/kotlin/link/infra/packwiz/installer/metadata/curseforge/UpdateDeserializer.kt @@ -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> { + override fun deserialize( + json: JsonElement?, + typeOfT: Type?, + context: JsonDeserializationContext? + ): Map { + val out = mutableMapOf() + for ((k, v) in json!!.asJsonObject.entrySet()) { + if (k == "curseforge") { + out[k] = context!!.deserialize(v, CurseForgeUpdateData::class.java) + } + } + return out + } +} \ No newline at end of file diff --git a/src/main/kotlin/link/infra/packwiz/installer/ui/IUserInterface.kt b/src/main/kotlin/link/infra/packwiz/installer/ui/IUserInterface.kt index 7a19aed..55c1ead 100644 --- a/src/main/kotlin/link/infra/packwiz/installer/ui/IUserInterface.kt +++ b/src/main/kotlin/link/infra/packwiz/installer/ui/IUserInterface.kt @@ -23,6 +23,8 @@ interface IUserInterface { fun showCancellationDialog(): CancellationResult = CancellationResult.QUIT + fun awaitOptionalButton(showCancel: Boolean) + enum class ExceptionListResult { CONTINUE, CANCEL, IGNORE } diff --git a/src/main/kotlin/link/infra/packwiz/installer/ui/cli/CLIHandler.kt b/src/main/kotlin/link/infra/packwiz/installer/ui/cli/CLIHandler.kt index c1ab48a..50262a0 100644 --- a/src/main/kotlin/link/infra/packwiz/installer/ui/cli/CLIHandler.kt +++ b/src/main/kotlin/link/infra/packwiz/installer/ui/cli/CLIHandler.kt @@ -59,4 +59,8 @@ class CLIHandler : IUserInterface { } return ExceptionListResult.CANCEL } + + override fun awaitOptionalButton(showCancel: Boolean) { + // Do nothing + } } \ No newline at end of file diff --git a/src/main/kotlin/link/infra/packwiz/installer/ui/gui/GUIHandler.kt b/src/main/kotlin/link/infra/packwiz/installer/ui/gui/GUIHandler.kt index ae51df7..36a46f6 100644 --- a/src/main/kotlin/link/infra/packwiz/installer/ui/gui/GUIHandler.kt +++ b/src/main/kotlin/link/infra/packwiz/installer/ui/gui/GUIHandler.kt @@ -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() + } + } } \ No newline at end of file diff --git a/src/main/kotlin/link/infra/packwiz/installer/ui/gui/InstallWindow.kt b/src/main/kotlin/link/infra/packwiz/installer/ui/gui/InstallWindow.kt index 0eca323..9431372 100644 --- a/src/main/kotlin/link/infra/packwiz/installer/ui/gui/InstallWindow.kt +++ b/src/main/kotlin/link/infra/packwiz/installer/ui/gui/InstallWindow.kt @@ -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() + } } \ No newline at end of file