diff --git a/gradle.properties b/gradle.properties index 8e66c9e..5d3712a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ group=com.mineinabyss -version=2.0.0-alpha.5 +version=2.0.0-alpha.6 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 c26722b..170940c 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/Dirs.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/Dirs.kt @@ -6,21 +6,22 @@ import kotlin.io.path.* object Dirs { val home = Path(System.getProperty("user.home")) + val minecraft = when (OS.get()) { OS.WINDOWS -> Path(System.getenv("APPDATA")) / ".minecraft" - OS.MAC -> Path(System.getProperty("user.home")) / "Library/Application Support/minecraft" - OS.LINUX -> Path(System.getProperty("user.home")) / ".minecraft" + OS.MAC -> home / "Library/Application Support/minecraft" + OS.LINUX -> home / ".minecraft" } val mineinabyss = when (OS.get()) { OS.WINDOWS -> Path(System.getenv("APPDATA")) / ".mineinabyss" - OS.MAC -> Path(System.getProperty("user.home")) / "Library/Application Support/mineinabyss" - OS.LINUX -> Path(System.getProperty("user.home")) / ".mineinabyss" + OS.MAC -> home / "Library/Application Support/mineinabyss" + OS.LINUX -> home / ".mineinabyss" } val config = when (OS.get()) { OS.WINDOWS -> Path(System.getenv("APPDATA")) - OS.MAC -> Path(System.getProperty("user.home")) / "Library/Application Support" + OS.MAC -> home / "Library/Application Support" OS.LINUX -> home / ".config" } / "mineinabyss" 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 dec75ce..7bc68de 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/config/GameInstanceConfig.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/config/GameInstanceConfig.kt @@ -9,7 +9,10 @@ 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 com.mineinabyss.launchy.logic.urlToFileName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import kotlinx.serialization.Transient @@ -30,10 +33,10 @@ data class GameInstanceConfig( val overrideMinecraftDir: String? = null, ) { @Transient - val backgroundPath = Dirs.imageCache / "background-${backgroundURL.hashCode().toHexString()}" + val backgroundPath = Dirs.imageCache / "background-${urlToFileName(backgroundURL)}" @Transient - val logoPath = Dirs.imageCache / "icon-${logoURL.hashCode().toHexString()}" + val logoPath = Dirs.imageCache / "icon-${urlToFileName(logoURL)}" @Transient private var cachedBackground = mutableStateOf(null) @@ -41,9 +44,11 @@ data class GameInstanceConfig( @Transient private var cachedLogo = mutableStateOf(null) + @OptIn(ExperimentalCoroutinesApi::class) + @Transient + val downloadScope = CoroutineScope(Dispatchers.IO.limitedParallelism(1)) private suspend fun loadBackground() { - if (cachedBackground.value != null) return runCatching { Downloader.download(backgroundURL, backgroundPath, override = false) val painter = BitmapPainter(loadImageBitmap(backgroundPath.inputStream())) @@ -52,7 +57,6 @@ data class GameInstanceConfig( } private suspend fun loadLogo() { - if (cachedLogo.value != null) return runCatching { Downloader.download(logoURL, logoPath, override = false) val painter = BitmapPainter(loadImageBitmap(logoPath.inputStream())) @@ -61,16 +65,16 @@ data class GameInstanceConfig( } @Composable - fun getBackground(state: LaunchyState) = remember { + fun getBackground() = remember { cachedBackground.also { - if (it.value == null) state.ioScope.launch { loadBackground() } + if (it.value == null) downloadScope.launch { loadBackground() } } } @Composable - fun getLogo(state: LaunchyState) = remember { + fun getLogo() = remember { cachedLogo.also { - if (it.value == null) state.ioScope.launch { loadLogo() } + if (it.value == null) downloadScope.launch { loadLogo() } } } 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 5dc503e..0f97526 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/config/PlayerProfile.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/config/PlayerProfile.kt @@ -10,7 +10,9 @@ 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.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import kotlinx.serialization.Transient @@ -25,14 +27,20 @@ data class PlayerProfile( @Transient private val avatar = mutableStateOf(null) + @OptIn(ExperimentalCoroutinesApi::class) + @Transient + private val downloadScope = CoroutineScope(Dispatchers.IO.limitedParallelism(1)) + @Composable - fun getAvatar(state: LaunchyState): MutableState = remember { + fun getAvatar(): MutableState = remember { avatar.also { if (it.value != null) return@also - state.ioScope.launch { + downloadScope.launch { Downloader.downloadAvatar(uuid) - it.value = - BitmapPainter(loadImageBitmap(Dirs.avatar(uuid).inputStream()), filterQuality = FilterQuality.None) + it.value = BitmapPainter( + loadImageBitmap(Dirs.avatar(uuid).inputStream()), + filterQuality = FilterQuality.None + ) } } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/Downloader.kt b/src/main/kotlin/com/mineinabyss/launchy/logic/Downloader.kt index c7afcab..15933e1 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/Downloader.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/logic/Downloader.kt @@ -46,7 +46,7 @@ object Downloader { 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 = Dirs.cacheDir / "${writeTo.name}.cache" + val cacheFile = Dirs.cacheDir / "${urlToFileName(url)}.cache" if (writeTo.exists() && cacheFile.exists() && cacheFile.readText() == cache) return@runCatching cacheFile.createParentDirectories() cacheFile.deleteIfExists() diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/Helpers.kt b/src/main/kotlin/com/mineinabyss/launchy/logic/Helpers.kt index c727342..0ef9c0c 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/Helpers.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/logic/Helpers.kt @@ -2,6 +2,7 @@ package com.mineinabyss.launchy.logic import com.mineinabyss.launchy.ui.screens.Dialog import com.mineinabyss.launchy.ui.screens.dialog +import java.util.* fun Result.showDialogOnError(title: String? = null): Result { onFailure { dialog = Dialog.fromException(it, title) } @@ -9,3 +10,8 @@ fun Result.showDialogOnError(title: String? = null): Result { } fun Result.getOrShowDialog() = showDialogOnError().getOrNull() + + +fun urlToFileName(url: String): String { + return UUID.nameUUIDFromBytes(url.toByteArray()).toString() +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/Instances.kt b/src/main/kotlin/com/mineinabyss/launchy/logic/Instances.kt index b277ac9..73c2dfb 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/Instances.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/logic/Instances.kt @@ -9,11 +9,12 @@ import kotlin.io.path.deleteRecursively object Instances { @OptIn(ExperimentalPathApi::class) - fun GameInstance.delete(state: LaunchyState) { + fun GameInstance.delete(state: LaunchyState, deleteDotMinecraft: Boolean) { try { state.inProgressTasks["deleteInstance"] = InProgressTask("Deleting instance ${config.name}") state.gameInstances.remove(this) state.ioScope.launch { + if (deleteDotMinecraft) minecraftDir.deleteRecursively() configDir.deleteRecursively() } } finally { 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 d051d9f..a8572dc 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/PlayerAvatar.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/elements/PlayerAvatar.kt @@ -12,7 +12,7 @@ import com.mineinabyss.launchy.data.config.PlayerProfile @Composable fun PlayerAvatar(profile: PlayerProfile, modifier: Modifier = Modifier) { val state = LocalLaunchyState - val avatar: BitmapPainter? by profile.getAvatar(state) + val avatar: BitmapPainter? by profile.getAvatar() if (avatar != null) Image( painter = avatar!!, contentDescription = "Avatar", 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 f66e64e..07eba9f 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 @@ -48,7 +48,7 @@ fun InstanceCard( ) { val state = LocalLaunchyState val coroutineScope = rememberCoroutineScope() - val background by config.getBackground(state) + val background by config.getBackground() 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 ac23817..3667e5a 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 @@ -15,6 +15,7 @@ import com.mineinabyss.launchy.data.config.GameInstance import com.mineinabyss.launchy.data.config.GameInstanceConfig import com.mineinabyss.launchy.logic.Downloader import com.mineinabyss.launchy.logic.showDialogOnError +import com.mineinabyss.launchy.logic.urlToFileName import com.mineinabyss.launchy.state.InProgressTask import com.mineinabyss.launchy.ui.elements.AnimatedTab import com.mineinabyss.launchy.ui.elements.ComfyContent @@ -85,8 +86,8 @@ fun NewInstance() { TextButton(onClick = { urlValid = urlValid() if (!urlValid) return@TextButton - val taskKey = "import-cloud-instance-${urlText.hashCode()}" - val downloadPath = Dirs.tmp / "launchy-cloud-instance-${urlText.hashCode()}.yml" + val taskKey = "import-cloud-instance-${urlToFileName(urlText)}" + val downloadPath = Dirs.tmp / "launchy-cloud-instance-${urlToFileName(urlText)}.yml" downloadPath.deleteIfExists() coroutineScope.launch(Dispatchers.IO) { state.inProgressTasks[taskKey] = InProgressTask("Importing cloud instance") 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 02a4775..f95e906 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 @@ -25,7 +25,7 @@ import com.mineinabyss.launchy.ui.screens.LocalModpackState fun BoxScope.BackgroundImage(windowScope: WindowScope) { val pack = LocalModpackState val state = LocalLaunchyState - val background by pack.instance.config.getBackground(state) + val background by pack.instance.config.getBackground() AnimatedVisibility(background != null, enter = fadeIn(), exit = fadeOut()) { if (background == null) return@AnimatedVisibility windowScope.WindowDraggableArea { @@ -78,7 +78,7 @@ fun BoxScope.SlightBackgroundTint(modifier: Modifier = Modifier) { fun LogoLarge(modifier: Modifier) { val state = LocalLaunchyState val pack = LocalModpackState - val painter by pack.instance.config.getLogo(state) + val painter by pack.instance.config.getLogo() AnimatedVisibility( painter != null, enter = fadeIn() + expandVertically(clip = false) + fadeIn(), diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/InstanceSettingsScreen.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/InstanceSettingsScreen.kt index ed4b54a..b31e205 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/InstanceSettingsScreen.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/InstanceSettingsScreen.kt @@ -63,11 +63,19 @@ fun OptionsTab() { ComfyContent(Modifier.padding(16.dp)) { Column { TitleSmall("Danger zone") - TextButton(onClick = { - screen = Screen.Default - pack.instance.delete(state) - }) { - Text("Delete Instance", color = MaterialTheme.colorScheme.primary) + Row { + TextButton(onClick = { + screen = Screen.Default + pack.instance.delete(state, deleteDotMinecraft = false) + }) { + Text("Delete Instance from config", color = MaterialTheme.colorScheme.primary) + } + TextButton(onClick = { + screen = Screen.Default + pack.instance.delete(state, deleteDotMinecraft = true) + }) { + Text("Delete Instance and its .minecraft", color = MaterialTheme.colorScheme.error) + } } } }