From 27fa279eed1b3a4336c4453bbc48ef1ce768036a Mon Sep 17 00:00:00 2001 From: Danielle Voznyy Date: Thu, 7 Mar 2024 12:29:46 -0500 Subject: [PATCH] Support extra data in modrinth packs swap default jvm to openjdk, fix bug when leaving coroutine scope while downloading, minor ui fixes --- build.gradle.kts | 1 - gradle.properties | 2 +- .../com/mineinabyss/launchy/data/Dirs.kt | 9 ++- .../mineinabyss/launchy/data/config/Config.kt | 1 + .../launchy/data/config/GameInstance.kt | 35 ++-------- .../launchy/data/config/GameInstanceConfig.kt | 39 +++++++---- .../launchy/data/config/PlayerProfile.kt | 16 +++-- .../launchy/data/modpacks/ExtraPackInfo.kt | 14 ++++ .../mineinabyss/launchy/data/modpacks/Mod.kt | 2 +- .../launchy/data/modpacks/ModInfo.kt | 2 +- .../launchy/data/modpacks/Modpack.kt | 3 +- .../mineinabyss/launchy/data/modpacks/Mods.kt | 2 +- .../data/modpacks/formats/ExtraInfoFormat.kt | 34 ++++++++++ .../modpacks/formats/LaunchyPackFormat.kt | 2 +- .../modpacks/formats/ModrinthPackFormat.kt | 2 +- .../data/modpacks/formats/PackFormat.kt | 2 +- .../data/modpacks/source/PackSource.kt | 5 +- .../launchy/data/modpacks/source/PackType.kt | 16 +++-- .../com/mineinabyss/launchy/logic/Browser.kt | 13 +++- .../mineinabyss/launchy/logic/Downloader.kt | 66 +++++++++---------- .../com/mineinabyss/launchy/logic/Launcher.kt | 4 +- .../launchy/logic/ModDownloader.kt | 23 ++++--- .../launchy/logic/SuggestedJVMArgs.kt | 6 +- .../com/mineinabyss/launchy/state/JvmState.kt | 6 +- .../mineinabyss/launchy/state/LaunchyState.kt | 11 ++-- .../mineinabyss/launchy/ui/colors/Color.kt | 4 ++ .../mineinabyss/launchy/ui/colors/Theme.kt | 11 ++-- .../launchy/ui/dialogs/AuthDialog.kt | 6 -- .../launchy/ui/dialogs/SelectJVMDialog.kt | 3 +- .../launchy/ui/elements/PlayerAvatar.kt | 7 +- .../launchy/ui/screens/AccountsPopup.kt | 7 +- .../mineinabyss/launchy/ui/screens/Screen.kt | 8 ++- .../mineinabyss/launchy/ui/screens/Screens.kt | 7 ++ .../launchy/ui/screens/home/ModpackCard.kt | 4 +- .../screens/home/newinstance/NewInstance.kt | 5 +- .../screens/home/settings/SettingsScreen.kt | 12 ++++ .../screens/modpack/main/MainScreenImages.kt | 55 +++++++++------- .../ui/screens/modpack/main/ModpackScreen.kt | 6 +- .../modpack/main/buttons/InstallButton.kt | 3 +- .../modpack/main/buttons/PlayButton.kt | 6 +- .../kotlin/com/mineinabyss/launchy/util/OS.kt | 33 +++++++++- 41 files changed, 313 insertions(+), 180 deletions(-) create mode 100644 src/main/kotlin/com/mineinabyss/launchy/data/modpacks/ExtraPackInfo.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/ExtraInfoFormat.kt diff --git a/build.gradle.kts b/build.gradle.kts index ed33f82..e430883 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,7 +34,6 @@ dependencies { implementation("com.darkrockstudios:mpfilepicker:3.1.0") implementation("org.rauschig:jarchivelib:1.2.0") - implementation("edu.stanford.ejalbert:BrowserLauncher2:1.3") implementation("net.raphimc:MinecraftAuth:4.0.0") implementation("dev.3-3:jmccc-mcdownloader:3.1.4") diff --git a/gradle.properties b/gradle.properties index cd24856..0fd326a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ group=com.mineinabyss -version=2.0.0-alpha.3 +version=2.0.0-alpha.4 idofrontVersion=0.22.3 diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/Dirs.kt b/src/main/kotlin/com/mineinabyss/launchy/data/Dirs.kt index e43031a..28485d2 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/Dirs.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/Dirs.kt @@ -24,15 +24,16 @@ object Dirs { OS.LINUX -> home / ".config" } / "mineinabyss" + val cacheDir = config / "cache" + val imageCache = cacheDir / "images" + val jdks = mineinabyss / ".jdks" - val jdkGraal = mineinabyss / ".jdks" / "graalvm-jdk-17.zip" val tmp = config / ".tmp" val accounts = config / "accounts" - val avatars = config / "avatars" - fun avatar(uuid: UUID) = avatars / "$uuid.png" + fun avatar(uuid: UUID) = imageCache / "avatar-$uuid" val configFile = config / "mia-launcher.yml" val versionsFile = config / "mia-versions.yml" @@ -48,6 +49,8 @@ object Dirs { tmp.createDirectories() modpackConfigsDir.createDirectories() jdks.createDirectories() + cacheDir.createDirectories() + imageCache.createDirectories() } fun createConfigFiles() { diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/config/Config.kt b/src/main/kotlin/com/mineinabyss/launchy/data/config/Config.kt index 1a611ca..b99e43a 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/config/Config.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/config/Config.kt @@ -19,6 +19,7 @@ data class Config( val jvmArguments: String? = null, val memoryAllocation: Int? = null, val useRecommendedJvmArguments: Boolean = true, + val preferHue: Float? = null, ) { fun save() { Dirs.configFile.writeText(Formats.yaml.encodeToString(this)) diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/config/GameInstance.kt b/src/main/kotlin/com/mineinabyss/launchy/data/config/GameInstance.kt index 98c0f70..4e8d2eb 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/config/GameInstance.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/config/GameInstance.kt @@ -1,13 +1,9 @@ package com.mineinabyss.launchy.data.config -import androidx.compose.ui.graphics.FilterQuality -import androidx.compose.ui.graphics.painter.BitmapPainter -import androidx.compose.ui.res.loadImageBitmap import com.charleskorn.kaml.decodeFromStream import com.charleskorn.kaml.encodeToStream import com.mineinabyss.launchy.data.Dirs import com.mineinabyss.launchy.data.Formats -import com.mineinabyss.launchy.logic.Downloader import com.mineinabyss.launchy.state.LaunchyState import com.mineinabyss.launchy.state.modpack.ModpackState import com.mineinabyss.launchy.ui.screens.Dialog @@ -18,6 +14,8 @@ import kotlin.io.path.* class GameInstance( val configDir: Path, ) { + val overridesDir = configDir / "overrides" + init { require(configDir.isDirectory()) { "Game instance at $configDir must be a directory" } } @@ -27,27 +25,6 @@ class GameInstance( val minecraftDir = config.overrideMinecraftDir?.let { Path(it) } ?: Dirs.modpackDir(configDir.name) val userConfigFile = (configDir / "config.yml") - private val backgroundImagePath = configDir / "background.png" - private val logoPath = configDir / "logo.png" - private var cachedBackground: BitmapPainter? = null - private var cachedLogo: BitmapPainter? = null - - suspend fun getOrDownloadBackground(): BitmapPainter { - cachedBackground?.let { return it } - if (!backgroundImagePath.exists()) Downloader.download(config.backgroundURL, backgroundImagePath) - val painter = - BitmapPainter(loadImageBitmap(backgroundImagePath.inputStream()), filterQuality = FilterQuality.High) - cachedBackground = painter - return painter - } - - suspend fun getOrDownloadLogo(): BitmapPainter { - cachedLogo?.let { return it } - if (!logoPath.exists()) Downloader.download(config.logoURL, logoPath) - val painter = BitmapPainter(loadImageBitmap(logoPath.inputStream()), filterQuality = FilterQuality.High) - cachedLogo = painter - return painter - } suspend fun createModpackState(): ModpackState? { val userConfig = @@ -55,9 +32,11 @@ class GameInstance( else ModpackUserConfig() val modpack = config.source.loadInstance(this) .getOrElse { - dialog = Dialog.Error("Failed read instance", it - .stackTraceToString() - .split("\n").take(5).joinToString("\n")) + dialog = Dialog.Error( + "Failed read instance", it + .stackTraceToString() + .split("\n").take(5).joinToString("\n") + ) it.printStackTrace() return null } diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/config/GameInstanceConfig.kt b/src/main/kotlin/com/mineinabyss/launchy/data/config/GameInstanceConfig.kt index 0a38f34..5d55b32 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/config/GameInstanceConfig.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/config/GameInstanceConfig.kt @@ -1,18 +1,22 @@ package com.mineinabyss.launchy.data.config -import androidx.compose.ui.graphics.FilterQuality +import androidx.compose.runtime.Composable +import androidx.compose.runtime.produceState import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.res.loadImageBitmap import com.mineinabyss.launchy.data.Dirs import com.mineinabyss.launchy.data.Formats import com.mineinabyss.launchy.data.modpacks.source.PackSource import com.mineinabyss.launchy.logic.Downloader +import com.mineinabyss.launchy.state.LaunchyState +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import java.nio.file.Path import kotlin.io.path.div -import kotlin.io.path.exists import kotlin.io.path.inputStream +import kotlin.time.Duration.Companion.seconds @Serializable @OptIn(ExperimentalStdlibApi::class) @@ -27,10 +31,10 @@ data class GameInstanceConfig( val overrideMinecraftDir: String? = null, ) { @Transient - private val backgroundImagePath = Dirs.tmp / "background-${backgroundURL.hashCode().toHexString()}.png" + val backgroundPath = Dirs.imageCache / "background-${backgroundURL.hashCode().toHexString()}" @Transient - private val logoPath = Dirs.tmp / "icon-${backgroundURL.hashCode().toHexString()}.png" + val logoPath = Dirs.imageCache / "icon-${logoURL.hashCode().toHexString()}" @Transient private var cachedBackground: BitmapPainter? = null @@ -38,23 +42,36 @@ data class GameInstanceConfig( @Transient private var cachedLogo: BitmapPainter? = null - suspend fun loadBackgroundFromTmpFile(): BitmapPainter { + suspend fun loadBackgroundImage(): BitmapPainter { cachedBackground?.let { return it } - if (!backgroundImagePath.exists()) Downloader.download(backgroundURL, backgroundImagePath) - val painter = - BitmapPainter(loadImageBitmap(backgroundImagePath.inputStream()), filterQuality = FilterQuality.High) + Downloader.download(backgroundURL, backgroundPath, override = false) + val painter = BitmapPainter(loadImageBitmap(backgroundPath.inputStream())) cachedBackground = painter return painter } - suspend fun loadIconFromTmpFile(): BitmapPainter { + suspend fun loadLogo(): BitmapPainter { cachedLogo?.let { return it } - if (!logoPath.exists()) Downloader.download(logoURL, logoPath) - val painter = BitmapPainter(loadImageBitmap(logoPath.inputStream()), filterQuality = FilterQuality.High) + Downloader.download(logoURL, logoPath, override = false) + val painter = BitmapPainter(loadImageBitmap(logoPath.inputStream())) cachedLogo = painter return painter } + @Composable + fun produceBackgroundState(state: LaunchyState) = produceState(cachedBackground) { + state.downloadContext.launch { + value = loadBackgroundImage() + } + } + + @Composable + fun produceLogoState(state: LaunchyState) = produceState(cachedLogo) { + state.downloadContext.launch { + value = loadLogo() + } + } + companion object { fun read(path: Path) = Formats.yaml.decodeFromStream(serializer(), path.inputStream()) diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/config/PlayerProfile.kt b/src/main/kotlin/com/mineinabyss/launchy/data/config/PlayerProfile.kt index 3e163c3..0b7fabb 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/config/PlayerProfile.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/config/PlayerProfile.kt @@ -1,13 +1,15 @@ package com.mineinabyss.launchy.data.config -import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.FilterQuality +import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.res.loadImageBitmap import com.mineinabyss.launchy.data.Dirs import com.mineinabyss.launchy.data.serializers.UUIDSerializer import com.mineinabyss.launchy.logic.Downloader +import com.mineinabyss.launchy.state.LaunchyState +import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import java.util.* -import kotlin.io.path.exists import kotlin.io.path.inputStream @Serializable @@ -15,11 +17,11 @@ data class PlayerProfile( val name: String, val uuid: @Serializable(with = UUIDSerializer::class) UUID, ) { - suspend fun getAvatar(): ImageBitmap { + suspend fun getAvatar(state: LaunchyState): BitmapPainter { val avatarPath = Dirs.avatar(uuid) - - if (!avatarPath.exists()) Downloader.downloadAvatar(uuid) - - return loadImageBitmap(avatarPath.inputStream()) + state.downloadContext.launch { + Downloader.downloadAvatar(uuid) + }.join() + return BitmapPainter(loadImageBitmap(avatarPath.inputStream()), filterQuality = FilterQuality.None) } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/ExtraPackInfo.kt b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/ExtraPackInfo.kt new file mode 100644 index 0000000..9f215e5 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/ExtraPackInfo.kt @@ -0,0 +1,14 @@ +package com.mineinabyss.launchy.data.modpacks + +import kotlinx.serialization.Serializable + +@Serializable +class ModReference( + val urlContains: String, + val info: ModInfo? = null, +) +@Serializable +class ExtraPackInfo( + val groups: List = listOf(), + val modGroups: Map> = mapOf(), +) diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Mod.kt b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Mod.kt index 3b0d18d..0a5ca7e 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Mod.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Mod.kt @@ -5,7 +5,7 @@ import java.nio.file.Path import kotlin.io.path.div import kotlin.io.path.exists -class Mod( +data class Mod( val packDir: Path, val info: ModInfo ) { diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/ModInfo.kt b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/ModInfo.kt index 947891d..ecc480c 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/ModInfo.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/ModInfo.kt @@ -7,7 +7,7 @@ data class ModInfo( val name: String, val license: String = "Unknown", val homepage: String = "", - val desc: String, + val desc: String = "", val url: String, val configUrl: String = "", val configDesc: String = "", diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Modpack.kt b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Modpack.kt index 3830f3b..e5f3621 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Modpack.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Modpack.kt @@ -1,10 +1,9 @@ package com.mineinabyss.launchy.data.modpacks -import com.mineinabyss.launchy.data.config.GameInstance import java.nio.file.Path class Modpack( val dependencies: PackDependencies, val mods: Mods, - val overridesPath: Path? = null, + val overridesPaths: List = listOf(), ) diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Mods.kt b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Mods.kt index 4bc0e86..d79426c 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Mods.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Mods.kt @@ -3,7 +3,7 @@ package com.mineinabyss.launchy.data.modpacks import com.mineinabyss.launchy.data.GroupName import com.mineinabyss.launchy.data.ModName -class Mods( +data class Mods( val modGroups: Map>, ) { val groups = modGroups.keys diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/ExtraInfoFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/ExtraInfoFormat.kt new file mode 100644 index 0000000..35fed3d --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/ExtraInfoFormat.kt @@ -0,0 +1,34 @@ +package com.mineinabyss.launchy.data.modpacks.formats + +import com.mineinabyss.launchy.data.modpacks.ExtraPackInfo +import com.mineinabyss.launchy.data.modpacks.Group +import com.mineinabyss.launchy.data.modpacks.Mod +import com.mineinabyss.launchy.data.modpacks.Mods +import java.nio.file.Path + + +data class ExtraInfoFormat( + val format: PackFormat, + val extraInfoPack: ExtraPackInfo, +) : PackFormat by format { + override fun toGenericMods(minecraftDir: Path): Mods { + val originalMods = format.toGenericMods(minecraftDir) + val foundMods = mutableSetOf() + val mods: Map> = extraInfoPack.modGroups + .mapKeys { (name, _) -> extraInfoPack.groups.single { it.name == name } } + .mapValues { (_, mods) -> + mods.mapNotNull { ref -> + val found = originalMods.mods.find { mod -> ref.urlContains in mod.info.url } + if (found != null) foundMods.add(found) + if (found != null && ref.info != null) + found.copy(info = ref.info.copy(url = found.info.url)) + else found + }.toSet() + } + + val originalGroups = originalMods.modGroups.mapValues { + it.value.filterTo(mutableSetOf()) { mod -> mod !in foundMods } + } + return Mods(originalGroups + mods) + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/LaunchyPackFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/LaunchyPackFormat.kt index dfa91d6..f3305c2 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/LaunchyPackFormat.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/LaunchyPackFormat.kt @@ -22,5 +22,5 @@ data class LaunchyPackFormat( return PackDependencies(minecraft = minecraftVersion, fabricLoader = fabricVersion) } - override fun getOverridesPath(configDir: Path): Path? = null + override fun getOverridesPaths(configDir: Path): List = emptyList() } diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/ModrinthPackFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/ModrinthPackFormat.kt index 74eef0c..af05fb4 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/ModrinthPackFormat.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/ModrinthPackFormat.kt @@ -40,6 +40,6 @@ data class ModrinthPackFormat( override fun toGenericMods(minecraftDir: Path) = Mods.withSingleGroup(files.map { it.toMod(minecraftDir) }) - override fun getOverridesPath(configDir: Path): Path = configDir / "mrpack" / "overrides" + override fun getOverridesPaths(configDir: Path): List = listOf(configDir / "mrpack" / "overrides") } diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/PackFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/PackFormat.kt index c5c59c9..9cc86cd 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/PackFormat.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/PackFormat.kt @@ -9,5 +9,5 @@ sealed interface PackFormat { fun getDependencies(minecraftDir: Path): PackDependencies - fun getOverridesPath(configDir: Path): Path? + fun getOverridesPaths(configDir: Path): List } diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/source/PackSource.kt b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/source/PackSource.kt index 8f847f3..54e01c1 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/source/PackSource.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/source/PackSource.kt @@ -5,9 +5,6 @@ import com.mineinabyss.launchy.data.modpacks.Modpack import com.mineinabyss.launchy.logic.Downloader import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlin.io.path.createFile -import kotlin.io.path.createParentDirectories -import kotlin.io.path.exists @Serializable sealed class PackSource { @@ -18,7 +15,7 @@ sealed class PackSource { val format = type.getFormat(instance.configDir).getOrThrow() val mods = format.toGenericMods(instance.minecraftDir) val dependencies = format.getDependencies(instance.minecraftDir) - Modpack(dependencies, mods, format.getOverridesPath(instance.configDir)) + Modpack(dependencies, mods, format.getOverridesPaths(instance.configDir)) } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/source/PackType.kt b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/source/PackType.kt index 0dd4086..62345a8 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/source/PackType.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/source/PackType.kt @@ -2,13 +2,15 @@ package com.mineinabyss.launchy.data.modpacks.source import com.charleskorn.kaml.decodeFromStream import com.mineinabyss.launchy.data.Formats -import com.mineinabyss.launchy.data.config.unzip +import com.mineinabyss.launchy.data.modpacks.ExtraPackInfo +import com.mineinabyss.launchy.data.modpacks.formats.ExtraInfoFormat import com.mineinabyss.launchy.data.modpacks.formats.LaunchyPackFormat import com.mineinabyss.launchy.data.modpacks.formats.ModrinthPackFormat import com.mineinabyss.launchy.data.modpacks.formats.PackFormat import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.decodeFromStream +import org.rauschig.jarchivelib.ArchiverFactory import java.nio.file.Path import kotlin.io.path.* @@ -30,7 +32,7 @@ enum class PackType { if (this == Modrinth) { val unzipDir = configDir / "mrpack" unzipDir.deleteRecursively() - unzip(path, unzipDir) + ArchiverFactory.createArchiver("zip").extract(path.toFile(), unzipDir.toFile()) } } @@ -46,10 +48,16 @@ enum class PackType { Modrinth -> runCatching { val unzipDir = configDir / "mrpack" val index = unzipDir / "modrinth.index.json" - if(unzipDir.notExists()) { + if (unzipDir.notExists()) { afterDownload(configDir) } - Formats.json.decodeFromStream(index.inputStream()) + val extraInfoFile = (unzipDir / "launchy.yml").takeIf { it.isRegularFile() } + val extraInfo = extraInfoFile?.runCatching { + Formats.yaml.decodeFromStream(extraInfoFile.inputStream()) + }?.getOrNull() + val mrpack = Formats.json.decodeFromStream(index.inputStream()) + if (extraInfo != null) ExtraInfoFormat(mrpack, extraInfo) + else mrpack } } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/Browser.kt b/src/main/kotlin/com/mineinabyss/launchy/logic/Browser.kt index 9a3a5d9..6e4cf90 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/Browser.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/logic/Browser.kt @@ -1,9 +1,20 @@ package com.mineinabyss.launchy.logic +import com.mineinabyss.launchy.util.OS import java.awt.Desktop import java.net.URI object Browser { val desktop = Desktop.getDesktop() - fun browse(url: String) = synchronized(desktop) { desktop.browse(URI.create(url)) } + fun browse(url: String): Result<*> = synchronized(desktop) { + val os = OS.get() + runCatching { + when { + Desktop.isDesktopSupported() && desktop.isSupported(Desktop.Action.BROWSE) -> desktop.browse(URI.create(url)) + os == OS.LINUX -> Runtime.getRuntime().exec("xdg-open $url") + os == OS.MAC -> Runtime.getRuntime().exec("open $url") + else -> error("Unsupported OS") + } + } + } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/Downloader.kt b/src/main/kotlin/com/mineinabyss/launchy/logic/Downloader.kt index 995d2d8..6581ebf 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/Downloader.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/logic/Downloader.kt @@ -3,8 +3,7 @@ package com.mineinabyss.launchy.logic import com.mineinabyss.launchy.data.Dirs import com.mineinabyss.launchy.state.InProgressTask import com.mineinabyss.launchy.state.LaunchyState -import com.mineinabyss.launchy.ui.screens.Dialog -import com.mineinabyss.launchy.ui.screens.dialog +import com.mineinabyss.launchy.util.Arch import com.mineinabyss.launchy.util.OS import io.ktor.client.* import io.ktor.client.call.* @@ -23,29 +22,31 @@ import java.util.* import kotlin.io.path.* object Downloader { - val cacheDir = Dirs.config / "cache" val httpClient = HttpClient(CIO) { install(HttpTimeout) } suspend fun downloadAvatar(uuid: UUID) { - download("https://crafatar.com/avatars/$uuid?size=16&overlay", Dirs.avatar(uuid)) + download("https://crafatar.com/avatars/$uuid?size=16&overlay", Dirs.avatar(uuid), override = false) } + @OptIn(ExperimentalStdlibApi::class) suspend fun download( url: String, writeTo: Path, + override: Boolean = true, onFinishDownloadWhenChanged: () -> Unit = {}, onProgressUpdate: (progress: Progress) -> Unit = {}, ): Result { return runCatching { + if (!override && writeTo.exists()) return@runCatching val startTime = System.currentTimeMillis() writeTo.createParentDirectories() val headers = httpClient.head(url).headers - val lastModified = headers["Last-Modified"]?.fromHttpToGmtDate() - val length = headers["Content-Length"]?.toLongOrNull() + val lastModified = headers["Last-Modified"]?.fromHttpToGmtDate()?.timestamp?.toHexString() + val length = headers["Content-Length"]?.toLongOrNull()?.toHexString() val cache = "Last-Modified: $lastModified, Content-Length: $length" - val cacheFile = cacheDir / "${writeTo.name}.cache" + val cacheFile = Dirs.cacheDir / "${writeTo.name}.cache" if (writeTo.exists() && cacheFile.exists() && cacheFile.readText() == cache) return@runCatching cacheFile.createParentDirectories() cacheFile.deleteIfExists() @@ -95,57 +96,54 @@ object Downloader { ): Path? { try { state.inProgressTasks["installJDK"] = InProgressTask("Downloading Java environment") + val arch = Arch.get().openJDKArch + val os = OS.get().openJDKName + val url = "https://api.adoptium.net/v3/binary/latest/17/ga/$os/$arch/jre/hotspot/normal/eclipse" val javaInstallation = when (OS.get()) { OS.WINDOWS -> JavaInstallation( - "https://download.oracle.com/graalvm/17/latest/graalvm-jdk-17_windows-x64_bin.zip", + url, "bin/java.exe", ArchiverFactory.createArchiver(ArchiveFormat.ZIP) ) - OS.MAC -> when { - OS.isArm() -> JavaInstallation( - "https://download.oracle.com/graalvm/17/latest/graalvm-jdk-17_macos-aarch64_bin.tar.gz", - "Contents/Home/bin/java", - ArchiverFactory.createArchiver(ArchiveFormat.TAR, CompressionType.GZIP) - ) - else -> JavaInstallation( - "https://download.oracle.com/graalvm/17/latest/graalvm-jdk-17_macos-x64_bin.tar.gz", - "Contents/Home/bin/java", - ArchiverFactory.createArchiver(ArchiveFormat.TAR, CompressionType.GZIP) - ) - } + OS.MAC -> JavaInstallation( + url, + "Contents/Home/bin/java", + ArchiverFactory.createArchiver(ArchiveFormat.TAR, CompressionType.GZIP) + ) OS.LINUX -> JavaInstallation( - "https://download.oracle.com/graalvm/17/latest/graalvm-jdk-17_linux-x64_bin.tar.gz", + url, "bin/java", ArchiverFactory.createArchiver(ArchiveFormat.TAR, CompressionType.GZIP) ) } - val existingInstall = findGraalvmExtractedPath()?.resolve(javaInstallation.relativeJavaExecutable) - if (existingInstall?.exists() == true) return existingInstall - download(javaInstallation.url, Dirs.jdkGraal, onProgressUpdate = { + val downloadTo = Dirs.jdks / "openjdk-17${javaInstallation.archiver.filenameExtension}" + val extractTo = Dirs.jdks / "openjdk-17" + + val existingInstall = extractTo.resolve(javaInstallation.relativeJavaExecutable) + if (existingInstall.exists()) return existingInstall + download(javaInstallation.url, downloadTo, onProgressUpdate = { state.inProgressTasks["installJDK"] = - InProgressTask.WithPercentage( + InProgressTask.bytes( "Downloading Java environment", it.bytesDownloaded, - it.totalBytes, - "MB" + it.totalBytes ) }) state.inProgressTasks["installJDK"] = InProgressTask("Extracting Java environment") // Handle a case where the extraction failed and the folder exists but not the java executable - findGraalvmExtractedPath()?.takeIf { it.exists() }?.deleteRecursively() - javaInstallation.archiver.extract(Dirs.jdkGraal.toFile(), Dirs.jdks.toFile()) - return (findGraalvmExtractedPath() ?: return null) / javaInstallation.relativeJavaExecutable + extractTo.takeIf { it.exists() }?.deleteRecursively() + javaInstallation.archiver.extract(downloadTo.toFile(), extractTo.toFile()) + val entries = extractTo.listDirectoryEntries() + val jrePath = if (entries.size == 1) entries.first() else extractTo + downloadTo.deleteIfExists() + return jrePath / javaInstallation.relativeJavaExecutable } finally { state.inProgressTasks.remove("installJDK") } } - - fun findGraalvmExtractedPath() = Dirs.jdks - .listDirectoryEntries() - .firstOrNull { it.isDirectory() && it.name.startsWith("graalvm-jdk-17") } } data class Progress(val bytesDownloaded: Long, val totalBytes: Long, val timeElapsed: Long) { diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/Launcher.kt b/src/main/kotlin/com/mineinabyss/launchy/logic/Launcher.kt index f754e98..b064f3f 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/Launcher.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/logic/Launcher.kt @@ -27,6 +27,8 @@ import java.io.File import java.nio.file.Path import java.util.* import kotlin.io.path.createParentDirectories +import kotlin.io.path.exists +import kotlin.io.path.notExists object Launcher { @@ -34,7 +36,7 @@ object Launcher { val dir = MinecraftDirectory(pack.instance.minecraftDir.toFile()) val launcher = LauncherBuilder.buildDefault() val javaPath = state.jvm.javaPath - if(javaPath == null) { + if(javaPath == null || javaPath.notExists()) { dialog = Dialog.ChooseJVMPath return@coroutineScope } diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/ModDownloader.kt b/src/main/kotlin/com/mineinabyss/launchy/logic/ModDownloader.kt index 5600131..37c9722 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/ModDownloader.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/logic/ModDownloader.kt @@ -1,17 +1,17 @@ package com.mineinabyss.launchy.logic -import com.mineinabyss.launchy.data.Dirs -import com.mineinabyss.launchy.data.config.unzip import com.mineinabyss.launchy.data.modpacks.Mod import com.mineinabyss.launchy.data.modpacks.PackDependencies import com.mineinabyss.launchy.state.InProgressTask import com.mineinabyss.launchy.state.LaunchyState import com.mineinabyss.launchy.state.modpack.ModpackState import kotlinx.coroutines.* +import org.rauschig.jarchivelib.ArchiverFactory import java.util.concurrent.CancellationException import kotlin.io.path.ExperimentalPathApi import kotlin.io.path.copyToRecursively import kotlin.io.path.deleteIfExists +import kotlin.io.path.extension object ModDownloader { val installModLoadersId = "installMCAndModLoaders" @@ -71,8 +71,9 @@ object ModDownloader { downloads.inProgressConfigs[mod] = it } toggles.downloadConfigURLs[mod] = mod.info.configUrl - unzip(config, Dirs.mineinabyss) - config.toFile().delete() + ArchiverFactory.createArchiver(config.extension) + .extract(config.toFile(), instance.overridesDir.toFile()) + config.deleteIfExists() saveToConfig() println("Successfully downloaded $name config") } catch (ex: CancellationException) { @@ -117,11 +118,13 @@ object ModDownloader { fun ModpackState.copyOverrides(state: LaunchyState) { try { state.inProgressTasks[copyOverridesId] = InProgressTask("Copying overrides") - modpack.overridesPath?.copyToRecursively( - target = instance.minecraftDir, - followLinks = false, - overwrite = true, - ) + modpack.overridesPaths.forEach { + it.copyToRecursively( + target = instance.minecraftDir, + followLinks = false, + overwrite = true, + ) + } } finally { state.inProgressTasks.remove(copyOverridesId) } @@ -138,7 +141,7 @@ object ModDownloader { toggles.checkNonDownloadedMods() val modDownloads = launch { queued.downloads.map { mod -> - launch(Dispatchers.IO) { + state.downloadContext.launch { download(state, mod) toggles.checkNonDownloadedMods() } diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/SuggestedJVMArgs.kt b/src/main/kotlin/com/mineinabyss/launchy/logic/SuggestedJVMArgs.kt index 041bbe9..dc5a8bb 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/SuggestedJVMArgs.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/logic/SuggestedJVMArgs.kt @@ -6,8 +6,10 @@ package com.mineinabyss.launchy.logic */ object SuggestedJVMArgs { val memory: Int = 2048 + val baseFlags = + "-XX:+UnlockExperimentalVMOptions -XX:+UnlockDiagnosticVMOptions -XX:+AlwaysActAsServerClassMachine -XX:+AlwaysPreTouch -XX:+DisableExplicitGC -XX:+UseNUMA -XX:NmethodSweepActivity=1 -XX:ReservedCodeCacheSize=400M -XX:NonNMethodCodeHeapSize=12M -XX:ProfiledCodeHeapSize=194M -XX:NonProfiledCodeHeapSize=194M -XX:-DontCompileHugeMethods -XX:MaxNodeLimit=240000 -XX:NodeLimitFudgeFactor=8000 -XX:+UseVectorCmov -XX:+PerfDisableSharedMem -XX:+UseFastUnorderedTimeStamps -XX:+UseCriticalJavaThreadPriority -XX:ThreadPriorityPolicy=1 -XX:AllocatePrefetchStyle=3" + val graalVMBaseFlags = + "-XX:+UnlockExperimentalVMOptions -XX:+UnlockDiagnosticVMOptions -XX:+AlwaysActAsServerClassMachine -XX:+AlwaysPreTouch -XX:+DisableExplicitGC -XX:+UseNUMA -XX:AllocatePrefetchStyle=3 -XX:NmethodSweepActivity=1 -XX:ReservedCodeCacheSize=400M -XX:NonNMethodCodeHeapSize=12M -XX:ProfiledCodeHeapSize=194M -XX:NonProfiledCodeHeapSize=194M -XX:-DontCompileHugeMethods -XX:+PerfDisableSharedMem -XX:+UseFastUnorderedTimeStamps -XX:+UseCriticalJavaThreadPriority -XX:+EagerJVMCI -Dgraal.TuneInlinerExploration=1 -Dgraal.CompilerConfiguration=enterprise" val clientG1GC = "-XX:+UseG1GC -XX:MaxGCPauseMillis=37 -XX:+PerfDisableSharedMem -XX:G1HeapRegionSize=16M -XX:G1NewSizePercent=23 -XX:G1ReservePercent=20 -XX:SurvivorRatio=32 -XX:G1MixedGCCountTarget=3 -XX:G1HeapWastePercent=20 -XX:InitiatingHeapOccupancyPercent=10 -XX:G1RSetUpdatingPauseTimePercent=0 -XX:MaxTenuringThreshold=1 -XX:G1SATBBufferEnqueueingThresholdPercent=30 -XX:G1ConcMarkStepDurationMillis=5.0 -XX:G1ConcRSHotCardLimit=16 -XX:G1ConcRefinementServiceIntervalMillis=150 -XX:GCTimeRatio=99" - val graalVM = - "-XX:+UnlockExperimentalVMOptions -XX:+UnlockDiagnosticVMOptions -XX:+AlwaysActAsServerClassMachine -XX:+AlwaysPreTouch -XX:+DisableExplicitGC -XX:+UseNUMA -XX:AllocatePrefetchStyle=3 -XX:NmethodSweepActivity=1 -XX:ReservedCodeCacheSize=400M -XX:NonNMethodCodeHeapSize=12M -XX:ProfiledCodeHeapSize=194M -XX:NonProfiledCodeHeapSize=194M -XX:-DontCompileHugeMethods -XX:+PerfDisableSharedMem -XX:+UseFastUnorderedTimeStamps -XX:+UseCriticalJavaThreadPriority -XX:+EagerJVMCI -Dgraal.TuneInlinerExploration=1 -Dgraal.CompilerConfiguration=enterprise" } diff --git a/src/main/kotlin/com/mineinabyss/launchy/state/JvmState.kt b/src/main/kotlin/com/mineinabyss/launchy/state/JvmState.kt index 506e2d6..51fee27 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/state/JvmState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/state/JvmState.kt @@ -17,9 +17,11 @@ class JvmState( var useRecommendedJvmArgs by mutableStateOf(config.useRecommendedJvmArguments) val suggestedArgs get() = buildString { if("graalvm" in javaPath.toString()) { - append(SuggestedJVMArgs.graalVM) - append(" ") + append(SuggestedJVMArgs.graalVMBaseFlags) + } else { + append(SuggestedJVMArgs.baseFlags) } + append(" ") append(SuggestedJVMArgs.clientG1GC) } val jvmArgs by derivedStateOf { diff --git a/src/main/kotlin/com/mineinabyss/launchy/state/LaunchyState.kt b/src/main/kotlin/com/mineinabyss/launchy/state/LaunchyState.kt index a3100d5..b2e14c8 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/state/LaunchyState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/state/LaunchyState.kt @@ -1,26 +1,26 @@ package com.mineinabyss.launchy.state import androidx.compose.runtime.* -import com.mineinabyss.launchy.data.Dirs import com.mineinabyss.launchy.data.config.Config import com.mineinabyss.launchy.data.config.GameInstance import com.mineinabyss.launchy.state.modpack.ModpackState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import java.util.* -import kotlin.io.path.div -import kotlin.io.path.exists class LaunchyState( // Config should never be mutated unless it also updates UI state private val config: Config, private val instances: List ) { - val installationCoroutineScope = CoroutineScope(Dispatchers.IO) + @OptIn(ExperimentalCoroutinesApi::class) + val downloadContext = CoroutineScope(Dispatchers.IO.limitedParallelism(10)) val profile = ProfileState(config) var modpackState: ModpackState? by mutableStateOf(null) private val launchedProcesses = mutableStateMapOf() val jvm = JvmState(config) + var preferHue: Float by mutableStateOf(config.preferHue ?: 0f) val gameInstances = mutableStateListOf().apply { addAll(instances) @@ -49,7 +49,8 @@ class LaunchyState( javaPath = jvm.javaPath?.toString(), jvmArguments = jvm.userJvmArgs, memoryAllocation = jvm.userMemoryAllocation, - useRecommendedJvmArguments = jvm.useRecommendedJvmArgs + useRecommendedJvmArguments = jvm.useRecommendedJvmArgs, + preferHue = preferHue, ).save() } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/colors/Color.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/colors/Color.kt index e023f13..538c70a 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/colors/Color.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/colors/Color.kt @@ -1,6 +1,10 @@ package com.mineinabyss.launchy.ui.colors +import androidx.compose.animation.animateColorAsState import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color val md_theme_light_primary = Color(0xFF924C00) diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/colors/Theme.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/colors/Theme.kt index f18424a..a84f7e3 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/colors/Theme.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/colors/Theme.kt @@ -1,20 +1,21 @@ package com.mineinabyss.launchy.ui.colors import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import com.mineinabyss.launchy.ui.LaunchyTypography var currentHue by mutableStateOf(0f) @Composable fun AppTheme( - useDarkTheme: Boolean = true, - content: @Composable() () -> Unit + content: @Composable () -> Unit ) { - val animatedHue by animateFloatAsState(currentHue, animationSpec = tween(durationMillis = 1000)) + val animatedHue by animateFloatAsState(currentHue, animationSpec = tween(durationMillis = 500)) val colors = LaunchyColors(animatedHue) MaterialTheme( diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/dialogs/AuthDialog.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/dialogs/AuthDialog.kt index 7491d54..d2233a1 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/dialogs/AuthDialog.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/dialogs/AuthDialog.kt @@ -92,12 +92,6 @@ fun AuthDialog( ?.let { Browser.browse(it.item.url) } }, ) - IconButton( - onClick = { clipboard.setText(AnnotatedString(state.profile.authCode!!)) }, - colors = PrimaryIconButtonColors - ) { - Icon(Icons.Rounded.ContentCopy, "Copy") - } } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/dialogs/SelectJVMDialog.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/dialogs/SelectJVMDialog.kt index 0e7520c..967ceae 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/dialogs/SelectJVMDialog.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/dialogs/SelectJVMDialog.kt @@ -8,7 +8,6 @@ import com.mineinabyss.launchy.LocalLaunchyState import com.mineinabyss.launchy.logic.Downloader import com.mineinabyss.launchy.ui.elements.LaunchyDialog import com.mineinabyss.launchy.ui.screens.* -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @Composable @@ -19,7 +18,7 @@ fun SelectJVMDialog() { title = { Text("Install java", style = LocalTextStyle.current) }, onAccept = { dialog = Dialog.None - state.installationCoroutineScope.launch { + state.downloadContext.launch { val jdkPath = runCatching { Downloader.installJDK(state) }.getOrElse { diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/PlayerAvatar.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/elements/PlayerAvatar.kt index 5320789..5c3ee77 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/PlayerAvatar.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/elements/PlayerAvatar.kt @@ -1,19 +1,22 @@ package com.mineinabyss.launchy.ui.elements import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.layout.ContentScale +import com.mineinabyss.launchy.LocalLaunchyState import com.mineinabyss.launchy.data.config.PlayerProfile +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @Composable fun PlayerAvatar(profile: PlayerProfile, modifier: Modifier = Modifier) { var avatar: BitmapPainter? by remember { mutableStateOf(null) } + val state = LocalLaunchyState LaunchedEffect(profile) { - avatar = BitmapPainter(profile.getAvatar(), filterQuality = FilterQuality.None) + avatar = profile.getAvatar(state) } if (avatar != null) Image( painter = avatar!!, diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/AccountsPopup.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/AccountsPopup.kt index 4f614b2..d3b7622 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/AccountsPopup.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/AccountsPopup.kt @@ -15,7 +15,7 @@ import com.mineinabyss.launchy.LocalLaunchyState import com.mineinabyss.launchy.logic.Auth.logout @Composable -fun AccountsPopup(onLogout : () -> Unit) { +fun AccountsPopup(onLogout: () -> Unit) { val state = LocalLaunchyState Surface( tonalElevation = 2.dp, @@ -25,7 +25,10 @@ fun AccountsPopup(onLogout : () -> Unit) { val currentProfile = state.profile.currentProfile if (currentProfile != null) { IconButton( - onClick = { state.profile.logout(currentProfile.uuid) }, + onClick = { + state.profile.logout(currentProfile.uuid) + onLogout() + }, ) { Icon( Icons.AutoMirrored.Rounded.Logout, diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/Screen.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/Screen.kt index 0fc2561..bb137e3 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/Screen.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/Screen.kt @@ -5,9 +5,11 @@ sealed class Screen( val showTitle: Boolean = false, val showSidebar: Boolean = false, ) { - object Default : Screen(transparentTopBar = true, showTitle = true, showSidebar = true) - object NewInstance: Screen(transparentTopBar = true, showTitle = true, showSidebar = true) - object Settings : Screen(transparentTopBar = true, showTitle = true, showSidebar = true) + interface OnLeftSidebar + + object Default : Screen(transparentTopBar = true, showTitle = true, showSidebar = true), OnLeftSidebar + object NewInstance: Screen(transparentTopBar = true, showTitle = true, showSidebar = true), OnLeftSidebar + object Settings : Screen(transparentTopBar = true, showTitle = true, showSidebar = true), OnLeftSidebar object InstanceSettings : Screen(showTitle = true) object Instance : Screen(transparentTopBar = true) diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/Screens.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/Screens.kt index 40fa739..6ce7e0a 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/Screens.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/Screens.kt @@ -15,6 +15,7 @@ import com.mineinabyss.launchy.LocalLaunchyState import com.mineinabyss.launchy.state.InProgressTask import com.mineinabyss.launchy.state.modpack.ModpackState import com.mineinabyss.launchy.ui.AppTopBar +import com.mineinabyss.launchy.ui.colors.currentHue import com.mineinabyss.launchy.ui.dialogs.AuthDialog import com.mineinabyss.launchy.ui.dialogs.SelectJVMDialog import com.mineinabyss.launchy.ui.elements.LaunchyDialog @@ -60,6 +61,12 @@ fun Screens() = Scaffold( LeftSidebar() } + val isDefault = screen is Screen.OnLeftSidebar + + LaunchedEffect(isDefault, state.preferHue) { + if (isDefault) currentHue = state.preferHue + } + AppTopBar( state = TopBar, transparent = screen.transparentTopBar, diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/ModpackCard.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/ModpackCard.kt index 45f2367..2142b87 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/ModpackCard.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/ModpackCard.kt @@ -47,9 +47,7 @@ fun InstanceCard( ) { val state = LocalLaunchyState val coroutineScope = rememberCoroutineScope() - val background by produceState(null) { - value = instance?.getOrDownloadBackground() ?: config.loadBackgroundFromTmpFile() - } + val background by config.produceBackgroundState(state) Card( onClick = { instance ?: return@Card diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/newinstance/NewInstance.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/newinstance/NewInstance.kt index 43e3a7e..8847697 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/newinstance/NewInstance.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/newinstance/NewInstance.kt @@ -121,10 +121,11 @@ fun NewInstance() { exit = slideOutHorizontally() + fadeOut() ) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + ComfyWidth { + Text("Confirm import", style = MaterialTheme.typography.headlineMedium) + } ComfyContent { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text("Confirm import", style = MaterialTheme.typography.headlineMedium) - var nameText by remember { mutableStateOf(importingInstance?.name ?: "") } fun nameValid() = nameText.matches(validInstanceNameRegex) fun packWithNameExists() = Dirs.modpackConfigDir(nameText).exists() diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/settings/SettingsScreen.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/settings/SettingsScreen.kt index 5965ae2..dd69247 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/settings/SettingsScreen.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/settings/SettingsScreen.kt @@ -42,6 +42,18 @@ fun SettingsScreen() { ComfyContent { var directoryPickerShown by remember { mutableStateOf(false) } Column(Modifier.padding(16.dp).verticalScroll(scrollState), verticalArrangement = Arrangement.spacedBy(16.dp)) { + Column { + Text("Hue", style = MaterialTheme.typography.titleSmall) + Row { + Slider( + value = state.preferHue, + onValueChange = { state.preferHue = it }, + valueRange = 0f..1f, + modifier = Modifier.weight(1f) + ) + } + } + if (directoryPickerShown) FileDialog(onCloseRequest = { if (it != null) { state.jvm.javaPath = it diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/MainScreenImages.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/MainScreenImages.kt index afc7602..4888ff9 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/MainScreenImages.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/MainScreenImages.kt @@ -1,37 +1,40 @@ package com.mineinabyss.launchy.ui.screens.modpack.main +import androidx.compose.animation.* import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.window.WindowDraggableArea import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.WindowScope +import com.mineinabyss.launchy.LocalLaunchyState import com.mineinabyss.launchy.ui.screens.LocalModpackState -import org.jetbrains.skia.Bitmap @Composable fun BoxScope.BackgroundImage(windowScope: WindowScope) { val pack = LocalModpackState - val background by produceState(null) { - value = pack.instance.getOrDownloadBackground() - } - if(background == null) return - windowScope.WindowDraggableArea { - Image( - painter = background!!, - contentDescription = "Modpack background", - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize() - ) + val state = LocalLaunchyState + val background by pack.instance.config.produceBackgroundState(state) + AnimatedVisibility(background != null, enter = fadeIn(), exit = fadeOut()) { + if (background == null) return@AnimatedVisibility + windowScope.WindowDraggableArea { + Image( + painter = background!!, + contentDescription = "Modpack background", + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + } } BackgroundTint() } @@ -52,6 +55,7 @@ fun BoxScope.BackgroundTint() { ) } } + @Composable fun BoxScope.SlightBackgroundTint(modifier: Modifier = Modifier) { val colors = listOf( @@ -71,15 +75,20 @@ fun BoxScope.SlightBackgroundTint(modifier: Modifier = Modifier) { @Composable fun LogoLarge(modifier: Modifier) { + val state = LocalLaunchyState val pack = LocalModpackState - val painter by produceState(null) { - value = pack.instance.getOrDownloadLogo() + val painter by pack.instance.config.produceLogoState(state) + AnimatedVisibility( + painter != null, + enter = fadeIn() + expandVertically(clip = false) + fadeIn(), + modifier = Modifier.widthIn(0.dp, 500.dp).then(modifier) + ) { + if (painter == null) return@AnimatedVisibility + Image( + painter = painter!!, + contentDescription = "Modpack logo", + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.FillWidth + ) } - if(painter == null) return - Image( - painter = painter!!, - contentDescription = "Modpack logo", - modifier = Modifier.widthIn(0.dp, 500.dp).fillMaxSize().then(modifier), - contentScale = ContentScale.FillWidth - ) } diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/ModpackScreen.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/ModpackScreen.kt index 38623ca..61589ad 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/ModpackScreen.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/ModpackScreen.kt @@ -28,14 +28,14 @@ fun ModpackScreen() { Modifier.align(Alignment.Center) .heightIn(0.dp, 550.dp) .fillMaxSize(), - verticalArrangement = Arrangement.SpaceBetween, + verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { - LogoLarge(Modifier.weight(3f)) + LogoLarge(Modifier.weight(3f, false)) Row( horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().weight(1f), + modifier = Modifier.fillMaxWidth().weight(1f, false), ) { PlayButton(hideText = false, packState.instance) { packState } SettingsButton() diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/InstallButton.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/InstallButton.kt index 2aa200b..15c8060 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/InstallButton.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/InstallButton.kt @@ -22,11 +22,10 @@ import kotlinx.coroutines.launch fun InstallButton(enabled: Boolean, modifier: Modifier = Modifier) { val state = LocalLaunchyState val packState = LocalModpackState - val coroutineScope = rememberCoroutineScope() PrimaryButton( enabled = enabled, onClick = { - coroutineScope.launch { + state.downloadContext.launch { packState.install(state) } }, diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/PlayButton.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/PlayButton.kt index 9c741f9..975e6f2 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/PlayButton.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/PlayButton.kt @@ -66,7 +66,7 @@ fun PlayButton( when { // Assume this means not launched before packState.userAgreedDeps == null -> { - coroutineScope.launch(Dispatchers.IO) { + state.downloadContext.launch { packState.install(state).join() Launcher.launch(state, packState, state.profile) } @@ -78,13 +78,13 @@ fun PlayButton( acceptText = "Download", declineText = "Skip", onAccept = { - coroutineScope.launch(Dispatchers.IO) { + state.downloadContext.launch { packState.install(state).join() Launcher.launch(state, packState, state.profile) } }, onDecline = { - coroutineScope.launch(Dispatchers.IO) { + state.downloadContext.launch { packState.install(state).join() Launcher.launch(state, packState, state.profile) } diff --git a/src/main/kotlin/com/mineinabyss/launchy/util/OS.kt b/src/main/kotlin/com/mineinabyss/launchy/util/OS.kt index 7dc9eb0..339ff7d 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/util/OS.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/OS.kt @@ -1,7 +1,11 @@ package com.mineinabyss.launchy.util -enum class OS { - WINDOWS, MAC, LINUX; +sealed class OS( + val openJDKName: String +) { + data object WINDOWS: OS("windows") + data object LINUX: OS("linux") + data object MAC: OS("mac") companion object { fun isArm(): Boolean { @@ -19,3 +23,28 @@ enum class OS { } } } + +sealed class Arch( + val openJDKArch: String +) { + data object X64: Arch("x64") + data object X86: Arch("x86") + data object ARM64: Arch("aarch64") + data object ARM32: Arch("arm") + data object Unknown: Arch("unknown") + + + + companion object { + fun get(): Arch { + val archString = System.getProperty("os.arch", "unknown") + return when(archString) { + "amd64", "x86_64" -> X64 + "x86" -> X86 + "aarch64" -> ARM64 + "arm" -> ARM32 + else -> Unknown + } + } + } +}