mirror of
https://github.com/packwiz/packwiz-installer.git
synced 2025-04-19 13:06:30 +02:00
Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
7420866dfc | ||
|
1ebb28c3cc | ||
|
c9543f74ee | ||
|
b2421cfea7 | ||
|
6f05ac6bf0 | ||
|
7b6daaf7e5 | ||
|
758385c225 | ||
|
304fb802ed | ||
|
cc063773d8 | ||
|
1deed7dd0d | ||
|
ad951b9b44 | ||
|
4e415c1e1a | ||
|
84bbbe0770 |
27
.github/workflows/pr.yml
vendored
Normal file
27
.github/workflows/pr.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
name: Java Gradle Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Set up JDK 8
|
||||||
|
uses: actions/setup-java@v2
|
||||||
|
with:
|
||||||
|
java-version: '8'
|
||||||
|
distribution: 'temurin'
|
||||||
|
cache: gradle
|
||||||
|
- name: Build with Gradle
|
||||||
|
run: ./gradlew build
|
||||||
|
- name: Cleanup Gradle Cache
|
||||||
|
# Remove some files from the Gradle cache, so they aren't cached by GitHub Actions.
|
||||||
|
# Restoring these files from a GitHub Actions cache might cause problems for future builds.
|
||||||
|
run: |
|
||||||
|
rm -f ~/.gradle/caches/modules-2/modules-2.lock
|
||||||
|
rm -f ~/.gradle/caches/modules-2/gc.properties
|
5
.github/workflows/snapshot.yml
vendored
5
.github/workflows/snapshot.yml
vendored
@ -1,6 +1,9 @@
|
|||||||
name: Java Gradle Snapshot
|
name: Java Gradle Snapshot
|
||||||
|
|
||||||
on: ["push", "pull_request"]
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'main'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
@ -21,6 +21,7 @@ import java.nio.file.StandardCopyOption
|
|||||||
|
|
||||||
internal class DownloadTask private constructor(val metadata: IndexFile.File, val index: IndexFile, private val downloadSide: Side) : IOptionDetails {
|
internal class DownloadTask private constructor(val metadata: IndexFile.File, val index: IndexFile, private val downloadSide: Side) : IOptionDetails {
|
||||||
var cachedFile: ManifestFile.File? = null
|
var cachedFile: ManifestFile.File? = null
|
||||||
|
private set
|
||||||
|
|
||||||
private var err: Exception? = null
|
private var err: Exception? = null
|
||||||
val exceptionDetails get() = err?.let { e -> ExceptionDetails(name, e) }
|
val exceptionDetails get() = err?.let { e -> ExceptionDetails(name, e) }
|
||||||
@ -28,16 +29,30 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, va
|
|||||||
fun failed() = err != null
|
fun failed() = err != null
|
||||||
|
|
||||||
var alreadyUpToDate = false
|
var alreadyUpToDate = false
|
||||||
|
private set
|
||||||
private var metadataRequired = true
|
private var metadataRequired = true
|
||||||
private var invalidated = false
|
private var invalidated = false
|
||||||
// If file is new or isOptional changed to true, the option needs to be presented again
|
// If file is new or isOptional changed to true, the option needs to be presented again
|
||||||
private var newOptional = true
|
private var newOptional = true
|
||||||
|
var completionStatus = CompletionStatus.INCOMPLETE
|
||||||
|
private set
|
||||||
|
|
||||||
|
enum class CompletionStatus {
|
||||||
|
INCOMPLETE,
|
||||||
|
DOWNLOADED,
|
||||||
|
ALREADY_EXISTS_CACHED,
|
||||||
|
ALREADY_EXISTS_VALIDATED,
|
||||||
|
SKIPPED_DISABLED,
|
||||||
|
SKIPPED_WRONG_SIDE,
|
||||||
|
DELETED_DISABLED,
|
||||||
|
DELETED_WRONG_SIDE;
|
||||||
|
}
|
||||||
|
|
||||||
val isOptional get() = metadata.linkedFile?.option?.optional ?: false
|
val isOptional get() = metadata.linkedFile?.option?.optional ?: false
|
||||||
|
|
||||||
fun isNewOptional() = isOptional && newOptional
|
fun isNewOptional() = isOptional && newOptional
|
||||||
|
|
||||||
fun correctSide() = metadata.linkedFile?.side?.hasSide(downloadSide) ?: true
|
fun correctSide() = metadata.linkedFile?.side?.let { downloadSide.hasSide(it) } ?: true
|
||||||
|
|
||||||
override val name get() = metadata.name
|
override val name get() = metadata.name
|
||||||
|
|
||||||
@ -76,6 +91,7 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, va
|
|||||||
if (currHash == cachedFile.hash) { // Already up to date
|
if (currHash == cachedFile.hash) { // Already up to date
|
||||||
alreadyUpToDate = true
|
alreadyUpToDate = true
|
||||||
metadataRequired = false
|
metadataRequired = false
|
||||||
|
completionStatus = CompletionStatus.ALREADY_EXISTS_CACHED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cachedFile.isOptional) {
|
if (cachedFile.isOptional) {
|
||||||
@ -109,9 +125,9 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, va
|
|||||||
cachedFile.optionValue = linkedFile.option.defaultValue
|
cachedFile.optionValue = linkedFile.option.defaultValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cachedFile.isOptional = isOptional
|
|
||||||
cachedFile.onlyOtherSide = !correctSide()
|
|
||||||
}
|
}
|
||||||
|
cachedFile.isOptional = isOptional
|
||||||
|
cachedFile.onlyOtherSide = !correctSide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -143,6 +159,7 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, va
|
|||||||
fileSource.buffer().readAll(blackholeSink())
|
fileSource.buffer().readAll(blackholeSink())
|
||||||
if (hash == fileSource.hash) {
|
if (hash == fileSource.hash) {
|
||||||
alreadyUpToDate = true
|
alreadyUpToDate = true
|
||||||
|
completionStatus = CompletionStatus.ALREADY_EXISTS_VALIDATED
|
||||||
|
|
||||||
// Update the manifest file
|
// Update the manifest file
|
||||||
cachedFile = (cachedFile ?: ManifestFile.File()).also {
|
cachedFile = (cachedFile ?: ManifestFile.File()).also {
|
||||||
@ -181,10 +198,18 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, va
|
|||||||
if (it.cachedLocation != null) {
|
if (it.cachedLocation != null) {
|
||||||
// Ensure wrong-side or optional false files are removed
|
// Ensure wrong-side or optional false files are removed
|
||||||
try {
|
try {
|
||||||
Files.deleteIfExists(it.cachedLocation!!.nioPath)
|
completionStatus = if (Files.deleteIfExists(it.cachedLocation!!.nioPath)) {
|
||||||
|
if (correctSide()) { CompletionStatus.DELETED_DISABLED } else { CompletionStatus.DELETED_WRONG_SIDE }
|
||||||
|
} else {
|
||||||
|
if (correctSide()) { CompletionStatus.SKIPPED_DISABLED } else { CompletionStatus.SKIPPED_WRONG_SIDE }
|
||||||
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.warn("Failed to delete file", e)
|
Log.warn("Failed to delete file", e)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
completionStatus =
|
||||||
|
if (correctSide()) { CompletionStatus.SKIPPED_DISABLED }
|
||||||
|
else { CompletionStatus.SKIPPED_WRONG_SIDE }
|
||||||
}
|
}
|
||||||
it.cachedLocation = null
|
it.cachedLocation = null
|
||||||
return
|
return
|
||||||
@ -284,6 +309,8 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, va
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
completionStatus = CompletionStatus.DOWNLOADED
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -48,6 +48,7 @@ class LauncherUtils internal constructor(private val opts: UpdateManager.Options
|
|||||||
val modLoaders = hashMapOf(
|
val modLoaders = hashMapOf(
|
||||||
"net.minecraft" to "minecraft",
|
"net.minecraft" to "minecraft",
|
||||||
"net.minecraftforge" to "forge",
|
"net.minecraftforge" to "forge",
|
||||||
|
"net.neoforged" to "neoforge",
|
||||||
"net.fabricmc.fabric-loader" to "fabric",
|
"net.fabricmc.fabric-loader" to "fabric",
|
||||||
"org.quiltmc.quilt-loader" to "quilt",
|
"org.quiltmc.quilt-loader" to "quilt",
|
||||||
"com.mumfrey.liteloader" to "liteloader"
|
"com.mumfrey.liteloader" to "liteloader"
|
||||||
@ -59,6 +60,7 @@ class LauncherUtils internal constructor(private val opts: UpdateManager.Options
|
|||||||
"org.lwjgl" to -1,
|
"org.lwjgl" to -1,
|
||||||
"org.lwjgl3" to -1,
|
"org.lwjgl3" to -1,
|
||||||
"net.minecraftforge" to 5,
|
"net.minecraftforge" to 5,
|
||||||
|
"net.neoforged" to 5,
|
||||||
"net.fabricmc.fabric-loader" to 10,
|
"net.fabricmc.fabric-loader" to 10,
|
||||||
"org.quiltmc.quilt-loader" to 10,
|
"org.quiltmc.quilt-loader" to 10,
|
||||||
"com.mumfrey.liteloader" to 10,
|
"com.mumfrey.liteloader" to 10,
|
||||||
@ -137,4 +139,4 @@ class LauncherUtils internal constructor(private val opts: UpdateManager.Options
|
|||||||
|
|
||||||
return LauncherStatus.NO_CHANGES
|
return LauncherStatus.NO_CHANGES
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,10 +103,13 @@ class Main(args: Array<String>) {
|
|||||||
val manifestFile = ui.wrap("Invalid manifest file path") {
|
val manifestFile = ui.wrap("Invalid manifest file path") {
|
||||||
packFolder / (cmd.getOptionValue("meta-file") ?: "packwiz.json")
|
packFolder / (cmd.getOptionValue("meta-file") ?: "packwiz.json")
|
||||||
}
|
}
|
||||||
|
val timeout = ui.wrap("Invalid timeout value") {
|
||||||
|
cmd.getOptionValue("timeout")?.toLong() ?: 10
|
||||||
|
}
|
||||||
|
|
||||||
// Start update process!
|
// Start update process!
|
||||||
try {
|
try {
|
||||||
UpdateManager(UpdateManager.Options(packFile, manifestFile, packFolder, multimcFolder, side), ui)
|
UpdateManager(UpdateManager.Options(packFile, manifestFile, packFolder, multimcFolder, side, timeout), ui)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
ui.showErrorAndExit("Update process failed", e)
|
ui.showErrorAndExit("Update process failed", e)
|
||||||
}
|
}
|
||||||
@ -123,6 +126,7 @@ class Main(args: Array<String>) {
|
|||||||
options.addOption(null, "pack-folder", true, "Folder to install the pack to (defaults to the JAR directory)")
|
options.addOption(null, "pack-folder", true, "Folder to install the pack to (defaults to the JAR directory)")
|
||||||
options.addOption(null, "multimc-folder", true, "The MultiMC pack folder (defaults to the parent of the pack directory)")
|
options.addOption(null, "multimc-folder", true, "The MultiMC pack folder (defaults to the parent of the pack directory)")
|
||||||
options.addOption(null, "meta-file", true, "JSON file to store pack metadata, relative to the pack folder (defaults to packwiz.json)")
|
options.addOption(null, "meta-file", true, "JSON file to store pack metadata, relative to the pack folder (defaults to packwiz.json)")
|
||||||
|
options.addOption("t", "timeout", true, "Seconds to wait before automatically launching when asking about optional mods (defaults to 10)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: link these somehow so they're only defined once?
|
// TODO: link these somehow so they're only defined once?
|
||||||
|
@ -23,7 +23,6 @@ import link.infra.packwiz.installer.ui.IUserInterface.ExceptionListResult
|
|||||||
import link.infra.packwiz.installer.ui.data.InstallProgress
|
import link.infra.packwiz.installer.ui.data.InstallProgress
|
||||||
import link.infra.packwiz.installer.util.Log
|
import link.infra.packwiz.installer.util.Log
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import java.io.FileWriter
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
@ -48,7 +47,8 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
val manifestFile: PackwizFilePath,
|
val manifestFile: PackwizFilePath,
|
||||||
val packFolder: PackwizFilePath,
|
val packFolder: PackwizFilePath,
|
||||||
val multimcFolder: PackwizFilePath,
|
val multimcFolder: PackwizFilePath,
|
||||||
val side: Side
|
val side: Side,
|
||||||
|
val timeout: Long,
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: make this return a value based on results?
|
// TODO: make this return a value based on results?
|
||||||
@ -127,40 +127,44 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
|
|
||||||
ui.submitProgress(InstallProgress("Checking local files..."))
|
ui.submitProgress(InstallProgress("Checking local files..."))
|
||||||
|
|
||||||
// Invalidation checking must be done here, as it must happen before pack/index hashes are checked
|
// If the side changes, invalidate EVERYTHING (even when the index hasn't changed)
|
||||||
|
val invalidateAll = opts.side != manifest.cachedSide
|
||||||
val invalidatedUris: MutableList<PackwizFilePath> = ArrayList()
|
val invalidatedUris: MutableList<PackwizFilePath> = ArrayList()
|
||||||
for ((fileUri, file) in manifest.cachedFiles) {
|
if (!invalidateAll) {
|
||||||
// ignore onlyOtherSide files
|
// Invalidation checking must be done here, as it must happen before pack/index hashes are checked
|
||||||
if (file.onlyOtherSide) {
|
for ((fileUri, file) in manifest.cachedFiles) {
|
||||||
continue
|
// ignore onlyOtherSide files
|
||||||
}
|
if (file.onlyOtherSide) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
var invalid = false
|
var invalid = false
|
||||||
// if isn't optional, or is optional but optionValue == true
|
// if isn't optional, or is optional but optionValue == true
|
||||||
if (!file.isOptional || file.optionValue) {
|
if (!file.isOptional || file.optionValue) {
|
||||||
if (file.cachedLocation != null) {
|
if (file.cachedLocation != null) {
|
||||||
if (!file.cachedLocation!!.nioPath.toFile().exists()) {
|
if (!file.cachedLocation!!.nioPath.toFile().exists()) {
|
||||||
|
invalid = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if cachedLocation == null, should probably be installed!!
|
||||||
invalid = true
|
invalid = true
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
// if cachedLocation == null, should probably be installed!!
|
if (invalid) {
|
||||||
invalid = true
|
Log.info("File ${fileUri.filename} invalidated, marked for redownloading")
|
||||||
|
invalidatedUris.add(fileUri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (invalid) {
|
|
||||||
Log.info("File ${fileUri.filename} invalidated, marked for redownloading")
|
|
||||||
invalidatedUris.add(fileUri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (manifest.packFileHash?.let { it == packFileSource.hash } == true && invalidatedUris.isEmpty()) {
|
if (manifest.packFileHash?.let { it == packFileSource.hash } == true && invalidatedUris.isEmpty()) {
|
||||||
// todo: --force?
|
// todo: --force?
|
||||||
ui.submitProgress(InstallProgress("Modpack is already up to date!", 1, 1))
|
ui.submitProgress(InstallProgress("Modpack is already up to date!", 1, 1))
|
||||||
if (manifest.cachedFiles.any { it.value.isOptional }) {
|
if (manifest.cachedFiles.any { it.value.isOptional }) {
|
||||||
ui.awaitOptionalButton(false)
|
ui.awaitOptionalButton(false, opts.timeout)
|
||||||
}
|
}
|
||||||
if (!ui.optionsButtonPressed) {
|
if (!ui.optionsButtonPressed) {
|
||||||
return
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,6 +181,7 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
pf.index.hashFormat,
|
pf.index.hashFormat,
|
||||||
manifest,
|
manifest,
|
||||||
invalidatedUris,
|
invalidatedUris,
|
||||||
|
invalidateAll,
|
||||||
clientHolder
|
clientHolder
|
||||||
)
|
)
|
||||||
} catch (e1: Exception) {
|
} catch (e1: Exception) {
|
||||||
@ -196,24 +201,26 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
|
|
||||||
manifest.cachedSide = opts.side
|
manifest.cachedSide = opts.side
|
||||||
try {
|
try {
|
||||||
FileWriter(opts.manifestFile.nioPath.toFile()).use { writer -> gson.toJson(manifest, writer) }
|
Files.newBufferedWriter(opts.manifestFile.nioPath, StandardCharsets.UTF_8).use { writer -> gson.toJson(manifest, writer) }
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
ui.showErrorAndExit("Failed to save local manifest file", e)
|
ui.showErrorAndExit("Failed to save local manifest file", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processIndex(indexUri: PackwizPath<*>, indexHash: Hash<*>, hashFormat: HashFormat<*>, manifest: ManifestFile, invalidatedFiles: List<PackwizFilePath>, clientHolder: ClientHolder) {
|
private fun processIndex(indexUri: PackwizPath<*>, indexHash: Hash<*>, hashFormat: HashFormat<*>, manifest: ManifestFile, invalidatedFiles: List<PackwizFilePath>, invalidateAll: Boolean, clientHolder: ClientHolder) {
|
||||||
if (manifest.indexFileHash == indexHash && invalidatedFiles.isEmpty()) {
|
if (!invalidateAll) {
|
||||||
ui.submitProgress(InstallProgress("Modpack files are already up to date!", 1, 1))
|
if (manifest.indexFileHash == indexHash && invalidatedFiles.isEmpty()) {
|
||||||
if (manifest.cachedFiles.any { it.value.isOptional }) {
|
ui.submitProgress(InstallProgress("Modpack files are already up to date!", 1, 1))
|
||||||
ui.awaitOptionalButton(false)
|
if (manifest.cachedFiles.any { it.value.isOptional }) {
|
||||||
}
|
ui.awaitOptionalButton(false, opts.timeout)
|
||||||
if (!ui.optionsButtonPressed) {
|
}
|
||||||
return
|
if (!ui.optionsButtonPressed) {
|
||||||
}
|
return
|
||||||
if (ui.cancelButtonPressed) {
|
}
|
||||||
showCancellationDialog()
|
if (ui.cancelButtonPressed) {
|
||||||
return
|
showCancellationDialog()
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
manifest.indexFileHash = indexHash
|
manifest.indexFileHash = indexHash
|
||||||
@ -240,31 +247,17 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
}
|
}
|
||||||
|
|
||||||
ui.submitProgress(InstallProgress("Checking local files..."))
|
ui.submitProgress(InstallProgress("Checking local files..."))
|
||||||
// TODO: use kotlin filtering/FP rather than an iterator?
|
|
||||||
val it: MutableIterator<Map.Entry<PackwizFilePath, ManifestFile.File>> = manifest.cachedFiles.entries.iterator()
|
val it: MutableIterator<Map.Entry<PackwizFilePath, ManifestFile.File>> = manifest.cachedFiles.entries.iterator()
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
val (uri, file) = it.next()
|
val (uri, file) = it.next()
|
||||||
if (file.cachedLocation != null) {
|
if (file.cachedLocation != null) {
|
||||||
var alreadyDeleted = false
|
if (indexFile.files.none { it.file.rebase(opts.packFolder) == uri }) { // File has been removed from the index
|
||||||
// Delete if option value has been set to false
|
|
||||||
if (file.isOptional && !file.optionValue) {
|
|
||||||
try {
|
try {
|
||||||
Files.deleteIfExists(file.cachedLocation!!.nioPath)
|
Files.deleteIfExists(file.cachedLocation!!.nioPath)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.warn("Failed to delete optional disabled file", e)
|
Log.warn("Failed to delete file removed from index", e)
|
||||||
}
|
|
||||||
// Set to null, as it doesn't exist anymore
|
|
||||||
file.cachedLocation = null
|
|
||||||
alreadyDeleted = true
|
|
||||||
}
|
|
||||||
if (indexFile.files.none { it.file.rebase(opts.packFolder) == uri }) { // File has been removed from the index
|
|
||||||
if (!alreadyDeleted) {
|
|
||||||
try {
|
|
||||||
Files.deleteIfExists(file.cachedLocation!!.nioPath)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.warn("Failed to delete file removed from index", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Log.info("Deleted ${file.cachedLocation!!.filename} (removed from pack)")
|
||||||
it.remove()
|
it.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -281,9 +274,6 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
Log.warn("Index is empty!")
|
Log.warn("Index is empty!")
|
||||||
}
|
}
|
||||||
val tasks = createTasksFromIndex(indexFile, opts.side)
|
val tasks = createTasksFromIndex(indexFile, opts.side)
|
||||||
// If the side changes, invalidate EVERYTHING just in case
|
|
||||||
// Might not be needed, but done just to be safe
|
|
||||||
val invalidateAll = opts.side != manifest.cachedSide
|
|
||||||
if (invalidateAll) {
|
if (invalidateAll) {
|
||||||
Log.info("Side changed, invalidating all mods")
|
Log.info("Side changed, invalidating all mods")
|
||||||
}
|
}
|
||||||
@ -338,7 +328,7 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
if (!ui.optionsButtonPressed) {
|
if (!ui.optionsButtonPressed) {
|
||||||
// TODO: this is so ugly
|
// TODO: this is so ugly
|
||||||
ui.submitProgress(InstallProgress("Reconfigure optional mods?", 0,1))
|
ui.submitProgress(InstallProgress("Reconfigure optional mods?", 0,1))
|
||||||
ui.awaitOptionalButton(true)
|
ui.awaitOptionalButton(true, opts.timeout)
|
||||||
if (ui.cancelButtonPressed) {
|
if (ui.cancelButtonPressed) {
|
||||||
showCancellationDialog()
|
showCancellationDialog()
|
||||||
return
|
return
|
||||||
@ -397,7 +387,16 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
val progress = if (exDetails != null) {
|
val progress = if (exDetails != null) {
|
||||||
"Failed to download ${exDetails.name}: ${exDetails.exception.message}"
|
"Failed to download ${exDetails.name}: ${exDetails.exception.message}"
|
||||||
} else {
|
} else {
|
||||||
"Downloaded ${task.name}"
|
when (task.completionStatus) {
|
||||||
|
DownloadTask.CompletionStatus.INCOMPLETE -> "${task.name} pending (you should never see this...)"
|
||||||
|
DownloadTask.CompletionStatus.DOWNLOADED -> "Downloaded ${task.name}"
|
||||||
|
DownloadTask.CompletionStatus.ALREADY_EXISTS_CACHED -> "${task.name} already exists (cached)"
|
||||||
|
DownloadTask.CompletionStatus.ALREADY_EXISTS_VALIDATED -> "${task.name} already exists (validated)"
|
||||||
|
DownloadTask.CompletionStatus.SKIPPED_DISABLED -> "Skipped ${task.name} (disabled)"
|
||||||
|
DownloadTask.CompletionStatus.SKIPPED_WRONG_SIDE -> "Skipped ${task.name} (wrong side)"
|
||||||
|
DownloadTask.CompletionStatus.DELETED_DISABLED -> "Deleted ${task.name} (disabled)"
|
||||||
|
DownloadTask.CompletionStatus.DELETED_WRONG_SIDE -> "Deleted ${task.name} (wrong side)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ui.submitProgress(InstallProgress(progress, i + 1, tasks.size))
|
ui.submitProgress(InstallProgress(progress, i + 1, tasks.size))
|
||||||
|
|
||||||
|
@ -49,14 +49,15 @@ private val APIKey = "JDJhJDEwJHNBWVhqblU1N0EzSmpzcmJYM3JVdk92UWk2NHBLS3BnQ2VpbG
|
|||||||
@Throws(JsonSyntaxException::class, JsonIOException::class)
|
@Throws(JsonSyntaxException::class, JsonIOException::class)
|
||||||
fun resolveCfMetadata(mods: List<IndexFile.File>, packFolder: PackwizFilePath, clientHolder: ClientHolder): List<ExceptionDetails> {
|
fun resolveCfMetadata(mods: List<IndexFile.File>, packFolder: PackwizFilePath, clientHolder: ClientHolder): List<ExceptionDetails> {
|
||||||
val failures = mutableListOf<ExceptionDetails>()
|
val failures = mutableListOf<ExceptionDetails>()
|
||||||
val fileIdMap = mutableMapOf<Int, IndexFile.File>()
|
val fileIdMap = mutableMapOf<Int, List<IndexFile.File>>()
|
||||||
|
|
||||||
for (mod in mods) {
|
for (mod in mods) {
|
||||||
if (!mod.linkedFile!!.update.contains("curseforge")) {
|
if (!mod.linkedFile!!.update.contains("curseforge")) {
|
||||||
failures.add(ExceptionDetails(mod.linkedFile!!.name, Exception("Failed to resolve CurseForge metadata: no CurseForge update section")))
|
failures.add(ExceptionDetails(mod.linkedFile!!.name, Exception("Failed to resolve CurseForge metadata: no CurseForge update section")))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fileIdMap[(mod.linkedFile!!.update["curseforge"] as CurseForgeUpdateData).fileId] = mod
|
val fileId = (mod.linkedFile!!.update["curseforge"] as CurseForgeUpdateData).fileId
|
||||||
|
fileIdMap[fileId] = (fileIdMap[fileId] ?: listOf()) + mod
|
||||||
}
|
}
|
||||||
|
|
||||||
val reqData = GetFilesRequest(fileIdMap.keys.toList())
|
val reqData = GetFilesRequest(fileIdMap.keys.toList())
|
||||||
@ -77,7 +78,7 @@ fun resolveCfMetadata(mods: List<IndexFile.File>, packFolder: PackwizFilePath, c
|
|||||||
val resData = Gson().fromJson(res.body!!.charStream(), GetFilesResponse::class.java)
|
val resData = Gson().fromJson(res.body!!.charStream(), GetFilesResponse::class.java)
|
||||||
res.closeQuietly()
|
res.closeQuietly()
|
||||||
|
|
||||||
val manualDownloadMods = mutableMapOf<Int, Pair<IndexFile.File, Int>>()
|
val manualDownloadMods = mutableMapOf<Int, List<Int>>()
|
||||||
for (file in resData.data) {
|
for (file in resData.data) {
|
||||||
if (!fileIdMap.contains(file.id)) {
|
if (!fileIdMap.contains(file.id)) {
|
||||||
failures.add(ExceptionDetails(file.id.toString(),
|
failures.add(ExceptionDetails(file.id.toString(),
|
||||||
@ -85,12 +86,14 @@ fun resolveCfMetadata(mods: List<IndexFile.File>, packFolder: PackwizFilePath, c
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (file.downloadUrl == null) {
|
if (file.downloadUrl == null) {
|
||||||
manualDownloadMods[file.modId] = Pair(fileIdMap[file.id]!!, file.id)
|
manualDownloadMods[file.modId] = (manualDownloadMods[file.modId] ?: listOf()) + file.id
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
fileIdMap[file.id]!!.linkedFile!!.resolvedUpdateData["curseforge"] =
|
for (indexFile in fileIdMap[file.id]!!) {
|
||||||
HttpUrlPath(file.downloadUrl!!.toHttpUrl())
|
indexFile.linkedFile!!.resolvedUpdateData["curseforge"] =
|
||||||
|
HttpUrlPath(file.downloadUrl!!.toHttpUrl())
|
||||||
|
}
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
failures.add(ExceptionDetails(file.id.toString(),
|
failures.add(ExceptionDetails(file.id.toString(),
|
||||||
Exception("Failed to parse URL: ${file.downloadUrl} for ID ${file.id}, Project ID ${file.modId}", e)))
|
Exception("Failed to parse URL: ${file.downloadUrl} for ID ${file.id}, Project ID ${file.modId}", e)))
|
||||||
@ -99,10 +102,13 @@ fun resolveCfMetadata(mods: List<IndexFile.File>, packFolder: PackwizFilePath, c
|
|||||||
|
|
||||||
// Some file types don't show up in the API at all! (e.g. shaderpacks)
|
// Some file types don't show up in the API at all! (e.g. shaderpacks)
|
||||||
// Add unresolved files to manualDownloadMods
|
// Add unresolved files to manualDownloadMods
|
||||||
for ((fileId, file) in fileIdMap) {
|
for ((fileId, indexFiles) in fileIdMap) {
|
||||||
if (file.linkedFile != null) {
|
for (file in indexFiles) {
|
||||||
if (file.linkedFile!!.resolvedUpdateData["curseforge"] == null) {
|
if (file.linkedFile != null) {
|
||||||
manualDownloadMods[(file.linkedFile!!.update["curseforge"] as CurseForgeUpdateData).projectId] = Pair(file, fileId)
|
if (file.linkedFile!!.resolvedUpdateData["curseforge"] == null) {
|
||||||
|
val projectId = (file.linkedFile!!.update["curseforge"] as CurseForgeUpdateData).projectId
|
||||||
|
manualDownloadMods[projectId] = (manualDownloadMods[projectId] ?: listOf()) + fileId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -133,9 +139,19 @@ fun resolveCfMetadata(mods: List<IndexFile.File>, packFolder: PackwizFilePath, c
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
val modFile = manualDownloadMods[mod.id]!!
|
for (fileId in manualDownloadMods[mod.id]!!) {
|
||||||
failures.add(ExceptionDetails(mod.name, Exception("This mod is excluded from the CurseForge API and must be downloaded manually.\n" +
|
if (!fileIdMap.contains(fileId)) {
|
||||||
"Please go to ${mod.links?.websiteUrl}/files/${modFile.second} and save this file to ${modFile.first.destURI.rebase(packFolder).nioPath.absolute()}")))
|
failures.add(ExceptionDetails(mod.name,
|
||||||
|
Exception("Failed to find file from result: file ID $fileId")))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for (indexFile in fileIdMap[fileId]!!) {
|
||||||
|
var modUrl = "${mod.links?.websiteUrl}/files/${fileId}"
|
||||||
|
failures.add(ExceptionDetails(indexFile.name, Exception("This mod is excluded from the CurseForge API and must be downloaded manually.\n" +
|
||||||
|
"Please go to ${modUrl} and save this file to ${indexFile.destURI.rebase(packFolder).nioPath.absolute()}"), modUrl))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,9 +8,12 @@ import java.net.SocketTimeoutException
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class ClientHolder {
|
class ClientHolder {
|
||||||
|
// Tries 10s timeouts (default), then 15s timeouts, then 60s timeouts
|
||||||
|
private val retryTimes = arrayOf(15, 60)
|
||||||
|
|
||||||
// TODO: a button to increase timeouts temporarily when retrying? manual retry button?
|
// TODO: a button to increase timeouts temporarily when retrying? manual retry button?
|
||||||
val okHttpClient by lazy { OkHttpClient.Builder()
|
val okHttpClient by lazy { OkHttpClient.Builder()
|
||||||
// Retry requests up to 3 times, increasing the timeouts slightly if it failed
|
// Retry requests according to retryTimes list
|
||||||
.addInterceptor {
|
.addInterceptor {
|
||||||
val req = it.request()
|
val req = it.request()
|
||||||
|
|
||||||
@ -24,20 +27,20 @@ class ClientHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var tryCount = 0
|
var tryCount = 0
|
||||||
while (res == null && tryCount < 3) {
|
while (res == null && tryCount < retryTimes.size) {
|
||||||
tryCount++
|
Log.info("OkHttp connection to ${req.url} timed out; retrying... (${tryCount + 1}/${retryTimes.size})")
|
||||||
|
|
||||||
Log.info("OkHttp connection to ${req.url} timed out; retrying... ($tryCount/3)")
|
|
||||||
|
|
||||||
val longerTimeoutChain = it
|
val longerTimeoutChain = it
|
||||||
.withConnectTimeout(10 * tryCount, TimeUnit.SECONDS)
|
.withConnectTimeout(retryTimes[tryCount], TimeUnit.SECONDS)
|
||||||
.withReadTimeout(10 * tryCount, TimeUnit.SECONDS)
|
.withReadTimeout(retryTimes[tryCount], TimeUnit.SECONDS)
|
||||||
.withWriteTimeout(10 * tryCount, TimeUnit.SECONDS)
|
.withWriteTimeout(retryTimes[tryCount], TimeUnit.SECONDS)
|
||||||
try {
|
try {
|
||||||
res = longerTimeoutChain.proceed(req)
|
res = longerTimeoutChain.proceed(req)
|
||||||
} catch (e: SocketTimeoutException) {
|
} catch (e: SocketTimeoutException) {
|
||||||
lastException = e
|
lastException = e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tryCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
res ?: throw lastException!!
|
res ?: throw lastException!!
|
||||||
|
@ -4,42 +4,29 @@ import cc.ekblad.toml.model.TomlValue
|
|||||||
import cc.ekblad.toml.tomlMapper
|
import cc.ekblad.toml.tomlMapper
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
enum class Side {
|
enum class Side(sideName: String) {
|
||||||
@SerializedName("client")
|
@SerializedName("client")
|
||||||
CLIENT("client"),
|
CLIENT("client"),
|
||||||
@SerializedName("server")
|
@SerializedName("server")
|
||||||
SERVER("server"),
|
SERVER("server"),
|
||||||
@SerializedName("both")
|
@SerializedName("both")
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
BOTH("both", arrayOf(CLIENT, SERVER));
|
BOTH("both") {
|
||||||
|
override fun hasSide(tSide: Side): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private val sideName: String
|
private val sideName: String
|
||||||
private val depSides: Array<Side>?
|
|
||||||
|
|
||||||
constructor(sideName: String) {
|
init {
|
||||||
this.sideName = sideName.lowercase()
|
this.sideName = sideName.lowercase()
|
||||||
depSides = null
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(sideName: String, depSides: Array<Side>) {
|
|
||||||
this.sideName = sideName.lowercase()
|
|
||||||
this.depSides = depSides
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString() = sideName
|
override fun toString() = sideName
|
||||||
|
|
||||||
fun hasSide(tSide: Side): Boolean {
|
open fun hasSide(tSide: Side): Boolean {
|
||||||
if (this == tSide) {
|
return this == tSide || tSide == BOTH
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (depSides != null) {
|
|
||||||
for (depSide in depSides) {
|
|
||||||
if (depSide == tSide) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -23,9 +23,12 @@ interface IUserInterface {
|
|||||||
|
|
||||||
fun showCancellationDialog(): CancellationResult = CancellationResult.QUIT
|
fun showCancellationDialog(): CancellationResult = CancellationResult.QUIT
|
||||||
|
|
||||||
fun showUpdateConfirmationDialog(oldVersions: List<Pair<String, String?>>, newVersions: List<Pair<String, String?>>): UpdateConfirmationResult = UpdateConfirmationResult.CANCELLED
|
fun showUpdateConfirmationDialog(oldVersions: List<Pair<String, String?>>, newVersions: List<Pair<String, String?>>): UpdateConfirmationResult {
|
||||||
|
// Always update metadata when using the CLI
|
||||||
|
return UpdateConfirmationResult.UPDATE
|
||||||
|
}
|
||||||
|
|
||||||
fun awaitOptionalButton(showCancel: Boolean)
|
fun awaitOptionalButton(showCancel: Boolean, timeout: Long)
|
||||||
|
|
||||||
enum class ExceptionListResult {
|
enum class ExceptionListResult {
|
||||||
CONTINUE, CANCEL, IGNORE
|
CONTINUE, CANCEL, IGNORE
|
||||||
|
@ -63,7 +63,7 @@ class CLIHandler : IUserInterface {
|
|||||||
return ExceptionListResult.CANCEL
|
return ExceptionListResult.CANCEL
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun awaitOptionalButton(showCancel: Boolean) {
|
override fun awaitOptionalButton(showCancel: Boolean, timeout: Long) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,5 +2,6 @@ package link.infra.packwiz.installer.ui.data
|
|||||||
|
|
||||||
data class ExceptionDetails(
|
data class ExceptionDetails(
|
||||||
val name: String,
|
val name: String,
|
||||||
val exception: Exception
|
val exception: Exception,
|
||||||
)
|
val modUrl: String? = null
|
||||||
|
)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package link.infra.packwiz.installer.ui.gui
|
package link.infra.packwiz.installer.ui.gui
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.util.Log
|
||||||
import link.infra.packwiz.installer.ui.IUserInterface
|
import link.infra.packwiz.installer.ui.IUserInterface
|
||||||
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
@ -24,6 +25,24 @@ class ExceptionListWindow(eList: List<ExceptionDetails>, future: CompletableFutu
|
|||||||
fun getExceptionAt(index: Int) = details[index].exception
|
fun getExceptionAt(index: Int) = details[index].exception
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openUrl(url: String) {
|
||||||
|
try {
|
||||||
|
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
||||||
|
Desktop.getDesktop().browse(URI(url))
|
||||||
|
} else {
|
||||||
|
val process = Runtime.getRuntime().exec(arrayOf("xdg-open", url));
|
||||||
|
val exitValue = process.waitFor()
|
||||||
|
if (exitValue > 0) {
|
||||||
|
Log.warn("Failed to open $url: xdg-open exited with code $exitValue")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.warn("Failed to open $url", e)
|
||||||
|
} catch (e: URISyntaxException) {
|
||||||
|
Log.warn("Failed to open $url", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the dialog.
|
* Create the dialog.
|
||||||
*/
|
*/
|
||||||
@ -112,6 +131,19 @@ class ExceptionListWindow(eList: List<ExceptionDetails>, future: CompletableFutu
|
|||||||
this@ExceptionListWindow.dispose()
|
this@ExceptionListWindow.dispose()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
val missingMods = eList.filter { it.modUrl != null }.map { it.modUrl!! }.toSet()
|
||||||
|
|
||||||
|
if (!missingMods.isEmpty()) {
|
||||||
|
add(JButton("Open missing mods").apply {
|
||||||
|
toolTipText = "Open missing mods in your browser"
|
||||||
|
addActionListener {
|
||||||
|
missingMods.forEach {
|
||||||
|
openUrl(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}, BorderLayout.EAST)
|
}, BorderLayout.EAST)
|
||||||
|
|
||||||
// Errored label
|
// Errored label
|
||||||
@ -122,16 +154,8 @@ class ExceptionListWindow(eList: List<ExceptionDetails>, future: CompletableFutu
|
|||||||
// Left buttons
|
// Left buttons
|
||||||
add(JPanel().apply {
|
add(JPanel().apply {
|
||||||
add(JButton("Report issue").apply {
|
add(JButton("Report issue").apply {
|
||||||
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
addActionListener {
|
||||||
addActionListener {
|
openUrl("https://github.com/packwiz/packwiz-installer/issues/new")
|
||||||
try {
|
|
||||||
Desktop.getDesktop().browse(URI("https://github.com/packwiz/packwiz-installer/issues/new"))
|
|
||||||
} catch (e: IOException) {
|
|
||||||
// lol the button just won't work i guess
|
|
||||||
} catch (e: URISyntaxException) {}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
isEnabled = false
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, BorderLayout.WEST)
|
}, BorderLayout.WEST)
|
||||||
@ -150,4 +174,4 @@ class ExceptionListWindow(eList: List<ExceptionDetails>, future: CompletableFutu
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,13 @@ import link.infra.packwiz.installer.ui.data.IOptionDetails
|
|||||||
import link.infra.packwiz.installer.ui.data.InstallProgress
|
import link.infra.packwiz.installer.ui.data.InstallProgress
|
||||||
import link.infra.packwiz.installer.util.Log
|
import link.infra.packwiz.installer.util.Log
|
||||||
import java.awt.EventQueue
|
import java.awt.EventQueue
|
||||||
|
import java.util.Timer
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import javax.swing.JDialog
|
import javax.swing.JDialog
|
||||||
import javax.swing.JOptionPane
|
import javax.swing.JOptionPane
|
||||||
import javax.swing.UIManager
|
import javax.swing.UIManager
|
||||||
|
import kotlin.concurrent.timer
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class GUIHandler : IUserInterface {
|
class GUIHandler : IUserInterface {
|
||||||
@ -220,12 +222,28 @@ class GUIHandler : IUserInterface {
|
|||||||
return future.get()
|
return future.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun awaitOptionalButton(showCancel: Boolean) {
|
override fun awaitOptionalButton(showCancel: Boolean, timeout: Long) {
|
||||||
EventQueue.invokeAndWait {
|
EventQueue.invokeAndWait {
|
||||||
frmPackwizlauncher.showOk(!showCancel)
|
frmPackwizlauncher.showOk(!showCancel)
|
||||||
}
|
}
|
||||||
visibleCountdownLatch.await()
|
visibleCountdownLatch.await()
|
||||||
|
|
||||||
|
var closeTimer: Timer? = null
|
||||||
|
if (timeout >= 0) {
|
||||||
|
var count = 0
|
||||||
|
closeTimer = timer("timeout", true, 0, 1000) {
|
||||||
|
if (count >= timeout) {
|
||||||
|
optionalSelectedLatch.countDown()
|
||||||
|
cancel()
|
||||||
|
} else {
|
||||||
|
frmPackwizlauncher.timeoutOk(timeout - count)
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
optionalSelectedLatch.await()
|
optionalSelectedLatch.await()
|
||||||
|
closeTimer?.cancel()
|
||||||
EventQueue.invokeLater {
|
EventQueue.invokeLater {
|
||||||
frmPackwizlauncher.hideOk()
|
frmPackwizlauncher.hideOk()
|
||||||
}
|
}
|
||||||
|
@ -121,4 +121,8 @@ class InstallWindow(private val handler: GUIHandler) : JFrame() {
|
|||||||
}
|
}
|
||||||
buttonsPanel.revalidate()
|
buttonsPanel.revalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun timeoutOk(remaining: Long) {
|
||||||
|
btnOk.text = "Continue ($remaining)"
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user