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