From 001de7f36991422076a9ce0eb4c8bdf6f3643bb8 Mon Sep 17 00:00:00 2001 From: Danielle Voznyy Date: Fri, 31 May 2024 22:31:52 -0400 Subject: [PATCH 1/5] refactor: Split by feature and data/ui layers --- build.gradle.kts | 3 ++ gradle/libs.versions.toml | 4 ++ .../kotlin/com/mineinabyss/launchy/Main.kt | 17 ++++--- .../launchy/{logic => auth/data}/Auth.kt | 15 +++--- .../config => auth/data}/PlayerProfile.kt | 8 ++-- .../auth => auth/data}/SessionStorage.kt | 5 +- .../{ui/dialogs => auth/ui}/AuthDialog.kt | 10 ++-- .../{state => auth/ui}/ProfileState.kt | 6 +-- .../ui/components}/AccountsPopup.kt | 4 +- .../{data/config => config/data}/Config.kt | 7 +-- .../config => config/data}/GameInstance.kt | 17 ++----- .../data}/GameInstanceConfig.kt | 12 ++--- .../data}/InstanceUserConfig.kt | 14 +++--- .../{logic => core/data}/Downloader.kt | 13 ++--- .../launchy/{data => core/ui}/Constants.kt | 2 +- .../launchy/{ui/screens => core/ui}/Dialog.kt | 4 +- .../launchy/{state => core/ui}/JvmState.kt | 6 +-- .../{state => core/ui}/LaunchyState.kt | 10 ++-- .../launchy/{ui/screens => core/ui}/Screen.kt | 5 +- .../{ui/screens => core/ui}/Screens.kt | 40 ++++++++-------- .../{ui/state => core/ui}/TopBarState.kt | 2 +- .../launchy/{state => core/ui}/UIState.kt | 4 +- .../ui/components}/AnimatedTab.kt | 2 +- .../ui/components}/Buttons.kt | 2 +- .../ui/components}/ComfyContent.kt | 4 +- .../elements => core/ui/components}/Dialog.kt | 2 +- .../components}/LaunchyWindowDraggableArea.kt | 5 +- .../ui/components}/LeftSidebar.kt | 8 ++-- .../ui/components}/PlayerAvatar.kt | 4 +- .../ui/components}/SingleFileDialog.kt | 4 +- .../ui/components}/Tooltip.kt | 2 +- .../{ui => core/ui/components}/TopBar.kt | 7 ++- .../ui/components}/Typography.kt | 2 +- .../{ => core}/ui/dialogs/SelectJVMDialog.kt | 16 +++---- .../{ui/colors => core/ui/theme}/Color.kt | 6 +-- .../{ui/colors => core/ui/theme}/Theme.kt | 3 +- .../{ui => core/ui/theme}/Typography.kt | 2 +- .../data}/DownloadQueueState.kt | 11 +++-- .../data}/DownloadState.kt | 6 +-- .../data}/ModDownloader.kt | 21 +++++---- .../data}/source/PackSource.kt | 12 ++--- .../data}/source/PackType.kt | 10 ++-- .../data}/GameInstanceState.kt | 9 ++-- .../data}/InstanceModLoaders.kt | 2 +- .../{logic => instance/data}/Launcher.kt | 14 +++--- .../{data/modpacks => instance/data}/Mod.kt | 5 +- .../modpacks => instance/data}/ModConfig.kt | 4 +- .../Group.kt => instance/data/ModGroup.kt} | 4 +- .../data}/ModTogglesState.kt | 10 ++-- .../modpacks => instance/data}/Modpack.kt | 2 +- .../{data/modpacks => instance/data}/Mods.kt | 14 +++--- .../{logic => instance/data}/ToggleMods.kt | 5 +- .../data}/formats/ExtraInfoFormat.kt | 11 ++--- .../data/formats}/ExtraPackInfo.kt | 6 ++- .../data}/formats/LaunchyPackFormat.kt | 8 ++-- .../data}/formats/ModDownloadPath.kt | 2 +- .../data}/formats/ModrinthPackFormat.kt | 10 ++-- .../data}/formats/PackFormat.kt | 6 +-- .../main => instance/ui}/InstanceScreen.kt | 14 +++--- .../ui}/InstanceSettingsScreen.kt | 35 +++++++------- .../ui/components}/ImportSettingsDialog.kt | 10 ++-- .../ui/components}/MainScreenImages.kt | 4 +- .../ui/components}/buttons/AuthButton.kt | 12 +++-- .../ui/components}/buttons/InstallButton.kt | 12 ++--- .../ui/components}/buttons/NewsButton.kt | 4 +- .../ui/components}/buttons/PlayButton.kt | 24 +++++----- .../ui/components}/buttons/SettingsButton.kt | 6 +-- .../ui/components}/buttons/UpdateButton.kt | 6 +-- .../components/buttons}/UpdateInfoButton.kt | 4 +- .../ui/components}/settings/InfoBar.kt | 14 +++--- .../ui/components}/settings/ModGroup.kt | 14 +++--- .../ui/components}/settings/ModInfoDisplay.kt | 18 +++---- .../ui/components}/settings/TripleSwitch.kt | 12 ++--- .../ui}/NewInstance.kt | 32 ++++++------- .../data}/Instances.kt | 19 ++++---- .../ui/InstanceListScreen.kt} | 16 ++++--- .../instance_list/ui/InstanceListViewModel.kt | 10 ++++ .../ui/components}/AddNewModpackCard.kt | 6 +-- .../ui/components}/InstanceCard.kt | 28 +++++------ .../ui/components}/InstanceList.kt | 4 +- .../ui}/SettingsScreen.kt | 10 ++-- .../screens/modpack/main/FirstLaunchDialog.kt | 47 ------------------- .../data}/GithubUpdateChecker.kt | 7 +-- .../launchy/{logic => util}/AppDispatchers.kt | 6 +-- .../com/mineinabyss/launchy/util/Arch.kt | 25 ++++++++++ .../launchy/{logic => util}/DesktopHelpers.kt | 3 +- .../launchy/{data => util}/Dirs.kt | 5 +- .../launchy/{data => util}/Formats.kt | 2 +- .../launchy/{logic => util}/Helpers.kt | 6 +-- .../launchy/{state => util}/InProgressTask.kt | 2 +- .../kotlin/com/mineinabyss/launchy/util/OS.kt | 24 ---------- .../launchy/{logic => util}/Progress.kt | 2 +- .../{logic => util}/SuggestedJVMArgs.kt | 2 +- .../launchy/{logic => util}/Tasks.kt | 2 +- .../launchy/{data => util}/Typealiases.kt | 2 +- .../launchy/{logic => util}/UpdateResult.kt | 4 +- .../{logic => util}/hashing/Hashing.kt | 2 +- .../serializers/UUIDSerializer.kt | 2 +- 98 files changed, 436 insertions(+), 470 deletions(-) rename src/main/kotlin/com/mineinabyss/launchy/{logic => auth/data}/Auth.kt (89%) rename src/main/kotlin/com/mineinabyss/launchy/{data/config => auth/data}/PlayerProfile.kt (88%) rename src/main/kotlin/com/mineinabyss/launchy/{data/auth => auth/data}/SessionStorage.kt (92%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/dialogs => auth/ui}/AuthDialog.kt (93%) rename src/main/kotlin/com/mineinabyss/launchy/{state => auth/ui}/ProfileState.kt (76%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens => auth/ui/components}/AccountsPopup.kt (92%) rename src/main/kotlin/com/mineinabyss/launchy/{data/config => config/data}/Config.kt (84%) rename src/main/kotlin/com/mineinabyss/launchy/{data/config => config/data}/GameInstance.kt (84%) rename src/main/kotlin/com/mineinabyss/launchy/{data/config => config/data}/GameInstanceConfig.kt (90%) rename src/main/kotlin/com/mineinabyss/launchy/{data/config => config/data}/InstanceUserConfig.kt (84%) rename src/main/kotlin/com/mineinabyss/launchy/{logic => core/data}/Downloader.kt (95%) rename src/main/kotlin/com/mineinabyss/launchy/{data => core/ui}/Constants.kt (86%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens => core/ui}/Dialog.kt (88%) rename src/main/kotlin/com/mineinabyss/launchy/{state => core/ui}/JvmState.kt (88%) rename src/main/kotlin/com/mineinabyss/launchy/{state => core/ui}/LaunchyState.kt (87%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens => core/ui}/Screen.kt (86%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens => core/ui}/Screens.kt (85%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/state => core/ui}/TopBarState.kt (98%) rename src/main/kotlin/com/mineinabyss/launchy/{state => core/ui}/UIState.kt (77%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/elements => core/ui/components}/AnimatedTab.kt (91%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/elements => core/ui/components}/Buttons.kt (97%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/elements => core/ui/components}/ComfyContent.kt (94%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/elements => core/ui/components}/Dialog.kt (98%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/elements => core/ui/components}/LaunchyWindowDraggableArea.kt (81%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens => core/ui/components}/LeftSidebar.kt (94%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/elements => core/ui/components}/PlayerAvatar.kt (86%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/elements => core/ui/components}/SingleFileDialog.kt (96%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/elements => core/ui/components}/Tooltip.kt (94%) rename src/main/kotlin/com/mineinabyss/launchy/{ui => core/ui/components}/TopBar.kt (96%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/elements => core/ui/components}/Typography.kt (96%) rename src/main/kotlin/com/mineinabyss/launchy/{ => core}/ui/dialogs/SelectJVMDialog.kt (80%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/colors => core/ui/theme}/Color.kt (96%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/colors => core/ui/theme}/Theme.kt (88%) rename src/main/kotlin/com/mineinabyss/launchy/{ui => core/ui/theme}/Typography.kt (97%) rename src/main/kotlin/com/mineinabyss/launchy/{state/modpack => downloads/data}/DownloadQueueState.kt (86%) rename src/main/kotlin/com/mineinabyss/launchy/{state/modpack => downloads/data}/DownloadState.kt (82%) rename src/main/kotlin/com/mineinabyss/launchy/{logic => downloads/data}/ModDownloader.kt (92%) rename src/main/kotlin/com/mineinabyss/launchy/{data/modpacks => downloads/data}/source/PackSource.kt (87%) rename src/main/kotlin/com/mineinabyss/launchy/{data/modpacks => downloads/data}/source/PackType.kt (83%) rename src/main/kotlin/com/mineinabyss/launchy/{state/modpack => instance/data}/GameInstanceState.kt (79%) rename src/main/kotlin/com/mineinabyss/launchy/{data/modpacks => instance/data}/InstanceModLoaders.kt (94%) rename src/main/kotlin/com/mineinabyss/launchy/{logic => instance/data}/Launcher.kt (93%) rename src/main/kotlin/com/mineinabyss/launchy/{data/modpacks => instance/data}/Mod.kt (79%) rename src/main/kotlin/com/mineinabyss/launchy/{data/modpacks => instance/data}/ModConfig.kt (83%) rename src/main/kotlin/com/mineinabyss/launchy/{data/modpacks/Group.kt => instance/data/ModGroup.kt} (75%) rename src/main/kotlin/com/mineinabyss/launchy/{state/modpack => instance/data}/ModTogglesState.kt (83%) rename src/main/kotlin/com/mineinabyss/launchy/{data/modpacks => instance/data}/Modpack.kt (76%) rename src/main/kotlin/com/mineinabyss/launchy/{data/modpacks => instance/data}/Mods.kt (56%) rename src/main/kotlin/com/mineinabyss/launchy/{logic => instance/data}/ToggleMods.kt (91%) rename src/main/kotlin/com/mineinabyss/launchy/{data/modpacks => instance/data}/formats/ExtraInfoFormat.kt (77%) rename src/main/kotlin/com/mineinabyss/launchy/{data/modpacks => instance/data/formats}/ExtraPackInfo.kt (53%) rename src/main/kotlin/com/mineinabyss/launchy/{data/modpacks => instance/data}/formats/LaunchyPackFormat.kt (84%) rename src/main/kotlin/com/mineinabyss/launchy/{data/modpacks => instance/data}/formats/ModDownloadPath.kt (95%) rename src/main/kotlin/com/mineinabyss/launchy/{data/modpacks => instance/data}/formats/ModrinthPackFormat.kt (83%) rename src/main/kotlin/com/mineinabyss/launchy/{data/modpacks => instance/data}/formats/PackFormat.kt (55%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens/modpack/main => instance/ui}/InstanceScreen.kt (73%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens/modpack/settings => instance/ui}/InstanceSettingsScreen.kt (86%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens/modpack/main => instance/ui/components}/ImportSettingsDialog.kt (86%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens/modpack/main => instance/ui/components}/MainScreenImages.kt (96%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens/modpack/main => instance/ui/components}/buttons/AuthButton.kt (64%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens/modpack/main => instance/ui/components}/buttons/InstallButton.kt (86%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens/modpack/main => instance/ui/components}/buttons/NewsButton.kt (89%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens/modpack/main => instance/ui/components}/buttons/PlayButton.kt (86%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens/modpack/main => instance/ui/components}/buttons/SettingsButton.kt (74%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens/modpack/main => instance/ui/components}/buttons/UpdateButton.kt (77%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens/modpack/main => instance/ui/components/buttons}/UpdateInfoButton.kt (95%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens/modpack => instance/ui/components}/settings/InfoBar.kt (89%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens/modpack => instance/ui/components}/settings/ModGroup.kt (89%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens/modpack => instance/ui/components}/settings/ModInfoDisplay.kt (94%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens/modpack => instance/ui/components}/settings/TripleSwitch.kt (93%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens/home/newinstance => instance_creation/ui}/NewInstance.kt (91%) rename src/main/kotlin/com/mineinabyss/launchy/{logic => instance_list/data}/Instances.kt (81%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens/home/HomeScreen.kt => instance_list/ui/InstanceListScreen.kt} (80%) create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListViewModel.kt rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens/home => instance_list/ui/components}/AddNewModpackCard.kt (89%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens/home => instance_list/ui/components}/InstanceCard.kt (82%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens/home => instance_list/ui/components}/InstanceList.kt (94%) rename src/main/kotlin/com/mineinabyss/launchy/{ui/screens/home/settings => settings/ui}/SettingsScreen.kt (96%) delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/FirstLaunchDialog.kt rename src/main/kotlin/com/mineinabyss/launchy/{logic => updater/data}/GithubUpdateChecker.kt (91%) rename src/main/kotlin/com/mineinabyss/launchy/{logic => util}/AppDispatchers.kt (87%) create mode 100644 src/main/kotlin/com/mineinabyss/launchy/util/Arch.kt rename src/main/kotlin/com/mineinabyss/launchy/{logic => util}/DesktopHelpers.kt (93%) rename src/main/kotlin/com/mineinabyss/launchy/{data => util}/Dirs.kt (92%) rename src/main/kotlin/com/mineinabyss/launchy/{data => util}/Formats.kt (89%) rename src/main/kotlin/com/mineinabyss/launchy/{logic => util}/Helpers.kt (72%) rename src/main/kotlin/com/mineinabyss/launchy/{state => util}/InProgressTask.kt (90%) rename src/main/kotlin/com/mineinabyss/launchy/{logic => util}/Progress.kt (82%) rename src/main/kotlin/com/mineinabyss/launchy/{logic => util}/SuggestedJVMArgs.kt (97%) rename src/main/kotlin/com/mineinabyss/launchy/{logic => util}/Tasks.kt (75%) rename src/main/kotlin/com/mineinabyss/launchy/{data => util}/Typealiases.kt (79%) rename src/main/kotlin/com/mineinabyss/launchy/{logic => util}/UpdateResult.kt (81%) rename src/main/kotlin/com/mineinabyss/launchy/{logic => util}/hashing/Hashing.kt (94%) rename src/main/kotlin/com/mineinabyss/launchy/{data => util}/serializers/UUIDSerializer.kt (93%) diff --git a/build.gradle.kts b/build.gradle.kts index 2f48904..9e50732 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,6 +26,9 @@ dependencies { } implementation(compose.material3) implementation(compose.materialIconsExtended) + implementation(libs.lifecycle.viewmodel.compose) + implementation(libs.koin.compose) + implementation(idofrontLibs.kotlinx.serialization.json) implementation(idofrontLibs.kotlinx.serialization.kaml) implementation(libs.ktor.core) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 265cf86..34f156e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,8 @@ jmccc = "3.1.4" ktor = "2.3.11" minecraftAuth = "4.0.2" mpfilepicker = "3.1.0" +lifecycleViewmodelCompose = "2.8.0-beta02" +koinCompose = "3.6.0-wasm-alpha2" [libraries] jarchivelib = { module = "org.rauschig:jarchivelib", version.ref = "jarchivelib" } @@ -14,3 +16,5 @@ ktor-cio-jvm = { module = "io.ktor:ktor-client-cio-jvm", version.ref = "ktor" } ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } minecraftAuth = { module = "net.raphimc:MinecraftAuth", version.ref = "minecraftAuth" } mpfilepicker = { module = "com.darkrockstudios:mpfilepicker", version.ref = "mpfilepicker" } +lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" } +koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koinCompose" } diff --git a/src/main/kotlin/com/mineinabyss/launchy/Main.kt b/src/main/kotlin/com/mineinabyss/launchy/Main.kt index 7d11b8f..f331fa5 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/Main.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/Main.kt @@ -15,15 +15,14 @@ import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState -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.LaunchyState -import com.mineinabyss.launchy.ui.colors.AppTheme -import com.mineinabyss.launchy.ui.screens.Screens -import com.mineinabyss.launchy.ui.state.TopBarProvider -import com.mineinabyss.launchy.ui.state.TopBarState -import com.mineinabyss.launchy.util.OS +import com.mineinabyss.launchy.config.data.Config +import com.mineinabyss.launchy.config.data.GameInstance +import com.mineinabyss.launchy.core.ui.LaunchyState +import com.mineinabyss.launchy.core.ui.Screens +import com.mineinabyss.launchy.core.ui.TopBarProvider +import com.mineinabyss.launchy.core.ui.TopBarState +import com.mineinabyss.launchy.core.ui.theme.AppTheme +import com.mineinabyss.launchy.util.Dirs import java.awt.Dimension private val LaunchyStateProvider = compositionLocalOf { error("No local versions provided") } diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/Auth.kt b/src/main/kotlin/com/mineinabyss/launchy/auth/data/Auth.kt similarity index 89% rename from src/main/kotlin/com/mineinabyss/launchy/logic/Auth.kt rename to src/main/kotlin/com/mineinabyss/launchy/auth/data/Auth.kt index 117eaa2..e22ac2e 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/Auth.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/auth/data/Auth.kt @@ -1,12 +1,11 @@ -package com.mineinabyss.launchy.logic +package com.mineinabyss.launchy.auth.data -import com.mineinabyss.launchy.data.auth.SessionStorage -import com.mineinabyss.launchy.data.config.PlayerProfile -import com.mineinabyss.launchy.state.InProgressTask -import com.mineinabyss.launchy.state.LaunchyState -import com.mineinabyss.launchy.state.ProfileState -import com.mineinabyss.launchy.ui.screens.Dialog -import com.mineinabyss.launchy.ui.screens.dialog +import com.mineinabyss.launchy.auth.ui.ProfileState +import com.mineinabyss.launchy.core.ui.Dialog +import com.mineinabyss.launchy.core.ui.LaunchyState +import com.mineinabyss.launchy.core.ui.dialog +import com.mineinabyss.launchy.util.DesktopHelpers +import com.mineinabyss.launchy.util.InProgressTask import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/config/PlayerProfile.kt b/src/main/kotlin/com/mineinabyss/launchy/auth/data/PlayerProfile.kt similarity index 88% rename from src/main/kotlin/com/mineinabyss/launchy/data/config/PlayerProfile.kt rename to src/main/kotlin/com/mineinabyss/launchy/auth/data/PlayerProfile.kt index 942b0d7..dc7f3c1 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/config/PlayerProfile.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/auth/data/PlayerProfile.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.data.config +package com.mineinabyss.launchy.auth.data import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState @@ -7,9 +7,9 @@ import androidx.compose.runtime.remember 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.core.data.Downloader +import com.mineinabyss.launchy.util.Dirs +import com.mineinabyss.launchy.util.serializers.UUIDSerializer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/auth/SessionStorage.kt b/src/main/kotlin/com/mineinabyss/launchy/auth/data/SessionStorage.kt similarity index 92% rename from src/main/kotlin/com/mineinabyss/launchy/data/auth/SessionStorage.kt rename to src/main/kotlin/com/mineinabyss/launchy/auth/data/SessionStorage.kt index 540e9f7..a794fcb 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/auth/SessionStorage.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/auth/data/SessionStorage.kt @@ -1,9 +1,8 @@ -package com.mineinabyss.launchy.data.auth +package com.mineinabyss.launchy.auth.data import com.google.gson.GsonBuilder import com.google.gson.JsonParser -import com.mineinabyss.launchy.data.Dirs -import com.mineinabyss.launchy.logic.Auth +import com.mineinabyss.launchy.util.Dirs import kotlinx.serialization.Serializable import net.raphimc.minecraftauth.step.java.session.StepFullJavaSession.FullJavaSession import java.util.* diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/dialogs/AuthDialog.kt b/src/main/kotlin/com/mineinabyss/launchy/auth/ui/AuthDialog.kt similarity index 93% rename from src/main/kotlin/com/mineinabyss/launchy/ui/dialogs/AuthDialog.kt rename to src/main/kotlin/com/mineinabyss/launchy/auth/ui/AuthDialog.kt index 9bf4291..b8a7dfe 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/dialogs/AuthDialog.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/auth/ui/AuthDialog.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.dialogs +package com.mineinabyss.launchy.auth.ui import androidx.compose.foundation.layout.Row import androidx.compose.foundation.text.ClickableText @@ -15,10 +15,10 @@ import androidx.compose.ui.text.* import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.sp import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.logic.DesktopHelpers -import com.mineinabyss.launchy.ui.elements.LaunchyDialog -import com.mineinabyss.launchy.ui.screens.Dialog -import com.mineinabyss.launchy.ui.screens.dialog +import com.mineinabyss.launchy.core.ui.Dialog +import com.mineinabyss.launchy.core.ui.components.LaunchyDialog +import com.mineinabyss.launchy.core.ui.dialog +import com.mineinabyss.launchy.util.DesktopHelpers @OptIn(ExperimentalTextApi::class) @Composable diff --git a/src/main/kotlin/com/mineinabyss/launchy/state/ProfileState.kt b/src/main/kotlin/com/mineinabyss/launchy/auth/ui/ProfileState.kt similarity index 76% rename from src/main/kotlin/com/mineinabyss/launchy/state/ProfileState.kt rename to src/main/kotlin/com/mineinabyss/launchy/auth/ui/ProfileState.kt index a7e3160..cc4d294 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/state/ProfileState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/auth/ui/ProfileState.kt @@ -1,10 +1,10 @@ -package com.mineinabyss.launchy.state +package com.mineinabyss.launchy.auth.ui import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import com.mineinabyss.launchy.data.config.Config -import com.mineinabyss.launchy.data.config.PlayerProfile +import com.mineinabyss.launchy.auth.data.PlayerProfile +import com.mineinabyss.launchy.config.data.Config import net.raphimc.minecraftauth.step.java.session.StepFullJavaSession.FullJavaSession class ProfileState( diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/AccountsPopup.kt b/src/main/kotlin/com/mineinabyss/launchy/auth/ui/components/AccountsPopup.kt similarity index 92% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/AccountsPopup.kt rename to src/main/kotlin/com/mineinabyss/launchy/auth/ui/components/AccountsPopup.kt index d3b7622..ee25b23 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/AccountsPopup.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/auth/ui/components/AccountsPopup.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens +package com.mineinabyss.launchy.auth.ui.components import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape @@ -12,7 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.logic.Auth.logout +import com.mineinabyss.launchy.auth.data.Auth.logout @Composable fun AccountsPopup(onLogout: () -> Unit) { diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/config/Config.kt b/src/main/kotlin/com/mineinabyss/launchy/config/data/Config.kt similarity index 84% rename from src/main/kotlin/com/mineinabyss/launchy/data/config/Config.kt rename to src/main/kotlin/com/mineinabyss/launchy/config/data/Config.kt index 39b18bb..36f5aa8 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/config/Config.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/config/data/Config.kt @@ -1,8 +1,9 @@ -package com.mineinabyss.launchy.data.config +package com.mineinabyss.launchy.config.data import com.charleskorn.kaml.decodeFromStream -import com.mineinabyss.launchy.data.Dirs -import com.mineinabyss.launchy.data.Formats +import com.mineinabyss.launchy.auth.data.PlayerProfile +import com.mineinabyss.launchy.util.Dirs +import com.mineinabyss.launchy.util.Formats import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlin.io.path.inputStream diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/config/GameInstance.kt b/src/main/kotlin/com/mineinabyss/launchy/config/data/GameInstance.kt similarity index 84% rename from src/main/kotlin/com/mineinabyss/launchy/data/config/GameInstance.kt rename to src/main/kotlin/com/mineinabyss/launchy/config/data/GameInstance.kt index 01696b7..20110cf 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/config/GameInstance.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/config/data/GameInstance.kt @@ -1,20 +1,13 @@ -package com.mineinabyss.launchy.data.config +package com.mineinabyss.launchy.config.data import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.charleskorn.kaml.encodeToStream -import com.mineinabyss.launchy.data.Dirs -import com.mineinabyss.launchy.data.Formats -import com.mineinabyss.launchy.logic.AppDispatchers -import com.mineinabyss.launchy.logic.Downloader -import com.mineinabyss.launchy.logic.UpdateResult -import com.mineinabyss.launchy.logic.showDialogOnError -import com.mineinabyss.launchy.state.InProgressTask -import com.mineinabyss.launchy.state.LaunchyState -import com.mineinabyss.launchy.state.modpack.GameInstanceState -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import com.mineinabyss.launchy.core.data.Downloader +import com.mineinabyss.launchy.core.ui.LaunchyState +import com.mineinabyss.launchy.instance.data.GameInstanceState +import com.mineinabyss.launchy.util.* import kotlinx.coroutines.launch import java.nio.file.Path import kotlin.io.path.* diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/config/GameInstanceConfig.kt b/src/main/kotlin/com/mineinabyss/launchy/config/data/GameInstanceConfig.kt similarity index 90% rename from src/main/kotlin/com/mineinabyss/launchy/data/config/GameInstanceConfig.kt rename to src/main/kotlin/com/mineinabyss/launchy/config/data/GameInstanceConfig.kt index 492bf0b..2e39f95 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/config/GameInstanceConfig.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/config/data/GameInstanceConfig.kt @@ -1,15 +1,15 @@ -package com.mineinabyss.launchy.data.config +package com.mineinabyss.launchy.config.data import androidx.compose.runtime.mutableStateOf 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.data.modpacks.source.PackSource -import com.mineinabyss.launchy.logic.Downloader -import com.mineinabyss.launchy.logic.urlToFileName +import com.mineinabyss.launchy.core.data.Downloader +import com.mineinabyss.launchy.downloads.data.source.PackSource +import com.mineinabyss.launchy.util.Dirs +import com.mineinabyss.launchy.util.Formats +import com.mineinabyss.launchy.util.urlToFileName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/config/InstanceUserConfig.kt b/src/main/kotlin/com/mineinabyss/launchy/config/data/InstanceUserConfig.kt similarity index 84% rename from src/main/kotlin/com/mineinabyss/launchy/data/config/InstanceUserConfig.kt rename to src/main/kotlin/com/mineinabyss/launchy/config/data/InstanceUserConfig.kt index 4e3dc58..ce6db76 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/config/InstanceUserConfig.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/config/data/InstanceUserConfig.kt @@ -1,12 +1,12 @@ -package com.mineinabyss.launchy.data.config +package com.mineinabyss.launchy.config.data import com.charleskorn.kaml.decodeFromStream -import com.mineinabyss.launchy.data.Formats -import com.mineinabyss.launchy.data.GroupName -import com.mineinabyss.launchy.data.ModID -import com.mineinabyss.launchy.data.modpacks.InstanceModLoaders -import com.mineinabyss.launchy.logic.ModDownloader -import com.mineinabyss.launchy.logic.hashing.Hashing.checksum +import com.mineinabyss.launchy.downloads.data.ModDownloader +import com.mineinabyss.launchy.instance.data.InstanceModLoaders +import com.mineinabyss.launchy.util.Formats +import com.mineinabyss.launchy.util.GroupName +import com.mineinabyss.launchy.util.ModID +import com.mineinabyss.launchy.util.hashing.Hashing.checksum import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import kotlinx.serialization.encodeToString diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/Downloader.kt b/src/main/kotlin/com/mineinabyss/launchy/core/data/Downloader.kt similarity index 95% rename from src/main/kotlin/com/mineinabyss/launchy/logic/Downloader.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/data/Downloader.kt index 5eb93c3..dfe5370 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/Downloader.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/data/Downloader.kt @@ -1,11 +1,8 @@ -package com.mineinabyss.launchy.logic - -import com.mineinabyss.launchy.data.Dirs -import com.mineinabyss.launchy.data.config.GameInstance -import com.mineinabyss.launchy.state.InProgressTask -import com.mineinabyss.launchy.state.LaunchyState -import com.mineinabyss.launchy.util.Arch -import com.mineinabyss.launchy.util.OS +package com.mineinabyss.launchy.core.data + +import com.mineinabyss.launchy.config.data.GameInstance +import com.mineinabyss.launchy.core.ui.LaunchyState +import com.mineinabyss.launchy.util.* import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.cio.* diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/Constants.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/Constants.kt similarity index 86% rename from src/main/kotlin/com/mineinabyss/launchy/data/Constants.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/Constants.kt index f4ad58d..ca06584 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/Constants.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/Constants.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.data +package com.mineinabyss.launchy.core.ui import androidx.compose.ui.unit.dp diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/Dialog.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/Dialog.kt similarity index 88% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/Dialog.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/Dialog.kt index 9627664..7ae708a 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/Dialog.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/Dialog.kt @@ -1,6 +1,6 @@ -package com.mineinabyss.launchy.ui.screens +package com.mineinabyss.launchy.core.ui -import com.mineinabyss.launchy.data.config.GameInstanceConfig +import com.mineinabyss.launchy.config.data.GameInstanceConfig sealed interface Dialog { object None : Dialog diff --git a/src/main/kotlin/com/mineinabyss/launchy/state/JvmState.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/JvmState.kt similarity index 88% rename from src/main/kotlin/com/mineinabyss/launchy/state/JvmState.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/JvmState.kt index 51fee27..7a39ebb 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/state/JvmState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/JvmState.kt @@ -1,11 +1,11 @@ -package com.mineinabyss.launchy.state +package com.mineinabyss.launchy.core.ui import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import com.mineinabyss.launchy.data.config.Config -import com.mineinabyss.launchy.logic.SuggestedJVMArgs +import com.mineinabyss.launchy.config.data.Config +import com.mineinabyss.launchy.util.SuggestedJVMArgs import kotlin.io.path.Path class JvmState( diff --git a/src/main/kotlin/com/mineinabyss/launchy/state/LaunchyState.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyState.kt similarity index 87% rename from src/main/kotlin/com/mineinabyss/launchy/state/LaunchyState.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyState.kt index a80249a..3d60fd8 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/state/LaunchyState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyState.kt @@ -1,9 +1,11 @@ -package com.mineinabyss.launchy.state +package com.mineinabyss.launchy.core.ui import androidx.compose.runtime.* -import com.mineinabyss.launchy.data.config.Config -import com.mineinabyss.launchy.data.config.GameInstance -import com.mineinabyss.launchy.state.modpack.GameInstanceState +import com.mineinabyss.launchy.auth.ui.ProfileState +import com.mineinabyss.launchy.config.data.Config +import com.mineinabyss.launchy.config.data.GameInstance +import com.mineinabyss.launchy.instance.data.GameInstanceState +import com.mineinabyss.launchy.util.InProgressTask import java.util.* class LaunchyState( diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/Screen.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/Screen.kt similarity index 86% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/Screen.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/Screen.kt index 9824dc6..cd47aa5 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/Screen.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/Screen.kt @@ -1,5 +1,8 @@ -package com.mineinabyss.launchy.ui.screens +package com.mineinabyss.launchy.core.ui +import kotlinx.serialization.Serializable + +@Serializable sealed class Screen( val transparentTopBar: Boolean = true, val showTitle: Boolean = false, diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/Screens.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/Screens.kt similarity index 85% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/Screens.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/Screens.kt index dae0914..531bcb2 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/Screens.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/Screens.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens +package com.mineinabyss.launchy.core.ui import androidx.compose.animation.* import androidx.compose.animation.core.animateDpAsState @@ -14,24 +14,24 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.logic.AppUpdateState -import com.mineinabyss.launchy.logic.DesktopHelpers -import com.mineinabyss.launchy.logic.GithubUpdateChecker -import com.mineinabyss.launchy.state.InProgressTask -import com.mineinabyss.launchy.state.modpack.GameInstanceState -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 -import com.mineinabyss.launchy.ui.screens.home.HomeScreen -import com.mineinabyss.launchy.ui.screens.home.newinstance.NewInstance -import com.mineinabyss.launchy.ui.screens.home.settings.SettingsScreen -import com.mineinabyss.launchy.ui.screens.modpack.main.InstanceScreen -import com.mineinabyss.launchy.ui.screens.modpack.main.SlightBackgroundTint -import com.mineinabyss.launchy.ui.screens.modpack.settings.InfoBarProperties -import com.mineinabyss.launchy.ui.screens.modpack.settings.InstanceSettingsScreen -import com.mineinabyss.launchy.ui.state.TopBar +import com.mineinabyss.launchy.auth.ui.AuthDialog +import com.mineinabyss.launchy.core.ui.components.AppTopBar +import com.mineinabyss.launchy.core.ui.components.LaunchyDialog +import com.mineinabyss.launchy.core.ui.components.LeftSidebar +import com.mineinabyss.launchy.core.ui.dialogs.SelectJVMDialog +import com.mineinabyss.launchy.core.ui.theme.currentHue +import com.mineinabyss.launchy.instance.data.GameInstanceState +import com.mineinabyss.launchy.instance.ui.InstanceScreen +import com.mineinabyss.launchy.instance.ui.InstanceSettingsScreen +import com.mineinabyss.launchy.instance.ui.components.SlightBackgroundTint +import com.mineinabyss.launchy.instance.ui.components.settings.InfoBarProperties +import com.mineinabyss.launchy.instance_creation.ui.NewInstance +import com.mineinabyss.launchy.instance_list.ui.HomeScreen +import com.mineinabyss.launchy.settings.ui.SettingsScreen +import com.mineinabyss.launchy.updater.data.AppUpdateState +import com.mineinabyss.launchy.updater.data.GithubUpdateChecker +import com.mineinabyss.launchy.util.DesktopHelpers +import com.mineinabyss.launchy.util.InProgressTask var screen: Screen by mutableStateOf(Screen.Default) @@ -69,6 +69,7 @@ fun Screens() = Scaffold( Screen(Screen.Instance) { InstanceScreen() } Screen(Screen.InstanceSettings, transition = Transitions.SlideUp) { InstanceSettingsScreen() } } + Screen(Screen.Default) { HomeScreen() } Screen(Screen.NewInstance) { NewInstance() } Screen(Screen.Settings) { SettingsScreen() } @@ -116,7 +117,6 @@ fun Screens() = Scaffold( } } ) - when (val castDialog = dialog) { Dialog.None -> {} Dialog.Auth -> AuthDialog( diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/state/TopBarState.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/TopBarState.kt similarity index 98% rename from src/main/kotlin/com/mineinabyss/launchy/ui/state/TopBarState.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/TopBarState.kt index b40e89e..5870a13 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/state/TopBarState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/TopBarState.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.state +package com.mineinabyss.launchy.core.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.compositionLocalOf diff --git a/src/main/kotlin/com/mineinabyss/launchy/state/UIState.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/UIState.kt similarity index 77% rename from src/main/kotlin/com/mineinabyss/launchy/state/UIState.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/UIState.kt index 58831f8..4859b33 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/state/UIState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/UIState.kt @@ -1,9 +1,9 @@ -package com.mineinabyss.launchy.state +package com.mineinabyss.launchy.core.ui import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import com.mineinabyss.launchy.data.config.Config +import com.mineinabyss.launchy.config.data.Config class UIState(config: Config) { var preferHue: Float by mutableStateOf(config.preferHue ?: 0f) diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/AnimatedTab.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/AnimatedTab.kt similarity index 91% rename from src/main/kotlin/com/mineinabyss/launchy/ui/elements/AnimatedTab.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/components/AnimatedTab.kt index 87d1e88..98e07f0 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/AnimatedTab.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/AnimatedTab.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.elements +package com.mineinabyss.launchy.core.ui.components import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/Buttons.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/Buttons.kt similarity index 97% rename from src/main/kotlin/com/mineinabyss/launchy/ui/elements/Buttons.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/components/Buttons.kt index 137f807..764b862 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/Buttons.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/Buttons.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.elements +package com.mineinabyss.launchy.core.ui.components import androidx.compose.material3.* import androidx.compose.runtime.Composable diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/ComfyContent.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/ComfyContent.kt similarity index 94% rename from src/main/kotlin/com/mineinabyss/launchy/ui/elements/ComfyContent.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/components/ComfyContent.kt index e6aecff..2e20fe4 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/ComfyContent.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/ComfyContent.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.elements +package com.mineinabyss.launchy.core.ui.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth @@ -12,7 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.ui.screens.screen +import com.mineinabyss.launchy.core.ui.screen @Composable fun ComfyWidth( diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/Dialog.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/Dialog.kt similarity index 98% rename from src/main/kotlin/com/mineinabyss/launchy/ui/elements/Dialog.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/components/Dialog.kt index 9aba7dd..7ed1ee8 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/Dialog.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/Dialog.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.elements +package com.mineinabyss.launchy.core.ui.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.* diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/LaunchyWindowDraggableArea.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/LaunchyWindowDraggableArea.kt similarity index 81% rename from src/main/kotlin/com/mineinabyss/launchy/ui/elements/LaunchyWindowDraggableArea.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/components/LaunchyWindowDraggableArea.kt index 96a84ac..e88927e 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/LaunchyWindowDraggableArea.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/LaunchyWindowDraggableArea.kt @@ -1,13 +1,12 @@ -package com.mineinabyss.launchy.ui.elements +package com.mineinabyss.launchy.core.ui.components import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.window.WindowDraggableArea import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.WindowScope -import com.mineinabyss.launchy.ui.state.TopBarProvider +import com.mineinabyss.launchy.core.ui.TopBarProvider @Composable fun WindowScope.BetterWindowDraggableArea( diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/LeftSidebar.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/LeftSidebar.kt similarity index 94% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/LeftSidebar.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/components/LeftSidebar.kt index 514d268..e582469 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/LeftSidebar.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/LeftSidebar.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens +package com.mineinabyss.launchy.core.ui.components import androidx.compose.animation.* import androidx.compose.foundation.Image @@ -22,8 +22,10 @@ import androidx.compose.ui.res.useResource import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.logic.Auth -import com.mineinabyss.launchy.ui.elements.PlayerAvatar +import com.mineinabyss.launchy.auth.data.Auth +import com.mineinabyss.launchy.auth.ui.components.AccountsPopup +import com.mineinabyss.launchy.core.ui.Screen +import com.mineinabyss.launchy.core.ui.screen import kotlinx.coroutines.launch @Composable diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/PlayerAvatar.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/PlayerAvatar.kt similarity index 86% rename from src/main/kotlin/com/mineinabyss/launchy/ui/elements/PlayerAvatar.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/components/PlayerAvatar.kt index a8572dc..3d6dbfd 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/PlayerAvatar.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/PlayerAvatar.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.elements +package com.mineinabyss.launchy.core.ui.components import androidx.compose.foundation.Image import androidx.compose.runtime.Composable @@ -7,7 +7,7 @@ import androidx.compose.ui.Modifier 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 com.mineinabyss.launchy.auth.data.PlayerProfile @Composable fun PlayerAvatar(profile: PlayerProfile, modifier: Modifier = Modifier) { diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/SingleFileDialog.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/SingleFileDialog.kt similarity index 96% rename from src/main/kotlin/com/mineinabyss/launchy/ui/elements/SingleFileDialog.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/components/SingleFileDialog.kt index a68671a..64d94be 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/SingleFileDialog.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/SingleFileDialog.kt @@ -1,10 +1,10 @@ -package com.mineinabyss.launchy.ui.elements +package com.mineinabyss.launchy.core.ui.components import androidx.compose.runtime.Composable import androidx.compose.ui.window.AwtWindow import com.darkrockstudios.libraries.mpfilepicker.DirectoryPicker import com.darkrockstudios.libraries.mpfilepicker.FilePicker -import com.mineinabyss.launchy.data.Dirs +import com.mineinabyss.launchy.util.Dirs import com.mineinabyss.launchy.util.OS import java.awt.FileDialog import java.awt.Frame diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/Tooltip.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/Tooltip.kt similarity index 94% rename from src/main/kotlin/com/mineinabyss/launchy/ui/elements/Tooltip.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/components/Tooltip.kt index b748fe8..39bc3a9 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/Tooltip.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/Tooltip.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.elements +package com.mineinabyss.launchy.core.ui.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/TopBar.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/TopBar.kt similarity index 96% rename from src/main/kotlin/com/mineinabyss/launchy/ui/TopBar.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/components/TopBar.kt index 487205d..a28b577 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/TopBar.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/TopBar.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui +package com.mineinabyss.launchy.core.ui.components import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.slideIn @@ -27,9 +27,8 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.window.WindowPlacement import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.data.Constants -import com.mineinabyss.launchy.ui.elements.BetterWindowDraggableArea -import com.mineinabyss.launchy.ui.state.TopBarState +import com.mineinabyss.launchy.core.ui.Constants +import com.mineinabyss.launchy.core.ui.TopBarState @Composable fun WindowButton(icon: ImageVector, onClick: () -> Unit) { diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/Typography.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/Typography.kt similarity index 96% rename from src/main/kotlin/com/mineinabyss/launchy/ui/elements/Typography.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/components/Typography.kt index d696e58..f87ecef 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/Typography.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/Typography.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.elements +package com.mineinabyss.launchy.core.ui.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/dialogs/SelectJVMDialog.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/dialogs/SelectJVMDialog.kt similarity index 80% rename from src/main/kotlin/com/mineinabyss/launchy/ui/dialogs/SelectJVMDialog.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/dialogs/SelectJVMDialog.kt index 489c601..1086dd1 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/dialogs/SelectJVMDialog.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/dialogs/SelectJVMDialog.kt @@ -1,17 +1,17 @@ -package com.mineinabyss.launchy.ui.dialogs +package com.mineinabyss.launchy.core.ui.dialogs import androidx.compose.material.LocalTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.logic.AppDispatchers -import com.mineinabyss.launchy.logic.Downloader -import com.mineinabyss.launchy.ui.elements.LaunchyDialog -import com.mineinabyss.launchy.ui.screens.Dialog -import com.mineinabyss.launchy.ui.screens.Screen -import com.mineinabyss.launchy.ui.screens.dialog -import com.mineinabyss.launchy.ui.screens.screen +import com.mineinabyss.launchy.core.data.Downloader +import com.mineinabyss.launchy.core.ui.Dialog +import com.mineinabyss.launchy.core.ui.Screen +import com.mineinabyss.launchy.core.ui.components.LaunchyDialog +import com.mineinabyss.launchy.core.ui.dialog +import com.mineinabyss.launchy.core.ui.screen +import com.mineinabyss.launchy.util.AppDispatchers import kotlinx.coroutines.launch @Composable diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/colors/Color.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/theme/Color.kt similarity index 96% rename from src/main/kotlin/com/mineinabyss/launchy/ui/colors/Color.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/theme/Color.kt index 538c70a..c1bd28c 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/colors/Color.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/theme/Color.kt @@ -1,10 +1,6 @@ -package com.mineinabyss.launchy.ui.colors +package com.mineinabyss.launchy.core.ui.theme -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/core/ui/theme/Theme.kt similarity index 88% rename from src/main/kotlin/com/mineinabyss/launchy/ui/colors/Theme.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/theme/Theme.kt index a84f7e3..07755d2 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/colors/Theme.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/theme/Theme.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.colors +package com.mineinabyss.launchy.core.ui.theme import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween @@ -7,7 +7,6 @@ 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) diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/Typography.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/theme/Typography.kt similarity index 97% rename from src/main/kotlin/com/mineinabyss/launchy/ui/Typography.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/theme/Typography.kt index 7f905f5..18359e7 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/Typography.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/theme/Typography.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui +package com.mineinabyss.launchy.core.ui.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.font.FontFamily diff --git a/src/main/kotlin/com/mineinabyss/launchy/state/modpack/DownloadQueueState.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/DownloadQueueState.kt similarity index 86% rename from src/main/kotlin/com/mineinabyss/launchy/state/modpack/DownloadQueueState.kt rename to src/main/kotlin/com/mineinabyss/launchy/downloads/data/DownloadQueueState.kt index 10089f0..58342a9 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/state/modpack/DownloadQueueState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/DownloadQueueState.kt @@ -1,10 +1,11 @@ -package com.mineinabyss.launchy.state.modpack +package com.mineinabyss.launchy.downloads.data import androidx.compose.runtime.* -import com.mineinabyss.launchy.data.ModID -import com.mineinabyss.launchy.data.config.DownloadInfo -import com.mineinabyss.launchy.data.config.InstanceUserConfig -import com.mineinabyss.launchy.data.modpacks.Modpack +import com.mineinabyss.launchy.config.data.DownloadInfo +import com.mineinabyss.launchy.config.data.InstanceUserConfig +import com.mineinabyss.launchy.instance.data.ModTogglesState +import com.mineinabyss.launchy.instance.data.Modpack +import com.mineinabyss.launchy.util.ModID class DownloadQueueState( private val userConfig: InstanceUserConfig, diff --git a/src/main/kotlin/com/mineinabyss/launchy/state/modpack/DownloadState.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/DownloadState.kt similarity index 82% rename from src/main/kotlin/com/mineinabyss/launchy/state/modpack/DownloadState.kt rename to src/main/kotlin/com/mineinabyss/launchy/downloads/data/DownloadState.kt index 96208ab..da7193d 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/state/modpack/DownloadState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/DownloadState.kt @@ -1,10 +1,10 @@ -package com.mineinabyss.launchy.state.modpack +package com.mineinabyss.launchy.downloads.data import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateMapOf -import com.mineinabyss.launchy.data.modpacks.Mod -import com.mineinabyss.launchy.logic.Progress +import com.mineinabyss.launchy.instance.data.Mod +import com.mineinabyss.launchy.util.Progress class DownloadState { val inProgressMods = mutableStateMapOf() diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/ModDownloader.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/ModDownloader.kt similarity index 92% rename from src/main/kotlin/com/mineinabyss/launchy/logic/ModDownloader.kt rename to src/main/kotlin/com/mineinabyss/launchy/downloads/data/ModDownloader.kt index 4f51b2f..8cfc747 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/ModDownloader.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/ModDownloader.kt @@ -1,13 +1,14 @@ -package com.mineinabyss.launchy.logic - -import com.mineinabyss.launchy.data.ModID -import com.mineinabyss.launchy.data.config.DownloadInfo -import com.mineinabyss.launchy.data.config.HashCheck -import com.mineinabyss.launchy.data.modpacks.InstanceModLoaders -import com.mineinabyss.launchy.data.modpacks.Mod -import com.mineinabyss.launchy.state.InProgressTask -import com.mineinabyss.launchy.state.LaunchyState -import com.mineinabyss.launchy.state.modpack.GameInstanceState +package com.mineinabyss.launchy.downloads.data + +import com.mineinabyss.launchy.config.data.DownloadInfo +import com.mineinabyss.launchy.config.data.HashCheck +import com.mineinabyss.launchy.core.data.Downloader +import com.mineinabyss.launchy.core.ui.LaunchyState +import com.mineinabyss.launchy.instance.data.GameInstanceState +import com.mineinabyss.launchy.instance.data.InstanceModLoaders +import com.mineinabyss.launchy.instance.data.Launcher +import com.mineinabyss.launchy.instance.data.Mod +import com.mineinabyss.launchy.util.* import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/source/PackSource.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/source/PackSource.kt similarity index 87% rename from src/main/kotlin/com/mineinabyss/launchy/data/modpacks/source/PackSource.kt rename to src/main/kotlin/com/mineinabyss/launchy/downloads/data/source/PackSource.kt index 6f0a2fc..ca2eed1 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/source/PackSource.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/source/PackSource.kt @@ -1,10 +1,10 @@ -package com.mineinabyss.launchy.data.modpacks.source +package com.mineinabyss.launchy.downloads.data.source -import com.mineinabyss.launchy.data.config.GameInstance -import com.mineinabyss.launchy.data.modpacks.Modpack -import com.mineinabyss.launchy.logic.AppDispatchers -import com.mineinabyss.launchy.logic.Downloader -import com.mineinabyss.launchy.logic.UpdateResult +import com.mineinabyss.launchy.config.data.GameInstance +import com.mineinabyss.launchy.core.data.Downloader +import com.mineinabyss.launchy.instance.data.Modpack +import com.mineinabyss.launchy.util.AppDispatchers +import com.mineinabyss.launchy.util.UpdateResult import kotlinx.coroutines.launch import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/source/PackType.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/source/PackType.kt similarity index 83% rename from src/main/kotlin/com/mineinabyss/launchy/data/modpacks/source/PackType.kt rename to src/main/kotlin/com/mineinabyss/launchy/downloads/data/source/PackType.kt index 62345a8..bc58118 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/source/PackType.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/source/PackType.kt @@ -1,12 +1,8 @@ -package com.mineinabyss.launchy.data.modpacks.source +package com.mineinabyss.launchy.downloads.data.source import com.charleskorn.kaml.decodeFromStream -import com.mineinabyss.launchy.data.Formats -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 com.mineinabyss.launchy.instance.data.formats.* +import com.mineinabyss.launchy.util.Formats import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.decodeFromStream diff --git a/src/main/kotlin/com/mineinabyss/launchy/state/modpack/GameInstanceState.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/GameInstanceState.kt similarity index 79% rename from src/main/kotlin/com/mineinabyss/launchy/state/modpack/GameInstanceState.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/GameInstanceState.kt index 80112d7..f2dd86e 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/state/modpack/GameInstanceState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/GameInstanceState.kt @@ -1,8 +1,9 @@ -package com.mineinabyss.launchy.state.modpack +package com.mineinabyss.launchy.instance.data -import com.mineinabyss.launchy.data.config.GameInstance -import com.mineinabyss.launchy.data.config.InstanceUserConfig -import com.mineinabyss.launchy.data.modpacks.Modpack +import com.mineinabyss.launchy.config.data.GameInstance +import com.mineinabyss.launchy.config.data.InstanceUserConfig +import com.mineinabyss.launchy.downloads.data.DownloadQueueState +import com.mineinabyss.launchy.downloads.data.DownloadState class GameInstanceState( val instance: GameInstance, diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/InstanceModLoaders.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModLoaders.kt similarity index 94% rename from src/main/kotlin/com/mineinabyss/launchy/data/modpacks/InstanceModLoaders.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModLoaders.kt index 84e50b8..f1047b8 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/InstanceModLoaders.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModLoaders.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.data.modpacks +package com.mineinabyss.launchy.instance.data import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/Launcher.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/Launcher.kt similarity index 93% rename from src/main/kotlin/com/mineinabyss/launchy/logic/Launcher.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/Launcher.kt index cad9641..9ab42c7 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/Launcher.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/Launcher.kt @@ -1,11 +1,11 @@ -package com.mineinabyss.launchy.logic +package com.mineinabyss.launchy.instance.data -import com.mineinabyss.launchy.data.modpacks.InstanceModLoaders -import com.mineinabyss.launchy.state.LaunchyState -import com.mineinabyss.launchy.state.ProfileState -import com.mineinabyss.launchy.state.modpack.GameInstanceState -import com.mineinabyss.launchy.ui.screens.Dialog -import com.mineinabyss.launchy.ui.screens.dialog +import com.mineinabyss.launchy.auth.data.Auth +import com.mineinabyss.launchy.auth.ui.ProfileState +import com.mineinabyss.launchy.core.ui.Dialog +import com.mineinabyss.launchy.core.ui.LaunchyState +import com.mineinabyss.launchy.core.ui.dialog +import com.mineinabyss.launchy.util.AppDispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Mod.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/Mod.kt similarity index 79% rename from src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Mod.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/Mod.kt index d0ec966..b12e57e 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Mod.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/Mod.kt @@ -1,7 +1,6 @@ -package com.mineinabyss.launchy.data.modpacks +package com.mineinabyss.launchy.instance.data -import com.mineinabyss.launchy.data.Dirs -import com.mineinabyss.launchy.data.modpacks.formats.ModrinthPackFormat +import com.mineinabyss.launchy.instance.data.formats.ModrinthPackFormat import io.ktor.http.* import java.nio.file.Path import kotlin.io.path.div diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/ModConfig.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/ModConfig.kt similarity index 83% rename from src/main/kotlin/com/mineinabyss/launchy/data/modpacks/ModConfig.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/ModConfig.kt index 9057ae2..ba21442 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/ModConfig.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/ModConfig.kt @@ -1,6 +1,6 @@ -package com.mineinabyss.launchy.data.modpacks +package com.mineinabyss.launchy.instance.data -import com.mineinabyss.launchy.data.modpacks.formats.ModDownloadPath +import com.mineinabyss.launchy.instance.data.formats.ModDownloadPath import kotlinx.serialization.Serializable @Serializable diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Group.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/ModGroup.kt similarity index 75% rename from src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Group.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/ModGroup.kt index f307895..e5aa556 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Group.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/ModGroup.kt @@ -1,9 +1,9 @@ -package com.mineinabyss.launchy.data.modpacks +package com.mineinabyss.launchy.instance.data import kotlinx.serialization.Serializable @Serializable -data class Group( +data class ModGroup( val name: String, val enabledByDefault: Boolean = false, val forceEnabled: Boolean = false, diff --git a/src/main/kotlin/com/mineinabyss/launchy/state/modpack/ModTogglesState.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/ModTogglesState.kt similarity index 83% rename from src/main/kotlin/com/mineinabyss/launchy/state/modpack/ModTogglesState.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/ModTogglesState.kt index 2b3ec8a..90fbe38 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/state/modpack/ModTogglesState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/ModTogglesState.kt @@ -1,12 +1,10 @@ -package com.mineinabyss.launchy.state.modpack +package com.mineinabyss.launchy.instance.data import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import com.mineinabyss.launchy.data.config.InstanceUserConfig -import com.mineinabyss.launchy.data.modpacks.Mod -import com.mineinabyss.launchy.data.modpacks.Modpack -import com.mineinabyss.launchy.logic.ToggleMods.setModEnabled -import com.mineinabyss.launchy.state.mutableStateSetOf +import com.mineinabyss.launchy.config.data.InstanceUserConfig +import com.mineinabyss.launchy.core.ui.mutableStateSetOf +import com.mineinabyss.launchy.instance.data.ToggleMods.setModEnabled class ModTogglesState( val modpack: Modpack, diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Modpack.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/Modpack.kt similarity index 76% rename from src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Modpack.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/Modpack.kt index 2b3f87a..ad4fa70 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Modpack.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/Modpack.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.data.modpacks +package com.mineinabyss.launchy.instance.data import java.nio.file.Path diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Mods.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/Mods.kt similarity index 56% rename from src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Mods.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/Mods.kt index 731033a..8627965 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Mods.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/Mods.kt @@ -1,29 +1,29 @@ -package com.mineinabyss.launchy.data.modpacks +package com.mineinabyss.launchy.instance.data -import com.mineinabyss.launchy.data.GroupName -import com.mineinabyss.launchy.data.ModID +import com.mineinabyss.launchy.util.GroupName +import com.mineinabyss.launchy.util.ModID data class Mods( - val modGroups: Map>, + val modGroups: Map>, ) { val groups = modGroups.keys val mods = modGroups.values.flatten().toSet() - private val nameToGroup: Map = groups.associateBy { it.name } + private val nameToGroup: Map = groups.associateBy { it.name } private val idToMod: Map = modGroups.values .flatten() .associateBy { it.modId } // fun getModById(id: ModID): Mod? = idToMod[id] - fun getGroup(name: GroupName): Group? = nameToGroup[name] + fun getGroup(name: GroupName): ModGroup? = nameToGroup[name] companion object { const val VERSIONS_URL = "https://raw.githubusercontent.com/MineInAbyss/launchy/master/versions.yml" fun withSingleGroup(mods: Collection) = Mods( modGroups = mapOf( - Group("Default", forceEnabled = true) to mods.toSet() + ModGroup("Default", forceEnabled = true) to mods.toSet() ) ) } diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/ToggleMods.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/ToggleMods.kt similarity index 91% rename from src/main/kotlin/com/mineinabyss/launchy/logic/ToggleMods.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/ToggleMods.kt index 9c42dcb..45b899c 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/ToggleMods.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/ToggleMods.kt @@ -1,7 +1,4 @@ -package com.mineinabyss.launchy.logic - -import com.mineinabyss.launchy.data.modpacks.Mod -import com.mineinabyss.launchy.state.modpack.ModTogglesState +package com.mineinabyss.launchy.instance.data object ToggleMods { fun ModTogglesState.setModEnabled(mod: Mod, enabled: Boolean) { diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/ExtraInfoFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ExtraInfoFormat.kt similarity index 77% rename from src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/ExtraInfoFormat.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ExtraInfoFormat.kt index 3ff04cf..7a43fd6 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/ExtraInfoFormat.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ExtraInfoFormat.kt @@ -1,9 +1,8 @@ -package com.mineinabyss.launchy.data.modpacks.formats +package com.mineinabyss.launchy.instance.data.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 com.mineinabyss.launchy.instance.data.Mod +import com.mineinabyss.launchy.instance.data.ModGroup +import com.mineinabyss.launchy.instance.data.Mods import java.nio.file.Path @@ -14,7 +13,7 @@ data class ExtraInfoFormat( override fun toGenericMods(downloadsDir: Path): Mods { val originalMods = format.toGenericMods(downloadsDir) val foundMods = mutableSetOf() - val mods: Map> = extraInfoPack.modGroups + val mods: Map> = extraInfoPack.modGroups .mapKeys { (name, _) -> extraInfoPack.groups.single { it.name == name } } .mapValues { (_, mods) -> mods.mapNotNull { ref -> diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/ExtraPackInfo.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ExtraPackInfo.kt similarity index 53% rename from src/main/kotlin/com/mineinabyss/launchy/data/modpacks/ExtraPackInfo.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ExtraPackInfo.kt index e96c0f2..b2434a0 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/ExtraPackInfo.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ExtraPackInfo.kt @@ -1,5 +1,7 @@ -package com.mineinabyss.launchy.data.modpacks +package com.mineinabyss.launchy.instance.data.formats +import com.mineinabyss.launchy.instance.data.ModConfig +import com.mineinabyss.launchy.instance.data.ModGroup import kotlinx.serialization.Serializable @Serializable @@ -9,6 +11,6 @@ class ModReference( ) @Serializable class ExtraPackInfo( - val groups: List = listOf(), + val groups: List = listOf(), val modGroups: Map> = mapOf(), ) diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/LaunchyPackFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/LaunchyPackFormat.kt similarity index 84% rename from src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/LaunchyPackFormat.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/LaunchyPackFormat.kt index 94dfa6a..091ce34 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/LaunchyPackFormat.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/LaunchyPackFormat.kt @@ -1,7 +1,7 @@ -package com.mineinabyss.launchy.data.modpacks.formats +package com.mineinabyss.launchy.instance.data.formats -import com.mineinabyss.launchy.data.GroupName -import com.mineinabyss.launchy.data.modpacks.* +import com.mineinabyss.launchy.instance.data.* +import com.mineinabyss.launchy.util.GroupName import kotlinx.serialization.Serializable import java.nio.file.Path @@ -9,7 +9,7 @@ import java.nio.file.Path data class LaunchyPackFormat( val fabricVersion: String? = null, val minecraftVersion: String, - val groups: Set, + val groups: Set, private val modGroups: Map>, ) : PackFormat { override fun toGenericMods(downloadsDir: Path): Mods { diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/ModDownloadPath.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ModDownloadPath.kt similarity index 95% rename from src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/ModDownloadPath.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ModDownloadPath.kt index 2b421fc..1de0025 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/ModDownloadPath.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ModDownloadPath.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.data.modpacks.formats +package com.mineinabyss.launchy.instance.data.formats import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/ModrinthPackFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ModrinthPackFormat.kt similarity index 83% rename from src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/ModrinthPackFormat.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ModrinthPackFormat.kt index f711224..75168cb 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/ModrinthPackFormat.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ModrinthPackFormat.kt @@ -1,9 +1,9 @@ -package com.mineinabyss.launchy.data.modpacks.formats +package com.mineinabyss.launchy.instance.data.formats -import com.mineinabyss.launchy.data.modpacks.InstanceModLoaders -import com.mineinabyss.launchy.data.modpacks.Mod -import com.mineinabyss.launchy.data.modpacks.ModConfig -import com.mineinabyss.launchy.data.modpacks.Mods +import com.mineinabyss.launchy.instance.data.InstanceModLoaders +import com.mineinabyss.launchy.instance.data.Mod +import com.mineinabyss.launchy.instance.data.ModConfig +import com.mineinabyss.launchy.instance.data.Mods import kotlinx.serialization.Serializable import java.nio.file.Path import kotlin.io.path.div diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/PackFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/PackFormat.kt similarity index 55% rename from src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/PackFormat.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/PackFormat.kt index 3d723a1..1afd084 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/formats/PackFormat.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/PackFormat.kt @@ -1,7 +1,7 @@ -package com.mineinabyss.launchy.data.modpacks.formats +package com.mineinabyss.launchy.instance.data.formats -import com.mineinabyss.launchy.data.modpacks.InstanceModLoaders -import com.mineinabyss.launchy.data.modpacks.Mods +import com.mineinabyss.launchy.instance.data.InstanceModLoaders +import com.mineinabyss.launchy.instance.data.Mods import java.nio.file.Path sealed interface PackFormat { diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/InstanceScreen.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceScreen.kt similarity index 73% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/InstanceScreen.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceScreen.kt index 1964e39..2bbf31d 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/InstanceScreen.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceScreen.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens.modpack.main +package com.mineinabyss.launchy.instance.ui import androidx.compose.animation.AnimatedVisibility import androidx.compose.desktop.ui.tooling.preview.Preview @@ -8,11 +8,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.ui.screens.LocalGameInstanceState -import com.mineinabyss.launchy.ui.screens.modpack.main.buttons.PlayButton -import com.mineinabyss.launchy.ui.screens.modpack.main.buttons.SettingsButton -import com.mineinabyss.launchy.ui.screens.modpack.main.buttons.UpdateButton -import com.mineinabyss.launchy.ui.state.windowScope +import com.mineinabyss.launchy.core.ui.LocalGameInstanceState +import com.mineinabyss.launchy.core.ui.windowScope +import com.mineinabyss.launchy.instance.ui.components.BackgroundImage +import com.mineinabyss.launchy.instance.ui.components.LogoLarge +import com.mineinabyss.launchy.instance.ui.components.buttons.PlayButton +import com.mineinabyss.launchy.instance.ui.components.buttons.SettingsButton +import com.mineinabyss.launchy.instance.ui.components.buttons.UpdateButton @ExperimentalComposeUiApi @Preview diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/InstanceSettingsScreen.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceSettingsScreen.kt similarity index 86% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/InstanceSettingsScreen.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceSettingsScreen.kt index 598a8b5..01bb935 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/InstanceSettingsScreen.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceSettingsScreen.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens.modpack.settings +package com.mineinabyss.launchy.instance.ui import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.VerticalScrollbar @@ -17,27 +17,28 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.data.Constants.SETTINGS_HORIZONTAL_PADDING -import com.mineinabyss.launchy.data.modpacks.Group -import com.mineinabyss.launchy.data.modpacks.Mod -import com.mineinabyss.launchy.data.modpacks.ModConfig -import com.mineinabyss.launchy.logic.AppDispatchers -import com.mineinabyss.launchy.logic.DesktopHelpers -import com.mineinabyss.launchy.logic.Instances.delete -import com.mineinabyss.launchy.logic.Instances.updateInstance -import com.mineinabyss.launchy.logic.ModDownloader.checkHashes -import com.mineinabyss.launchy.state.InProgressTask -import com.mineinabyss.launchy.ui.elements.* -import com.mineinabyss.launchy.ui.screens.LocalGameInstanceState -import com.mineinabyss.launchy.ui.screens.Screen -import com.mineinabyss.launchy.ui.screens.screen +import com.mineinabyss.launchy.core.ui.Constants.SETTINGS_HORIZONTAL_PADDING +import com.mineinabyss.launchy.core.ui.LocalGameInstanceState +import com.mineinabyss.launchy.core.ui.Screen +import com.mineinabyss.launchy.core.ui.components.* +import com.mineinabyss.launchy.core.ui.screen +import com.mineinabyss.launchy.downloads.data.ModDownloader.checkHashes +import com.mineinabyss.launchy.instance.data.Mod +import com.mineinabyss.launchy.instance.data.ModConfig +import com.mineinabyss.launchy.instance.data.ModGroup +import com.mineinabyss.launchy.instance.ui.components.settings.InfoBar +import com.mineinabyss.launchy.instance.ui.components.settings.ModGroup +import com.mineinabyss.launchy.instance_list.data.Instances.delete +import com.mineinabyss.launchy.instance_list.data.Instances.updateInstance +import com.mineinabyss.launchy.util.AppDispatchers +import com.mineinabyss.launchy.util.DesktopHelpers +import com.mineinabyss.launchy.util.InProgressTask import kotlinx.coroutines.launch import kotlin.io.path.listDirectoryEntries @Composable @Preview fun InstanceSettingsScreen() { - val state = LocalGameInstanceState var selectedTabIndex by remember { mutableStateOf(0) } ComfyWidth { Column { @@ -180,7 +181,7 @@ fun ModManagement() { ModGroup(group, mods) } if (userMods.isNotEmpty()) item { - ModGroup(Group("User mods", forceEnabled = true), userMods) + ModGroup(ModGroup("User mods", forceEnabled = true), userMods) } } VerticalScrollbar( diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/ImportSettingsDialog.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/ImportSettingsDialog.kt similarity index 86% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/ImportSettingsDialog.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/ImportSettingsDialog.kt index c037e62..d0dca80 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/ImportSettingsDialog.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/ImportSettingsDialog.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens.modpack.main +package com.mineinabyss.launchy.instance.ui.components import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn @@ -6,10 +6,10 @@ import androidx.compose.animation.fadeOut import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.data.Dirs -import com.mineinabyss.launchy.ui.elements.LaunchyDialog -import com.mineinabyss.launchy.ui.screens.Screen -import com.mineinabyss.launchy.ui.screens.screen +import com.mineinabyss.launchy.core.ui.Screen +import com.mineinabyss.launchy.core.ui.components.LaunchyDialog +import com.mineinabyss.launchy.core.ui.screen +import com.mineinabyss.launchy.util.Dirs import kotlin.io.path.copyTo import kotlin.io.path.div diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/MainScreenImages.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/MainScreenImages.kt similarity index 96% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/MainScreenImages.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/MainScreenImages.kt index d674420..ed7ddb1 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/MainScreenImages.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/MainScreenImages.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens.modpack.main +package com.mineinabyss.launchy.instance.ui.components import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically @@ -20,7 +20,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import androidx.compose.ui.window.WindowScope import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.ui.screens.LocalGameInstanceState +import com.mineinabyss.launchy.core.ui.LocalGameInstanceState @Composable fun BoxScope.BackgroundImage(windowScope: WindowScope) { diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/AuthButton.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/AuthButton.kt similarity index 64% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/AuthButton.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/AuthButton.kt index 2b23a68..5f87db9 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/AuthButton.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/AuthButton.kt @@ -1,12 +1,14 @@ -package com.mineinabyss.launchy.ui.screens.modpack.main.buttons +package com.mineinabyss.launchy.instance.ui.components.buttons import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.Login -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.logic.Auth -import com.mineinabyss.launchy.ui.elements.PrimaryButton +import com.mineinabyss.launchy.auth.data.Auth +import com.mineinabyss.launchy.core.ui.components.PrimaryButton import kotlinx.coroutines.launch @Composable diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/InstallButton.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/InstallButton.kt similarity index 86% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/InstallButton.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/InstallButton.kt index f6f16e9..479c1e2 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/InstallButton.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/InstallButton.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens.modpack.main.buttons +package com.mineinabyss.launchy.instance.ui.components.buttons import androidx.compose.animation.* import androidx.compose.foundation.layout.Row @@ -12,11 +12,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.logic.AppDispatchers -import com.mineinabyss.launchy.logic.ModDownloader.startInstall -import com.mineinabyss.launchy.ui.elements.OutlinedRedButton -import com.mineinabyss.launchy.ui.elements.PrimaryButton -import com.mineinabyss.launchy.ui.screens.LocalGameInstanceState +import com.mineinabyss.launchy.core.ui.LocalGameInstanceState +import com.mineinabyss.launchy.core.ui.components.OutlinedRedButton +import com.mineinabyss.launchy.core.ui.components.PrimaryButton +import com.mineinabyss.launchy.downloads.data.ModDownloader.startInstall +import com.mineinabyss.launchy.util.AppDispatchers import kotlinx.coroutines.launch @Composable diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/NewsButton.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/NewsButton.kt similarity index 89% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/NewsButton.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/NewsButton.kt index 439d6ef..8edd303 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/NewsButton.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/NewsButton.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens.modpack.main.buttons +package com.mineinabyss.launchy.instance.ui.components.buttons import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope @@ -15,7 +15,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.ui.elements.SecondaryButton +import com.mineinabyss.launchy.core.ui.components.SecondaryButton @Composable fun NewsButton(hasUpdates: Boolean) { diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/PlayButton.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/PlayButton.kt similarity index 86% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/PlayButton.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/PlayButton.kt index 2ce7973..71d3257 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/PlayButton.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/PlayButton.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens.modpack.main.buttons +package com.mineinabyss.launchy.instance.ui.components.buttons import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues @@ -16,17 +16,17 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.data.config.GameInstance -import com.mineinabyss.launchy.logic.AppDispatchers -import com.mineinabyss.launchy.logic.AppDispatchers.launchOrShowDialog -import com.mineinabyss.launchy.logic.Instances.updateInstance -import com.mineinabyss.launchy.logic.Launcher -import com.mineinabyss.launchy.logic.ModDownloader.startInstall -import com.mineinabyss.launchy.state.modpack.GameInstanceState -import com.mineinabyss.launchy.ui.elements.PrimaryButtonColors -import com.mineinabyss.launchy.ui.elements.SecondaryButtonColors -import com.mineinabyss.launchy.ui.screens.Dialog -import com.mineinabyss.launchy.ui.screens.dialog +import com.mineinabyss.launchy.config.data.GameInstance +import com.mineinabyss.launchy.core.ui.Dialog +import com.mineinabyss.launchy.core.ui.components.PrimaryButtonColors +import com.mineinabyss.launchy.core.ui.components.SecondaryButtonColors +import com.mineinabyss.launchy.core.ui.dialog +import com.mineinabyss.launchy.downloads.data.ModDownloader.startInstall +import com.mineinabyss.launchy.instance.data.GameInstanceState +import com.mineinabyss.launchy.instance.data.Launcher +import com.mineinabyss.launchy.instance_list.data.Instances.updateInstance +import com.mineinabyss.launchy.util.AppDispatchers +import com.mineinabyss.launchy.util.AppDispatchers.launchOrShowDialog import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/SettingsButton.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/SettingsButton.kt similarity index 74% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/SettingsButton.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/SettingsButton.kt index 9a527d0..da10acf 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/SettingsButton.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/SettingsButton.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens.modpack.main.buttons +package com.mineinabyss.launchy.instance.ui.components.buttons import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Settings @@ -6,8 +6,8 @@ import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import com.mineinabyss.launchy.ui.screens.Screen -import com.mineinabyss.launchy.ui.screens.screen +import com.mineinabyss.launchy.core.ui.Screen +import com.mineinabyss.launchy.core.ui.screen @Composable fun SettingsButton() { diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/UpdateButton.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/UpdateButton.kt similarity index 77% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/UpdateButton.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/UpdateButton.kt index 58b44fe..5274469 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/UpdateButton.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/UpdateButton.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens.modpack.main.buttons +package com.mineinabyss.launchy.instance.ui.components.buttons import androidx.compose.foundation.layout.Box import androidx.compose.material.icons.Icons @@ -8,8 +8,8 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.logic.Instances.updateInstance -import com.mineinabyss.launchy.ui.screens.LocalGameInstanceState +import com.mineinabyss.launchy.core.ui.LocalGameInstanceState +import com.mineinabyss.launchy.instance_list.data.Instances.updateInstance @Composable fun UpdateButton() { diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/UpdateInfoButton.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/UpdateInfoButton.kt similarity index 95% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/UpdateInfoButton.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/UpdateInfoButton.kt index 1de3779..3d81cd7 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/UpdateInfoButton.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/UpdateInfoButton.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens.modpack.main +package com.mineinabyss.launchy.instance.ui.components.buttons import androidx.compose.animation.* import androidx.compose.animation.core.tween @@ -19,7 +19,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.ui.screens.LocalGameInstanceState +import com.mineinabyss.launchy.core.ui.LocalGameInstanceState @Composable fun UpdateInfoButton() { diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/InfoBar.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/InfoBar.kt similarity index 89% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/InfoBar.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/InfoBar.kt index 75e9d54..b546a67 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/InfoBar.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/InfoBar.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens.modpack.settings +package com.mineinabyss.launchy.instance.ui.components.settings import androidx.compose.animation.* import androidx.compose.animation.core.animateIntAsState @@ -20,12 +20,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.data.Constants -import com.mineinabyss.launchy.data.Constants.SETTINGS_HORIZONTAL_PADDING -import com.mineinabyss.launchy.ui.elements.Tooltip -import com.mineinabyss.launchy.ui.screens.LocalGameInstanceState -import com.mineinabyss.launchy.ui.screens.modpack.main.buttons.InstallButton -import com.mineinabyss.launchy.ui.screens.modpack.main.buttons.RetryFailedButton +import com.mineinabyss.launchy.core.ui.Constants +import com.mineinabyss.launchy.core.ui.Constants.SETTINGS_HORIZONTAL_PADDING +import com.mineinabyss.launchy.core.ui.LocalGameInstanceState +import com.mineinabyss.launchy.core.ui.components.Tooltip +import com.mineinabyss.launchy.instance.ui.components.buttons.InstallButton +import com.mineinabyss.launchy.instance.ui.components.buttons.RetryFailedButton object InfoBarProperties { val height = 64.dp diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/ModGroup.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModGroup.kt similarity index 89% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/ModGroup.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModGroup.kt index 62c8e2d..d4b0445 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/ModGroup.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModGroup.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens.modpack.settings +package com.mineinabyss.launchy.instance.ui.components.settings import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateDpAsState @@ -20,15 +20,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.data.modpacks.Group -import com.mineinabyss.launchy.data.modpacks.Mod -import com.mineinabyss.launchy.logic.ToggleMods.setModEnabled -import com.mineinabyss.launchy.ui.elements.Tooltip -import com.mineinabyss.launchy.ui.screens.LocalGameInstanceState +import com.mineinabyss.launchy.core.ui.LocalGameInstanceState +import com.mineinabyss.launchy.core.ui.components.Tooltip +import com.mineinabyss.launchy.instance.data.Mod +import com.mineinabyss.launchy.instance.data.ModGroup +import com.mineinabyss.launchy.instance.data.ToggleMods.setModEnabled import com.mineinabyss.launchy.util.Option @Composable -fun ModGroup(group: Group, mods: Collection) { +fun ModGroup(group: ModGroup, mods: Collection) { var expanded by remember { mutableStateOf(false) } val arrowRotationState by animateFloatAsState(targetValue = if (expanded) 180f else 0f) val state = LocalGameInstanceState diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/ModInfoDisplay.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModInfoDisplay.kt similarity index 94% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/ModInfoDisplay.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModInfoDisplay.kt index ac40edc..bb382cb 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/ModInfoDisplay.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModInfoDisplay.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens.modpack.settings +package com.mineinabyss.launchy.instance.ui.components.settings import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState @@ -19,17 +19,17 @@ import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.data.modpacks.Group -import com.mineinabyss.launchy.data.modpacks.Mod -import com.mineinabyss.launchy.logic.DesktopHelpers -import com.mineinabyss.launchy.logic.ToggleMods.setModConfigEnabled -import com.mineinabyss.launchy.logic.ToggleMods.setModEnabled -import com.mineinabyss.launchy.ui.elements.Tooltip -import com.mineinabyss.launchy.ui.screens.LocalGameInstanceState +import com.mineinabyss.launchy.core.ui.LocalGameInstanceState +import com.mineinabyss.launchy.core.ui.components.Tooltip +import com.mineinabyss.launchy.instance.data.Mod +import com.mineinabyss.launchy.instance.data.ModGroup +import com.mineinabyss.launchy.instance.data.ToggleMods.setModConfigEnabled +import com.mineinabyss.launchy.instance.data.ToggleMods.setModEnabled +import com.mineinabyss.launchy.util.DesktopHelpers @OptIn(ExperimentalFoundationApi::class) @Composable -fun ModInfoDisplay(group: Group, mod: Mod) { +fun ModInfoDisplay(group: ModGroup, mod: Mod) { val state = LocalGameInstanceState val modEnabled by derivedStateOf { mod in state.toggles.enabledMods } val configEnabled by derivedStateOf { mod in state.toggles.enabledConfigs } diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/TripleSwitch.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/TripleSwitch.kt similarity index 93% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/TripleSwitch.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/TripleSwitch.kt index c0d72ed..6fcb1e5 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/TripleSwitch.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/TripleSwitch.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens.modpack.settings +package com.mineinabyss.launchy.instance.ui.components.settings import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.layout.Arrangement @@ -16,16 +16,16 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.data.Constants -import com.mineinabyss.launchy.data.modpacks.Group -import com.mineinabyss.launchy.data.modpacks.Mod -import com.mineinabyss.launchy.ui.screens.LocalGameInstanceState +import com.mineinabyss.launchy.core.ui.Constants +import com.mineinabyss.launchy.core.ui.LocalGameInstanceState +import com.mineinabyss.launchy.instance.data.Mod +import com.mineinabyss.launchy.instance.data.ModGroup import com.mineinabyss.launchy.util.Option @Composable fun ToggleButtons( onSwitch: (Option) -> Unit, - group: Group, + group: ModGroup, mods: Collection, ) { val state = LocalGameInstanceState diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/newinstance/NewInstance.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/NewInstance.kt similarity index 91% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/newinstance/NewInstance.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/NewInstance.kt index fa73456..ad1c596 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/newinstance/NewInstance.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/NewInstance.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens.home.newinstance +package com.mineinabyss.launchy.instance_creation.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -17,21 +17,21 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.data.Dirs -import com.mineinabyss.launchy.data.config.GameInstance -import com.mineinabyss.launchy.data.config.GameInstanceConfig -import com.mineinabyss.launchy.logic.AppDispatchers -import com.mineinabyss.launchy.logic.Downloader -import com.mineinabyss.launchy.logic.showDialogOnError -import com.mineinabyss.launchy.state.InProgressTask -import com.mineinabyss.launchy.ui.elements.AnimatedTab -import com.mineinabyss.launchy.ui.elements.ComfyContent -import com.mineinabyss.launchy.ui.elements.ComfyTitle -import com.mineinabyss.launchy.ui.elements.ComfyWidth -import com.mineinabyss.launchy.ui.screens.Screen -import com.mineinabyss.launchy.ui.screens.home.InstanceCard -import com.mineinabyss.launchy.ui.screens.modpack.settings.InstanceProperties -import com.mineinabyss.launchy.ui.screens.screen +import com.mineinabyss.launchy.config.data.GameInstance +import com.mineinabyss.launchy.config.data.GameInstanceConfig +import com.mineinabyss.launchy.core.data.Downloader +import com.mineinabyss.launchy.core.ui.Screen +import com.mineinabyss.launchy.core.ui.components.AnimatedTab +import com.mineinabyss.launchy.core.ui.components.ComfyContent +import com.mineinabyss.launchy.core.ui.components.ComfyTitle +import com.mineinabyss.launchy.core.ui.components.ComfyWidth +import com.mineinabyss.launchy.core.ui.screen +import com.mineinabyss.launchy.instance.ui.InstanceProperties +import com.mineinabyss.launchy.instance_list.ui.components.InstanceCard +import com.mineinabyss.launchy.util.AppDispatchers +import com.mineinabyss.launchy.util.Dirs +import com.mineinabyss.launchy.util.InProgressTask +import com.mineinabyss.launchy.util.showDialogOnError import kotlinx.coroutines.launch import kotlin.io.path.deleteIfExists import kotlin.io.path.exists diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/Instances.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/Instances.kt similarity index 81% rename from src/main/kotlin/com/mineinabyss/launchy/logic/Instances.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance_list/data/Instances.kt index 9dc3144..8723fed 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/Instances.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/Instances.kt @@ -1,12 +1,15 @@ -package com.mineinabyss.launchy.logic +package com.mineinabyss.launchy.instance_list.data -import com.mineinabyss.launchy.data.Dirs -import com.mineinabyss.launchy.data.config.GameInstance -import com.mineinabyss.launchy.data.config.GameInstanceConfig -import com.mineinabyss.launchy.state.InProgressTask -import com.mineinabyss.launchy.state.LaunchyState -import com.mineinabyss.launchy.ui.screens.Screen -import com.mineinabyss.launchy.ui.screens.screen +import com.mineinabyss.launchy.config.data.GameInstance +import com.mineinabyss.launchy.config.data.GameInstanceConfig +import com.mineinabyss.launchy.core.data.Downloader +import com.mineinabyss.launchy.core.ui.LaunchyState +import com.mineinabyss.launchy.core.ui.Screen +import com.mineinabyss.launchy.core.ui.screen +import com.mineinabyss.launchy.util.AppDispatchers +import com.mineinabyss.launchy.util.Dirs +import com.mineinabyss.launchy.util.InProgressTask +import com.mineinabyss.launchy.util.showDialogOnError import kotlinx.coroutines.launch import kotlin.io.path.* diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/HomeScreen.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListScreen.kt similarity index 80% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/HomeScreen.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListScreen.kt index 3b2293c..a164577 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListScreen.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens.home +package com.mineinabyss.launchy.instance_list.ui import androidx.compose.foundation.LocalScrollbarStyle import androidx.compose.foundation.VerticalScrollbar @@ -8,15 +8,16 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.LocalLaunchyState +import androidx.lifecycle.viewmodel.compose.viewModel +import com.mineinabyss.launchy.instance_list.ui.components.InstanceList @Composable -fun HomeScreen() { - val state = LocalLaunchyState - +fun HomeScreen(viewModel: InstanceListViewModel = viewModel()) { Box { val scrollState = rememberLazyListState() BoxWithConstraints { @@ -40,9 +41,12 @@ fun HomeScreen() { Spacer(Modifier.height(16.dp)) } item { + val instances by viewModel.gameInstances.collectAsState() + val lastPlayed by viewModel.lastPlayed.collectAsState() InstanceList( "Instances", - state.gameInstances.sortedByDescending { state.lastPlayed[it.config.name] }) + instances.sortedByDescending { lastPlayed[it.config.name] } + ) } // item { // ModpackGroup("Find more", state.downloadedModpacks) diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListViewModel.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListViewModel.kt new file mode 100644 index 0000000..d3fdf85 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListViewModel.kt @@ -0,0 +1,10 @@ +package com.mineinabyss.launchy.instance_list.ui + +import androidx.lifecycle.ViewModel +import com.mineinabyss.launchy.config.data.GameInstance +import kotlinx.coroutines.flow.MutableStateFlow + +class InstanceListViewModel : ViewModel() { + val gameInstances = MutableStateFlow(listOf()) + val lastPlayed = MutableStateFlow(mapOf()) +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/AddNewModpackCard.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/AddNewModpackCard.kt similarity index 89% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/AddNewModpackCard.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/AddNewModpackCard.kt index 6e7cc7e..4bb2872 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/AddNewModpackCard.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/AddNewModpackCard.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens.home +package com.mineinabyss.launchy.instance_list.ui.components import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable @@ -15,8 +15,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.ui.screens.Screen -import com.mineinabyss.launchy.ui.screens.screen +import com.mineinabyss.launchy.core.ui.Screen +import com.mineinabyss.launchy.core.ui.screen @Composable fun AddNewModpackCard(modifier: Modifier = Modifier) { diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/InstanceCard.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceCard.kt similarity index 82% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/InstanceCard.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceCard.kt index 578a61f..78c7264 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/InstanceCard.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceCard.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens.home +package com.mineinabyss.launchy.instance_list.ui.components import androidx.compose.animation.fadeIn import androidx.compose.foundation.Image @@ -22,19 +22,19 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.data.config.GameInstance -import com.mineinabyss.launchy.data.config.GameInstanceConfig -import com.mineinabyss.launchy.state.InProgressTask -import com.mineinabyss.launchy.ui.colors.LaunchyColors -import com.mineinabyss.launchy.ui.colors.currentHue -import com.mineinabyss.launchy.ui.elements.Tooltip -import com.mineinabyss.launchy.ui.screens.Screen -import com.mineinabyss.launchy.ui.screens.home.InstanceCardStyle.cardHeight -import com.mineinabyss.launchy.ui.screens.home.InstanceCardStyle.cardPadding -import com.mineinabyss.launchy.ui.screens.home.InstanceCardStyle.cardWidth -import com.mineinabyss.launchy.ui.screens.modpack.main.SlightBackgroundTint -import com.mineinabyss.launchy.ui.screens.modpack.main.buttons.PlayButton -import com.mineinabyss.launchy.ui.screens.screen +import com.mineinabyss.launchy.config.data.GameInstance +import com.mineinabyss.launchy.config.data.GameInstanceConfig +import com.mineinabyss.launchy.core.ui.Screen +import com.mineinabyss.launchy.core.ui.components.Tooltip +import com.mineinabyss.launchy.core.ui.screen +import com.mineinabyss.launchy.core.ui.theme.LaunchyColors +import com.mineinabyss.launchy.core.ui.theme.currentHue +import com.mineinabyss.launchy.instance.ui.components.SlightBackgroundTint +import com.mineinabyss.launchy.instance.ui.components.buttons.PlayButton +import com.mineinabyss.launchy.instance_list.ui.components.InstanceCardStyle.cardHeight +import com.mineinabyss.launchy.instance_list.ui.components.InstanceCardStyle.cardPadding +import com.mineinabyss.launchy.instance_list.ui.components.InstanceCardStyle.cardWidth +import com.mineinabyss.launchy.util.InProgressTask import kotlinx.coroutines.launch object InstanceCardStyle { diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/InstanceList.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceList.kt similarity index 94% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/InstanceList.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceList.kt index 911045f..70ab9fe 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/InstanceList.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceList.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens.home +package com.mineinabyss.launchy.instance_list.ui.components import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.grid.GridCells @@ -11,7 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.data.config.GameInstance +import com.mineinabyss.launchy.config.data.GameInstance @Composable fun InstanceList(title: String, packs: List) { diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/settings/SettingsScreen.kt b/src/main/kotlin/com/mineinabyss/launchy/settings/ui/SettingsScreen.kt similarity index 96% rename from src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/settings/SettingsScreen.kt rename to src/main/kotlin/com/mineinabyss/launchy/settings/ui/SettingsScreen.kt index 2fae540..cf4e31b 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/settings/SettingsScreen.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/settings/ui/SettingsScreen.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.ui.screens.home.settings +package com.mineinabyss.launchy.settings.ui import androidx.compose.animation.AnimatedVisibility import androidx.compose.desktop.ui.tooling.preview.Preview @@ -15,10 +15,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.data.Dirs -import com.mineinabyss.launchy.logic.DesktopHelpers -import com.mineinabyss.launchy.logic.SuggestedJVMArgs -import com.mineinabyss.launchy.ui.elements.* +import com.mineinabyss.launchy.core.ui.components.* +import com.mineinabyss.launchy.util.DesktopHelpers +import com.mineinabyss.launchy.util.Dirs +import com.mineinabyss.launchy.util.SuggestedJVMArgs @Composable @Preview diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/FirstLaunchDialog.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/FirstLaunchDialog.kt deleted file mode 100644 index 5d01be8..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/FirstLaunchDialog.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.mineinabyss.launchy.ui.screens.modpack.main - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.window.WindowDraggableArea -import androidx.compose.material.Text -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.ui.elements.LaunchyDialog -import com.mineinabyss.launchy.ui.state.windowScope - -@Composable -fun FirstLaunchDialog() { - val state = LocalLaunchyState - if (state.onboardingComplete) return - - val complete = { state.onboardingComplete = true} - // Overlay that prevents clicking behind it - windowScope.WindowDraggableArea { - Box(Modifier.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.6f)).fillMaxSize()) - } - - LaunchyDialog( - title = { Text("Welcome to Launchy!") }, - onAccept = complete, - onDecline = complete, - onDismiss = complete, - acceptText = "Ok", - declineText = null, - content = { - Text( - """Launchy is a launcher & mod installer provided by the MineInAbyss team. - You can launch the game by connecting your Microsoft account. - It comes bundled with a bunch of recommended mods for performance and quality of life. - You can change these settings later in the settings screen.""".trimIndent(), - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(bottom = 10.dp), - color = MaterialTheme.colorScheme.onSurface, - ) - } - ) -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/GithubUpdateChecker.kt b/src/main/kotlin/com/mineinabyss/launchy/updater/data/GithubUpdateChecker.kt similarity index 91% rename from src/main/kotlin/com/mineinabyss/launchy/logic/GithubUpdateChecker.kt rename to src/main/kotlin/com/mineinabyss/launchy/updater/data/GithubUpdateChecker.kt index ba42475..48b516e 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/GithubUpdateChecker.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/updater/data/GithubUpdateChecker.kt @@ -1,7 +1,8 @@ -package com.mineinabyss.launchy.logic +package com.mineinabyss.launchy.updater.data -import com.mineinabyss.launchy.data.Constants -import com.mineinabyss.launchy.data.Formats +import com.mineinabyss.launchy.core.data.Downloader +import com.mineinabyss.launchy.core.ui.Constants +import com.mineinabyss.launchy.util.Formats import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/AppDispatchers.kt b/src/main/kotlin/com/mineinabyss/launchy/util/AppDispatchers.kt similarity index 87% rename from src/main/kotlin/com/mineinabyss/launchy/logic/AppDispatchers.kt rename to src/main/kotlin/com/mineinabyss/launchy/util/AppDispatchers.kt index 4ba4034..0ca6332 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/AppDispatchers.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/AppDispatchers.kt @@ -1,7 +1,7 @@ -package com.mineinabyss.launchy.logic +package com.mineinabyss.launchy.util -import com.mineinabyss.launchy.ui.screens.Dialog -import com.mineinabyss.launchy.ui.screens.dialog +import com.mineinabyss.launchy.core.ui.Dialog +import com.mineinabyss.launchy.core.ui.dialog import kotlinx.coroutines.* import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext diff --git a/src/main/kotlin/com/mineinabyss/launchy/util/Arch.kt b/src/main/kotlin/com/mineinabyss/launchy/util/Arch.kt new file mode 100644 index 0000000..39e4483 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/util/Arch.kt @@ -0,0 +1,25 @@ +package com.mineinabyss.launchy.util + +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 + } + } + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/DesktopHelpers.kt b/src/main/kotlin/com/mineinabyss/launchy/util/DesktopHelpers.kt similarity index 93% rename from src/main/kotlin/com/mineinabyss/launchy/logic/DesktopHelpers.kt rename to src/main/kotlin/com/mineinabyss/launchy/util/DesktopHelpers.kt index 9715889..260a16f 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/DesktopHelpers.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/DesktopHelpers.kt @@ -1,6 +1,5 @@ -package com.mineinabyss.launchy.logic +package com.mineinabyss.launchy.util -import com.mineinabyss.launchy.util.OS import java.awt.Desktop import java.net.URI import java.nio.file.Path diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/Dirs.kt b/src/main/kotlin/com/mineinabyss/launchy/util/Dirs.kt similarity index 92% rename from src/main/kotlin/com/mineinabyss/launchy/data/Dirs.kt rename to src/main/kotlin/com/mineinabyss/launchy/util/Dirs.kt index 6bcd506..e1fdb7e 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/Dirs.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/Dirs.kt @@ -1,7 +1,6 @@ -package com.mineinabyss.launchy.data +package com.mineinabyss.launchy.util -import com.mineinabyss.launchy.data.config.GameInstance -import com.mineinabyss.launchy.util.OS +import com.mineinabyss.launchy.config.data.GameInstance import java.util.* import kotlin.io.path.* diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/Formats.kt b/src/main/kotlin/com/mineinabyss/launchy/util/Formats.kt similarity index 89% rename from src/main/kotlin/com/mineinabyss/launchy/data/Formats.kt rename to src/main/kotlin/com/mineinabyss/launchy/util/Formats.kt index daa98fc..d4e45e9 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/Formats.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/Formats.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.data +package com.mineinabyss.launchy.util import com.charleskorn.kaml.Yaml import com.charleskorn.kaml.YamlConfiguration diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/Helpers.kt b/src/main/kotlin/com/mineinabyss/launchy/util/Helpers.kt similarity index 72% rename from src/main/kotlin/com/mineinabyss/launchy/logic/Helpers.kt rename to src/main/kotlin/com/mineinabyss/launchy/util/Helpers.kt index 0ef9c0c..a338ff8 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/Helpers.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/Helpers.kt @@ -1,7 +1,7 @@ -package com.mineinabyss.launchy.logic +package com.mineinabyss.launchy.util -import com.mineinabyss.launchy.ui.screens.Dialog -import com.mineinabyss.launchy.ui.screens.dialog +import com.mineinabyss.launchy.core.ui.Dialog +import com.mineinabyss.launchy.core.ui.dialog import java.util.* fun Result.showDialogOnError(title: String? = null): Result { diff --git a/src/main/kotlin/com/mineinabyss/launchy/state/InProgressTask.kt b/src/main/kotlin/com/mineinabyss/launchy/util/InProgressTask.kt similarity index 90% rename from src/main/kotlin/com/mineinabyss/launchy/state/InProgressTask.kt rename to src/main/kotlin/com/mineinabyss/launchy/util/InProgressTask.kt index 50324c3..e714791 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/state/InProgressTask.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/InProgressTask.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.state +package com.mineinabyss.launchy.util open class InProgressTask(val name: String) { class WithPercentage( diff --git a/src/main/kotlin/com/mineinabyss/launchy/util/OS.kt b/src/main/kotlin/com/mineinabyss/launchy/util/OS.kt index 339ff7d..f294ce0 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/util/OS.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/OS.kt @@ -24,27 +24,3 @@ sealed 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 - } - } - } -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/Progress.kt b/src/main/kotlin/com/mineinabyss/launchy/util/Progress.kt similarity index 82% rename from src/main/kotlin/com/mineinabyss/launchy/logic/Progress.kt rename to src/main/kotlin/com/mineinabyss/launchy/util/Progress.kt index 5f80636..cb87667 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/Progress.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/Progress.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.logic +package com.mineinabyss.launchy.util data class Progress(val bytesDownloaded: Long, val totalBytes: Long, val timeElapsed: Long) { val percent: Float diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/SuggestedJVMArgs.kt b/src/main/kotlin/com/mineinabyss/launchy/util/SuggestedJVMArgs.kt similarity index 97% rename from src/main/kotlin/com/mineinabyss/launchy/logic/SuggestedJVMArgs.kt rename to src/main/kotlin/com/mineinabyss/launchy/util/SuggestedJVMArgs.kt index ccbe96d..9ce3a31 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/SuggestedJVMArgs.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/SuggestedJVMArgs.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.logic +package com.mineinabyss.launchy.util /** * Arguments recommended by diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/Tasks.kt b/src/main/kotlin/com/mineinabyss/launchy/util/Tasks.kt similarity index 75% rename from src/main/kotlin/com/mineinabyss/launchy/logic/Tasks.kt rename to src/main/kotlin/com/mineinabyss/launchy/util/Tasks.kt index 5761c81..c6ea1e5 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/Tasks.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/Tasks.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.logic +package com.mineinabyss.launchy.util object Tasks { val installModLoadersId = "installMCAndModLoaders" diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/Typealiases.kt b/src/main/kotlin/com/mineinabyss/launchy/util/Typealiases.kt similarity index 79% rename from src/main/kotlin/com/mineinabyss/launchy/data/Typealiases.kt rename to src/main/kotlin/com/mineinabyss/launchy/util/Typealiases.kt index c90f8f6..182e325 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/Typealiases.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/Typealiases.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.data +package com.mineinabyss.launchy.util typealias ModName = String typealias GroupName = String diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/UpdateResult.kt b/src/main/kotlin/com/mineinabyss/launchy/util/UpdateResult.kt similarity index 81% rename from src/main/kotlin/com/mineinabyss/launchy/logic/UpdateResult.kt rename to src/main/kotlin/com/mineinabyss/launchy/util/UpdateResult.kt index 9c1b01f..2226601 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/UpdateResult.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/UpdateResult.kt @@ -1,4 +1,6 @@ -package com.mineinabyss.launchy.logic +package com.mineinabyss.launchy.util + +import com.mineinabyss.launchy.core.data.Downloader sealed interface UpdateResult { val headers: Downloader.ModifyHeaders diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/hashing/Hashing.kt b/src/main/kotlin/com/mineinabyss/launchy/util/hashing/Hashing.kt similarity index 94% rename from src/main/kotlin/com/mineinabyss/launchy/logic/hashing/Hashing.kt rename to src/main/kotlin/com/mineinabyss/launchy/util/hashing/Hashing.kt index 68db3b7..73689d8 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/hashing/Hashing.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/hashing/Hashing.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.logic.hashing +package com.mineinabyss.launchy.util.hashing import java.io.InputStream import java.nio.file.Path diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/serializers/UUIDSerializer.kt b/src/main/kotlin/com/mineinabyss/launchy/util/serializers/UUIDSerializer.kt similarity index 93% rename from src/main/kotlin/com/mineinabyss/launchy/data/serializers/UUIDSerializer.kt rename to src/main/kotlin/com/mineinabyss/launchy/util/serializers/UUIDSerializer.kt index ce9c323..8bd1c23 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/serializers/UUIDSerializer.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/serializers/UUIDSerializer.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.data.serializers +package com.mineinabyss.launchy.util.serializers import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind From 8978a73d9b23b317ffe160a566002a8ef1299708 Mon Sep 17 00:00:00 2001 From: Danielle Voznyy Date: Fri, 31 May 2024 23:51:46 -0400 Subject: [PATCH 2/5] refactor: Start updating architecture for instance list and instance features --- .../kotlin/com/mineinabyss/launchy/Main.kt | 4 +- .../launchy/auth/data/PlayerProfile.kt | 2 +- .../mineinabyss/launchy/config/data/Config.kt | 18 +- .../launchy/config/data/ConfigDataSource.kt | 18 ++ .../launchy/config/data/GameInstanceConfig.kt | 86 -------- .../com/mineinabyss/launchy/core/ui/Dialog.kt | 2 +- .../launchy/core/ui/LaunchyState.kt | 12 +- .../mineinabyss/launchy/core/ui/Screens.kt | 8 +- .../core/ui/dialogs/SelectJVMDialog.kt | 2 +- .../downloads/data/DownloadQueueState.kt | 8 +- .../{core => downloads}/data/Downloader.kt | 12 +- .../launchy/downloads/data/ModDownloader.kt | 7 +- .../downloads/data/source/PackSource.kt | 20 +- .../instance/data/GameInstanceConfig.kt | 49 +++++ .../data/GameInstanceDataSource.kt} | 63 ++++-- .../instance/data/GameInstanceState.kt | 31 --- .../data/{Mods.kt => InstanceModList.kt} | 22 +- .../data/InstanceUserConfig.kt | 3 +- .../launchy/instance/data/Launcher.kt | 1 + .../launchy/instance/data/ModTogglesState.kt | 46 ----- .../launchy/instance/data/Modpack.kt | 2 +- .../launchy/instance/data/ToggleMods.kt | 29 --- .../instance/data/formats/ExtraInfoFormat.kt | 7 +- .../data/formats/LaunchyPackFormat.kt | 5 +- .../data/formats/ModrinthPackFormat.kt | 4 +- .../instance/data/formats/PackFormat.kt | 4 +- .../launchy/instance/ui/InstallState.kt | 17 ++ .../instance/ui/InstanceSettingsScreen.kt | 194 ------------------ .../launchy/instance/ui/InstanceViewModel.kt | 127 ++++++++++++ .../launchy/instance/ui/ModGroupUiState.kt | 8 + .../instance/ui/ModListInteractions.kt | 9 + .../launchy/instance/ui/ModListUiState.kt | 9 + .../launchy/instance/ui/ModUiState.kt | 43 ++++ ...MainScreenImages.kt => BackgroundImage.kt} | 36 +--- .../instance/ui/components/LogoLarge.kt | 34 +++ .../ui/components/buttons/InstallButton.kt | 37 ++-- .../ui/components/buttons/PlayButton.kt | 6 +- .../ui/components/settings/InfoBar.kt | 107 ---------- .../ui/components/settings/ModGroup.kt | 33 ++- .../ui/components/settings/ModInfoDisplay.kt | 24 +-- .../ui/components/settings/TripleSwitch.kt | 8 +- .../settings/infobar/ActionButton.kt | 36 ++++ .../ui/components/settings/infobar/InfoBar.kt | 81 ++++++++ .../settings/infobar/InfoBarProperties.kt | 7 + .../instance/ui/screens/InstanceProperties.kt | 53 +++++ .../ui/{ => screens}/InstanceScreen.kt | 2 +- .../ui/screens/InstanceSettingsScreen.kt | 45 ++++ .../instance/ui/screens/ModManagementTab.kt | 94 +++++++++ .../launchy/instance/ui/screens/OptionsTab.kt | 76 +++++++ .../instance_creation/ui/NewInstance.kt | 18 +- .../launchy/instance_list/data/Instances.kt | 10 +- .../data/LocalInstancesDataSource.kt | 19 ++ .../instance_list/ui/InstanceListViewModel.kt | 12 +- .../ui/components/InstanceCard.kt | 6 +- .../ui/components/InstanceList.kt | 4 +- .../updater/data/GithubUpdateChecker.kt | 2 +- .../launchy/util/AppDispatchers.kt | 8 +- .../com/mineinabyss/launchy/util/Dirs.kt | 4 +- .../mineinabyss/launchy/util/UpdateResult.kt | 2 +- 59 files changed, 922 insertions(+), 714 deletions(-) create mode 100644 src/main/kotlin/com/mineinabyss/launchy/config/data/ConfigDataSource.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/config/data/GameInstanceConfig.kt rename src/main/kotlin/com/mineinabyss/launchy/{core => downloads}/data/Downloader.kt (94%) create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/data/GameInstanceConfig.kt rename src/main/kotlin/com/mineinabyss/launchy/{config/data/GameInstance.kt => instance/data/GameInstanceDataSource.kt} (59%) delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/data/GameInstanceState.kt rename src/main/kotlin/com/mineinabyss/launchy/instance/data/{Mods.kt => InstanceModList.kt} (57%) rename src/main/kotlin/com/mineinabyss/launchy/{config => instance}/data/InstanceUserConfig.kt (95%) delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/data/ModTogglesState.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/data/ToggleMods.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstallState.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceSettingsScreen.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceViewModel.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModGroupUiState.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModListInteractions.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModListUiState.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModUiState.kt rename src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/{MainScreenImages.kt => BackgroundImage.kt} (61%) create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/LogoLarge.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/InfoBar.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/ActionButton.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/InfoBar.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/InfoBarProperties.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceProperties.kt rename src/main/kotlin/com/mineinabyss/launchy/instance/ui/{ => screens}/InstanceScreen.kt (97%) create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceSettingsScreen.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/ModManagementTab.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/OptionsTab.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance_list/data/LocalInstancesDataSource.kt diff --git a/src/main/kotlin/com/mineinabyss/launchy/Main.kt b/src/main/kotlin/com/mineinabyss/launchy/Main.kt index f331fa5..c970050 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/Main.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/Main.kt @@ -16,12 +16,12 @@ import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState import com.mineinabyss.launchy.config.data.Config -import com.mineinabyss.launchy.config.data.GameInstance import com.mineinabyss.launchy.core.ui.LaunchyState import com.mineinabyss.launchy.core.ui.Screens import com.mineinabyss.launchy.core.ui.TopBarProvider import com.mineinabyss.launchy.core.ui.TopBarState import com.mineinabyss.launchy.core.ui.theme.AppTheme +import com.mineinabyss.launchy.instance.data.GameInstanceDataSource import com.mineinabyss.launchy.util.Dirs import java.awt.Dimension @@ -38,7 +38,7 @@ fun main() { Dirs.createDirs() Dirs.createConfigFiles() val config = Config.read().getOrElse { Config() } - val instances = GameInstance.readAll(Dirs.modpackConfigsDir) + val instances = GameInstanceDataSource.readAll(Dirs.modpackConfigsDir) value = LaunchyState(config, instances) } val onClose: () -> Unit = { diff --git a/src/main/kotlin/com/mineinabyss/launchy/auth/data/PlayerProfile.kt b/src/main/kotlin/com/mineinabyss/launchy/auth/data/PlayerProfile.kt index dc7f3c1..eeeea28 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/auth/data/PlayerProfile.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/auth/data/PlayerProfile.kt @@ -7,7 +7,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.res.loadImageBitmap -import com.mineinabyss.launchy.core.data.Downloader +import com.mineinabyss.launchy.downloads.data.Downloader import com.mineinabyss.launchy.util.Dirs import com.mineinabyss.launchy.util.serializers.UUIDSerializer import kotlinx.coroutines.CoroutineScope diff --git a/src/main/kotlin/com/mineinabyss/launchy/config/data/Config.kt b/src/main/kotlin/com/mineinabyss/launchy/config/data/Config.kt index 36f5aa8..399e1e5 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/config/data/Config.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/config/data/Config.kt @@ -1,13 +1,7 @@ package com.mineinabyss.launchy.config.data -import com.charleskorn.kaml.decodeFromStream import com.mineinabyss.launchy.auth.data.PlayerProfile -import com.mineinabyss.launchy.util.Dirs -import com.mineinabyss.launchy.util.Formats import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlin.io.path.inputStream -import kotlin.io.path.writeText @Serializable @@ -22,14 +16,4 @@ data class Config( val preferHue: Float? = null, val startInFullscreen: Boolean = false, val lastPlayedMap: Map = mapOf(), -) { - fun save() { - Dirs.configFile.writeText(Formats.yaml.encodeToString(this)) - } - - companion object { - fun read(): Result = runCatching { - Formats.yaml.decodeFromStream(serializer(), Dirs.configFile.inputStream()) - }.onFailure { it.printStackTrace() } - } -} +) diff --git a/src/main/kotlin/com/mineinabyss/launchy/config/data/ConfigDataSource.kt b/src/main/kotlin/com/mineinabyss/launchy/config/data/ConfigDataSource.kt new file mode 100644 index 0000000..1f70f26 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/config/data/ConfigDataSource.kt @@ -0,0 +1,18 @@ +package com.mineinabyss.launchy.config.data + +import com.charleskorn.kaml.decodeFromStream +import com.mineinabyss.launchy.util.Dirs +import com.mineinabyss.launchy.util.Formats +import kotlinx.serialization.encodeToString +import kotlin.io.path.inputStream +import kotlin.io.path.writeText + +class ConfigDataSource { + fun readConfig(): Result = runCatching { + Formats.yaml.decodeFromStream(Dirs.configFile.inputStream()) + }.onFailure { it.printStackTrace() } + + fun saveConfig(config: Config) { + Dirs.configFile.writeText(Formats.yaml.encodeToString(config)) + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/config/data/GameInstanceConfig.kt b/src/main/kotlin/com/mineinabyss/launchy/config/data/GameInstanceConfig.kt deleted file mode 100644 index 2e39f95..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/config/data/GameInstanceConfig.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.mineinabyss.launchy.config.data - -import androidx.compose.runtime.mutableStateOf -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.core.data.Downloader -import com.mineinabyss.launchy.downloads.data.source.PackSource -import com.mineinabyss.launchy.util.Dirs -import com.mineinabyss.launchy.util.Formats -import com.mineinabyss.launchy.util.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 -import java.nio.file.Path -import kotlin.io.path.div -import kotlin.io.path.inputStream -import kotlin.io.path.outputStream - -@Serializable -data class GameInstanceConfig( - val name: String, - val description: String, - val backgroundURL: String, - val logoURL: String, - val source: PackSource, - val hue: Float = 0f, - val cloudInstanceURL: String? = null, - val overrideMinecraftDir: String? = null, -) { - @Transient - val backgroundPath = Dirs.imageCache / "background-${urlToFileName(backgroundURL)}" - - @Transient - val logoPath = Dirs.imageCache / "icon-${urlToFileName(logoURL)}" - - @Transient - private var cachedBackground = mutableStateOf(null) - - @Transient - private var cachedLogo = mutableStateOf(null) - - @OptIn(ExperimentalCoroutinesApi::class) - @Transient - val downloadScope = CoroutineScope(Dispatchers.IO.limitedParallelism(1)) - - private suspend fun loadBackground() { - runCatching { - Downloader.download(backgroundURL, backgroundPath, Downloader.Options(overwrite = false)) - val painter = BitmapPainter(loadImageBitmap(backgroundPath.inputStream())) - cachedBackground.value = painter - }.onFailure { it.printStackTrace() } - } - - private suspend fun loadLogo() { - runCatching { - Downloader.download(logoURL, logoPath, Downloader.Options(overwrite = false)) - val painter = BitmapPainter(loadImageBitmap(logoPath.inputStream())) - cachedLogo.value = painter - }.onFailure { it.printStackTrace() } - } - - fun getBackgroundAsState() = - cachedBackground.also { - if (it.value == null) downloadScope.launch { loadBackground() } - } - - fun getLogoAsState() = - cachedLogo.also { - if (it.value == null) downloadScope.launch { loadLogo() } - } - - fun saveTo(path: Path) = runCatching { - Formats.yaml.encodeToStream(this, path.outputStream()) - } - - companion object { - fun read(path: Path) = runCatching { - Formats.yaml.decodeFromStream(serializer(), path.inputStream()) - }.onFailure { it.printStackTrace() } - } -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/Dialog.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/Dialog.kt index 7ae708a..b5a4124 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/core/ui/Dialog.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/Dialog.kt @@ -1,6 +1,6 @@ package com.mineinabyss.launchy.core.ui -import com.mineinabyss.launchy.config.data.GameInstanceConfig +import com.mineinabyss.launchy.instance.data.GameInstanceConfig sealed interface Dialog { object None : Dialog diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyState.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyState.kt index 3d60fd8..755a4b9 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyState.kt @@ -3,15 +3,15 @@ package com.mineinabyss.launchy.core.ui import androidx.compose.runtime.* import com.mineinabyss.launchy.auth.ui.ProfileState import com.mineinabyss.launchy.config.data.Config -import com.mineinabyss.launchy.config.data.GameInstance -import com.mineinabyss.launchy.instance.data.GameInstanceState +import com.mineinabyss.launchy.instance.data.GameInstanceDataSource +import com.mineinabyss.launchy.instance.ui.GameInstanceState import com.mineinabyss.launchy.util.InProgressTask import java.util.* class LaunchyState( // Config should never be mutated unless it also updates UI state private val config: Config, - private val instances: List + private val instances: List ) { val profile = ProfileState(config) var instanceState: GameInstanceState? by mutableStateOf(null) @@ -22,14 +22,14 @@ class LaunchyState( putAll(config.lastPlayedMap) } - val gameInstances = mutableStateListOf().apply { + val gameInstances = mutableStateListOf().apply { addAll(instances) } val inProgressTasks = mutableStateMapOf() - fun processFor(instance: GameInstance): Process? = launchedProcesses[instance.minecraftDir.toString()] - fun setProcessFor(instance: GameInstance, process: Process?) { + fun processFor(instance: GameInstanceDataSource): Process? = launchedProcesses[instance.minecraftDir.toString()] + fun setProcessFor(instance: GameInstanceDataSource, process: Process?) { if (process == null) launchedProcesses.remove(instance.minecraftDir.toString()) else launchedProcesses[instance.minecraftDir.toString()] = process } diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/Screens.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/Screens.kt index 531bcb2..422cd5d 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/core/ui/Screens.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/Screens.kt @@ -20,11 +20,11 @@ import com.mineinabyss.launchy.core.ui.components.LaunchyDialog import com.mineinabyss.launchy.core.ui.components.LeftSidebar import com.mineinabyss.launchy.core.ui.dialogs.SelectJVMDialog import com.mineinabyss.launchy.core.ui.theme.currentHue -import com.mineinabyss.launchy.instance.data.GameInstanceState -import com.mineinabyss.launchy.instance.ui.InstanceScreen -import com.mineinabyss.launchy.instance.ui.InstanceSettingsScreen +import com.mineinabyss.launchy.instance.ui.GameInstanceState import com.mineinabyss.launchy.instance.ui.components.SlightBackgroundTint -import com.mineinabyss.launchy.instance.ui.components.settings.InfoBarProperties +import com.mineinabyss.launchy.instance.ui.components.settings.infobar.InfoBarProperties +import com.mineinabyss.launchy.instance.ui.screens.InstanceScreen +import com.mineinabyss.launchy.instance.ui.screens.InstanceSettingsScreen import com.mineinabyss.launchy.instance_creation.ui.NewInstance import com.mineinabyss.launchy.instance_list.ui.HomeScreen import com.mineinabyss.launchy.settings.ui.SettingsScreen diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/dialogs/SelectJVMDialog.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/dialogs/SelectJVMDialog.kt index 1086dd1..5a5ae61 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/core/ui/dialogs/SelectJVMDialog.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/dialogs/SelectJVMDialog.kt @@ -5,12 +5,12 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.core.data.Downloader import com.mineinabyss.launchy.core.ui.Dialog import com.mineinabyss.launchy.core.ui.Screen import com.mineinabyss.launchy.core.ui.components.LaunchyDialog import com.mineinabyss.launchy.core.ui.dialog import com.mineinabyss.launchy.core.ui.screen +import com.mineinabyss.launchy.downloads.data.Downloader import com.mineinabyss.launchy.util.AppDispatchers import kotlinx.coroutines.launch diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/DownloadQueueState.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/DownloadQueueState.kt index 58342a9..1b07bdf 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/DownloadQueueState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/DownloadQueueState.kt @@ -1,16 +1,16 @@ package com.mineinabyss.launchy.downloads.data import androidx.compose.runtime.* -import com.mineinabyss.launchy.config.data.DownloadInfo -import com.mineinabyss.launchy.config.data.InstanceUserConfig -import com.mineinabyss.launchy.instance.data.ModTogglesState +import com.mineinabyss.launchy.instance.data.DownloadInfo +import com.mineinabyss.launchy.instance.data.InstanceUserConfig import com.mineinabyss.launchy.instance.data.Modpack +import com.mineinabyss.launchy.instance.ui.InstanceViewModel import com.mineinabyss.launchy.util.ModID class DownloadQueueState( private val userConfig: InstanceUserConfig, val modpack: Modpack, - val toggles: ModTogglesState + val toggles: InstanceViewModel ) { /** Live mod download info, including mods that have been removed from the latest modpack version. */ val modDownloadInfo = mutableStateMapOf().apply { diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/data/Downloader.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/Downloader.kt similarity index 94% rename from src/main/kotlin/com/mineinabyss/launchy/core/data/Downloader.kt rename to src/main/kotlin/com/mineinabyss/launchy/downloads/data/Downloader.kt index dfe5370..55d1b4a 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/core/data/Downloader.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/Downloader.kt @@ -1,7 +1,7 @@ -package com.mineinabyss.launchy.core.data +package com.mineinabyss.launchy.downloads.data -import com.mineinabyss.launchy.config.data.GameInstance import com.mineinabyss.launchy.core.ui.LaunchyState +import com.mineinabyss.launchy.instance.data.GameInstanceDataSource import com.mineinabyss.launchy.util.* import io.ktor.client.* import io.ktor.client.call.* @@ -34,12 +34,12 @@ object Downloader { headers["Content-Length"]?.toLongOrNull() ?: 0 ) - fun fileFor(instance: GameInstance, url: String) = + fun fileFor(instance: GameInstanceDataSource, url: String) = Dirs.cacheDir(instance) / "${urlToFileName(url)}.header" } } - suspend fun checkUpdates(instance: GameInstance, url: String): UpdateResult { + suspend fun checkUpdates(instance: GameInstanceDataSource, url: String): UpdateResult { val headers = ModifyHeaders.of(httpClient.head(url).headers) val cache = headers.toCacheString() val cacheFile = ModifyHeaders.fileFor(instance, url) @@ -50,7 +50,7 @@ object Downloader { } } - fun saveHeaders(instance: GameInstance, url: String, headers: ModifyHeaders) { + fun saveHeaders(instance: GameInstanceDataSource, url: String, headers: ModifyHeaders) { ModifyHeaders.fileFor(instance, url).createParentDirectories().apply { deleteIfExists() createFile() @@ -184,6 +184,6 @@ object Downloader { val overwrite: Boolean = true, val whenChanged: () -> Unit = {}, val onProgressUpdate: (progress: Progress) -> Unit = {}, - val saveModifyHeadersFor: GameInstance? = null, + val saveModifyHeadersFor: GameInstanceDataSource? = null, ) } diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/ModDownloader.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/ModDownloader.kt index 8cfc747..6731d8d 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/ModDownloader.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/ModDownloader.kt @@ -1,13 +1,12 @@ package com.mineinabyss.launchy.downloads.data -import com.mineinabyss.launchy.config.data.DownloadInfo -import com.mineinabyss.launchy.config.data.HashCheck -import com.mineinabyss.launchy.core.data.Downloader import com.mineinabyss.launchy.core.ui.LaunchyState -import com.mineinabyss.launchy.instance.data.GameInstanceState +import com.mineinabyss.launchy.instance.data.DownloadInfo +import com.mineinabyss.launchy.instance.data.HashCheck import com.mineinabyss.launchy.instance.data.InstanceModLoaders import com.mineinabyss.launchy.instance.data.Launcher import com.mineinabyss.launchy.instance.data.Mod +import com.mineinabyss.launchy.instance.ui.GameInstanceState import com.mineinabyss.launchy.util.* import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/source/PackSource.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/source/PackSource.kt index ca2eed1..15b802e 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/source/PackSource.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/source/PackSource.kt @@ -1,7 +1,7 @@ package com.mineinabyss.launchy.downloads.data.source -import com.mineinabyss.launchy.config.data.GameInstance -import com.mineinabyss.launchy.core.data.Downloader +import com.mineinabyss.launchy.downloads.data.Downloader +import com.mineinabyss.launchy.instance.data.GameInstanceDataSource import com.mineinabyss.launchy.instance.data.Modpack import com.mineinabyss.launchy.util.AppDispatchers import com.mineinabyss.launchy.util.UpdateResult @@ -12,29 +12,29 @@ import kotlin.io.path.notExists @Serializable sealed class PackSource { - abstract suspend fun loadInstance(instance: GameInstance): Result + abstract suspend fun loadInstance(instance: GameInstanceDataSource): Result - abstract suspend fun updateInstance(instance: GameInstance): Result + abstract suspend fun updateInstance(instance: GameInstanceDataSource): Result @Serializable @SerialName("localFile") class LocalFile(val type: PackType) : PackSource() { - override suspend fun loadInstance(instance: GameInstance): Result = runCatching { + override suspend fun loadInstance(instance: GameInstanceDataSource): Result = runCatching { val format = type.getFormat(instance.configDir).getOrThrow() val mods = format.toGenericMods(instance.downloadsDir) val modLoaders = format.getModLoaders() Modpack(modLoaders, mods, format.getOverridesPaths(instance.configDir)) } - override suspend fun updateInstance(instance: GameInstance): Result { - return runCatching { GameInstance(instance.configDir) } + override suspend fun updateInstance(instance: GameInstanceDataSource): Result { + return runCatching { GameInstanceDataSource(instance.configDir) } } } @SerialName("downloadFromURL") @Serializable class DownloadFromURL(val url: String, val type: PackType) : PackSource() { - override suspend fun loadInstance(instance: GameInstance): Result { + override suspend fun loadInstance(instance: GameInstanceDataSource): Result { val downloadTo = type.getFilePath(instance.configDir) if (downloadTo.notExists()) { Downloader.download(url, downloadTo, options = Downloader.Options(saveModifyHeadersFor = instance)) @@ -48,12 +48,12 @@ sealed class PackSource { return LocalFile(type).loadInstance(instance) } - override suspend fun updateInstance(instance: GameInstance): Result { + override suspend fun updateInstance(instance: GameInstanceDataSource): Result { return runCatching { val downloadTo = type.getFilePath(instance.configDir) Downloader.download(url, downloadTo, options = Downloader.Options(saveModifyHeadersFor = instance)) type.afterDownload(instance.configDir) - GameInstance(instance.configDir) + GameInstanceDataSource(instance.configDir) } } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/GameInstanceConfig.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/GameInstanceConfig.kt new file mode 100644 index 0000000..c33eeb5 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/GameInstanceConfig.kt @@ -0,0 +1,49 @@ +package com.mineinabyss.launchy.instance.data + +import androidx.compose.runtime.Immutable +import com.charleskorn.kaml.decodeFromStream +import com.mineinabyss.launchy.downloads.data.source.PackSource +import com.mineinabyss.launchy.util.Dirs +import com.mineinabyss.launchy.util.Formats +import com.mineinabyss.launchy.util.urlToFileName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import java.nio.file.Path +import kotlin.io.path.div +import kotlin.io.path.inputStream + +@Immutable +@Serializable +data class GameInstanceConfig( + val name: String, + val description: String, + val backgroundURL: String, + val logoURL: String, + val source: PackSource, + val hue: Float = 0f, + val cloudInstanceURL: String? = null, + val overrideMinecraftDir: String? = null, +) { + @OptIn(ExperimentalCoroutinesApi::class) + @Transient + val downloadScope = CoroutineScope(Dispatchers.IO.limitedParallelism(1)) + + @Transient + val backgroundPath = Dirs.imageCache / "background-${urlToFileName(backgroundURL)}" + + @Transient + val logoPath = Dirs.imageCache / "icon-${urlToFileName(logoURL)}" + +// fun saveTo(path: Path) = runCatching { +// Formats.yaml.encodeToStream(this, path.outputStream()) +// } + + companion object { + fun read(path: Path) = runCatching { + Formats.yaml.decodeFromStream(serializer(), path.inputStream()) + }.onFailure { it.printStackTrace() } + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/config/data/GameInstance.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/GameInstanceDataSource.kt similarity index 59% rename from src/main/kotlin/com/mineinabyss/launchy/config/data/GameInstance.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/GameInstanceDataSource.kt index 20110cf..33e36cb 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/config/data/GameInstance.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/GameInstanceDataSource.kt @@ -1,24 +1,28 @@ -package com.mineinabyss.launchy.config.data +package com.mineinabyss.launchy.instance.data import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.res.loadImageBitmap import com.charleskorn.kaml.encodeToStream -import com.mineinabyss.launchy.core.data.Downloader import com.mineinabyss.launchy.core.ui.LaunchyState -import com.mineinabyss.launchy.instance.data.GameInstanceState +import com.mineinabyss.launchy.downloads.data.Downloader +import com.mineinabyss.launchy.instance.ui.GameInstanceState import com.mineinabyss.launchy.util.* +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.nio.file.Path import kotlin.io.path.* -class GameInstance( +class GameInstanceDataSource( val configDir: Path, + val config: GameInstanceConfig, ) { val instanceFile = configDir / "instance.yml" - val config: GameInstanceConfig = GameInstanceConfig.read(instanceFile).getOrThrow() - val overridesDir = configDir / "overrides" + val imageLoaderDispatcher = Dispatchers.IO.limitedParallelism(1) val minecraftDir = config.overrideMinecraftDir?.let { Path(it) } ?: Dirs.modpackDir(configDir.name) @@ -31,6 +35,9 @@ class GameInstance( var updatesAvailable by mutableStateOf(false) var enabled: Boolean by mutableStateOf(true) + suspend fun loadModList(): InstanceModList { + TODO() + } suspend fun createModpackState(state: LaunchyState, awaitUpdatesCheck: Boolean = false): GameInstanceState? { val userConfig = InstanceUserConfig.load(userConfigFile).getOrNull() ?: InstanceUserConfig() @@ -45,9 +52,9 @@ class GameInstance( val cloudUrl = config.cloudInstanceURL if (cloudUrl != null) { AppDispatchers.IO.launch { - val result = Downloader.checkUpdates(this@GameInstance, cloudUrl) + val result = Downloader.checkUpdates(this@GameInstanceDataSource, cloudUrl) if (result !is UpdateResult.UpToDate) updatesAvailable = true - }.also { if(awaitUpdatesCheck) it.join() } + }.also { if (awaitUpdatesCheck) it.join() } } return GameInstanceState(this, modpack, userConfig) } @@ -72,20 +79,38 @@ class GameInstance( cloud.config.copy(cloudInstanceURL = cloud.url), (instanceDir / "instance.yml").outputStream() ) - val instance = GameInstance(instanceDir) + val instance = GameInstanceDataSource(instanceDir) Downloader.saveHeaders(instance, cloud.url, cloud.headers) state.gameInstances += instance } + } - fun readAll(rootDir: Path): List { - return rootDir - .listDirectoryEntries() - .filter { it.isDirectory() } - .mapNotNull { - runCatching { GameInstance(it) } - .onFailure { it.printStackTrace() } - .getOrNull() - } - } + private suspend fun loadBackground() { + runCatching { + Downloader.download(config.backgroundURL, config.backgroundPath, Downloader.Options(overwrite = false)) + val painter = BitmapPainter(loadImageBitmap(config.backgroundPath.inputStream())) + cachedBackground = painter + }.onFailure { it.printStackTrace() } + } + + private suspend fun loadLogo() { + runCatching { + Downloader.download(config.logoURL, config.logoPath, Downloader.Options(overwrite = false)) + val painter = BitmapPainter(loadImageBitmap(config.logoPath.inputStream())) + cachedLogo = painter + }.onFailure { it.printStackTrace() } + } + + private var cachedBackground: BitmapPainter? = null + private var cachedLogo: BitmapPainter? = null + + suspend fun getBackground() = withContext(imageLoaderDispatcher) { + if (cachedBackground == null) loadLogo() + cachedBackground + } + + suspend fun getLogo(): BitmapPainter? = withContext(imageLoaderDispatcher) { + if (cachedLogo == null) loadLogo() + cachedLogo } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/GameInstanceState.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/GameInstanceState.kt deleted file mode 100644 index f2dd86e..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/GameInstanceState.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.mineinabyss.launchy.instance.data - -import com.mineinabyss.launchy.config.data.GameInstance -import com.mineinabyss.launchy.config.data.InstanceUserConfig -import com.mineinabyss.launchy.downloads.data.DownloadQueueState -import com.mineinabyss.launchy.downloads.data.DownloadState - -class GameInstanceState( - val instance: GameInstance, - val modpack: Modpack, - private val userConfig: InstanceUserConfig -) { - val toggles: ModTogglesState = ModTogglesState(modpack, userConfig) - val queued = DownloadQueueState(userConfig, modpack, toggles) - val downloads = DownloadState() - - fun saveToConfig() { - userConfig.copy( - fullEnabledGroups = modpack.mods.modGroups - .filter { toggles.enabledMods.containsAll(it.value) }.keys - .map { it.name }.toSet(), - userAgreedDeps = queued.userAgreedModLoaders, - toggledMods = toggles.enabledMods.mapTo(mutableSetOf()) { it.modId }, - toggledConfigs = toggles.enabledConfigs.mapTo(mutableSetOf()) { it.modId } + toggles.enabledMods.filter { it.info.forceConfigDownload } - .mapTo(mutableSetOf()) { it.info.name }, - seenGroups = modpack.mods.groups.map { it.name }.toSet(), - modDownloadInfo = queued.modDownloadInfo, -// configDownloadInfo = toggles.downloadConfigURLs.mapKeys { it.key.info.name }, - ).save(instance.userConfigFile) - } -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/Mods.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModList.kt similarity index 57% rename from src/main/kotlin/com/mineinabyss/launchy/instance/data/Mods.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModList.kt index 8627965..a87cc30 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/Mods.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModList.kt @@ -1,9 +1,11 @@ package com.mineinabyss.launchy.instance.data +import androidx.compose.runtime.Immutable import com.mineinabyss.launchy.util.GroupName import com.mineinabyss.launchy.util.ModID -data class Mods( +@Immutable +data class InstanceModList( val modGroups: Map>, ) { val groups = modGroups.keys @@ -18,13 +20,13 @@ data class Mods( fun getModById(id: ModID): Mod? = idToMod[id] fun getGroup(name: GroupName): ModGroup? = nameToGroup[name] - companion object { - const val VERSIONS_URL = "https://raw.githubusercontent.com/MineInAbyss/launchy/master/versions.yml" - - fun withSingleGroup(mods: Collection) = Mods( - modGroups = mapOf( - ModGroup("Default", forceEnabled = true) to mods.toSet() - ) - ) - } +// companion object { +// const val VERSIONS_URL = "https://raw.githubusercontent.com/MineInAbyss/launchy/master/versions.yml" +// +// fun withSingleGroup(mods: Collection) = InstanceModList( +// modGroups = mapOf( +// ModGroup("Default", forceEnabled = true) to mods.toSet() +// ) +// ) +// } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/config/data/InstanceUserConfig.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceUserConfig.kt similarity index 95% rename from src/main/kotlin/com/mineinabyss/launchy/config/data/InstanceUserConfig.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceUserConfig.kt index ce6db76..3bd94e1 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/config/data/InstanceUserConfig.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceUserConfig.kt @@ -1,8 +1,7 @@ -package com.mineinabyss.launchy.config.data +package com.mineinabyss.launchy.instance.data import com.charleskorn.kaml.decodeFromStream import com.mineinabyss.launchy.downloads.data.ModDownloader -import com.mineinabyss.launchy.instance.data.InstanceModLoaders import com.mineinabyss.launchy.util.Formats import com.mineinabyss.launchy.util.GroupName import com.mineinabyss.launchy.util.ModID diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/Launcher.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/Launcher.kt index 9ab42c7..8afde45 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/Launcher.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/Launcher.kt @@ -5,6 +5,7 @@ import com.mineinabyss.launchy.auth.ui.ProfileState import com.mineinabyss.launchy.core.ui.Dialog import com.mineinabyss.launchy.core.ui.LaunchyState import com.mineinabyss.launchy.core.ui.dialog +import com.mineinabyss.launchy.instance.ui.GameInstanceState import com.mineinabyss.launchy.util.AppDispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/ModTogglesState.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/ModTogglesState.kt deleted file mode 100644 index 90fbe38..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/ModTogglesState.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.mineinabyss.launchy.instance.data - -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import com.mineinabyss.launchy.config.data.InstanceUserConfig -import com.mineinabyss.launchy.core.ui.mutableStateSetOf -import com.mineinabyss.launchy.instance.data.ToggleMods.setModEnabled - -class ModTogglesState( - val modpack: Modpack, - val modpackConfig: InstanceUserConfig -) { - val availableMods = mutableStateSetOf().apply { - addAll(modpack.mods.mods) - } - val enabledMods = mutableStateSetOf().apply { - addAll(modpackConfig.toggledMods.mapNotNull { modpack.mods.getModById(it) }) - val defaultEnabled = modpack.mods.groups - .filter { it.enabledByDefault } - .map { it.name } - modpackConfig.seenGroups - val fullEnabled = modpackConfig.fullEnabledGroups - val forceEnabled = modpack.mods.groups.filter { it.forceEnabled }.map { it.name } - val forceDisabled = modpack.mods.groups.filter { it.forceDisabled } - val fullDisabled = modpackConfig.fullDisabledGroups - addAll(((fullEnabled + defaultEnabled + forceEnabled).toSet()) - .mapNotNull { modpack.mods.getGroup(it) } - .mapNotNull { modpack.mods.modGroups[it] }.flatten() - ) - removeAll((forceDisabled + fullDisabled).toSet().mapNotNull { modpack.mods.modGroups[it] }.flatten().toSet()) - } - - val disabledMods: Set by derivedStateOf { modpack.mods.mods - enabledMods } - - val enabledModsWithConfig by derivedStateOf { - enabledMods.filter { it.info.configUrl != "" } - } - - val enabledConfigs: MutableSet = mutableStateSetOf().apply { - addAll(modpackConfig.toggledConfigs.mapNotNull { modpack.mods.getModById(it) }) - } - - init { - // trigger update incase we have dependencies - enabledMods.forEach { setModEnabled(it, true) } - } -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/Modpack.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/Modpack.kt index ad4fa70..8bc08ae 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/Modpack.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/Modpack.kt @@ -4,6 +4,6 @@ import java.nio.file.Path class Modpack( val modLoaders: InstanceModLoaders, - val mods: Mods, + val mods: InstanceModList, val overridesPaths: List = listOf(), ) diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/ToggleMods.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/ToggleMods.kt deleted file mode 100644 index 45b899c..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/ToggleMods.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.mineinabyss.launchy.instance.data - -object ToggleMods { - fun ModTogglesState.setModEnabled(mod: Mod, enabled: Boolean) { - if (enabled) { - enabledMods += mod - enabledMods.filter { !mod.compatibleWith(it) } - .forEach { setModEnabled(it, false) } - disabledMods.filter { it.info.name in mod.info.requires }.forEach { setModEnabled(it, true) } - } else { - enabledMods -= mod - // if a mod is disabled, disable all mods that depend on it - enabledMods.filter { it.info.requires.contains(mod.info.name) }.forEach { setModEnabled(it, false) } - // if a mod is disabled, and the dependency is only used by this mod, disable the dependency too, unless it's not marked as a dependency - enabledMods.filter { dep -> - mod.info.requires.contains(dep.info.name) // if the mod depends on this dependency - && dep.info.dependency // if the dependency is marked as a dependency - && enabledMods.none { it.info.requires.contains(dep.info.name) } // and no other mod depends on this dependency -// && !versions.modGroups.filterValues { it.contains(dep) }.keys.any { it.forceEnabled } // and the group the dependency is in is not force enabled - }.forEach { setModEnabled(it, false) } - } - setModConfigEnabled(mod, enabled) - } - - fun ModTogglesState.setModConfigEnabled(mod: Mod, enabled: Boolean) { - if (mod.info.configUrl.isNotBlank() && enabled) enabledConfigs.add(mod) - else enabledConfigs.remove(mod) - } -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ExtraInfoFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ExtraInfoFormat.kt index 7a43fd6..72a29bf 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ExtraInfoFormat.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ExtraInfoFormat.kt @@ -1,8 +1,8 @@ package com.mineinabyss.launchy.instance.data.formats +import com.mineinabyss.launchy.instance.data.InstanceModList import com.mineinabyss.launchy.instance.data.Mod import com.mineinabyss.launchy.instance.data.ModGroup -import com.mineinabyss.launchy.instance.data.Mods import java.nio.file.Path @@ -10,7 +10,7 @@ data class ExtraInfoFormat( val format: PackFormat, val extraInfoPack: ExtraPackInfo, ) : PackFormat by format { - override fun toGenericMods(downloadsDir: Path): Mods { + override fun toGenericMods(downloadsDir: Path): InstanceModList { val originalMods = format.toGenericMods(downloadsDir) val foundMods = mutableSetOf() val mods: Map> = extraInfoPack.modGroups @@ -28,7 +28,8 @@ data class ExtraInfoFormat( val originalGroups = originalMods.modGroups.mapValues { it.value.filterTo(mutableSetOf()) { mod -> mod !in foundMods } } - return Mods((originalGroups + mods) + return InstanceModList( + (originalGroups + mods) .filter { (_, mods) -> mods.isNotEmpty() }) } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/LaunchyPackFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/LaunchyPackFormat.kt index 091ce34..e279bf1 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/LaunchyPackFormat.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/LaunchyPackFormat.kt @@ -12,8 +12,9 @@ data class LaunchyPackFormat( val groups: Set, private val modGroups: Map>, ) : PackFormat { - override fun toGenericMods(downloadsDir: Path): Mods { - return Mods(modGroups + override fun toGenericMods(downloadsDir: Path): InstanceModList { + return InstanceModList( + modGroups .mapKeys { (name, _) -> groups.single { it.name == name } } .mapValues { (_, mods) -> mods.map { diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ModrinthPackFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ModrinthPackFormat.kt index 75168cb..96528fd 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ModrinthPackFormat.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ModrinthPackFormat.kt @@ -1,9 +1,9 @@ package com.mineinabyss.launchy.instance.data.formats +import com.mineinabyss.launchy.instance.data.InstanceModList import com.mineinabyss.launchy.instance.data.InstanceModLoaders import com.mineinabyss.launchy.instance.data.Mod import com.mineinabyss.launchy.instance.data.ModConfig -import com.mineinabyss.launchy.instance.data.Mods import kotlinx.serialization.Serializable import java.nio.file.Path import kotlin.io.path.div @@ -47,7 +47,7 @@ data class ModrinthPackFormat( } override fun toGenericMods(downloadsDir: Path) = - Mods.withSingleGroup(files.map { it.toMod(downloadsDir) }) + InstanceModList.withSingleGroup(files.map { it.toMod(downloadsDir) }) override fun getOverridesPaths(configDir: Path): List = listOf(configDir / "mrpack" / "overrides") } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/PackFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/PackFormat.kt index 1afd084..751fd9b 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/PackFormat.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/PackFormat.kt @@ -1,11 +1,11 @@ package com.mineinabyss.launchy.instance.data.formats +import com.mineinabyss.launchy.instance.data.InstanceModList import com.mineinabyss.launchy.instance.data.InstanceModLoaders -import com.mineinabyss.launchy.instance.data.Mods import java.nio.file.Path sealed interface PackFormat { - fun toGenericMods(downloadsDir: Path): Mods + fun toGenericMods(downloadsDir: Path): InstanceModList fun getModLoaders(): InstanceModLoaders diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstallState.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstallState.kt new file mode 100644 index 0000000..b1ae3f4 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstallState.kt @@ -0,0 +1,17 @@ +package com.mineinabyss.launchy.instance.ui + +import com.mineinabyss.launchy.util.ModID + +sealed interface InstallState { + data object InProgress : InstallState + data class Queued( + val modLoaderUpdateAvailable: Boolean, + val install: List, + val update: List, + val remove: List, + val failures: List, + ) : InstallState + + data object AllInstalled : InstallState + data object Error : InstallState +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceSettingsScreen.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceSettingsScreen.kt deleted file mode 100644 index 01bb935..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceSettingsScreen.kt +++ /dev/null @@ -1,194 +0,0 @@ -package com.mineinabyss.launchy.instance.ui - -import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.VerticalScrollbar -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollbarAdapter -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.FileOpen -import androidx.compose.material.icons.rounded.Folder -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.core.ui.Constants.SETTINGS_HORIZONTAL_PADDING -import com.mineinabyss.launchy.core.ui.LocalGameInstanceState -import com.mineinabyss.launchy.core.ui.Screen -import com.mineinabyss.launchy.core.ui.components.* -import com.mineinabyss.launchy.core.ui.screen -import com.mineinabyss.launchy.downloads.data.ModDownloader.checkHashes -import com.mineinabyss.launchy.instance.data.Mod -import com.mineinabyss.launchy.instance.data.ModConfig -import com.mineinabyss.launchy.instance.data.ModGroup -import com.mineinabyss.launchy.instance.ui.components.settings.InfoBar -import com.mineinabyss.launchy.instance.ui.components.settings.ModGroup -import com.mineinabyss.launchy.instance_list.data.Instances.delete -import com.mineinabyss.launchy.instance_list.data.Instances.updateInstance -import com.mineinabyss.launchy.util.AppDispatchers -import com.mineinabyss.launchy.util.DesktopHelpers -import com.mineinabyss.launchy.util.InProgressTask -import kotlinx.coroutines.launch -import kotlin.io.path.listDirectoryEntries - -@Composable -@Preview -fun InstanceSettingsScreen() { - var selectedTabIndex by remember { mutableStateOf(0) } - ComfyWidth { - Column { - PrimaryTabRow(selectedTabIndex = selectedTabIndex, containerColor = Color.Transparent) { - Tab( - text = { Text("Manage Mods") }, - selected = selectedTabIndex == 0, - onClick = { selectedTabIndex = 0 } - ) - Tab( - text = { Text("Options") }, - selected = selectedTabIndex == 1, - onClick = { selectedTabIndex = 1 } - ) - } - Box(Modifier.fillMaxSize()) { - AnimatedTab(selectedTabIndex == 0) { - ModManagement() - } - AnimatedTab(selectedTabIndex == 1) { - OptionsTab() - } - } - } - } -} - -@Composable -fun InstanceProperties( - minecraftDir: String, - onChangeMinecraftDir: (String) -> Unit -) { - var directoryPickerShown by remember { mutableStateOf(false) } - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - DirectoryDialog( - directoryPickerShown, - title = "Choose your .minecraft directory", - fallbackTitle = "Choose a file in your .minecraft directory", - onCloseRequest = { - if (it != null) onChangeMinecraftDir(it.toString()) - directoryPickerShown = false - }, - ) - Column(Modifier.padding(start = 8.dp)) { - OutlinedTextField( - value = minecraftDir, - singleLine = true, - leadingIcon = { Icon(Icons.Rounded.Folder, contentDescription = "Directory") }, - trailingIcon = { - IconButton(onClick = { directoryPickerShown = true }) { - Icon(Icons.Rounded.FileOpen, contentDescription = "Choose") - } - }, - onValueChange = { onChangeMinecraftDir(it) }, - label = { Text(".minecraft directory") }, - modifier = Modifier.fillMaxWidth() - ) - } - } -} - -@Composable -fun OptionsTab() { - val state = LocalLaunchyState - val pack = LocalGameInstanceState - - ComfyContent(Modifier.padding(16.dp)) { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - TitleSmall("Mods") - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - OutlinedButton(onClick = { pack.instance.updateInstance(state) }) { - Text("Force update Instance") - } - OutlinedButton(onClick = { DesktopHelpers.openDirectory(pack.instance.minecraftDir) }) { - Text("Open .minecraft folder") - } - OutlinedButton(onClick = { - AppDispatchers.IO.launch { - state.runTask("checkHashes", InProgressTask("Checking hashes")) { - pack.checkHashes(pack.queued.modDownloadInfo).forEach { (modId, newInfo) -> - pack.queued.modDownloadInfo[modId] = newInfo - } - } - } - }) { - Text("Re-check hashes") - } - } - - TitleSmall("Danger zone") - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - OutlinedButton(onClick = { - screen = Screen.Default - pack.instance.delete(state, deleteDotMinecraft = false) - }) { - Text("Delete Instance from config") - } - OutlinedButton( - onClick = { - screen = Screen.Default - pack.instance.delete(state, deleteDotMinecraft = true) - }, - colors = ButtonDefaults.outlinedButtonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer, - ) - ) { - Text("Delete Instance and its .minecraft") - } - } - } - } -} - -@Composable -fun ModManagement() { - val state = LocalGameInstanceState - Scaffold( - containerColor = Color.Transparent, - bottomBar = { InfoBar() }, - ) { paddingValues -> - val userMods by remember { - mutableStateOf( - state.instance.userMods.listDirectoryEntries("*.jar").map { - Mod( - downloadDir = it, - modId = it.fileName.toString(), - info = ModConfig(name = it.fileName.toString()), - desiredHashes = null - ) - } - ) - } - Box(Modifier.padding(paddingValues)) { - Box(Modifier.padding(horizontal = SETTINGS_HORIZONTAL_PADDING)) { - val lazyListState = rememberLazyListState() - LazyColumn(Modifier.fillMaxSize().padding(end = 12.dp), lazyListState) { - item { Spacer(Modifier.height(4.dp)) } - items(state.modpack.mods.modGroups.toList()) { (group, mods) -> - ModGroup(group, mods) - } - if (userMods.isNotEmpty()) item { - ModGroup(ModGroup("User mods", forceEnabled = true), userMods) - } - } - VerticalScrollbar( - modifier = Modifier.fillMaxHeight().align(Alignment.CenterEnd).padding(vertical = 2.dp), - adapter = rememberScrollbarAdapter(lazyListState) - ) - } - } - } -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceViewModel.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceViewModel.kt new file mode 100644 index 0000000..3e05ffc --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceViewModel.kt @@ -0,0 +1,127 @@ +package com.mineinabyss.launchy.instance.ui + +import androidx.lifecycle.ViewModel +import com.mineinabyss.launchy.instance.data.GameInstanceDataSource +import com.mineinabyss.launchy.util.AppDispatchers +import com.mineinabyss.launchy.util.ModID +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.withContext +import org.to2mbn.jmccc.mcdownloader.download.Downloader + +class InstanceViewModel( + val downloader: Downloader, +) : ViewModel() { + val modsState: StateFlow get() = _modsState + val installState = MutableStateFlow(InstallState.InProgress) + + private val instance = MutableStateFlow(null) + private val _modsState = MutableStateFlow(ModListUiState.Loading) + val installQueueState = MutableStateFlow(InstallState.InProgress) + + @OptIn(ExperimentalCoroutinesApi::class) + val modList = instance.mapLatest { + withContext(AppDispatchers.IO) { + it?.loadModList() + } + } + + //TODO read + val userInstalledMods = MutableStateFlow>(emptyList()) + + val enabledMods = MutableStateFlow(listOf()) +// val instanceUIState = +// val modpack: Modpack +// val modpackConfig: InstanceUserConfig + + // trigger update incase we have dependencies +// enabledMods.forEach { setModEnabled(it, true) } + + +// val availableMods = mutableStateSetOf().apply { +// addAll(modpack.mods.mods) +// } +// val enabledMods = mutableStateSetOf().apply { +// addAll(modpackConfig.toggledMods.mapNotNull { modpack.mods.getModById(it) }) +// val defaultEnabled = modpack.mods.groups +// .filter { it.enabledByDefault } +// .map { it.name } - modpackConfig.seenGroups +// val fullEnabled = modpackConfig.fullEnabledGroups +// val forceEnabled = modpack.mods.groups.filter { it.forceEnabled }.map { it.name } +// val forceDisabled = modpack.mods.groups.filter { it.forceDisabled } +// val fullDisabled = modpackConfig.fullDisabledGroups +// addAll(((fullEnabled + defaultEnabled + forceEnabled).toSet()) +// .mapNotNull { modpack.mods.getGroup(it) } +// .mapNotNull { modpack.mods.modGroups[it] }.flatten() +// ) +// removeAll((forceDisabled + fullDisabled).toSet().mapNotNull { modpack.mods.modGroups[it] }.flatten().toSet()) +// } +// +// val disabledMods: Set by derivedStateOf { modpack.mods.mods - enabledMods } + +// val enabledModsWithConfig by derivedStateOf { +// enabledMods.filter { it.info.configUrl != "" } +// } +// +// val enabledConfigs: MutableSet = mutableStateSetOf().apply { +// addAll(modpackConfig.toggledConfigs.mapNotNull { modpack.mods.getModById(it) }) +// } + +// val queued = DownloadQueueState(userConfig, modpack, toggles) +// val downloads = DownloadState() + + fun setModState(mod: ModID, enabled: Boolean) { + if (enabled) enabledMods.value += mod + else enabledMods.value -= mod + } + + fun installMods() { + TODO() + } + + fun launch() { + TODO() + } +// fun saveToConfig() { +// userConfig.copy( +// fullEnabledGroups = modpack.mods.modGroups +// .filter { toggles.enabledMods.containsAll(it.value) }.keys +// .map { it.name }.toSet(), +// userAgreedDeps = queued.userAgreedModLoaders, +// toggledMods = toggles.enabledMods.mapTo(mutableSetOf()) { it.modId }, +// toggledConfigs = toggles.enabledConfigs.mapTo(mutableSetOf()) { it.modId } + toggles.enabledMods.filter { it.info.forceConfigDownload } +// .mapTo(mutableSetOf()) { it.info.name }, +// seenGroups = modpack.mods.groups.map { it.name }.toSet(), +// modDownloadInfo = queued.modDownloadInfo, +//// configDownloadInfo = toggles.downloadConfigURLs.mapKeys { it.key.info.name }, +// ).save(instance.userConfigFile) +// } + +// fun setModEnabled(mod: Mod, enabled: Boolean) { +// if (enabled) { +// enabledMods += mod +// enabledMods.filter { !mod.compatibleWith(it) } +// .forEach { setModEnabled(it, false) } +// disabledMods.filter { it.info.name in mod.info.requires }.forEach { setModEnabled(it, true) } +// } else { +// enabledMods -= mod +// // if a mod is disabled, disable all mods that depend on it +// enabledMods.filter { it.info.requires.contains(mod.info.name) }.forEach { setModEnabled(it, false) } +// // if a mod is disabled, and the dependency is only used by this mod, disable the dependency too, unless it's not marked as a dependency +// enabledMods.filter { dep -> +// mod.info.requires.contains(dep.info.name) // if the mod depends on this dependency +// && dep.info.dependency // if the dependency is marked as a dependency +// && enabledMods.none { it.info.requires.contains(dep.info.name) } // and no other mod depends on this dependency +//// && !versions.modGroups.filterValues { it.contains(dep) }.keys.any { it.forceEnabled } // and the group the dependency is in is not force enabled +// }.forEach { setModEnabled(it, false) } +// } +// setModConfigEnabled(mod, enabled) +// } +// +// fun setModConfigEnabled(mod: Mod, enabled: Boolean) { +// if (mod.info.configUrl.isNotBlank() && enabled) enabledConfigs.add(mod) +// else enabledConfigs.remove(mod) +// } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModGroupUiState.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModGroupUiState.kt new file mode 100644 index 0000000..52d7145 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModGroupUiState.kt @@ -0,0 +1,8 @@ +package com.mineinabyss.launchy.instance.ui + +data class ModGroupUiState( + val title: String, + val enabled: Boolean, + val forceEnabled: Boolean, + val mods: List +) diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModListInteractions.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModListInteractions.kt new file mode 100644 index 0000000..e33e2a0 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModListInteractions.kt @@ -0,0 +1,9 @@ +package com.mineinabyss.launchy.instance.ui + +import com.mineinabyss.launchy.util.ModID +import com.mineinabyss.launchy.util.Option + +class ModListInteractions( + val onToggleGroup: (Option) -> Unit, + val onToggleMod: (ModID, Boolean) -> Unit, +) diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModListUiState.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModListUiState.kt new file mode 100644 index 0000000..0172c77 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModListUiState.kt @@ -0,0 +1,9 @@ +package com.mineinabyss.launchy.instance.ui + +sealed interface ModListUiState { + object Loading : ModListUiState + data class Error(val message: String) : ModListUiState + class Loaded( + val groups: List, + ) : ModListUiState +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModUiState.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModUiState.kt new file mode 100644 index 0000000..41e8c6c --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModUiState.kt @@ -0,0 +1,43 @@ +package com.mineinabyss.launchy.instance.ui + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material.icons.rounded.Download +import androidx.compose.material.icons.rounded.Error +import androidx.compose.material.icons.rounded.Update +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.graphics.vector.ImageVector +import com.mineinabyss.launchy.util.ModID + +data class ModUiState( + val id: ModID, + val enabled: Boolean, + val queueState: ModQueueState +) + + +enum class ModQueueState { + RETRY_DOWNLOAD, + DELETE, + INSTALL, + UPDATE, + NONE; + + companion object { + fun surfaceColor(state: ModQueueState) = when (state) { + RETRY_DOWNLOAD -> MaterialTheme.colorScheme.error + DELETE -> MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.25f) + INSTALL -> MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.25f) + UPDATE -> MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.1f) + NONE -> MaterialTheme.colorScheme.surface + } + + fun infoIcon(state: ModQueueState): ImageVector? = when (state) { + RETRY_DOWNLOAD -> Icons.Rounded.Error + DELETE -> Icons.Rounded.Delete + INSTALL -> Icons.Rounded.Download + UPDATE -> Icons.Rounded.Update + NONE -> null + } + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/MainScreenImages.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/BackgroundImage.kt similarity index 61% rename from src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/MainScreenImages.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/BackgroundImage.kt index ed7ddb1..ed38f52 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/MainScreenImages.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/BackgroundImage.kt @@ -1,7 +1,6 @@ package com.mineinabyss.launchy.instance.ui.components import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image @@ -10,27 +9,21 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.window.WindowDraggableArea import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember 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.unit.dp import androidx.compose.ui.window.WindowScope -import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.core.ui.LocalGameInstanceState @Composable -fun BoxScope.BackgroundImage(windowScope: WindowScope) { - val pack = LocalGameInstanceState - val background by remember { pack.instance.config.getBackgroundAsState() } - AnimatedVisibility(background != null, enter = fadeIn(), exit = fadeOut()) { - if (background == null) return@AnimatedVisibility +fun BoxScope.BackgroundImage(painter: BitmapPainter?, windowScope: WindowScope) { + AnimatedVisibility(painter != null, enter = fadeIn(), exit = fadeOut()) { + if (painter == null) return@AnimatedVisibility windowScope.WindowDraggableArea { Image( - painter = background!!, + painter = painter!!, contentDescription = "Modpack background", contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize() @@ -74,22 +67,3 @@ fun BoxScope.SlightBackgroundTint(modifier: Modifier = Modifier) { } } -@Composable -fun LogoLarge(modifier: Modifier) { - LocalLaunchyState - val pack = LocalGameInstanceState - val painter by remember { pack.instance.config.getLogoAsState() } - 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 - ) - } -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/LogoLarge.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/LogoLarge.kt new file mode 100644 index 0000000..36fc4f1 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/LogoLarge.kt @@ -0,0 +1,34 @@ +package com.mineinabyss.launchy.instance.ui.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.widthIn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import com.mineinabyss.launchy.LocalLaunchyState +import com.mineinabyss.launchy.core.ui.LocalGameInstanceState + +@Composable +fun LogoLarge(painter: BitmapPainter?, modifier: Modifier) { + LocalLaunchyState + val pack = LocalGameInstanceState + 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 + ) + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/InstallButton.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/InstallButton.kt index 479c1e2..082fad7 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/InstallButton.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/InstallButton.kt @@ -8,14 +8,19 @@ import androidx.compose.material.icons.rounded.Download import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel import com.mineinabyss.launchy.LocalLaunchyState import com.mineinabyss.launchy.core.ui.LocalGameInstanceState import com.mineinabyss.launchy.core.ui.components.OutlinedRedButton import com.mineinabyss.launchy.core.ui.components.PrimaryButton import com.mineinabyss.launchy.downloads.data.ModDownloader.startInstall +import com.mineinabyss.launchy.instance.ui.InstallState +import com.mineinabyss.launchy.instance.ui.InstanceViewModel import com.mineinabyss.launchy.util.AppDispatchers import kotlinx.coroutines.launch @@ -34,32 +39,28 @@ fun RetryFailedButton(enabled: Boolean) { Text("Retry ${packState.queued.failures.size} failed downloads") } } + @Composable -fun InstallButton(enabled: Boolean, modifier: Modifier = Modifier) { - val state = LocalLaunchyState - val packState = LocalGameInstanceState +fun InstallButton( + modifier: Modifier = Modifier, + viewModel: InstanceViewModel = viewModel(), +) { + val state by viewModel.installState.collectAsState() PrimaryButton( - enabled = enabled, - onClick = { - AppDispatchers.profileLaunch.launch { - packState.startInstall(state, ignoreCachedCheck = true) - } - }, + enabled = state == InstallState.Queued, + onClick = { viewModel.installMods() }, modifier = modifier.width(150.dp) ) { Row(verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Rounded.Download, "Download") - val queued = packState.queued AnimatedVisibility(true, Modifier.animateContentSize()) { - val isDownloading = packState.downloads.isDownloading - InstallTextAnimatedVisibility(queued.areOperationsQueued && !isDownloading) { - Text("Install") - } - InstallTextAnimatedVisibility(!queued.areOperationsQueued && !isDownloading) { - Text("Installed") + val text = when (state) { + InstallState.Queued, InstallState.Error -> "Install" + InstallState.AllInstalled -> "Installed" + InstallState.InProgress -> "Installing" } - InstallTextAnimatedVisibility(isDownloading) { - Text("Installing") + AnimatedContent(text) { + Text(it) } } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/PlayButton.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/PlayButton.kt index 71d3257..ce9d1e7 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/PlayButton.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/PlayButton.kt @@ -16,14 +16,14 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.config.data.GameInstance import com.mineinabyss.launchy.core.ui.Dialog import com.mineinabyss.launchy.core.ui.components.PrimaryButtonColors import com.mineinabyss.launchy.core.ui.components.SecondaryButtonColors import com.mineinabyss.launchy.core.ui.dialog import com.mineinabyss.launchy.downloads.data.ModDownloader.startInstall -import com.mineinabyss.launchy.instance.data.GameInstanceState +import com.mineinabyss.launchy.instance.data.GameInstanceDataSource import com.mineinabyss.launchy.instance.data.Launcher +import com.mineinabyss.launchy.instance.ui.GameInstanceState import com.mineinabyss.launchy.instance_list.data.Instances.updateInstance import com.mineinabyss.launchy.util.AppDispatchers import com.mineinabyss.launchy.util.AppDispatchers.launchOrShowDialog @@ -33,7 +33,7 @@ import kotlinx.coroutines.launch @Composable fun PlayButton( hideText: Boolean = false, - instance: GameInstance, + instance: GameInstanceDataSource, modifier: Modifier = Modifier, getModpackState: suspend () -> GameInstanceState?, ) { diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/InfoBar.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/InfoBar.kt deleted file mode 100644 index b546a67..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/InfoBar.kt +++ /dev/null @@ -1,107 +0,0 @@ -package com.mineinabyss.launchy.instance.ui.components.settings - -import androidx.compose.animation.* -import androidx.compose.animation.core.animateIntAsState -import androidx.compose.foundation.TooltipArea -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Delete -import androidx.compose.material.icons.rounded.Download -import androidx.compose.material.icons.rounded.HistoryEdu -import androidx.compose.material.icons.rounded.Update -import androidx.compose.material3.Icon -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.core.ui.Constants -import com.mineinabyss.launchy.core.ui.Constants.SETTINGS_HORIZONTAL_PADDING -import com.mineinabyss.launchy.core.ui.LocalGameInstanceState -import com.mineinabyss.launchy.core.ui.components.Tooltip -import com.mineinabyss.launchy.instance.ui.components.buttons.InstallButton -import com.mineinabyss.launchy.instance.ui.components.buttons.RetryFailedButton - -object InfoBarProperties { - val height = 64.dp -} -@Composable -fun InfoBar(modifier: Modifier = Modifier) { - val state = LocalLaunchyState - val packState = LocalGameInstanceState - Surface( - tonalElevation = 2.dp, - shadowElevation = 0.dp, - shape = RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp), - modifier = Modifier.fillMaxWidth().height(InfoBarProperties.height).then(modifier), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(horizontal = SETTINGS_HORIZONTAL_PADDING, vertical = 6.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - InstallButton( - state.processFor(packState.instance) == null - && !packState.downloads.isDownloading - && (packState.queued.areOperationsQueued || packState.queued.userAgreedModLoaders == null) - && state.inProgressTasks.isEmpty(), - Modifier.width(Constants.SETTINGS_PRIMARY_BUTTON_WIDTH) - ) - val failures = packState.queued.failures.isNotEmpty() - AnimatedVisibility(failures) { - RetryFailedButton(failures) - } - ActionButton( - shown = packState.queued.areModLoaderUpdatesAvailable, - icon = Icons.Rounded.HistoryEdu, - desc = "Mod loader updates:\n${packState.queued.userAgreedModLoaders?.fullVersionName ?: "Not installed"} -> ${packState.modpack.modLoaders.fullVersionName}", - count = 1 - ) - ActionButton( - shown = packState.queued.areUpdatesQueued, - icon = Icons.Rounded.Update, - desc = "Queued updates", - count = packState.queued.updates.size - ) - ActionButton( - shown = packState.queued.areNewDownloadsQueued, - icon = Icons.Rounded.Download, - desc = "Queued downloads for new mods", - count = packState.queued.newDownloads.size - ) - ActionButton( - shown = packState.queued.areDeletionsQueued, - icon = Icons.Rounded.Delete, - desc = "Queued mod deletions", - count = packState.queued.deletions.size - ) - } - } -} - -@Composable -fun ActionButton(shown: Boolean, icon: ImageVector, desc: String, count: Int? = null) { - AnimatedVisibility( - shown, - enter = fadeIn() + expandHorizontally(expandFrom = Alignment.Start), - exit = fadeOut() + shrinkHorizontally(shrinkTowards = Alignment.Start) - ) { - Row { - TooltipArea(tooltip = { Tooltip(desc) }) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon(icon, desc, modifier = Modifier.padding(end = 4.dp).alignByBaseline()) - if (count != null) { - val animatedCount by animateIntAsState(targetValue = count) - Text(animatedCount.toString(), modifier = Modifier.alignByBaseline()) - } - } - } - } - } -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModGroup.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModGroup.kt index d4b0445..aa7e896 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModGroup.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModGroup.kt @@ -22,20 +22,21 @@ import androidx.compose.ui.draw.rotate import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.core.ui.LocalGameInstanceState import com.mineinabyss.launchy.core.ui.components.Tooltip -import com.mineinabyss.launchy.instance.data.Mod -import com.mineinabyss.launchy.instance.data.ModGroup -import com.mineinabyss.launchy.instance.data.ToggleMods.setModEnabled -import com.mineinabyss.launchy.util.Option +import com.mineinabyss.launchy.instance.ui.ModGroupUiState +import com.mineinabyss.launchy.instance.ui.ModListInteractions @Composable -fun ModGroup(group: ModGroup, mods: Collection) { +fun ModGroup( + group: ModGroupUiState, + interactions: ModListInteractions +) { var expanded by remember { mutableStateOf(false) } val arrowRotationState by animateFloatAsState(targetValue = if (expanded) 180f else 0f) val state = LocalGameInstanceState - val modsChanged = mods.any { - it in state.queued.deletions || it in state.queued.newDownloads || it in state.queued.failures - } +// val modsChanged = mods.any { +// it in state.queued.deletions || it in state.queued.newDownloads || it in state.queued.failures +// } val tonalElevation by animateDpAsState(if (expanded) 1.6.dp else 1.dp) Column { @@ -52,20 +53,14 @@ fun ModGroup(group: ModGroup, mods: Collection) { ) { ToggleButtons( - onSwitch = { option -> - val mods = state.modpack.mods.modGroups[group] - if (option == Option.ENABLED) - mods?.forEach { state.toggles.setModEnabled(it, true) } - else if (option == Option.DISABLED) - mods?.forEach { state.toggles.setModEnabled(it, false) } - }, + onSwitch = { option -> interactions.onToggleGroup(option) }, group = group, - mods = mods + mods = group.mods ) Spacer(Modifier.width(10.dp)) Text( - group.name, Modifier.weight(1f), + group.title, Modifier.weight(1f), style = MaterialTheme.typography.bodyLarge, ) AnimatedVisibility(modsChanged, enter = fadeIn(), exit = fadeOut()) { @@ -87,7 +82,9 @@ fun ModGroup(group: ModGroup, mods: Collection) { modifier = Modifier.fillMaxWidth().padding(top = 4.dp, bottom = 8.dp, start = 10.dp) ) { Column { - for (mod in mods) ModInfoDisplay(group, mod) + for (mod in group.mods) key(mod.id) { + ModInfoDisplay(group, mod) + } } } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModInfoDisplay.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModInfoDisplay.kt index bb382cb..7ab2c81 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModInfoDisplay.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModInfoDisplay.kt @@ -17,40 +17,28 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.scale -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.core.ui.LocalGameInstanceState import com.mineinabyss.launchy.core.ui.components.Tooltip -import com.mineinabyss.launchy.instance.data.Mod -import com.mineinabyss.launchy.instance.data.ModGroup import com.mineinabyss.launchy.instance.data.ToggleMods.setModConfigEnabled import com.mineinabyss.launchy.instance.data.ToggleMods.setModEnabled +import com.mineinabyss.launchy.instance.ui.ModGroupUiState +import com.mineinabyss.launchy.instance.ui.ModQueueState +import com.mineinabyss.launchy.instance.ui.ModUiState import com.mineinabyss.launchy.util.DesktopHelpers @OptIn(ExperimentalFoundationApi::class) @Composable -fun ModInfoDisplay(group: ModGroup, mod: Mod) { +fun ModInfoDisplay(group: ModGroupUiState, mod: ModUiState) { val state = LocalGameInstanceState val modEnabled by derivedStateOf { mod in state.toggles.enabledMods } val configEnabled by derivedStateOf { mod in state.toggles.enabledConfigs } var configExpanded by remember { mutableStateOf(false) } val configTabState by animateFloatAsState(targetValue = if (configExpanded) 180f else 0f) - val surfaceColor = when (mod) { - in state.queued.failures -> MaterialTheme.colorScheme.error - in state.queued.deletions -> MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.25f) - in state.queued.newDownloads -> MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.25f) - in state.queued.updates -> MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.1f) - else -> MaterialTheme.colorScheme.surface - } + val surfaceColor = remember(mod.queueState) { ModQueueState.surfaceColor(mod.queueState) } + val infoIcon = remember(mod.queueState) { ModQueueState.infoIcon(mod.queueState) } - val infoIcon: ImageVector? = when (mod) { - in state.queued.failures -> Icons.Rounded.Error - in state.queued.deletions -> Icons.Rounded.Delete - in state.queued.newDownloads -> Icons.Rounded.Download - in state.queued.updates -> Icons.Rounded.Update - else -> null - } Surface( modifier = Modifier.fillMaxWidth(), color = surfaceColor, diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/TripleSwitch.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/TripleSwitch.kt index 6fcb1e5..d01ef1a 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/TripleSwitch.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/TripleSwitch.kt @@ -18,15 +18,15 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.core.ui.Constants import com.mineinabyss.launchy.core.ui.LocalGameInstanceState -import com.mineinabyss.launchy.instance.data.Mod -import com.mineinabyss.launchy.instance.data.ModGroup +import com.mineinabyss.launchy.instance.ui.ModGroupUiState +import com.mineinabyss.launchy.instance.ui.ModUiState import com.mineinabyss.launchy.util.Option @Composable fun ToggleButtons( onSwitch: (Option) -> Unit, - group: ModGroup, - mods: Collection, + group: ModGroupUiState, + mods: List, ) { val state = LocalGameInstanceState val offColor = Color.Transparent diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/ActionButton.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/ActionButton.kt new file mode 100644 index 0000000..393d6c6 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/ActionButton.kt @@ -0,0 +1,36 @@ +package com.mineinabyss.launchy.instance.ui.components.settings.infobar + +import androidx.compose.animation.* +import androidx.compose.animation.core.animateIntAsState +import androidx.compose.foundation.TooltipArea +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import com.mineinabyss.launchy.core.ui.components.Tooltip + +@Composable +fun ActionButton(shown: Boolean, icon: ImageVector, desc: String, count: Int? = null) { + AnimatedVisibility( + shown, + enter = fadeIn() + expandHorizontally(expandFrom = Alignment.Start), + exit = fadeOut() + shrinkHorizontally(shrinkTowards = Alignment.Start) + ) { + Row { + TooltipArea(tooltip = { Tooltip(desc) }) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(icon, desc, modifier = Modifier.padding(end = 4.dp).alignByBaseline()) + if (count != null) { + val animatedCount by animateIntAsState(targetValue = count) + Text(animatedCount.toString(), modifier = Modifier.alignByBaseline()) + } + } + } + } + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/InfoBar.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/InfoBar.kt new file mode 100644 index 0000000..21b8641 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/InfoBar.kt @@ -0,0 +1,81 @@ +package com.mineinabyss.launchy.instance.ui.components.settings.infobar + +import androidx.compose.animation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material.icons.rounded.Download +import androidx.compose.material.icons.rounded.HistoryEdu +import androidx.compose.material.icons.rounded.Update +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.mineinabyss.launchy.core.ui.Constants +import com.mineinabyss.launchy.core.ui.Constants.SETTINGS_HORIZONTAL_PADDING +import com.mineinabyss.launchy.instance.ui.InstallState +import com.mineinabyss.launchy.instance.ui.InstanceViewModel +import com.mineinabyss.launchy.instance.ui.components.buttons.InstallButton +import com.mineinabyss.launchy.instance.ui.components.buttons.RetryFailedButton + +@Composable +fun InfoBar( + instance: InstanceViewModel = viewModel(), + modifier: Modifier = Modifier +) { + val queuedState by instance.installQueueState.collectAsState() + val queue = when (queuedState) { + is InstallState.Queued -> queuedState as InstallState.Queued + else -> return + } + + Surface( + tonalElevation = 2.dp, + shadowElevation = 0.dp, + shape = RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp), + modifier = Modifier.fillMaxWidth().height(InfoBarProperties.height).then(modifier), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(horizontal = SETTINGS_HORIZONTAL_PADDING, vertical = 6.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + InstallButton(Modifier.width(Constants.SETTINGS_PRIMARY_BUTTON_WIDTH)) + val failures = queue.failures.isNotEmpty() + AnimatedVisibility(failures) { + RetryFailedButton(failures) + } + ActionButton( + shown = queue.modLoaderUpdateAvailable, + icon = Icons.Rounded.HistoryEdu, + desc = "Mod loader updates:\n${packState.queued.userAgreedModLoaders?.fullVersionName ?: "Not installed"} -> ${packState.modpack.modLoaders.fullVersionName}", + count = 1 + ) + ActionButton( + shown = queue.update.isNotEmpty(), + icon = Icons.Rounded.Update, + desc = "Queued updates", + count = queue.update.size + ) + ActionButton( + shown = queue.install.isNotEmpty(), + icon = Icons.Rounded.Download, + desc = "Queued downloads for new mods", + count = queue.install.size + ) + ActionButton( + shown = queue.remove.isNotEmpty(), + icon = Icons.Rounded.Delete, + desc = "Queued mod deletions", + count = queue.remove.size + ) + } + } +} + diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/InfoBarProperties.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/InfoBarProperties.kt new file mode 100644 index 0000000..4bc7407 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/InfoBarProperties.kt @@ -0,0 +1,7 @@ +package com.mineinabyss.launchy.instance.ui.components.settings.infobar + +import androidx.compose.ui.unit.dp + +object InfoBarProperties { + val height = 64.dp +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceProperties.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceProperties.kt new file mode 100644 index 0000000..9530c4c --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceProperties.kt @@ -0,0 +1,53 @@ +package com.mineinabyss.launchy.instance.ui.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.FileOpen +import androidx.compose.material.icons.rounded.Folder +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.mineinabyss.launchy.core.ui.components.DirectoryDialog + +@Composable +fun InstanceProperties( + minecraftDir: String, + onChangeMinecraftDir: (String) -> Unit +) { + var directoryPickerShown by remember { mutableStateOf(false) } + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + DirectoryDialog( + directoryPickerShown, + title = "Choose your .minecraft directory", + fallbackTitle = "Choose a file in your .minecraft directory", + onCloseRequest = { + if (it != null) onChangeMinecraftDir(it.toString()) + directoryPickerShown = false + }, + ) + Column(Modifier.padding(start = 8.dp)) { + OutlinedTextField( + value = minecraftDir, + singleLine = true, + leadingIcon = { Icon(Icons.Rounded.Folder, contentDescription = "Directory") }, + trailingIcon = { + IconButton(onClick = { directoryPickerShown = true }) { + Icon(Icons.Rounded.FileOpen, contentDescription = "Choose") + } + }, + onValueChange = { onChangeMinecraftDir(it) }, + label = { Text(".minecraft directory") }, + modifier = Modifier.fillMaxWidth() + ) + } + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceScreen.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceScreen.kt similarity index 97% rename from src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceScreen.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceScreen.kt index 2bbf31d..67eff4c 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceScreen.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceScreen.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.instance.ui +package com.mineinabyss.launchy.instance.ui.screens import androidx.compose.animation.AnimatedVisibility import androidx.compose.desktop.ui.tooling.preview.Preview diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceSettingsScreen.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceSettingsScreen.kt new file mode 100644 index 0000000..42837ba --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceSettingsScreen.kt @@ -0,0 +1,45 @@ +package com.mineinabyss.launchy.instance.ui.screens + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.PrimaryTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.mineinabyss.launchy.core.ui.components.AnimatedTab +import com.mineinabyss.launchy.core.ui.components.ComfyWidth + +@Composable +@Preview +fun InstanceSettingsScreen() { + var selectedTabIndex by remember { mutableStateOf(0) } + ComfyWidth { + Column { + PrimaryTabRow(selectedTabIndex = selectedTabIndex, containerColor = Color.Transparent) { + Tab( + text = { Text("Manage Mods") }, + selected = selectedTabIndex == 0, + onClick = { selectedTabIndex = 0 } + ) + Tab( + text = { Text("Options") }, + selected = selectedTabIndex == 1, + onClick = { selectedTabIndex = 1 } + ) + } + Box(Modifier.fillMaxSize()) { + AnimatedTab(selectedTabIndex == 0) { + ModManagementTab() + } + AnimatedTab(selectedTabIndex == 1) { + OptionsTab() + } + } + } + } +} + diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/ModManagementTab.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/ModManagementTab.kt new file mode 100644 index 0000000..7b57b74 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/ModManagementTab.kt @@ -0,0 +1,94 @@ +package com.mineinabyss.launchy.instance.ui.screens + +import androidx.compose.foundation.VerticalScrollbar +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollbarAdapter +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.mineinabyss.launchy.core.ui.Constants +import com.mineinabyss.launchy.core.ui.LocalGameInstanceState +import com.mineinabyss.launchy.instance.ui.InstanceViewModel +import com.mineinabyss.launchy.instance.ui.ModGroupUiState +import com.mineinabyss.launchy.instance.ui.ModListUiState +import com.mineinabyss.launchy.instance.ui.components.settings.ModGroup +import com.mineinabyss.launchy.instance.ui.components.settings.infobar.InfoBar + +@Composable +fun ModManagementTab( + instance: InstanceViewModel = viewModel() +) { + val state = LocalGameInstanceState + Scaffold( + containerColor = Color.Transparent, + bottomBar = { InfoBar() }, + ) { paddingValues -> +// val userMods by remember { +// mutableStateOf( +// state.instance.userMods.listDirectoryEntries("*.jar").map { +// Mod( +// downloadDir = it, +// modId = it.fileName.toString(), +// info = ModConfig(name = it.fileName.toString()), +// desiredHashes = null +// ) +// } +// ) +// } + val modsState by instance.modsState.collectAsState() + val groups = when (modsState) { + is ModListUiState.Loading -> { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + return@Scaffold + } + + is ModListUiState.Error -> { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text("Error loading mods: ${(modsState as ModListUiState.Error).message}") + } + return@Scaffold + } + + is ModListUiState.Loaded -> (modsState as ModListUiState.Loaded).groups + } + Box(Modifier.padding(paddingValues)) { + Box(Modifier.padding(horizontal = Constants.SETTINGS_HORIZONTAL_PADDING)) { + val userMods by instance.userInstalledMods.collectAsState() + val lazyListState = rememberLazyListState() + LazyColumn(Modifier.fillMaxSize().padding(end = 12.dp), lazyListState) { + item { Spacer(Modifier.height(4.dp)) } + items(groups) { group -> + ModGroup(group) + } + if (userMods.isNotEmpty()) item { + ModGroup( + ModGroupUiState( + title = "User mods", + enabled = true, + forceEnabled = true, + mods = userMods, + ) + ) + } + } + VerticalScrollbar( + modifier = Modifier.fillMaxHeight().align(Alignment.CenterEnd).padding(vertical = 2.dp), + adapter = rememberScrollbarAdapter(lazyListState) + ) + } + } + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/OptionsTab.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/OptionsTab.kt new file mode 100644 index 0000000..71ac4f3 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/OptionsTab.kt @@ -0,0 +1,76 @@ +package com.mineinabyss.launchy.instance.ui.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.mineinabyss.launchy.LocalLaunchyState +import com.mineinabyss.launchy.core.ui.LocalGameInstanceState +import com.mineinabyss.launchy.core.ui.Screen +import com.mineinabyss.launchy.core.ui.components.ComfyContent +import com.mineinabyss.launchy.core.ui.components.TitleSmall +import com.mineinabyss.launchy.core.ui.screen +import com.mineinabyss.launchy.util.AppDispatchers +import com.mineinabyss.launchy.util.DesktopHelpers +import com.mineinabyss.launchy.util.InProgressTask +import kotlinx.coroutines.launch + +@Composable +fun OptionsTab() { + val state = LocalLaunchyState + val pack = LocalGameInstanceState + + ComfyContent(Modifier.padding(16.dp)) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + TitleSmall("Mods") + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + OutlinedButton(onClick = { pack.instance.updateInstance(state) }) { + Text("Force update Instance") + } + OutlinedButton(onClick = { DesktopHelpers.openDirectory(pack.instance.minecraftDir) }) { + Text("Open .minecraft folder") + } + OutlinedButton(onClick = { + AppDispatchers.IO.launch { + state.runTask("checkHashes", InProgressTask("Checking hashes")) { + pack.checkHashes(pack.queued.modDownloadInfo).forEach { (modId, newInfo) -> + pack.queued.modDownloadInfo[modId] = newInfo + } + } + } + }) { + Text("Re-check hashes") + } + } + + TitleSmall("Danger zone") + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + OutlinedButton(onClick = { + screen = Screen.Default + pack.instance.delete(state, deleteDotMinecraft = false) + }) { + Text("Delete Instance from config") + } + OutlinedButton( + onClick = { + screen = Screen.Default + pack.instance.delete(state, deleteDotMinecraft = true) + }, + colors = ButtonDefaults.outlinedButtonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer, + ) + ) { + Text("Delete Instance and its .minecraft") + } + } + } + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/NewInstance.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/NewInstance.kt index ad1c596..ec01944 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/NewInstance.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/NewInstance.kt @@ -17,16 +17,16 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.config.data.GameInstance -import com.mineinabyss.launchy.config.data.GameInstanceConfig -import com.mineinabyss.launchy.core.data.Downloader import com.mineinabyss.launchy.core.ui.Screen import com.mineinabyss.launchy.core.ui.components.AnimatedTab import com.mineinabyss.launchy.core.ui.components.ComfyContent import com.mineinabyss.launchy.core.ui.components.ComfyTitle import com.mineinabyss.launchy.core.ui.components.ComfyWidth import com.mineinabyss.launchy.core.ui.screen -import com.mineinabyss.launchy.instance.ui.InstanceProperties +import com.mineinabyss.launchy.downloads.data.Downloader +import com.mineinabyss.launchy.instance.data.GameInstanceConfig +import com.mineinabyss.launchy.instance.data.GameInstanceDataSource +import com.mineinabyss.launchy.instance.ui.screens.InstanceProperties import com.mineinabyss.launchy.instance_list.ui.components.InstanceCard import com.mineinabyss.launchy.util.AppDispatchers import com.mineinabyss.launchy.util.Dirs @@ -42,7 +42,7 @@ val validInstanceNameRegex = Regex("^[a-zA-Z0-9_ ]+$") fun NewInstance() { val state = LocalLaunchyState var selectedTabIndex by remember { mutableStateOf(0) } - var importingInstance: GameInstance.CloudInstanceWithHeaders? by remember { mutableStateOf(null) } + var importingInstance: GameInstanceDataSource.CloudInstanceWithHeaders? by remember { mutableStateOf(null) } Column { ComfyWidth { PrimaryTabRow(selectedTabIndex = selectedTabIndex) { @@ -64,7 +64,7 @@ fun NewInstance() { } @Composable -fun ImportTab(visible: Boolean, onGetInstance: (GameInstance.CloudInstanceWithHeaders) -> Unit = {}) { +fun ImportTab(visible: Boolean, onGetInstance: (GameInstanceDataSource.CloudInstanceWithHeaders) -> Unit = {}) { val state = LocalLaunchyState AnimatedTab(visible) { Column { @@ -110,7 +110,7 @@ fun ImportTab(visible: Boolean, onGetInstance: (GameInstance.CloudInstanceWithHe } is Downloader.DownloadResult.Success -> { - GameInstance.CloudInstanceWithHeaders( + GameInstanceDataSource.CloudInstanceWithHeaders( config = GameInstanceConfig.read(downloadPath) .showDialogOnError("Failed to read cloud instance") .getOrThrow(), @@ -138,7 +138,7 @@ fun ImportTab(visible: Boolean, onGetInstance: (GameInstance.CloudInstanceWithHe } @Composable -fun ConfirmImportTab(visible: Boolean, cloudInstance: GameInstance.CloudInstanceWithHeaders?) { +fun ConfirmImportTab(visible: Boolean, cloudInstance: GameInstanceDataSource.CloudInstanceWithHeaders?) { if (cloudInstance == null) return val state = LocalLaunchyState AnimatedTab(visible) { @@ -190,7 +190,7 @@ fun ConfirmImportTab(visible: Boolean, cloudInstance: GameInstance.CloudInstance name = nameText, overrideMinecraftDir = minecraftDir.takeIf { it?.isNotEmpty() == true } ) - GameInstance.createCloudInstance( + GameInstanceDataSource.createCloudInstance( state, cloudInstance.copy(config = editedConfig) ) screen = Screen.Default diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/Instances.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/Instances.kt index 8723fed..42a99ca 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/Instances.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/Instances.kt @@ -1,11 +1,11 @@ package com.mineinabyss.launchy.instance_list.data -import com.mineinabyss.launchy.config.data.GameInstance -import com.mineinabyss.launchy.config.data.GameInstanceConfig -import com.mineinabyss.launchy.core.data.Downloader import com.mineinabyss.launchy.core.ui.LaunchyState import com.mineinabyss.launchy.core.ui.Screen import com.mineinabyss.launchy.core.ui.screen +import com.mineinabyss.launchy.downloads.data.Downloader +import com.mineinabyss.launchy.instance.data.GameInstanceConfig +import com.mineinabyss.launchy.instance.data.GameInstanceDataSource import com.mineinabyss.launchy.util.AppDispatchers import com.mineinabyss.launchy.util.Dirs import com.mineinabyss.launchy.util.InProgressTask @@ -15,7 +15,7 @@ import kotlin.io.path.* object Instances { @OptIn(ExperimentalPathApi::class) - fun GameInstance.delete(state: LaunchyState, deleteDotMinecraft: Boolean) { + fun GameInstanceDataSource.delete(state: LaunchyState, deleteDotMinecraft: Boolean) { state.gameInstances.remove(this) state.runTask("deleteInstance", InProgressTask("Deleting instance ${config.name}")) { AppDispatchers.IO.launch { @@ -25,7 +25,7 @@ object Instances { } } - fun GameInstance.updateInstance( + fun GameInstanceDataSource.updateInstance( state: LaunchyState, onSuccess: () -> Unit = {}, ) { diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/LocalInstancesDataSource.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/LocalInstancesDataSource.kt new file mode 100644 index 0000000..f689ac7 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/LocalInstancesDataSource.kt @@ -0,0 +1,19 @@ +package com.mineinabyss.launchy.instance_list.data + +import com.mineinabyss.launchy.instance.data.GameInstanceDataSource +import java.nio.file.Path +import kotlin.io.path.isDirectory +import kotlin.io.path.listDirectoryEntries + +class LocalInstancesDataSource( + val rootDir: Path, +) { + fun getInstanceList(): List = rootDir + .listDirectoryEntries() + .filter { it.isDirectory() } + .mapNotNull { + runCatching { GameInstanceDataSource(it) } + .onFailure { it.printStackTrace() } + .getOrNull() + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListViewModel.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListViewModel.kt index d3fdf85..c478b32 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListViewModel.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListViewModel.kt @@ -1,10 +1,18 @@ package com.mineinabyss.launchy.instance_list.ui import androidx.lifecycle.ViewModel -import com.mineinabyss.launchy.config.data.GameInstance +import androidx.lifecycle.viewModelScope +import com.mineinabyss.launchy.instance.data.GameInstanceDataSource import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch class InstanceListViewModel : ViewModel() { - val gameInstances = MutableStateFlow(listOf()) + init { + viewModelScope.launch { + + } + } + + val gameInstances = MutableStateFlow(listOf()) val lastPlayed = MutableStateFlow(mapOf()) } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceCard.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceCard.kt index 78c7264..e918154 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceCard.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceCard.kt @@ -22,13 +22,13 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.config.data.GameInstance -import com.mineinabyss.launchy.config.data.GameInstanceConfig import com.mineinabyss.launchy.core.ui.Screen import com.mineinabyss.launchy.core.ui.components.Tooltip import com.mineinabyss.launchy.core.ui.screen import com.mineinabyss.launchy.core.ui.theme.LaunchyColors import com.mineinabyss.launchy.core.ui.theme.currentHue +import com.mineinabyss.launchy.instance.data.GameInstanceConfig +import com.mineinabyss.launchy.instance.data.GameInstanceDataSource import com.mineinabyss.launchy.instance.ui.components.SlightBackgroundTint import com.mineinabyss.launchy.instance.ui.components.buttons.PlayButton import com.mineinabyss.launchy.instance_list.ui.components.InstanceCardStyle.cardHeight @@ -46,7 +46,7 @@ object InstanceCardStyle { @Composable fun InstanceCard( config: GameInstanceConfig, - instance: GameInstance? = null, + instance: GameInstanceDataSource? = null, modifier: Modifier = Modifier ) = MaterialTheme( colorScheme = LaunchyColors(config.hue).DarkColors diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceList.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceList.kt index 70ab9fe..5cead55 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceList.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceList.kt @@ -11,10 +11,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.config.data.GameInstance +import com.mineinabyss.launchy.instance.data.GameInstanceDataSource @Composable -fun InstanceList(title: String, packs: List) { +fun InstanceList(title: String, packs: List) { val state = LocalLaunchyState Column { // var showAll by remember { mutableStateOf(false) } diff --git a/src/main/kotlin/com/mineinabyss/launchy/updater/data/GithubUpdateChecker.kt b/src/main/kotlin/com/mineinabyss/launchy/updater/data/GithubUpdateChecker.kt index 48b516e..dd27cfd 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/updater/data/GithubUpdateChecker.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/updater/data/GithubUpdateChecker.kt @@ -1,7 +1,7 @@ package com.mineinabyss.launchy.updater.data -import com.mineinabyss.launchy.core.data.Downloader import com.mineinabyss.launchy.core.ui.Constants +import com.mineinabyss.launchy.downloads.data.Downloader import com.mineinabyss.launchy.util.Formats import io.ktor.client.request.* import io.ktor.client.statement.* diff --git a/src/main/kotlin/com/mineinabyss/launchy/util/AppDispatchers.kt b/src/main/kotlin/com/mineinabyss/launchy/util/AppDispatchers.kt index 0ca6332..be00afa 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/util/AppDispatchers.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/AppDispatchers.kt @@ -7,15 +7,11 @@ import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext object AppDispatchers { - @OptIn(ExperimentalCoroutinesApi::class) - val IOContext = Dispatchers.IO.limitedParallelism(10) - /** IO Dispatcher that won't get cancelled when a composable goes off screen. */ - val IO = CoroutineScope(IOContext) + val IO = Dispatchers.IO @OptIn(ExperimentalCoroutinesApi::class) - val profileLaunch = CoroutineScope(IOContext.limitedParallelism(1)) - + val profileLaunch = CoroutineScope(IO.limitedParallelism(1)) fun CoroutineScope.launchOrShowDialog( context: CoroutineContext = EmptyCoroutineContext, diff --git a/src/main/kotlin/com/mineinabyss/launchy/util/Dirs.kt b/src/main/kotlin/com/mineinabyss/launchy/util/Dirs.kt index e1fdb7e..58ad3fc 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/util/Dirs.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/Dirs.kt @@ -1,6 +1,6 @@ package com.mineinabyss.launchy.util -import com.mineinabyss.launchy.config.data.GameInstance +import com.mineinabyss.launchy.instance.data.GameInstanceDataSource import java.util.* import kotlin.io.path.* @@ -25,7 +25,7 @@ object Dirs { OS.LINUX -> home / ".config" } / "mineinabyss" - fun cacheDir(instance: GameInstance) = instance.configDir / "cache" + fun cacheDir(instance: GameInstanceDataSource) = instance.configDir / "cache" val imageCache = config / "cache" / "images" diff --git a/src/main/kotlin/com/mineinabyss/launchy/util/UpdateResult.kt b/src/main/kotlin/com/mineinabyss/launchy/util/UpdateResult.kt index 2226601..3bb1fd6 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/util/UpdateResult.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/UpdateResult.kt @@ -1,6 +1,6 @@ package com.mineinabyss.launchy.util -import com.mineinabyss.launchy.core.data.Downloader +import com.mineinabyss.launchy.downloads.data.Downloader sealed interface UpdateResult { val headers: Downloader.ModifyHeaders From 081a1690a4c59cb9f5c80630acbe3f0d95391f40 Mon Sep 17 00:00:00 2001 From: Danielle Voznyy Date: Sat, 1 Jun 2024 16:20:59 -0400 Subject: [PATCH 3/5] refactor: Continue architecture update, start migrating to koin and viewmodels --- build.gradle.kts | 7 + gradle/libs.versions.toml | 7 + .../kotlin/com/mineinabyss/launchy/Main.kt | 72 +++---- .../com/mineinabyss/launchy/auth/data/Auth.kt | 100 --------- .../launchy/auth/data/PlayerProfile.kt | 47 ----- .../launchy/auth/data/ProfileModel.kt | 11 + .../launchy/auth/data/ProfileRepository.kt | 45 +++++ .../launchy/auth/data/SessionStorage.kt | 50 ----- .../auth/data/identity/IdentityDataSource.kt | 81 ++++++++ .../auth/data/identity/SessionStorage.kt | 11 + .../mineinabyss/launchy/auth/ui/AuthDialog.kt | 7 +- .../launchy/auth/ui/ProfileState.kt | 17 -- .../launchy/auth/ui/ProfileUiState.kt | 8 + .../launchy/auth/ui/ProfileViewModel.kt | 73 +++++++ .../auth/ui/components/AccountsPopup.kt | 16 +- .../auth/ui/components/PlayerAvatar.kt | 37 ++++ .../mineinabyss/launchy/config/data/Config.kt | 4 +- .../launchy/config/data/ConfigModule.kt | 7 + .../launchy/core/data/FileSystemDataSource.kt | 26 +++ .../launchy/core/data/TasksRepository.kt | 28 +++ .../mineinabyss/launchy/core/di/CoreModule.kt | 10 + .../com/mineinabyss/launchy/core/ui/Dialog.kt | 7 +- .../launchy/core/ui/LaunchyState.kt | 69 ------- .../launchy/core/ui/LaunchyUiState.kt | 8 + .../launchy/core/ui/LaunchyViewModel.kt | 77 +++++++ .../com/mineinabyss/launchy/core/ui/Screen.kt | 20 -- .../core/ui/{UIState.kt => UiState.kt} | 6 +- .../core/ui/components/ComfyContent.kt | 2 +- .../ui/components/InProgressTasksIndicator.kt | 60 ++++++ .../launchy/core/ui/components/LeftSidebar.kt | 44 ++-- .../core/ui/components/PlayerAvatar.kt | 22 -- .../core/ui/dialogs/SelectJVMDialog.kt | 6 +- .../launchy/core/ui/screens/Screen.kt | 20 ++ .../launchy/core/ui/{ => screens}/Screens.kt | 90 +++------ .../downloads/data/DownloadQueueState.kt | 2 +- .../launchy/downloads/data/Downloader.kt | 14 +- .../launchy/downloads/data/ModDownloader.kt | 14 +- .../launchy/instance/data/DownloadInfo.kt | 34 ++++ .../instance/data/GameInstanceConfig.kt | 49 ----- .../instance/data/GameInstanceDataSource.kt | 189 +++++++----------- .../launchy/instance/data/HashCheck.kt | 5 + .../launchy/instance/data/InstanceModel.kt | 35 ++++ .../mineinabyss/launchy/instance/data/Mod.kt | 1 + .../instance/data/formats/ExtraPackInfo.kt | 2 +- .../data/formats/LaunchyPackFormat.kt | 6 +- .../data/formats/ModrinthPackFormat.kt | 2 +- .../instance/data/storage/InstanceConfig.kt | 29 +++ .../data/{ => storage}/InstanceUserConfig.kt | 35 +--- .../instance/data/{ => storage}/ModConfig.kt | 5 +- .../data/usermods/UserModsDataSource.kt | 8 + .../launchy/instance/ui/InstanceUiState.kt | 12 ++ .../launchy/instance/ui/InstanceViewModel.kt | 34 +++- .../launchy/instance/ui/ModGroupUiState.kt | 5 +- ...ListInteractions.kt => ModInteractions.kt} | 8 +- .../launchy/instance/ui/ModUiState.kt | 10 +- .../ui/components/ImportSettingsDialog.kt | 4 +- .../instance/ui/components/LogoLarge.kt | 6 +- .../ui/components/buttons/AuthButton.kt | 4 +- .../ui/components/buttons/PlayButton.kt | 66 +----- .../ui/components/buttons/SettingsButton.kt | 4 +- .../ui/components/buttons/UpdateButton.kt | 2 +- .../components/settings/ModConfigOptions.kt | 41 ++++ .../ui/components/settings/ModGroup.kt | 23 ++- .../ui/components/settings/ModInfoDisplay.kt | 91 +++------ .../ui/components/settings/infobar/InfoBar.kt | 2 +- .../instance/ui/screens/InstanceScreen.kt | 17 +- .../instance/ui/screens/ModManagementTab.kt | 33 +-- .../launchy/instance/ui/screens/OptionsTab.kt | 4 +- .../instance_creation/ui/NewInstance.kt | 8 +- .../instance_list/data/InstanceRepository.kt | 66 ++++++ .../launchy/instance_list/data/Instances.kt | 72 ------- .../data/LocalInstancesDataSource.kt | 51 ++++- .../data/RemoteInstanceDataSource.kt | 30 +++ .../ui/components/AddNewModpackCard.kt | 4 +- .../ui/components/InstanceCard.kt | 8 +- .../{instance => launcher}/data/Launcher.kt | 15 +- .../launcher/data/ProcessRepository.kt | 13 ++ .../launchy/launcher/ui/LauncherViewModel.kt | 59 ++++++ .../ui/JVMSettingsViewModel.kt} | 7 +- .../launchy/settings/ui/SettingsScreen.kt | 41 ++-- .../launchy/util/AppDispatchers.kt | 2 +- .../com/mineinabyss/launchy/util/Helpers.kt | 7 +- .../mineinabyss/launchy/util/KoinViewModel.kt | 12 ++ .../com/mineinabyss/launchy/util/Tasks.kt | 6 - .../mineinabyss/launchy/util/Typealiases.kt | 6 + 85 files changed, 1294 insertions(+), 1004 deletions(-) delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/auth/data/Auth.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/auth/data/PlayerProfile.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/auth/data/ProfileModel.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/auth/data/ProfileRepository.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/auth/data/SessionStorage.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/auth/data/identity/IdentityDataSource.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/auth/data/identity/SessionStorage.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/auth/ui/ProfileState.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/auth/ui/ProfileUiState.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/auth/ui/ProfileViewModel.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/auth/ui/components/PlayerAvatar.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/config/data/ConfigModule.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/core/data/FileSystemDataSource.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/core/data/TasksRepository.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/core/di/CoreModule.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyState.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyUiState.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyViewModel.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/core/ui/Screen.kt rename src/main/kotlin/com/mineinabyss/launchy/core/ui/{UIState.kt => UiState.kt} (65%) create mode 100644 src/main/kotlin/com/mineinabyss/launchy/core/ui/components/InProgressTasksIndicator.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/core/ui/components/PlayerAvatar.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/core/ui/screens/Screen.kt rename src/main/kotlin/com/mineinabyss/launchy/core/ui/{ => screens}/Screens.kt (64%) create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/data/DownloadInfo.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/data/GameInstanceConfig.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/data/HashCheck.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModel.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/data/storage/InstanceConfig.kt rename src/main/kotlin/com/mineinabyss/launchy/instance/data/{ => storage}/InstanceUserConfig.kt (58%) rename src/main/kotlin/com/mineinabyss/launchy/instance/data/{ => storage}/ModConfig.kt (82%) create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/data/usermods/UserModsDataSource.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceUiState.kt rename src/main/kotlin/com/mineinabyss/launchy/instance/ui/{ModListInteractions.kt => ModInteractions.kt} (53%) create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModConfigOptions.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance_list/data/InstanceRepository.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance_list/data/Instances.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance_list/data/RemoteInstanceDataSource.kt rename src/main/kotlin/com/mineinabyss/launchy/{instance => launcher}/data/Launcher.kt (91%) create mode 100644 src/main/kotlin/com/mineinabyss/launchy/launcher/data/ProcessRepository.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/launcher/ui/LauncherViewModel.kt rename src/main/kotlin/com/mineinabyss/launchy/{core/ui/JvmState.kt => settings/ui/JVMSettingsViewModel.kt} (90%) create mode 100644 src/main/kotlin/com/mineinabyss/launchy/util/KoinViewModel.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/util/Tasks.kt diff --git a/build.gradle.kts b/build.gradle.kts index 9e50732..6ae2923 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -41,6 +41,13 @@ dependencies { implementation(libs.minecraftAuth) implementation(libs.jmccc.mcdownloader) implementation(libs.jmccc) + + implementation(libs.multiplatform.settings) +// implementation(libs.multiplatform.settings.no.arg) + implementation(libs.multiplatform.settings.serialization) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.serialization.json) + implementation("io.github.irgaly.kfswatch:kfswatch:1.0.0") } idofront { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 34f156e..a2c97d7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ minecraftAuth = "4.0.2" mpfilepicker = "3.1.0" lifecycleViewmodelCompose = "2.8.0-beta02" koinCompose = "3.6.0-wasm-alpha2" +multiplatformSettings = "1.1.1" [libraries] jarchivelib = { module = "org.rauschig:jarchivelib", version.ref = "jarchivelib" } @@ -14,7 +15,13 @@ jmccc-mcdownloader = { module = "dev.3-3:jmccc-mcdownloader", version.ref = "jmc ktor-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } ktor-cio-jvm = { module = "io.ktor:ktor-client-cio-jvm", version.ref = "ktor" } ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } minecraftAuth = { module = "net.raphimc:MinecraftAuth", version.ref = "minecraftAuth" } mpfilepicker = { module = "com.darkrockstudios:mpfilepicker", version.ref = "mpfilepicker" } lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" } koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koinCompose" } +multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatformSettings" } +multiplatform-settings-no-arg = { module = "com.russhwolf:multiplatform-settings-no-arg", version.ref = "multiplatformSettings" } +multiplatform-settings-serialization = { module = "com.russhwolf:multiplatform-settings-serialization", version.ref = "multiplatformSettings" } + diff --git a/src/main/kotlin/com/mineinabyss/launchy/Main.kt b/src/main/kotlin/com/mineinabyss/launchy/Main.kt index c970050..449c381 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/Main.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/Main.kt @@ -1,13 +1,17 @@ package com.mineinabyss.launchy +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -15,35 +19,28 @@ import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState -import com.mineinabyss.launchy.config.data.Config -import com.mineinabyss.launchy.core.ui.LaunchyState -import com.mineinabyss.launchy.core.ui.Screens -import com.mineinabyss.launchy.core.ui.TopBarProvider -import com.mineinabyss.launchy.core.ui.TopBarState +import com.mineinabyss.launchy.config.data.configModule +import com.mineinabyss.launchy.core.di.coreModule +import com.mineinabyss.launchy.core.ui.* +import com.mineinabyss.launchy.core.ui.screens.Screens import com.mineinabyss.launchy.core.ui.theme.AppTheme -import com.mineinabyss.launchy.instance.data.GameInstanceDataSource -import com.mineinabyss.launchy.util.Dirs +import com.mineinabyss.launchy.util.koinViewModel +import org.koin.compose.KoinApplication import java.awt.Dimension -private val LaunchyStateProvider = compositionLocalOf { error("No local versions provided") } - -val LocalLaunchyState: LaunchyState - @Composable get() = LaunchyStateProvider.current - -fun main() { - application { +fun main() = application { + KoinApplication(application = { + modules( + coreModule(), + configModule(), + ) + }) { val windowState = rememberWindowState(placement = WindowPlacement.Floating) val icon = painterResource("icon.png") - val launchyState by produceState(null) { - Dirs.createDirs() - Dirs.createConfigFiles() - val config = Config.read().getOrElse { Config() } - val instances = GameInstanceDataSource.readAll(Dirs.modpackConfigsDir) - value = LaunchyState(config, instances) - } + val viewModel = koinViewModel() val onClose: () -> Unit = { exitApplication() - launchyState?.saveToConfig() + viewModel.saveToConfig() } Window( @@ -55,21 +52,28 @@ fun main() { ) { window.minimumSize = Dimension(600, 400) val topBarState = remember { TopBarState(onClose, windowState, this) } - val ready = launchyState != null + val uiState by viewModel.uiState.collectAsState() AppTheme { CompositionLocalProvider(TopBarProvider provides topBarState) { Scaffold { - AnimatedVisibility(!ready, exit = fadeOut()) { - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("Reading launchy config...") + AnimatedContent(uiState) { + when (val state = uiState) { + LaunchyUiState.Loading -> + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + + is LaunchyUiState.Ready -> + CompositionLocalProvider( + LocalUiState provides state.ui + ) { + Screens() + } } } - AnimatedVisibility(ready, enter = fadeIn()) { - CompositionLocalProvider( - LaunchyStateProvider provides launchyState!!, - ) { - Screens() - } + AnimatedVisibility(uiState is LaunchyUiState.Loading, exit = fadeOut()) { + } + AnimatedVisibility(uiState is LaunchyUiState.Ready, enter = fadeIn()) { } } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/auth/data/Auth.kt b/src/main/kotlin/com/mineinabyss/launchy/auth/data/Auth.kt deleted file mode 100644 index e22ac2e..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/auth/data/Auth.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.mineinabyss.launchy.auth.data - -import com.mineinabyss.launchy.auth.ui.ProfileState -import com.mineinabyss.launchy.core.ui.Dialog -import com.mineinabyss.launchy.core.ui.LaunchyState -import com.mineinabyss.launchy.core.ui.dialog -import com.mineinabyss.launchy.util.DesktopHelpers -import com.mineinabyss.launchy.util.InProgressTask -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import net.raphimc.minecraftauth.MinecraftAuth -import net.raphimc.minecraftauth.step.java.session.StepFullJavaSession.FullJavaSession -import net.raphimc.minecraftauth.step.msa.StepMsaDeviceCode.MsaDeviceCode -import net.raphimc.minecraftauth.step.msa.StepMsaDeviceCode.MsaDeviceCodeCallback -import java.util.* - - -object Auth { - val JAVA_DEVICE_CODE_LOGIN = MinecraftAuth.ALT_JAVA_DEVICE_CODE_LOGIN - //TODO override with our own oauth app -// .builder() -// .withClientId("00000000402b5328") -// .withScope("service::user.auth.xboxlive.com::MBI_SSL") -// .deviceCode() -// .withDeviceToken("Win32") -// .sisuTitleAuthentication("rp://api.minecraftservices.com/") -// .buildMinecraftJavaProfileStep(true) - - suspend fun authOrShowDialog( - state: LaunchyState, - profile: ProfileState, - onAuthenticate: (FullJavaSession) -> Unit = {}, - ) = coroutineScope { - launch(Dispatchers.IO) { - if (profile.currentProfile == null) dialog = Dialog.Auth - state.runTask("auth", InProgressTask("Authenticating")) { - authFlow( - profile, - onVerificationRequired = { - state.inProgressTasks.remove("auth") - DesktopHelpers.browse(it.redirectTo) - profile.authCode = it.code - dialog = Dialog.Auth - println(profile.authCode) - }, - onAuthenticate = { - launch(Dispatchers.IO) { - SessionStorage.save(it) - } - profile.currentSession = it - profile.currentProfile = PlayerProfile(it.mcProfile.name, it.mcProfile.id) - dialog = Dialog.None - onAuthenticate(it) - } - ) - } - } - } - - class VerificationRequired( - val code: String, - val redirectTo: String, - ) - - private fun authFlow( - state: ProfileState, - onVerificationRequired: (VerificationRequired) -> Unit, - onAuthenticate: (FullJavaSession) -> Unit, - ) { - MinecraftAuth.builder() - val httpClient = MinecraftAuth.createHttpClient() - val previousSession = state.currentProfile?.let { SessionStorage.load(it.uuid) } - if (previousSession != null) { - println("refreshing token") - val refreshedSession = JAVA_DEVICE_CODE_LOGIN.refresh(httpClient, previousSession) - onAuthenticate(refreshedSession) - return - } - val javaSession = JAVA_DEVICE_CODE_LOGIN.getFromInput( - httpClient, - MsaDeviceCodeCallback { msaDeviceCode: MsaDeviceCode -> - onVerificationRequired( - VerificationRequired( - msaDeviceCode.userCode, - msaDeviceCode.directVerificationUri - ) - ) - }) - onAuthenticate(javaSession) - } - - fun ProfileState.logout(uuid: UUID) { - SessionStorage.deleteIfExists(uuid) - if (currentProfile?.uuid == uuid) { - currentProfile = null - currentSession = null - } - } -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/auth/data/PlayerProfile.kt b/src/main/kotlin/com/mineinabyss/launchy/auth/data/PlayerProfile.kt deleted file mode 100644 index eeeea28..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/auth/data/PlayerProfile.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.mineinabyss.launchy.auth.data - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.graphics.FilterQuality -import androidx.compose.ui.graphics.painter.BitmapPainter -import androidx.compose.ui.res.loadImageBitmap -import com.mineinabyss.launchy.downloads.data.Downloader -import com.mineinabyss.launchy.util.Dirs -import com.mineinabyss.launchy.util.serializers.UUIDSerializer -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.launch -import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient -import java.util.* -import kotlin.io.path.inputStream - -@Serializable -data class PlayerProfile( - val name: String, - val uuid: @Serializable(with = UUIDSerializer::class) UUID, -) { - @Transient - private val avatar = mutableStateOf(null) - - @OptIn(ExperimentalCoroutinesApi::class) - @Transient - private val downloadScope = CoroutineScope(Dispatchers.IO.limitedParallelism(1)) - - @Composable - fun getAvatar(): MutableState = remember { - avatar.also { - if (it.value != null) return@also - downloadScope.launch { - Downloader.downloadAvatar(uuid, Downloader.Options(overwrite = false)) - it.value = BitmapPainter( - loadImageBitmap(Dirs.avatar(uuid).inputStream()), - filterQuality = FilterQuality.None - ) - } - } - } -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/auth/data/ProfileModel.kt b/src/main/kotlin/com/mineinabyss/launchy/auth/data/ProfileModel.kt new file mode 100644 index 0000000..e65b2ae --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/auth/data/ProfileModel.kt @@ -0,0 +1,11 @@ +package com.mineinabyss.launchy.auth.data + +import com.mineinabyss.launchy.util.serializers.UUIDSerializer +import kotlinx.serialization.Serializable +import java.util.* + +@Serializable +data class ProfileModel( + val name: String, + val uuid: @Serializable(with = UUIDSerializer::class) UUID, +) diff --git a/src/main/kotlin/com/mineinabyss/launchy/auth/data/ProfileRepository.kt b/src/main/kotlin/com/mineinabyss/launchy/auth/data/ProfileRepository.kt new file mode 100644 index 0000000..b58f9df --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/auth/data/ProfileRepository.kt @@ -0,0 +1,45 @@ +package com.mineinabyss.launchy.auth.data + +import com.mineinabyss.launchy.auth.data.identity.IdentityDataSource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.withContext +import net.raphimc.minecraftauth.step.java.session.StepFullJavaSession.FullJavaSession + +class ProfileRepository( + val identity: IdentityDataSource, +) { + private val _authRequest = MutableStateFlow(null) + + private val _currentSession = MutableStateFlow(null) + private val _currentProfile = MutableStateFlow(null) + + val authRequest = _authRequest.asStateFlow() + val currentProfile = _currentProfile.asStateFlow() + + fun useProfile(config: ProfileModel) { + _currentProfile.update { config } + _currentSession.update { null } + } + + fun logout() { + val uuid = _currentProfile.value?.uuid ?: return + identity.forgetSession(uuid) + _currentProfile.update { null } + _currentSession.update { null } + } + + suspend fun authenticateCurrentProfile() = withContext(Dispatchers.IO) { + val profile = _currentProfile.value + val session = identity.authFlow( + profile, + onVerificationRequired = { verification -> + _authRequest.update { verification } + } + ) + _currentProfile.update { ProfileModel(session.mcProfile.name, session.mcProfile.id) } + _currentSession.update { session } + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/auth/data/SessionStorage.kt b/src/main/kotlin/com/mineinabyss/launchy/auth/data/SessionStorage.kt deleted file mode 100644 index a794fcb..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/auth/data/SessionStorage.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.mineinabyss.launchy.auth.data - -import com.google.gson.GsonBuilder -import com.google.gson.JsonParser -import com.mineinabyss.launchy.util.Dirs -import kotlinx.serialization.Serializable -import net.raphimc.minecraftauth.step.java.session.StepFullJavaSession.FullJavaSession -import java.util.* -import kotlin.io.path.* - -@Serializable -data class SessionStorage( - val microsoftAccessToken: String, - val microsoftRefreshToken: String, - val minecraftAccessToken: String, - val xboxUserId: String, -) { - companion object { - val gson = GsonBuilder().setPrettyPrinting().create() - - fun load(uuid: UUID): FullJavaSession? { - val targetFile = (Dirs.accounts / "$uuid.json") - if (!targetFile.exists()) return null - return runCatching { - Auth.JAVA_DEVICE_CODE_LOGIN.fromJson( - JsonParser.parseString( - targetFile.readText() - ).asJsonObject - ) - }.onFailure { - println("Failed to load session for $uuid, ignoring file") - it.printStackTrace() - }.getOrNull() - } - - fun deleteIfExists(uuid: UUID) { - val targetFile = (Dirs.accounts / "$uuid.json") - targetFile.deleteIfExists() - } - - fun save(session: FullJavaSession) { - val json = Auth.JAVA_DEVICE_CODE_LOGIN.toJson(session) - val targetFile = (Dirs.accounts / "${session.mcProfile.id}.json").createParentDirectories() - targetFile.deleteIfExists() - targetFile.createFile() - targetFile.writeText(gson.toJson(json)) - - } - } -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/auth/data/identity/IdentityDataSource.kt b/src/main/kotlin/com/mineinabyss/launchy/auth/data/identity/IdentityDataSource.kt new file mode 100644 index 0000000..b1afa2a --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/auth/data/identity/IdentityDataSource.kt @@ -0,0 +1,81 @@ +package com.mineinabyss.launchy.auth.data.identity + +import com.google.gson.GsonBuilder +import com.google.gson.JsonParser +import com.mineinabyss.launchy.auth.data.ProfileModel +import com.mineinabyss.launchy.core.data.TasksRepository +import com.russhwolf.settings.Settings +import com.russhwolf.settings.set +import net.raphimc.minecraftauth.MinecraftAuth +import net.raphimc.minecraftauth.step.java.session.StepFullJavaSession.FullJavaSession +import net.raphimc.minecraftauth.step.msa.StepMsaDeviceCode.MsaDeviceCode +import net.raphimc.minecraftauth.step.msa.StepMsaDeviceCode.MsaDeviceCodeCallback +import java.util.* + + +class IdentityDataSource( + val settings: Settings, + val tasks: TasksRepository, +) { + private val httpClient = MinecraftAuth.createHttpClient() + private val deviceCodeLogin = MinecraftAuth.ALT_JAVA_DEVICE_CODE_LOGIN + //TODO override with our own oauth app +// .builder() +// .withClientId("00000000402b5328") +// .withScope("service::user.auth.xboxlive.com::MBI_SSL") +// .deviceCode() +// .withDeviceToken("Win32") +// .sisuTitleAuthentication("rp://api.minecraftservices.com/") +// .buildMinecraftJavaProfileStep(true) + + val gson = GsonBuilder().setPrettyPrinting().create() + + fun load(uuid: UUID): Result { + val saved = settings.getStringOrNull("session-$uuid") ?: return Result.success(null) + return runCatching { + deviceCodeLogin.fromJson(JsonParser.parseString(saved).asJsonObject) + } + } + + fun forgetSession(uuid: UUID) { + settings.remove("session-$uuid") + } + + fun save(session: FullJavaSession) { + val json = deviceCodeLogin.toJson(session) + settings["session-${session.mcProfile.id}"] = gson.toJson(json) + } + + class VerificationRequired( + val code: String, + val redirectTo: String, + ) + + fun authFlow( + profile: ProfileModel?, + onVerificationRequired: (VerificationRequired) -> Unit, + ): FullJavaSession { + // Attempt existing session refresh + val previousSession = profile?.uuid?.let { load(it) }?.getOrNull() + if (previousSession != null) { + println("Refreshing token") + runCatching { deviceCodeLogin.refresh(httpClient, previousSession) } + .onSuccess { + return it + } + } + // Prompt user to log in with device code + val javaSession = deviceCodeLogin.getFromInput( + httpClient, + MsaDeviceCodeCallback { msaDeviceCode: MsaDeviceCode -> + onVerificationRequired( + VerificationRequired( + msaDeviceCode.userCode, + msaDeviceCode.directVerificationUri + ) + ) + }) + save(javaSession) + return javaSession + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/auth/data/identity/SessionStorage.kt b/src/main/kotlin/com/mineinabyss/launchy/auth/data/identity/SessionStorage.kt new file mode 100644 index 0000000..e558d19 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/auth/data/identity/SessionStorage.kt @@ -0,0 +1,11 @@ +package com.mineinabyss.launchy.auth.data.identity + +import kotlinx.serialization.Serializable + +@Serializable +data class SessionStorage( + val microsoftAccessToken: String, + val microsoftRefreshToken: String, + val minecraftAccessToken: String, + val xboxUserId: String, +) diff --git a/src/main/kotlin/com/mineinabyss/launchy/auth/ui/AuthDialog.kt b/src/main/kotlin/com/mineinabyss/launchy/auth/ui/AuthDialog.kt index b8a7dfe..b2f1528 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/auth/ui/AuthDialog.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/auth/ui/AuthDialog.kt @@ -14,18 +14,17 @@ import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.* import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.sp -import com.mineinabyss.launchy.LocalLaunchyState import com.mineinabyss.launchy.core.ui.Dialog import com.mineinabyss.launchy.core.ui.components.LaunchyDialog -import com.mineinabyss.launchy.core.ui.dialog +import com.mineinabyss.launchy.core.ui.screens.dialog import com.mineinabyss.launchy.util.DesktopHelpers @OptIn(ExperimentalTextApi::class) @Composable fun AuthDialog( + state: Dialog.Auth, onDismissRequest: () -> Unit ) { - val state = LocalLaunchyState LaunchyDialog( title = { Text("Authenticate with Microsoft", style = LocalTextStyle.current) @@ -37,7 +36,7 @@ fun AuthDialog( declineText = null, ) { when { - state.profile.authCode != null -> { + state.verification.code != null -> { val clipboard = LocalClipboardManager.current val annotatedText = buildAnnotatedString { withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.onBackground)) { diff --git a/src/main/kotlin/com/mineinabyss/launchy/auth/ui/ProfileState.kt b/src/main/kotlin/com/mineinabyss/launchy/auth/ui/ProfileState.kt deleted file mode 100644 index cc4d294..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/auth/ui/ProfileState.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.mineinabyss.launchy.auth.ui - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import com.mineinabyss.launchy.auth.data.PlayerProfile -import com.mineinabyss.launchy.config.data.Config -import net.raphimc.minecraftauth.step.java.session.StepFullJavaSession.FullJavaSession - -class ProfileState( - val config: Config -) { - var authCode: String? by mutableStateOf(null) - - var currentSession: FullJavaSession? by mutableStateOf(null) - var currentProfile: PlayerProfile? by mutableStateOf(config.currentProfile) -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/auth/ui/ProfileUiState.kt b/src/main/kotlin/com/mineinabyss/launchy/auth/ui/ProfileUiState.kt new file mode 100644 index 0000000..2414aec --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/auth/ui/ProfileUiState.kt @@ -0,0 +1,8 @@ +package com.mineinabyss.launchy.auth.ui + +import androidx.compose.ui.graphics.painter.BitmapPainter + +data class ProfileUiState( + val username: String, + val avatar: BitmapPainter?, +) diff --git a/src/main/kotlin/com/mineinabyss/launchy/auth/ui/ProfileViewModel.kt b/src/main/kotlin/com/mineinabyss/launchy/auth/ui/ProfileViewModel.kt new file mode 100644 index 0000000..7a856af --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/auth/ui/ProfileViewModel.kt @@ -0,0 +1,73 @@ +package com.mineinabyss.launchy.auth.ui + +import androidx.compose.ui.graphics.FilterQuality +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.res.loadImageBitmap +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mineinabyss.launchy.auth.data.ProfileRepository +import com.mineinabyss.launchy.core.data.TasksRepository +import com.mineinabyss.launchy.core.ui.Dialog +import com.mineinabyss.launchy.core.ui.screens.dialog +import com.mineinabyss.launchy.downloads.data.Downloader +import com.mineinabyss.launchy.util.AppDispatchers +import com.mineinabyss.launchy.util.DesktopHelpers +import com.mineinabyss.launchy.util.Dirs +import com.mineinabyss.launchy.util.InProgressTask +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.* +import kotlin.io.path.inputStream + +class ProfileViewModel( + private val profileRepository: ProfileRepository, + private val tasks: TasksRepository, +) : ViewModel() { + private val _profile = MutableStateFlow(null) + + val profile = _profile.asStateFlow() + + init { + viewModelScope.launch { + profileRepository.authRequest.collectLatest { verification -> + if (verification == null) return@collectLatest + dialog = Dialog.Auth(verification) + DesktopHelpers.browse(verification.redirectTo) + } + } + viewModelScope.launch { + profileRepository.currentProfile.collectLatest { profile -> + if (profile == null) return@collectLatest + //TODO separate state for loading avatar? + val avatar = getAvatar(profile.uuid).getOrNull() + _profile.value = ProfileUiState(profile.name, avatar) + } + } + } + + fun authOrShowDialog() = viewModelScope.launch { + tasks.run("auth", InProgressTask("Authenticating")) { + val session = profileRepository.authenticateCurrentProfile() + + dialog = Dialog.None + } + } + + fun logout() { + profileRepository.logout() + _profile.value = null + } + + private suspend fun getAvatar(uuid: UUID): Result = withContext(AppDispatchers.IO) { + runCatching { + Downloader.downloadAvatar(uuid, Downloader.Options(overwrite = false)) + BitmapPainter( + loadImageBitmap(Dirs.avatar(uuid).inputStream()), + filterQuality = FilterQuality.None + ) + } + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/auth/ui/components/AccountsPopup.kt b/src/main/kotlin/com/mineinabyss/launchy/auth/ui/components/AccountsPopup.kt index ee25b23..abeb845 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/auth/ui/components/AccountsPopup.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/auth/ui/components/AccountsPopup.kt @@ -9,24 +9,28 @@ import androidx.compose.material.icons.automirrored.rounded.Logout import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.auth.data.Auth.logout +import com.mineinabyss.launchy.auth.ui.ProfileViewModel +import com.mineinabyss.launchy.util.koinViewModel @Composable -fun AccountsPopup(onLogout: () -> Unit) { - val state = LocalLaunchyState +fun AccountsPopup( + viewModel: ProfileViewModel = koinViewModel(), + onLogout: () -> Unit +) { Surface( tonalElevation = 2.dp, shape = RoundedCornerShape(20.dp), modifier = Modifier.size(48.dp) ) { - val currentProfile = state.profile.currentProfile + val currentProfile by viewModel.profile.collectAsState() if (currentProfile != null) { IconButton( onClick = { - state.profile.logout(currentProfile.uuid) + viewModel.logout() onLogout() }, ) { diff --git a/src/main/kotlin/com/mineinabyss/launchy/auth/ui/components/PlayerAvatar.kt b/src/main/kotlin/com/mineinabyss/launchy/auth/ui/components/PlayerAvatar.kt new file mode 100644 index 0000000..6971ce4 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/auth/ui/components/PlayerAvatar.kt @@ -0,0 +1,37 @@ +package com.mineinabyss.launchy.auth.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +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 androidx.compose.ui.res.loadImageBitmap +import androidx.compose.ui.res.useResource +import com.mineinabyss.launchy.auth.ui.ProfileUiState + +@Composable +fun PlayerAvatar( + profile: ProfileUiState?, + modifier: Modifier = Modifier +) { + val avatar = profile?.avatar + if (avatar == null) { + val missingSkin = remember { + useResource("missing_skin.png") { + BitmapPainter( + loadImageBitmap(it), + filterQuality = FilterQuality.None + ) + } + } + Image(missingSkin, "Not logged in", Modifier.fillMaxSize()) + } else Image( + painter = avatar, + contentDescription = "Avatar", + contentScale = ContentScale.FillWidth, + modifier = modifier + ) +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/config/data/Config.kt b/src/main/kotlin/com/mineinabyss/launchy/config/data/Config.kt index 399e1e5..4b6cb39 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/config/data/Config.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/config/data/Config.kt @@ -1,6 +1,6 @@ package com.mineinabyss.launchy.config.data -import com.mineinabyss.launchy.auth.data.PlayerProfile +import com.mineinabyss.launchy.auth.data.ProfileModel import kotlinx.serialization.Serializable @@ -8,7 +8,7 @@ import kotlinx.serialization.Serializable data class Config( val handledImportOptions: Boolean = false, val onboardingComplete: Boolean = false, - val currentProfile: PlayerProfile? = null, + val currentProfile: ProfileModel? = null, val javaPath: String? = null, val jvmArguments: String? = null, val memoryAllocation: Int? = null, diff --git a/src/main/kotlin/com/mineinabyss/launchy/config/data/ConfigModule.kt b/src/main/kotlin/com/mineinabyss/launchy/config/data/ConfigModule.kt new file mode 100644 index 0000000..f458ea6 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/config/data/ConfigModule.kt @@ -0,0 +1,7 @@ +package com.mineinabyss.launchy.config.data + +import org.koin.dsl.module + +fun configModule() = module { + single { ConfigDataSource() } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/data/FileSystemDataSource.kt b/src/main/kotlin/com/mineinabyss/launchy/core/data/FileSystemDataSource.kt new file mode 100644 index 0000000..d793889 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/core/data/FileSystemDataSource.kt @@ -0,0 +1,26 @@ +package com.mineinabyss.launchy.core.data + +import io.github.irgaly.kfswatch.KfsDirectoryWatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapLatest +import java.nio.file.Path +import kotlin.coroutines.coroutineContext +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.pathString + +class FileSystemDataSource { + @OptIn(ExperimentalCoroutinesApi::class) + suspend fun watchDirectory(path: Path): Flow> { + val watcher = KfsDirectoryWatcher(CoroutineScope(coroutineContext)) + watcher.add(path.pathString) + return watcher.onEventFlow.mapLatest { + path.listDirectoryEntries(glob = "*jar").toList() + } + } + + suspend fun scheduleWrite(path: Path, write: () -> Unit) { + + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/data/TasksRepository.kt b/src/main/kotlin/com/mineinabyss/launchy/core/data/TasksRepository.kt new file mode 100644 index 0000000..c0728a4 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/core/data/TasksRepository.kt @@ -0,0 +1,28 @@ +package com.mineinabyss.launchy.core.data + +import com.mineinabyss.launchy.util.InProgressTask +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +class TasksRepository { + private val _inProgress = MutableStateFlow(mapOf()) + val inProgress = _inProgress.asStateFlow() + + fun start(key: String, task: InProgressTask) { + _inProgress.update { it.plus(key to task) } + } + + fun finish(key: String) { + _inProgress.update { it.minus(key) } + } + + inline fun run(key: String, task: InProgressTask, run: () -> T): T { + try { + start(key, task) + return run() + } finally { + finish(key) + } + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/di/CoreModule.kt b/src/main/kotlin/com/mineinabyss/launchy/core/di/CoreModule.kt new file mode 100644 index 0000000..a00b2f5 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/core/di/CoreModule.kt @@ -0,0 +1,10 @@ +package com.mineinabyss.launchy.core.di + +import com.mineinabyss.launchy.core.data.TasksRepository +import com.mineinabyss.launchy.core.ui.LaunchyViewModel +import org.koin.dsl.module + +fun coreModule() = module { + single { TasksRepository() } + single { LaunchyViewModel(get()) } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/Dialog.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/Dialog.kt index b5a4124..2c686e0 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/core/ui/Dialog.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/Dialog.kt @@ -1,13 +1,14 @@ package com.mineinabyss.launchy.core.ui -import com.mineinabyss.launchy.instance.data.GameInstanceConfig +import com.mineinabyss.launchy.auth.data.identity.IdentityDataSource +import com.mineinabyss.launchy.instance.data.storage.InstanceConfig sealed interface Dialog { object None : Dialog object ChooseJVMPath : Dialog - object Auth : Dialog + data class Auth(val verification: IdentityDataSource.VerificationRequired) : Dialog class Options( val title: String, @@ -21,7 +22,7 @@ sealed interface Dialog { class Error(val title: String, val message: String) : Dialog class ConfirmImportModpackDialog( - val info: GameInstanceConfig + val info: InstanceConfig ) companion object { diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyState.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyState.kt deleted file mode 100644 index 755a4b9..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyState.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.mineinabyss.launchy.core.ui - -import androidx.compose.runtime.* -import com.mineinabyss.launchy.auth.ui.ProfileState -import com.mineinabyss.launchy.config.data.Config -import com.mineinabyss.launchy.instance.data.GameInstanceDataSource -import com.mineinabyss.launchy.instance.ui.GameInstanceState -import com.mineinabyss.launchy.util.InProgressTask -import java.util.* - -class LaunchyState( - // Config should never be mutated unless it also updates UI state - private val config: Config, - private val instances: List -) { - val profile = ProfileState(config) - var instanceState: GameInstanceState? by mutableStateOf(null) - private val launchedProcesses = mutableStateMapOf() - val jvm = JvmState(config) - val ui = UIState(config) - val lastPlayed = mutableStateMapOf().apply { - putAll(config.lastPlayedMap) - } - - val gameInstances = mutableStateListOf().apply { - addAll(instances) - } - - val inProgressTasks = mutableStateMapOf() - - fun processFor(instance: GameInstanceDataSource): Process? = launchedProcesses[instance.minecraftDir.toString()] - fun setProcessFor(instance: GameInstanceDataSource, process: Process?) { - if (process == null) launchedProcesses.remove(instance.minecraftDir.toString()) - else launchedProcesses[instance.minecraftDir.toString()] = process - } - - // If any state is true, we consider import handled and move on - var handledImportOptions by mutableStateOf( - config.handledImportOptions - ) - - var onboardingComplete by mutableStateOf(config.onboardingComplete) - - fun saveToConfig() { - config.copy( - handledImportOptions = handledImportOptions, - onboardingComplete = onboardingComplete, - currentProfile = profile.currentProfile, - javaPath = jvm.javaPath?.toString(), - jvmArguments = jvm.userJvmArgs, - memoryAllocation = jvm.userMemoryAllocation, - useRecommendedJvmArguments = jvm.useRecommendedJvmArgs, - preferHue = ui.preferHue, - startInFullscreen = ui.fullscreen, - lastPlayedMap = lastPlayed - ).save() - } - - inline fun runTask(key: String, task: InProgressTask, run: () -> T): T { - try { - inProgressTasks[key] = task - return run() - } finally { - inProgressTasks.remove(key) - } - } -} - -fun mutableStateSetOf() = Collections.newSetFromMap(mutableStateMapOf()) diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyUiState.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyUiState.kt new file mode 100644 index 0000000..d648396 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyUiState.kt @@ -0,0 +1,8 @@ +package com.mineinabyss.launchy.core.ui + +sealed interface LaunchyUiState { + object Loading : LaunchyUiState + data class Ready( + val ui: UiState, + ) : LaunchyUiState +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyViewModel.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyViewModel.kt new file mode 100644 index 0000000..163fdb4 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyViewModel.kt @@ -0,0 +1,77 @@ +package com.mineinabyss.launchy.core.ui + +import androidx.compose.runtime.* +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mineinabyss.launchy.config.data.Config +import com.mineinabyss.launchy.config.data.ConfigDataSource +import com.mineinabyss.launchy.instance.data.GameInstanceDataSource +import com.mineinabyss.launchy.util.AppDispatchers +import com.mineinabyss.launchy.util.Dirs +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class LaunchyViewModel( + val configDataSource: ConfigDataSource +) : ViewModel() { + private val _uiState = MutableStateFlow(LaunchyUiState.Loading) + private val _config = MutableStateFlow(Config()) + + val uiState = _uiState.asStateFlow() + val config = _config.asStateFlow() + + init { + viewModelScope.launch { + setupFilesystem() + tryLoadConfig() + val instances = GameInstanceDataSource.readAll(Dirs.modpackConfigsDir) + val config = config.value + _uiState.emit( + LaunchyUiState.Ready( + UiState(config), + instances, + ) + ) + } + } + + val lastPlayed = mutableStateMapOf().apply { + putAll(config.lastPlayedMap) + } + + // If any state is true, we consider import handled and move on + var handledImportOptions by mutableStateOf( + config.handledImportOptions + ) + + var onboardingComplete by mutableStateOf(config.onboardingComplete) + + + fun saveToConfig() { + config.value.copy( + handledImportOptions = handledImportOptions, + onboardingComplete = onboardingComplete, + currentProfile = profile.currentProfile, + javaPath = jvm.javaPath?.toString(), + jvmArguments = jvm.userJvmArgs, + memoryAllocation = jvm.userMemoryAllocation, + useRecommendedJvmArguments = jvm.useRecommendedJvmArgs, + preferHue = ui.preferHue, + startInFullscreen = ui.fullscreen, + lastPlayedMap = lastPlayed + ).save() + } + + private fun setupFilesystem() { + Dirs.createDirs() + Dirs.createConfigFiles() + } + + private suspend fun tryLoadConfig() = withContext(AppDispatchers.IO) { + configDataSource.readConfig().onSuccess { + _config.emit(it) + } + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/Screen.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/Screen.kt deleted file mode 100644 index cd47aa5..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/core/ui/Screen.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.mineinabyss.launchy.core.ui - -import kotlinx.serialization.Serializable - -@Serializable -sealed class Screen( - val transparentTopBar: Boolean = true, - val showTitle: Boolean = false, - val showSidebar: Boolean = false, -) { - 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/core/ui/UIState.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/UiState.kt similarity index 65% rename from src/main/kotlin/com/mineinabyss/launchy/core/ui/UIState.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/UiState.kt index 4859b33..d0baccb 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/core/ui/UIState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/UiState.kt @@ -1,11 +1,15 @@ package com.mineinabyss.launchy.core.ui +import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.mineinabyss.launchy.config.data.Config -class UIState(config: Config) { +val LocalUiState = compositionLocalOf { error("No UiState provided") } + +//TODO move into viewmodel +class UiState(config: Config) { var preferHue: Float by mutableStateOf(config.preferHue ?: 0f) var fullscreen: Boolean by mutableStateOf(config.startInFullscreen) } diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/ComfyContent.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/ComfyContent.kt index 2e20fe4..89e4326 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/ComfyContent.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/ComfyContent.kt @@ -12,7 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.core.ui.screen +import com.mineinabyss.launchy.core.ui.screens.screen @Composable fun ComfyWidth( diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/InProgressTasksIndicator.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/InProgressTasksIndicator.kt new file mode 100644 index 0000000..fae5424 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/InProgressTasksIndicator.kt @@ -0,0 +1,60 @@ +package com.mineinabyss.launchy.core.ui.components + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.layout.* +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.mineinabyss.launchy.core.data.TasksRepository +import com.mineinabyss.launchy.core.ui.screens.Screen +import com.mineinabyss.launchy.core.ui.screens.screen +import com.mineinabyss.launchy.instance.ui.components.SlightBackgroundTint +import com.mineinabyss.launchy.instance.ui.components.settings.infobar.InfoBarProperties +import com.mineinabyss.launchy.util.InProgressTask +import org.koin.compose.koinInject + +@Composable +fun InProgressTasksIndicator( + tasks: TasksRepository = koinInject() +) { + val progressBarHeight by animateDpAsState(if (screen == Screen.InstanceSettings) InfoBarProperties.height else 0.dp) + val inProgress by tasks.inProgress.collectAsState() + + if (inProgress.isNotEmpty()) Box(Modifier.fillMaxSize().padding(bottom = progressBarHeight)) { + val task = inProgress.values.first() + val textModifier = Modifier.align(Alignment.BottomStart).padding(start = 10.dp, bottom = 20.dp) + val progressBarModifier = Modifier.fillMaxWidth().align(Alignment.BottomCenter) + val progressBarColor = MaterialTheme.colorScheme.primaryContainer + SlightBackgroundTint(Modifier.height(50.dp)) + when (task) { + is InProgressTask.WithPercentage -> { + Text( + "${task.name}... (${task.current}/${task.total}${if (task.measurement != null) " ${task.measurement}" else ""})", + modifier = textModifier + ) + LinearProgressIndicator( + progress = task.current.toFloat() / task.total, + modifier = progressBarModifier, + color = progressBarColor + ) + } + + else -> { + Text( + "${task.name}...", + modifier = textModifier + ) + + LinearProgressIndicator( + modifier = progressBarModifier, + color = progressBarColor + ) + } + } + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/LeftSidebar.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/LeftSidebar.kt index e582469..e3ddd07 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/LeftSidebar.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/LeftSidebar.kt @@ -1,7 +1,6 @@ package com.mineinabyss.launchy.core.ui.components import androidx.compose.animation.* -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Add @@ -12,26 +11,22 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.FilterQuality -import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInRoot -import androidx.compose.ui.res.loadImageBitmap -import androidx.compose.ui.res.useResource import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.auth.data.Auth +import androidx.lifecycle.viewmodel.compose.viewModel +import com.mineinabyss.launchy.auth.ui.ProfileViewModel import com.mineinabyss.launchy.auth.ui.components.AccountsPopup -import com.mineinabyss.launchy.core.ui.Screen -import com.mineinabyss.launchy.core.ui.screen -import kotlinx.coroutines.launch +import com.mineinabyss.launchy.auth.ui.components.PlayerAvatar +import com.mineinabyss.launchy.core.ui.screens.Screen +import com.mineinabyss.launchy.core.ui.screens.screen @Composable -fun LeftSidebar() { - val state = LocalLaunchyState - val coroutineScope = rememberCoroutineScope() +fun LeftSidebar( + profileViewModel: ProfileViewModel = viewModel(), +) { var showAccountsPopup by remember { mutableStateOf(false) } var accountHeadPosition: LayoutCoordinates? by remember { mutableStateOf(null) } @@ -58,14 +53,12 @@ fun LeftSidebar() { screen = Screen.NewInstance } ) - val profile = state.profile.currentProfile + val profile by profileViewModel.profile.collectAsState() FloatingActionButton( onClick = { - if (state.profile.currentProfile == null) coroutineScope.launch { - if (profile == null) Auth.authOrShowDialog(state, state.profile) - } else { - showAccountsPopup = !showAccountsPopup - } + profileViewModel.authOrShowDialog() + if (profile == null) profileViewModel.authOrShowDialog() + else showAccountsPopup = !showAccountsPopup }, modifier = Modifier.size(48.dp).onGloballyPositioned { accountHeadPosition = it @@ -73,18 +66,7 @@ fun LeftSidebar() { containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.secondary, ) { - profile?.let { PlayerAvatar(profile, Modifier.fillMaxSize()) } - ?: run { - val missingSkin = remember { - useResource("missing_skin.png") { - BitmapPainter( - loadImageBitmap(it), - filterQuality = FilterQuality.None - ) - } - } - Image(missingSkin, "Not logged in", Modifier.fillMaxSize()) - } + PlayerAvatar(profile, Modifier.fillMaxSize()) } } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/PlayerAvatar.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/PlayerAvatar.kt deleted file mode 100644 index 3d6dbfd..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/PlayerAvatar.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.mineinabyss.launchy.core.ui.components - -import androidx.compose.foundation.Image -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.painter.BitmapPainter -import androidx.compose.ui.layout.ContentScale -import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.auth.data.PlayerProfile - -@Composable -fun PlayerAvatar(profile: PlayerProfile, modifier: Modifier = Modifier) { - val state = LocalLaunchyState - val avatar: BitmapPainter? by profile.getAvatar() - if (avatar != null) Image( - painter = avatar!!, - contentDescription = "Avatar", - contentScale = ContentScale.FillWidth, - modifier = modifier - ) -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/dialogs/SelectJVMDialog.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/dialogs/SelectJVMDialog.kt index 5a5ae61..b1d5f23 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/core/ui/dialogs/SelectJVMDialog.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/dialogs/SelectJVMDialog.kt @@ -6,10 +6,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import com.mineinabyss.launchy.LocalLaunchyState import com.mineinabyss.launchy.core.ui.Dialog -import com.mineinabyss.launchy.core.ui.Screen import com.mineinabyss.launchy.core.ui.components.LaunchyDialog -import com.mineinabyss.launchy.core.ui.dialog -import com.mineinabyss.launchy.core.ui.screen +import com.mineinabyss.launchy.core.ui.screens.Screen +import com.mineinabyss.launchy.core.ui.screens.dialog +import com.mineinabyss.launchy.core.ui.screens.screen import com.mineinabyss.launchy.downloads.data.Downloader import com.mineinabyss.launchy.util.AppDispatchers import kotlinx.coroutines.launch diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/screens/Screen.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/screens/Screen.kt new file mode 100644 index 0000000..ae89755 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/screens/Screen.kt @@ -0,0 +1,20 @@ +package com.mineinabyss.launchy.core.ui.screens + +import kotlinx.serialization.Serializable + +@Serializable +sealed class Screen( + val transparentTopBar: Boolean = true, + val showTitle: Boolean = false, + val showSidebar: Boolean = false, +) { + interface OnLeftSidebar + + data object Default : Screen(transparentTopBar = true, showTitle = true, showSidebar = true), OnLeftSidebar + data object NewInstance : Screen(transparentTopBar = true, showTitle = true, showSidebar = true), OnLeftSidebar + data object Settings : Screen(transparentTopBar = true, showTitle = true, showSidebar = true), OnLeftSidebar + + data object InstanceSettings : Screen(showTitle = true) + data object Instance : Screen(transparentTopBar = true) + +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/Screens.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/screens/Screens.kt similarity index 64% rename from src/main/kotlin/com/mineinabyss/launchy/core/ui/Screens.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/screens/Screens.kt index 422cd5d..293e8bc 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/core/ui/Screens.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/screens/Screens.kt @@ -1,28 +1,28 @@ -package com.mineinabyss.launchy.core.ui +package com.mineinabyss.launchy.core.ui.screens import androidx.compose.animation.* -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.foundation.layout.* -import androidx.compose.material.LinearProgressIndicator +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding import androidx.compose.material.LocalTextStyle import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Update import androidx.compose.material3.* import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.LocalLaunchyState import com.mineinabyss.launchy.auth.ui.AuthDialog +import com.mineinabyss.launchy.core.ui.Dialog +import com.mineinabyss.launchy.core.ui.LocalUiState +import com.mineinabyss.launchy.core.ui.TopBar import com.mineinabyss.launchy.core.ui.components.AppTopBar +import com.mineinabyss.launchy.core.ui.components.InProgressTasksIndicator import com.mineinabyss.launchy.core.ui.components.LaunchyDialog import com.mineinabyss.launchy.core.ui.components.LeftSidebar import com.mineinabyss.launchy.core.ui.dialogs.SelectJVMDialog import com.mineinabyss.launchy.core.ui.theme.currentHue -import com.mineinabyss.launchy.instance.ui.GameInstanceState -import com.mineinabyss.launchy.instance.ui.components.SlightBackgroundTint -import com.mineinabyss.launchy.instance.ui.components.settings.infobar.InfoBarProperties import com.mineinabyss.launchy.instance.ui.screens.InstanceScreen import com.mineinabyss.launchy.instance.ui.screens.InstanceSettingsScreen import com.mineinabyss.launchy.instance_creation.ui.NewInstance @@ -31,22 +31,22 @@ import com.mineinabyss.launchy.settings.ui.SettingsScreen import com.mineinabyss.launchy.updater.data.AppUpdateState import com.mineinabyss.launchy.updater.data.GithubUpdateChecker import com.mineinabyss.launchy.util.DesktopHelpers -import com.mineinabyss.launchy.util.InProgressTask var screen: Screen by mutableStateOf(Screen.Default) var dialog: Dialog by mutableStateOf(Dialog.None) var updateAvailable: AppUpdateState by mutableStateOf(AppUpdateState.Unknown) -private val ModpackStateProvider = compositionLocalOf { error("No local modpack provided") } +//private val ModpackStateProvider = compositionLocalOf { error("No local modpack provided") } val snackbarHostState = SnackbarHostState() -val LocalGameInstanceState: GameInstanceState - @Composable get() = ModpackStateProvider.current +//val LocalGameInstanceState: GameInstanceState +// @Composable get() = ModpackStateProvider.current @Composable -fun Screens() = Scaffold( +fun Screens( +) = Scaffold( snackbarHost = { SnackbarHost(snackbarHostState) }, floatingActionButton = { val update = updateAvailable @@ -62,14 +62,9 @@ fun Screens() = Scaffold( } } ) { - val state = LocalLaunchyState - val packState = state.instanceState - - if (packState != null) CompositionLocalProvider(ModpackStateProvider provides packState) { - Screen(Screen.Instance) { InstanceScreen() } - Screen(Screen.InstanceSettings, transition = Transitions.SlideUp) { InstanceSettingsScreen() } - } - + val ui = LocalUiState.current + Screen(Screen.Instance) { InstanceScreen() } + Screen(Screen.InstanceSettings, transition = Transitions.SlideUp) { InstanceSettingsScreen() } Screen(Screen.Default) { HomeScreen() } Screen(Screen.NewInstance) { NewInstance() } Screen(Screen.Settings) { SettingsScreen() } @@ -83,8 +78,8 @@ fun Screens() = Scaffold( val isDefault = screen is Screen.OnLeftSidebar - LaunchedEffect(isDefault, state.ui.preferHue) { - if (isDefault) currentHue = state.ui.preferHue + LaunchedEffect(isDefault, ui.preferHue) { + if (isDefault) currentHue = ui.preferHue } LaunchedEffect(Unit) { updateAvailable = GithubUpdateChecker.checkForUpdates() @@ -99,17 +94,18 @@ fun Screens() = Scaffold( onBackButtonClicked = { screen = when (screen) { Screen.Instance -> { - packState?.saveToConfig() + //TODO save states +// packState?.saveToConfig() Screen.Default } Screen.InstanceSettings -> { - packState?.saveToConfig() +// packState?.saveToConfig() Screen.Instance } Screen.Settings -> { - state.saveToConfig() +// state.saveToConfig() Screen.Default } @@ -119,7 +115,8 @@ fun Screens() = Scaffold( ) when (val castDialog = dialog) { Dialog.None -> {} - Dialog.Auth -> AuthDialog( + is Dialog.Auth -> AuthDialog( + castDialog, onDismissRequest = { dialog = Dialog.None }, ) @@ -145,42 +142,6 @@ fun Screens() = Scaffold( ) { Text(castDialog.message, style = LocalTextStyle.current) } } } - - val tasks = state.inProgressTasks - val progressBarHeight by animateDpAsState(if (screen == Screen.InstanceSettings) InfoBarProperties.height else 0.dp) - - if (tasks.isNotEmpty()) Box(Modifier.fillMaxSize().padding(bottom = progressBarHeight)) { - val task = tasks.values.first() - val textModifier = Modifier.align(Alignment.BottomStart).padding(start = 10.dp, bottom = 20.dp) - val progressBarModifier = Modifier.fillMaxWidth().align(Alignment.BottomCenter) - val progressBarColor = MaterialTheme.colorScheme.primaryContainer - SlightBackgroundTint(Modifier.height(50.dp)) - when (task) { - is InProgressTask.WithPercentage -> { - Text( - "${task.name}... (${task.current}/${task.total}${if (task.measurement != null) " ${task.measurement}" else ""})", - modifier = textModifier - ) - LinearProgressIndicator( - progress = task.current.toFloat() / task.total, - modifier = progressBarModifier, - color = progressBarColor - ) - } - - else -> { - Text( - "${task.name}...", - modifier = textModifier - ) - - LinearProgressIndicator( - modifier = progressBarModifier, - color = progressBarColor - ) - } - } - } } enum class Transitions { @@ -210,4 +171,5 @@ fun Screen( } } } + InProgressTasksIndicator() } diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/DownloadQueueState.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/DownloadQueueState.kt index 1b07bdf..1e2aa76 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/DownloadQueueState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/DownloadQueueState.kt @@ -2,8 +2,8 @@ package com.mineinabyss.launchy.downloads.data import androidx.compose.runtime.* import com.mineinabyss.launchy.instance.data.DownloadInfo -import com.mineinabyss.launchy.instance.data.InstanceUserConfig import com.mineinabyss.launchy.instance.data.Modpack +import com.mineinabyss.launchy.instance.data.storage.InstanceUserConfig import com.mineinabyss.launchy.instance.ui.InstanceViewModel import com.mineinabyss.launchy.util.ModID diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/Downloader.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/Downloader.kt index 55d1b4a..ca85c19 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/Downloader.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/Downloader.kt @@ -1,16 +1,19 @@ package com.mineinabyss.launchy.downloads.data -import com.mineinabyss.launchy.core.ui.LaunchyState +import com.mineinabyss.launchy.core.ui.LaunchyUiState import com.mineinabyss.launchy.instance.data.GameInstanceDataSource import com.mineinabyss.launchy.util.* import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.cio.* import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.request.* import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* import io.ktor.utils.io.* import io.ktor.utils.io.core.* +import kotlinx.serialization.json.Json import org.rauschig.jarchivelib.ArchiveFormat import org.rauschig.jarchivelib.Archiver import org.rauschig.jarchivelib.ArchiverFactory @@ -22,6 +25,13 @@ import kotlin.io.path.* object Downloader { val httpClient = HttpClient(CIO) { install(HttpTimeout) + install(ContentNegotiation) { + json(json = Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + }) + } } data class ModifyHeaders(val lastModified: String, val contentLength: Long) { @@ -119,7 +129,7 @@ object Downloader { /** @return Path to java executable */ @OptIn(ExperimentalPathApi::class) suspend fun installJDK( - state: LaunchyState, + state: LaunchyUiState, ): Path? { try { state.inProgressTasks["installJDK"] = InProgressTask("Downloading Java environment") diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/ModDownloader.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/ModDownloader.kt index 6731d8d..a45fd5d 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/ModDownloader.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/ModDownloader.kt @@ -1,12 +1,12 @@ package com.mineinabyss.launchy.downloads.data -import com.mineinabyss.launchy.core.ui.LaunchyState +import com.mineinabyss.launchy.core.ui.LaunchyUiState import com.mineinabyss.launchy.instance.data.DownloadInfo import com.mineinabyss.launchy.instance.data.HashCheck import com.mineinabyss.launchy.instance.data.InstanceModLoaders -import com.mineinabyss.launchy.instance.data.Launcher import com.mineinabyss.launchy.instance.data.Mod import com.mineinabyss.launchy.instance.ui.GameInstanceState +import com.mineinabyss.launchy.launcher.data.Launcher import com.mineinabyss.launchy.util.* import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -17,7 +17,7 @@ import kotlin.io.path.* object ModDownloader { - suspend fun GameInstanceState.installMCAndModLoaders(state: LaunchyState, modLoaders: InstanceModLoaders) { + suspend fun GameInstanceState.installMCAndModLoaders(state: LaunchyUiState, modLoaders: InstanceModLoaders) { state.runTask(Tasks.installModLoadersId, InProgressTask("Installing ${modLoaders.fullVersionName}")) { Launcher.download( modLoaders, @@ -67,7 +67,7 @@ object ModDownloader { * does not install any mod updates or new dep versions if they changed in the modpack. * Primarily the mod loader/minecraft version. */ - suspend fun GameInstanceState.ensureDependenciesReady(state: LaunchyState) = coroutineScope { + suspend fun GameInstanceState.ensureDependenciesReady(state: LaunchyUiState) = coroutineScope { val currentDeps = queued.userAgreedModLoaders if (currentDeps == null) { queued.userAgreedModLoaders = modpack.modLoaders @@ -98,13 +98,13 @@ object ModDownloader { (existingEntries - linked).forEach { it.deleteIfExists() } } - suspend fun GameInstanceState.prepareWithoutChangingInstalledMods(state: LaunchyState) { + suspend fun GameInstanceState.prepareWithoutChangingInstalledMods(state: LaunchyUiState) { ensureDependenciesReady(state) copyMods() } @OptIn(ExperimentalPathApi::class) - fun GameInstanceState.copyOverrides(state: LaunchyState) { + fun GameInstanceState.copyOverrides(state: LaunchyUiState) { state.runTask(Tasks.copyOverridesId, InProgressTask("Copying overrides")) { modpack.overridesPaths.forEach { it.copyToRecursively( @@ -136,7 +136,7 @@ object ModDownloader { * Updates mod loader versions and mods to latest modpack definition. */ suspend fun GameInstanceState.startInstall( - state: LaunchyState, + state: LaunchyUiState, ignoreCachedCheck: Boolean = false ): Result<*> = coroutineScope { queued.userAgreedModLoaders = modpack.modLoaders diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/DownloadInfo.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/DownloadInfo.kt new file mode 100644 index 0000000..32b4bcc --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/DownloadInfo.kt @@ -0,0 +1,34 @@ +package com.mineinabyss.launchy.instance.data + +import com.mineinabyss.launchy.downloads.data.ModDownloader +import com.mineinabyss.launchy.util.hashing.Hashing.checksum +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import java.nio.file.Path +import java.security.MessageDigest +import kotlin.io.path.Path +import kotlin.io.path.div +import kotlin.io.path.isRegularFile + +@Serializable +data class DownloadInfo( + val url: String, + val path: String, + val desiredHash: String?, + val hashCheck: HashCheck, + val result: ModDownloader.DownloadResult, +) { + @Transient + val systemPath = Path(path) + + fun failed(): Boolean { + return result == ModDownloader.DownloadResult.Failed + || systemPath.isRegularFile() + || (desiredHash != null && hashCheck == HashCheck.FAILED) + } + + fun calculateSha1Hash(minecraftDir: Path): String { + val md = MessageDigest.getInstance("SHA-1") + return (minecraftDir / systemPath).checksum(md) + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/GameInstanceConfig.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/GameInstanceConfig.kt deleted file mode 100644 index c33eeb5..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/GameInstanceConfig.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.mineinabyss.launchy.instance.data - -import androidx.compose.runtime.Immutable -import com.charleskorn.kaml.decodeFromStream -import com.mineinabyss.launchy.downloads.data.source.PackSource -import com.mineinabyss.launchy.util.Dirs -import com.mineinabyss.launchy.util.Formats -import com.mineinabyss.launchy.util.urlToFileName -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient -import java.nio.file.Path -import kotlin.io.path.div -import kotlin.io.path.inputStream - -@Immutable -@Serializable -data class GameInstanceConfig( - val name: String, - val description: String, - val backgroundURL: String, - val logoURL: String, - val source: PackSource, - val hue: Float = 0f, - val cloudInstanceURL: String? = null, - val overrideMinecraftDir: String? = null, -) { - @OptIn(ExperimentalCoroutinesApi::class) - @Transient - val downloadScope = CoroutineScope(Dispatchers.IO.limitedParallelism(1)) - - @Transient - val backgroundPath = Dirs.imageCache / "background-${urlToFileName(backgroundURL)}" - - @Transient - val logoPath = Dirs.imageCache / "icon-${urlToFileName(logoURL)}" - -// fun saveTo(path: Path) = runCatching { -// Formats.yaml.encodeToStream(this, path.outputStream()) -// } - - companion object { - fun read(path: Path) = runCatching { - Formats.yaml.decodeFromStream(serializer(), path.inputStream()) - }.onFailure { it.printStackTrace() } - } -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/GameInstanceDataSource.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/GameInstanceDataSource.kt index 33e36cb..bd95d3b 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/GameInstanceDataSource.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/GameInstanceDataSource.kt @@ -1,116 +1,77 @@ package com.mineinabyss.launchy.instance.data -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.compose.ui.graphics.painter.BitmapPainter -import androidx.compose.ui.res.loadImageBitmap -import com.charleskorn.kaml.encodeToStream -import com.mineinabyss.launchy.core.ui.LaunchyState -import com.mineinabyss.launchy.downloads.data.Downloader -import com.mineinabyss.launchy.instance.ui.GameInstanceState -import com.mineinabyss.launchy.util.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.nio.file.Path -import kotlin.io.path.* - -class GameInstanceDataSource( - val configDir: Path, - val config: GameInstanceConfig, -) { - val instanceFile = configDir / "instance.yml" - val overridesDir = configDir / "overrides" - val imageLoaderDispatcher = Dispatchers.IO.limitedParallelism(1) - - val minecraftDir = config.overrideMinecraftDir?.let { Path(it) } ?: Dirs.modpackDir(configDir.name) - - val modsDir = (minecraftDir / "mods").createDirectories() - val userMods = (minecraftDir / "modsFromUser").createDirectories() - - val downloadsDir: Path = minecraftDir / "launchyDownloads" - val userConfigFile = (configDir / "config.yml") - - var updatesAvailable by mutableStateOf(false) - var enabled: Boolean by mutableStateOf(true) - - suspend fun loadModList(): InstanceModList { - TODO() - } - suspend fun createModpackState(state: LaunchyState, awaitUpdatesCheck: Boolean = false): GameInstanceState? { - val userConfig = InstanceUserConfig.load(userConfigFile).getOrNull() ?: InstanceUserConfig() - - val modpack = state.runTask("loadingModpack ${config.name}", InProgressTask("Loading modpack ${config.name}")) { - config.source.loadInstance(this) - .showDialogOnError("Failed to read instance") - .getOrElse { - it.printStackTrace() - return null - } - } - val cloudUrl = config.cloudInstanceURL - if (cloudUrl != null) { - AppDispatchers.IO.launch { - val result = Downloader.checkUpdates(this@GameInstanceDataSource, cloudUrl) - if (result !is UpdateResult.UpToDate) updatesAvailable = true - }.also { if (awaitUpdatesCheck) it.join() } - } - return GameInstanceState(this, modpack, userConfig) - } - - init { - require(configDir.isDirectory()) { "Game instance at $configDir must be a directory" } - userMods - } - - data class CloudInstanceWithHeaders( - val config: GameInstanceConfig, - val url: String, - val headers: Downloader.ModifyHeaders, - ) - - companion object { - fun createCloudInstance(state: LaunchyState, cloud: CloudInstanceWithHeaders) { - val instanceDir = Dirs.modpackConfigDir(cloud.config.name) - instanceDir.createDirectories() - - Formats.yaml.encodeToStream( - cloud.config.copy(cloudInstanceURL = cloud.url), - (instanceDir / "instance.yml").outputStream() - ) - val instance = GameInstanceDataSource(instanceDir) - Downloader.saveHeaders(instance, cloud.url, cloud.headers) - state.gameInstances += instance - } - } - - private suspend fun loadBackground() { - runCatching { - Downloader.download(config.backgroundURL, config.backgroundPath, Downloader.Options(overwrite = false)) - val painter = BitmapPainter(loadImageBitmap(config.backgroundPath.inputStream())) - cachedBackground = painter - }.onFailure { it.printStackTrace() } - } - - private suspend fun loadLogo() { - runCatching { - Downloader.download(config.logoURL, config.logoPath, Downloader.Options(overwrite = false)) - val painter = BitmapPainter(loadImageBitmap(config.logoPath.inputStream())) - cachedLogo = painter - }.onFailure { it.printStackTrace() } - } - - private var cachedBackground: BitmapPainter? = null - private var cachedLogo: BitmapPainter? = null - - suspend fun getBackground() = withContext(imageLoaderDispatcher) { - if (cachedBackground == null) loadLogo() - cachedBackground - } - - suspend fun getLogo(): BitmapPainter? = withContext(imageLoaderDispatcher) { - if (cachedLogo == null) loadLogo() - cachedLogo - } -} +//@OptIn(ExperimentalCoroutinesApi::class) +//class GameInstanceDataSource( +// val configDir: Path, +// val config: InstanceConfig, +//) { +// suspend fun createModpackState(state: LaunchyUiState, awaitUpdatesCheck: Boolean = false): GameInstanceState? { +// val userConfig = InstanceUserConfig.load(userConfigFile).getOrNull() ?: InstanceUserConfig() +// +// val modpack = state.runTask("loadingModpack ${config.name}", InProgressTask("Loading modpack ${config.name}")) { +// config.source.loadInstance(this) +// .showDialogOnError("Failed to read instance") +// .getOrElse { +// it.printStackTrace() +// return null +// } +// } +// val cloudUrl = config.cloudInstanceURL +// if (cloudUrl != null) { +// AppDispatchers.IO.launch { +// val result = Downloader.checkUpdates(this@GameInstanceDataSource, cloudUrl) +// if (result !is UpdateResult.UpToDate) updatesAvailable = true +// }.also { if (awaitUpdatesCheck) it.join() } +// } +// return GameInstanceState(this, modpack, userConfig) +// } +// +// init { +// require(configDir.isDirectory()) { "Game instance at $configDir must be a directory" } +// userMods +// } +// +// companion object { +// fun createCloudInstance(state: LaunchyUiState, cloud: CloudInstanceWithHeaders) { +// val instanceDir = Dirs.modpackConfigDir(cloud.config.name) +// instanceDir.createDirectories() +// +// Formats.yaml.encodeToStream( +// cloud.config.copy(cloudInstanceURL = cloud.url), +// (instanceDir / "instance.yml").outputStream() +// ) +// val instance = GameInstanceDataSource(instanceDir) +// Downloader.saveHeaders(instance, cloud.url, cloud.headers) +// state.gameInstances += instance +// } +// } +// +// private suspend fun loadBackground() { +// runCatching { +// Downloader.download(config.backgroundURL, config.backgroundPath, Downloader.Options(overwrite = false)) +// val painter = BitmapPainter(loadImageBitmap(config.backgroundPath.inputStream())) +// cachedBackground = painter +// }.onFailure { it.printStackTrace() } +// } +// +// private suspend fun loadLogo() { +// runCatching { +// Downloader.download(config.logoURL, config.logoPath, Downloader.Options(overwrite = false)) +// val painter = BitmapPainter(loadImageBitmap(config.logoPath.inputStream())) +// cachedLogo = painter +// }.onFailure { it.printStackTrace() } +// } +// +// private var cachedBackground: BitmapPainter? = null +// private var cachedLogo: BitmapPainter? = null +// +// suspend fun getBackground() = withContext(imageLoaderDispatcher) { +// if (cachedBackground == null) loadLogo() +// cachedBackground +// } +// +// suspend fun getLogo(): BitmapPainter? = withContext(imageLoaderDispatcher) { +// if (cachedLogo == null) loadLogo() +// cachedLogo +// } +//} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/HashCheck.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/HashCheck.kt new file mode 100644 index 0000000..e5d4174 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/HashCheck.kt @@ -0,0 +1,5 @@ +package com.mineinabyss.launchy.instance.data + +enum class HashCheck { + UNKNOWN, VERIFIED, FAILED +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModel.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModel.kt new file mode 100644 index 0000000..d5437dc --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModel.kt @@ -0,0 +1,35 @@ +package com.mineinabyss.launchy.instance.data + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import com.mineinabyss.launchy.instance.data.storage.InstanceConfig +import com.mineinabyss.launchy.util.Dirs +import kotlinx.coroutines.Dispatchers +import java.nio.file.Path +import kotlin.io.path.Path +import kotlin.io.path.createDirectories +import kotlin.io.path.div +import kotlin.io.path.name + +data class InstanceModel( + val config: InstanceConfig, + val directory: Path, +) { + val instanceFile = directory / "instance.yml" + val backupInstanceFile = directory / "instance-backup.yml" + val overridesDir = directory / "overrides" + val imageLoaderDispatcher = Dispatchers.IO.limitedParallelism(1) + + val minecraftDir = config.overrideMinecraftDir?.let { Path(it) } ?: Dirs.modpackDir(directory.name) + + val modsDir = (minecraftDir / "mods").createDirectories() + val userMods = (minecraftDir / "modsFromUser").createDirectories() + + val downloadsDir: Path = minecraftDir / "launchyDownloads" + val userConfigFile = (directory / "config.yml") + + var updatesAvailable by mutableStateOf(false) + var enabled: Boolean by mutableStateOf(true) + +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/Mod.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/Mod.kt index b12e57e..6774169 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/Mod.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/Mod.kt @@ -1,6 +1,7 @@ package com.mineinabyss.launchy.instance.data import com.mineinabyss.launchy.instance.data.formats.ModrinthPackFormat +import com.mineinabyss.launchy.instance.data.storage.ModConfig import io.ktor.http.* import java.nio.file.Path import kotlin.io.path.div diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ExtraPackInfo.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ExtraPackInfo.kt index b2434a0..73bdea4 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ExtraPackInfo.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ExtraPackInfo.kt @@ -1,7 +1,7 @@ package com.mineinabyss.launchy.instance.data.formats -import com.mineinabyss.launchy.instance.data.ModConfig import com.mineinabyss.launchy.instance.data.ModGroup +import com.mineinabyss.launchy.instance.data.storage.ModConfig import kotlinx.serialization.Serializable @Serializable diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/LaunchyPackFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/LaunchyPackFormat.kt index e279bf1..7f2cca5 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/LaunchyPackFormat.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/LaunchyPackFormat.kt @@ -1,6 +1,10 @@ package com.mineinabyss.launchy.instance.data.formats -import com.mineinabyss.launchy.instance.data.* +import com.mineinabyss.launchy.instance.data.InstanceModList +import com.mineinabyss.launchy.instance.data.InstanceModLoaders +import com.mineinabyss.launchy.instance.data.Mod +import com.mineinabyss.launchy.instance.data.ModGroup +import com.mineinabyss.launchy.instance.data.storage.ModConfig import com.mineinabyss.launchy.util.GroupName import kotlinx.serialization.Serializable import java.nio.file.Path diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ModrinthPackFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ModrinthPackFormat.kt index 96528fd..da5c2ad 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ModrinthPackFormat.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ModrinthPackFormat.kt @@ -3,7 +3,7 @@ package com.mineinabyss.launchy.instance.data.formats import com.mineinabyss.launchy.instance.data.InstanceModList import com.mineinabyss.launchy.instance.data.InstanceModLoaders import com.mineinabyss.launchy.instance.data.Mod -import com.mineinabyss.launchy.instance.data.ModConfig +import com.mineinabyss.launchy.instance.data.storage.ModConfig import kotlinx.serialization.Serializable import java.nio.file.Path import kotlin.io.path.div diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/storage/InstanceConfig.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/storage/InstanceConfig.kt new file mode 100644 index 0000000..ff7a570 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/storage/InstanceConfig.kt @@ -0,0 +1,29 @@ +package com.mineinabyss.launchy.instance.data.storage + +import androidx.compose.runtime.Immutable +import com.mineinabyss.launchy.downloads.data.source.PackSource +import com.mineinabyss.launchy.util.Dirs +import com.mineinabyss.launchy.util.urlToFileName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlin.io.path.div + +@Immutable +@Serializable +data class InstanceConfig( + val name: String, + val description: String, + val backgroundURL: String, + val logoURL: String, + val source: PackSource, + val hue: Float = 0f, + val cloudInstanceURL: String? = null, + val overrideMinecraftDir: String? = null, +) { + + @Transient + val backgroundPath = Dirs.imageCache / "background-${urlToFileName(backgroundURL)}" + + @Transient + val logoPath = Dirs.imageCache / "icon-${urlToFileName(logoURL)}" +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceUserConfig.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/storage/InstanceUserConfig.kt similarity index 58% rename from src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceUserConfig.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/storage/InstanceUserConfig.kt index 3bd94e1..9b394d7 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceUserConfig.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/storage/InstanceUserConfig.kt @@ -1,45 +1,16 @@ -package com.mineinabyss.launchy.instance.data +package com.mineinabyss.launchy.instance.data.storage import com.charleskorn.kaml.decodeFromStream -import com.mineinabyss.launchy.downloads.data.ModDownloader +import com.mineinabyss.launchy.instance.data.DownloadInfo +import com.mineinabyss.launchy.instance.data.InstanceModLoaders import com.mineinabyss.launchy.util.Formats import com.mineinabyss.launchy.util.GroupName import com.mineinabyss.launchy.util.ModID -import com.mineinabyss.launchy.util.hashing.Hashing.checksum import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient import kotlinx.serialization.encodeToString import java.nio.file.Path -import java.security.MessageDigest import kotlin.io.path.* -enum class HashCheck { - UNKNOWN, VERIFIED, FAILED -} - -@Serializable -data class DownloadInfo( - val url: String, - val path: String, - val desiredHash: String?, - val hashCheck: HashCheck, - val result: ModDownloader.DownloadResult, -) { - @Transient - val systemPath = Path(path) - - fun failed(): Boolean { - return result == ModDownloader.DownloadResult.Failed - || systemPath.isRegularFile() - || (desiredHash != null && hashCheck == HashCheck.FAILED) - } - - fun calculateSha1Hash(minecraftDir: Path): String { - val md = MessageDigest.getInstance("SHA-1") - return (minecraftDir / systemPath).checksum(md) - } -} - @Serializable data class InstanceUserConfig( val userAgreedDeps: InstanceModLoaders? = null, diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/ModConfig.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/storage/ModConfig.kt similarity index 82% rename from src/main/kotlin/com/mineinabyss/launchy/instance/data/ModConfig.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/storage/ModConfig.kt index ba21442..34a4aa4 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/ModConfig.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/storage/ModConfig.kt @@ -1,11 +1,12 @@ -package com.mineinabyss.launchy.instance.data +package com.mineinabyss.launchy.instance.data.storage import com.mineinabyss.launchy.instance.data.formats.ModDownloadPath +import com.mineinabyss.launchy.util.ModID import kotlinx.serialization.Serializable @Serializable data class ModConfig( - val id: String? = null, + val id: ModID? = null, val name: String, val license: String = "", val homepage: String = "", diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/usermods/UserModsDataSource.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/usermods/UserModsDataSource.kt new file mode 100644 index 0000000..16fd273 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/usermods/UserModsDataSource.kt @@ -0,0 +1,8 @@ +package com.mineinabyss.launchy.instance.data.usermods + +import java.nio.file.Path + +class UserModsDataSource( + val modsFolder: Path +) { +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceUiState.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceUiState.kt new file mode 100644 index 0000000..ebd2f5b --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceUiState.kt @@ -0,0 +1,12 @@ +package com.mineinabyss.launchy.instance.ui + +import androidx.compose.ui.graphics.painter.BitmapPainter + +data class InstanceUiState( + val title: String, + val description: String, + val isCloudInstance: Boolean, + val logo: BitmapPainter?, + val background: BitmapPainter?, + val runningProcess: Process?, +) diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceViewModel.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceViewModel.kt index 3e05ffc..5d2c4a4 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceViewModel.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceViewModel.kt @@ -6,7 +6,7 @@ import com.mineinabyss.launchy.util.AppDispatchers import com.mineinabyss.launchy.util.ModID import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.withContext import org.to2mbn.jmccc.mcdownloader.download.Downloader @@ -14,22 +14,36 @@ import org.to2mbn.jmccc.mcdownloader.download.Downloader class InstanceViewModel( val downloader: Downloader, ) : ViewModel() { - val modsState: StateFlow get() = _modsState - val installState = MutableStateFlow(InstallState.InProgress) - - private val instance = MutableStateFlow(null) + private val currentInstance = MutableStateFlow(null) private val _modsState = MutableStateFlow(ModListUiState.Loading) - val installQueueState = MutableStateFlow(InstallState.InProgress) + private val _installState = MutableStateFlow(InstallState.InProgress) + private val _instanceUiState = MutableStateFlow(null) + + val modsState = _modsState.asStateFlow() + val instanceUiState = _instanceUiState.asStateFlow() + val installState = _installState.asStateFlow() @OptIn(ExperimentalCoroutinesApi::class) - val modList = instance.mapLatest { + val modList = currentInstance.mapLatest { withContext(AppDispatchers.IO) { it?.loadModList() } } //TODO read - val userInstalledMods = MutableStateFlow>(emptyList()) + val userInstalledMods = MutableStateFlow(null) +// val userMods by remember { +// mutableStateOf( +// state.instance.userMods.listDirectoryEntries("*.jar").map { +// Mod( +// downloadDir = it, +// modId = it.fileName.toString(), +// info = ModConfig(name = it.fileName.toString()), +// desiredHashes = null +// ) +// } +// ) +// } val enabledMods = MutableStateFlow(listOf()) // val instanceUIState = @@ -124,4 +138,8 @@ class InstanceViewModel( // if (mod.info.configUrl.isNotBlank() && enabled) enabledConfigs.add(mod) // else enabledConfigs.remove(mod) // } + + fun groupInteractionsFor(id: String): ModGroupInteractions = TODO() + + fun modInteractionsFor(id: ModID): ModInteractions = TODO() } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModGroupUiState.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModGroupUiState.kt index 52d7145..b8e00f3 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModGroupUiState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModGroupUiState.kt @@ -1,8 +1,11 @@ package com.mineinabyss.launchy.instance.ui +import com.mineinabyss.launchy.util.Option + data class ModGroupUiState( + val id: String, val title: String, val enabled: Boolean, - val forceEnabled: Boolean, + val force: Option, val mods: List ) diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModListInteractions.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModInteractions.kt similarity index 53% rename from src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModListInteractions.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModInteractions.kt index e33e2a0..3c82301 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModListInteractions.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModInteractions.kt @@ -1,9 +1,11 @@ package com.mineinabyss.launchy.instance.ui -import com.mineinabyss.launchy.util.ModID import com.mineinabyss.launchy.util.Option -class ModListInteractions( +data class ModGroupInteractions( val onToggleGroup: (Option) -> Unit, - val onToggleMod: (ModID, Boolean) -> Unit, +) + +data class ModInteractions( + val onToggleMod: (Boolean) -> Unit, ) diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModUiState.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModUiState.kt index 41e8c6c..8e4d440 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModUiState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModUiState.kt @@ -7,12 +7,20 @@ import androidx.compose.material.icons.rounded.Error import androidx.compose.material.icons.rounded.Update import androidx.compose.material3.MaterialTheme import androidx.compose.ui.graphics.vector.ImageVector +import com.mineinabyss.launchy.instance.data.storage.ModConfig import com.mineinabyss.launchy.util.ModID +import com.mineinabyss.launchy.util.ModName +import com.mineinabyss.launchy.util.Progress data class ModUiState( val id: ModID, val enabled: Boolean, - val queueState: ModQueueState + val configEnabled: Boolean, + val queueState: ModQueueState, + val info: ModConfig, + val incompatibleWith: List, + val dependsOn: List, + val installProgress: Progress?, ) diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/ImportSettingsDialog.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/ImportSettingsDialog.kt index d0dca80..68acece 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/ImportSettingsDialog.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/ImportSettingsDialog.kt @@ -6,9 +6,9 @@ import androidx.compose.animation.fadeOut import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.core.ui.Screen import com.mineinabyss.launchy.core.ui.components.LaunchyDialog -import com.mineinabyss.launchy.core.ui.screen +import com.mineinabyss.launchy.core.ui.screens.Screen +import com.mineinabyss.launchy.core.ui.screens.screen import com.mineinabyss.launchy.util.Dirs import kotlin.io.path.copyTo import kotlin.io.path.div diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/LogoLarge.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/LogoLarge.kt index 36fc4f1..663c101 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/LogoLarge.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/LogoLarge.kt @@ -11,13 +11,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.core.ui.LocalGameInstanceState @Composable fun LogoLarge(painter: BitmapPainter?, modifier: Modifier) { - LocalLaunchyState - val pack = LocalGameInstanceState AnimatedVisibility( painter != null, enter = fadeIn() + expandVertically(clip = false) + fadeIn(), @@ -25,7 +21,7 @@ fun LogoLarge(painter: BitmapPainter?, modifier: Modifier) { ) { if (painter == null) return@AnimatedVisibility Image( - painter = painter!!, + painter = painter, contentDescription = "Modpack logo", modifier = Modifier.fillMaxSize(), contentScale = ContentScale.FillWidth diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/AuthButton.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/AuthButton.kt index 5f87db9..9c35c07 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/AuthButton.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/AuthButton.kt @@ -7,7 +7,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.auth.data.Auth +import com.mineinabyss.launchy.auth.data.identity.IdentityDataSource import com.mineinabyss.launchy.core.ui.components.PrimaryButton import kotlinx.coroutines.launch @@ -20,7 +20,7 @@ fun AuthButton() { enabled = state.profile.currentSession == null, onClick = { coroutineScope.launch { - Auth.authOrShowDialog(state, state.profile) + IdentityDataSource.authOrShowDialog(state, state.profile) } }, ) { diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/PlayButton.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/PlayButton.kt index ce9d1e7..97248b0 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/PlayButton.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/PlayButton.kt @@ -15,32 +15,21 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.core.ui.Dialog import com.mineinabyss.launchy.core.ui.components.PrimaryButtonColors import com.mineinabyss.launchy.core.ui.components.SecondaryButtonColors -import com.mineinabyss.launchy.core.ui.dialog -import com.mineinabyss.launchy.downloads.data.ModDownloader.startInstall -import com.mineinabyss.launchy.instance.data.GameInstanceDataSource -import com.mineinabyss.launchy.instance.data.Launcher import com.mineinabyss.launchy.instance.ui.GameInstanceState -import com.mineinabyss.launchy.instance_list.data.Instances.updateInstance -import com.mineinabyss.launchy.util.AppDispatchers -import com.mineinabyss.launchy.util.AppDispatchers.launchOrShowDialog -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import com.mineinabyss.launchy.instance.ui.InstanceUiState +import com.mineinabyss.launchy.launcher.ui.LauncherViewModel +import com.mineinabyss.launchy.util.koinViewModel @Composable fun PlayButton( hideText: Boolean = false, - instance: GameInstanceDataSource, + instance: InstanceUiState, modifier: Modifier = Modifier, - getModpackState: suspend () -> GameInstanceState?, + launcher: LauncherViewModel = koinViewModel() ) { - val state = LocalLaunchyState - val process = state.processFor(instance) - val coroutineScope = rememberCoroutineScope() - val buttonIcon by remember(state.profile.currentProfile, process) { + val buttonIcon by remember(instance, instance.runningProcess) { mutableStateOf( when { state.profile.currentProfile == null -> Icons.Rounded.PlayDisabled @@ -60,48 +49,7 @@ fun PlayButton( Box { var foundPackState: GameInstanceState? by remember { mutableStateOf(null) } val onClick: () -> Unit = { - coroutineScope.launch(Dispatchers.IO) { - val packState = foundPackState ?: getModpackState() ?: return@launch - foundPackState = packState - val updatesAvailable = packState.instance.updatesAvailable - - if (process == null) { - when { - // Assume this means not launched before - packState.queued.userAgreedModLoaders == null -> { - AppDispatchers.profileLaunch.launchOrShowDialog { - packState.startInstall(state).getOrThrow() - Launcher.launch(state, packState, state.profile) - } - } - - updatesAvailable -> { - dialog = Dialog.Options( - title = "Update Available", - message = buildString { - appendLine("This cloud instance has updates available.") - appendLine("Would you like to download them now?") - }, - acceptText = "Download", - declineText = "Ignore", - onAccept = { packState.instance.updateInstance(state) }, - onDecline = { } - ) - } - - else -> { - AppDispatchers.profileLaunch.launchOrShowDialog { - packState.startInstall(state).getOrThrow() - println("Launching now!") - Launcher.launch(state, packState, state.profile) - } - } - } - } else { - process.destroyForcibly() - state.setProcessFor(packState.instance, null) - } - } + launcher.launch(instance) } val enabled = state.profile.currentProfile != null && foundPackState?.downloads?.isDownloading != true diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/SettingsButton.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/SettingsButton.kt index da10acf..e6040d8 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/SettingsButton.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/SettingsButton.kt @@ -6,8 +6,8 @@ import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import com.mineinabyss.launchy.core.ui.Screen -import com.mineinabyss.launchy.core.ui.screen +import com.mineinabyss.launchy.core.ui.screens.Screen +import com.mineinabyss.launchy.core.ui.screens.screen @Composable fun SettingsButton() { diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/UpdateButton.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/UpdateButton.kt index 5274469..dd18257 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/UpdateButton.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/UpdateButton.kt @@ -9,7 +9,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.mineinabyss.launchy.LocalLaunchyState import com.mineinabyss.launchy.core.ui.LocalGameInstanceState -import com.mineinabyss.launchy.instance_list.data.Instances.updateInstance +import com.mineinabyss.launchy.instance_list.data.InstanceRepository.updateInstance @Composable fun UpdateButton() { diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModConfigOptions.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModConfigOptions.kt new file mode 100644 index 0000000..c6b5248 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModConfigOptions.kt @@ -0,0 +1,41 @@ +package com.mineinabyss.launchy.instance.ui.components.settings + +import androidx.compose.runtime.Composable + +@Composable +fun ModConfigOptions() { +// Row( +// verticalAlignment = Alignment.CenterVertically, +// modifier = Modifier +// .clickable { +// if (!mod.info.forceConfigDownload) state.toggles.setModConfigEnabled( +// mod, +// !configEnabled +// ) +// } +// .fillMaxWidth() +// ) { +// Spacer(Modifier.width(20.dp)) +// Checkbox( +// checked = configEnabled || mod.info.forceConfigDownload, +// onCheckedChange = { +// if (!mod.info.forceConfigDownload) state.toggles.setModConfigEnabled(mod, !configEnabled) +// }, +// enabled = !mod.info.forceConfigDownload, +// ) +// Column { +// Text( +// "Download our recommended configuration", +// style = MaterialTheme.typography.bodyMedium +// ) +// if (mod.info.configDesc.isNotEmpty()) { +// Spacer(Modifier.width(4.dp)) +// Text( +// mod.info.configDesc, +// style = MaterialTheme.typography.bodySmall, +// modifier = Modifier.alpha(0.5f) +// ) +// } +// } +// } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModGroup.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModGroup.kt index aa7e896..8cf4871 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModGroup.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModGroup.kt @@ -20,25 +20,25 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.core.ui.LocalGameInstanceState +import androidx.compose.ui.util.fastAny +import androidx.lifecycle.viewmodel.compose.viewModel import com.mineinabyss.launchy.core.ui.components.Tooltip +import com.mineinabyss.launchy.instance.ui.InstanceViewModel +import com.mineinabyss.launchy.instance.ui.ModGroupInteractions import com.mineinabyss.launchy.instance.ui.ModGroupUiState -import com.mineinabyss.launchy.instance.ui.ModListInteractions +import com.mineinabyss.launchy.instance.ui.ModQueueState @Composable fun ModGroup( group: ModGroupUiState, - interactions: ModListInteractions + interactions: ModGroupInteractions, + viewModel: InstanceViewModel = viewModel() ) { var expanded by remember { mutableStateOf(false) } val arrowRotationState by animateFloatAsState(targetValue = if (expanded) 180f else 0f) - val state = LocalGameInstanceState - -// val modsChanged = mods.any { -// it in state.queued.deletions || it in state.queued.newDownloads || it in state.queued.failures -// } - + val changesQueued = group.mods.fastAny { it.queueState != ModQueueState.NONE } val tonalElevation by animateDpAsState(if (expanded) 1.6.dp else 1.dp) + Column { Surface( tonalElevation = tonalElevation, @@ -63,7 +63,7 @@ fun ModGroup( group.title, Modifier.weight(1f), style = MaterialTheme.typography.bodyLarge, ) - AnimatedVisibility(modsChanged, enter = fadeIn(), exit = fadeOut()) { + AnimatedVisibility(changesQueued, enter = fadeIn(), exit = fadeOut()) { TooltipArea( tooltip = { Tooltip("Some mods in this group have pending changes") } ) { @@ -83,7 +83,8 @@ fun ModGroup( ) { Column { for (mod in group.mods) key(mod.id) { - ModInfoDisplay(group, mod) + val modInteractions = remember(mod.id) { viewModel.modInteractionsFor(mod.id) } + ModInfoDisplay(group, mod, modInteractions) } } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModInfoDisplay.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModInfoDisplay.kt index 7ab2c81..4cb05f9 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModInfoDisplay.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModInfoDisplay.kt @@ -4,12 +4,12 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.TooltipArea -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material.LinearProgressIndicator import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.OpenInNew -import androidx.compose.material.icons.rounded.* +import androidx.compose.material.icons.rounded.Info +import androidx.compose.material.icons.rounded.Settings import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -18,52 +18,50 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.scale import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.core.ui.LocalGameInstanceState import com.mineinabyss.launchy.core.ui.components.Tooltip -import com.mineinabyss.launchy.instance.data.ToggleMods.setModConfigEnabled -import com.mineinabyss.launchy.instance.data.ToggleMods.setModEnabled import com.mineinabyss.launchy.instance.ui.ModGroupUiState +import com.mineinabyss.launchy.instance.ui.ModInteractions import com.mineinabyss.launchy.instance.ui.ModQueueState import com.mineinabyss.launchy.instance.ui.ModUiState import com.mineinabyss.launchy.util.DesktopHelpers +import com.mineinabyss.launchy.util.Option @OptIn(ExperimentalFoundationApi::class) @Composable -fun ModInfoDisplay(group: ModGroupUiState, mod: ModUiState) { - val state = LocalGameInstanceState - val modEnabled by derivedStateOf { mod in state.toggles.enabledMods } - val configEnabled by derivedStateOf { mod in state.toggles.enabledConfigs } - var configExpanded by remember { mutableStateOf(false) } - val configTabState by animateFloatAsState(targetValue = if (configExpanded) 180f else 0f) - +fun ModInfoDisplay( + group: ModGroupUiState, + mod: ModUiState, + interactions: ModInteractions, +) { val surfaceColor = remember(mod.queueState) { ModQueueState.surfaceColor(mod.queueState) } val infoIcon = remember(mod.queueState) { ModQueueState.infoIcon(mod.queueState) } Surface( modifier = Modifier.fillMaxWidth(), color = surfaceColor, - onClick = { if (!group.forceEnabled && !group.forceDisabled) state.toggles.setModEnabled(mod, !modEnabled) } + onClick = { if (group.force == Option.DEFAULT) interactions.onToggleMod(!mod.enabled) } ) { - if (state.downloads.inProgressMods.containsKey(mod) || state.downloads.inProgressConfigs.containsKey(mod)) { - val modProgress = state.downloads.inProgressMods[mod] - val configProgress = state.downloads.inProgressConfigs[mod] - val downloaded = (modProgress?.bytesDownloaded ?: 0L) + (configProgress?.bytesDownloaded ?: 0L) - val total = (modProgress?.totalBytes ?: 0L) + (configProgress?.totalBytes ?: 0L) + if (mod.installProgress != null) { + val downloaded = mod.installProgress.bytesDownloaded + val total = mod.installProgress.totalBytes LinearProgressIndicator( progress = if (total == 0L) 0f else downloaded.toFloat() / total, color = MaterialTheme.colorScheme.primaryContainer ) } - Column() { + var configExpanded by remember { mutableStateOf(false) } + val configTabState by animateFloatAsState(targetValue = if (configExpanded) 180f else 0f) + + Column { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.padding(end = 8.dp) ) { Checkbox( - enabled = !group.forceEnabled && !group.forceDisabled, - checked = modEnabled, - onCheckedChange = { state.toggles.setModEnabled(mod, !modEnabled) } + enabled = group.force == Option.DEFAULT, + checked = mod.enabled, + onCheckedChange = { interactions.onToggleMod(!mod.enabled) } ) if (infoIcon != null) Icon(infoIcon, "Mod Information", modifier = Modifier.padding(end = 8.dp)) @@ -71,9 +69,7 @@ fun ModInfoDisplay(group: ModGroupUiState, mod: ModUiState) { Row(Modifier.weight(6f)) { Text(mod.info.name, style = MaterialTheme.typography.bodyLarge) // build list of mods that are incompatible with this mod - val incompatibleMods = state.modpack.mods.mods - .filter { !mod.compatibleWith(it) } - .map { it.info.name } + val incompatibleMods = mod.incompatibleWith if (mod.info.requires.isNotEmpty() || incompatibleMods.isNotEmpty()) { TooltipArea( modifier = Modifier.alpha(0.5f), @@ -126,12 +122,7 @@ fun ModInfoDisplay(group: ModGroupUiState, mod: ModUiState) { TooltipArea( modifier = Modifier.alpha(0.5f), tooltip = { - Tooltip { - Text( - text = "Open homepage", - style = MaterialTheme.typography.labelMedium - ) - } + Tooltip("Open homepage") } ) { IconButton(onClick = { DesktopHelpers.browse(mod.info.homepage) }) { @@ -143,42 +134,12 @@ fun ModInfoDisplay(group: ModGroupUiState, mod: ModUiState) { } } } + AnimatedVisibility(configExpanded) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clickable { - if (!mod.info.forceConfigDownload) state.toggles.setModConfigEnabled( - mod, - !configEnabled - ) - } - .fillMaxWidth() - ) { - Spacer(Modifier.width(20.dp)) - Checkbox( - checked = configEnabled || mod.info.forceConfigDownload, - onCheckedChange = { - if (!mod.info.forceConfigDownload) state.toggles.setModConfigEnabled(mod, !configEnabled) - }, - enabled = !mod.info.forceConfigDownload, - ) - Column { - Text( - "Download our recommended configuration", - style = MaterialTheme.typography.bodyMedium - ) - if (mod.info.configDesc.isNotEmpty()) { - Spacer(Modifier.width(4.dp)) - Text( - mod.info.configDesc, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.alpha(0.5f) - ) - } - } - } + ModConfigOptions() } } } } + + diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/InfoBar.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/InfoBar.kt index 21b8641..d52dcd5 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/InfoBar.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/InfoBar.kt @@ -28,7 +28,7 @@ fun InfoBar( instance: InstanceViewModel = viewModel(), modifier: Modifier = Modifier ) { - val queuedState by instance.installQueueState.collectAsState() + val queuedState by instance._installQueueState.collectAsState() val queue = when (queuedState) { is InstallState.Queued -> queuedState as InstallState.Queued else -> return diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceScreen.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceScreen.kt index 67eff4c..8c67fb5 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceScreen.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceScreen.kt @@ -4,26 +4,31 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.core.ui.LocalGameInstanceState import com.mineinabyss.launchy.core.ui.windowScope +import com.mineinabyss.launchy.instance.ui.InstanceViewModel import com.mineinabyss.launchy.instance.ui.components.BackgroundImage import com.mineinabyss.launchy.instance.ui.components.LogoLarge import com.mineinabyss.launchy.instance.ui.components.buttons.PlayButton import com.mineinabyss.launchy.instance.ui.components.buttons.SettingsButton import com.mineinabyss.launchy.instance.ui.components.buttons.UpdateButton +import com.mineinabyss.launchy.util.koinViewModel @ExperimentalComposeUiApi @Preview @Composable -fun InstanceScreen() { - val packState = LocalGameInstanceState +fun InstanceScreen( + viewModel: InstanceViewModel = koinViewModel(), +) { + val instance by viewModel.instanceUiState.collectAsState() Box { - BackgroundImage(windowScope) + BackgroundImage(instance?.background, windowScope) Column( modifier = @@ -33,13 +38,13 @@ fun InstanceScreen() { verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { - LogoLarge(Modifier.weight(3f, false)) + LogoLarge(instance?.logo, Modifier.weight(3f, false)) Row( horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().weight(1f, false), ) { - PlayButton(hideText = false, packState.instance) { packState } + PlayButton(hideText = false, instance) { packState } AnimatedVisibility(packState.instance.updatesAvailable) { UpdateButton() } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/ModManagementTab.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/ModManagementTab.kt index 7b57b74..1ebc851 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/ModManagementTab.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/ModManagementTab.kt @@ -18,9 +18,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.mineinabyss.launchy.core.ui.Constants -import com.mineinabyss.launchy.core.ui.LocalGameInstanceState import com.mineinabyss.launchy.instance.ui.InstanceViewModel -import com.mineinabyss.launchy.instance.ui.ModGroupUiState import com.mineinabyss.launchy.instance.ui.ModListUiState import com.mineinabyss.launchy.instance.ui.components.settings.ModGroup import com.mineinabyss.launchy.instance.ui.components.settings.infobar.InfoBar @@ -29,23 +27,10 @@ import com.mineinabyss.launchy.instance.ui.components.settings.infobar.InfoBar fun ModManagementTab( instance: InstanceViewModel = viewModel() ) { - val state = LocalGameInstanceState Scaffold( containerColor = Color.Transparent, bottomBar = { InfoBar() }, ) { paddingValues -> -// val userMods by remember { -// mutableStateOf( -// state.instance.userMods.listDirectoryEntries("*.jar").map { -// Mod( -// downloadDir = it, -// modId = it.fileName.toString(), -// info = ModConfig(name = it.fileName.toString()), -// desiredHashes = null -// ) -// } -// ) -// } val modsState by instance.modsState.collectAsState() val groups = when (modsState) { is ModListUiState.Loading -> { @@ -71,17 +56,15 @@ fun ModManagementTab( LazyColumn(Modifier.fillMaxSize().padding(end = 12.dp), lazyListState) { item { Spacer(Modifier.height(4.dp)) } items(groups) { group -> - ModGroup(group) + val groupInteractions = instance.groupInteractionsFor(group.id) + ModGroup(group, groupInteractions) } - if (userMods.isNotEmpty()) item { - ModGroup( - ModGroupUiState( - title = "User mods", - enabled = true, - forceEnabled = true, - mods = userMods, - ) - ) + // TODO probably worth just merging into groups + userMods?.let { + item { + val groupInteractions = instance.groupInteractionsFor(it.id) + ModGroup(it, groupInteractions) + } } } VerticalScrollbar( diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/OptionsTab.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/OptionsTab.kt index 71ac4f3..3776df7 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/OptionsTab.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/OptionsTab.kt @@ -13,10 +13,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.LocalLaunchyState import com.mineinabyss.launchy.core.ui.LocalGameInstanceState -import com.mineinabyss.launchy.core.ui.Screen import com.mineinabyss.launchy.core.ui.components.ComfyContent import com.mineinabyss.launchy.core.ui.components.TitleSmall -import com.mineinabyss.launchy.core.ui.screen +import com.mineinabyss.launchy.core.ui.screens.Screen +import com.mineinabyss.launchy.core.ui.screens.screen import com.mineinabyss.launchy.util.AppDispatchers import com.mineinabyss.launchy.util.DesktopHelpers import com.mineinabyss.launchy.util.InProgressTask diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/NewInstance.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/NewInstance.kt index ec01944..ca3c8b6 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/NewInstance.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/NewInstance.kt @@ -17,15 +17,15 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.core.ui.Screen import com.mineinabyss.launchy.core.ui.components.AnimatedTab import com.mineinabyss.launchy.core.ui.components.ComfyContent import com.mineinabyss.launchy.core.ui.components.ComfyTitle import com.mineinabyss.launchy.core.ui.components.ComfyWidth -import com.mineinabyss.launchy.core.ui.screen +import com.mineinabyss.launchy.core.ui.screens.Screen +import com.mineinabyss.launchy.core.ui.screens.screen import com.mineinabyss.launchy.downloads.data.Downloader -import com.mineinabyss.launchy.instance.data.GameInstanceConfig import com.mineinabyss.launchy.instance.data.GameInstanceDataSource +import com.mineinabyss.launchy.instance.data.storage.InstanceConfig import com.mineinabyss.launchy.instance.ui.screens.InstanceProperties import com.mineinabyss.launchy.instance_list.ui.components.InstanceCard import com.mineinabyss.launchy.util.AppDispatchers @@ -111,7 +111,7 @@ fun ImportTab(visible: Boolean, onGetInstance: (GameInstanceDataSource.CloudInst is Downloader.DownloadResult.Success -> { GameInstanceDataSource.CloudInstanceWithHeaders( - config = GameInstanceConfig.read(downloadPath) + config = InstanceConfig.read(downloadPath) .showDialogOnError("Failed to read cloud instance") .getOrThrow(), url = urlText, diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/InstanceRepository.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/InstanceRepository.kt new file mode 100644 index 0000000..64c4648 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/InstanceRepository.kt @@ -0,0 +1,66 @@ +package com.mineinabyss.launchy.instance_list.data + +import com.mineinabyss.launchy.core.data.FileSystemDataSource +import com.mineinabyss.launchy.core.data.TasksRepository +import com.mineinabyss.launchy.core.ui.screens.Screen +import com.mineinabyss.launchy.core.ui.screens.screen +import com.mineinabyss.launchy.instance.data.InstanceModel +import com.mineinabyss.launchy.util.AppDispatchers +import com.mineinabyss.launchy.util.InProgressTask +import com.mineinabyss.launchy.util.InstanceKey +import com.mineinabyss.launchy.util.showDialogOnError +import io.ktor.http.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.withContext + +class InstanceRepository( + val local: LocalInstancesDataSource, + val remote: RemoteInstanceDataSource, + val tasks: TasksRepository, + val files: FileSystemDataSource +) { + private val _instances = MutableStateFlow(mapOf()) + + val instances = _instances.asStateFlow() + + suspend fun delete( + key: InstanceKey, + deleteMinecraftDir: Boolean + ) = withContext(AppDispatchers.IO) { + val instance = _instances.value[key] ?: return@withContext + tasks.run("deleteInstance", InProgressTask("Deleting instance ${instance.config.name}")) { + local.deleteInstance(instance, deleteMinecraftDir) + _instances.update { it.minus(key) } + } + } + + suspend fun fetchCloudInstanceUpdates( + key: InstanceKey + ) = withContext(AppDispatchers.IO) { + val instance = _instances.value[key] ?: return@withContext + screen = Screen.Default +// TODO enabled = false + tasks.run("updateInstance", InProgressTask("Updating instance: ${instance.config.name}")) { + val cloudUrl = instance.config.cloudInstanceURL ?: return@withContext + remote.fetchUpdatesForInstance(instance.config, Url(cloudUrl)) + .mapCatching { merged -> + val model = instance.copy(config = merged) + local.saveInstance(model) + model + } + .showDialogOnError("Failed to update instance ${instance.config.name}") + .onSuccess { merged -> + _instances.update { it + (key to merged) } + } + } + } + +// suspend fun saveOnChanges() { +// instances.distinctUntilChanged { old, new -> }.collectLatest { instances -> +// instances +// files.scheduleWrite() +// } +// } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/Instances.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/Instances.kt deleted file mode 100644 index 42a99ca..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/Instances.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.mineinabyss.launchy.instance_list.data - -import com.mineinabyss.launchy.core.ui.LaunchyState -import com.mineinabyss.launchy.core.ui.Screen -import com.mineinabyss.launchy.core.ui.screen -import com.mineinabyss.launchy.downloads.data.Downloader -import com.mineinabyss.launchy.instance.data.GameInstanceConfig -import com.mineinabyss.launchy.instance.data.GameInstanceDataSource -import com.mineinabyss.launchy.util.AppDispatchers -import com.mineinabyss.launchy.util.Dirs -import com.mineinabyss.launchy.util.InProgressTask -import com.mineinabyss.launchy.util.showDialogOnError -import kotlinx.coroutines.launch -import kotlin.io.path.* - -object Instances { - @OptIn(ExperimentalPathApi::class) - fun GameInstanceDataSource.delete(state: LaunchyState, deleteDotMinecraft: Boolean) { - state.gameInstances.remove(this) - state.runTask("deleteInstance", InProgressTask("Deleting instance ${config.name}")) { - AppDispatchers.IO.launch { - if (deleteDotMinecraft) minecraftDir.deleteRecursively() - configDir.deleteRecursively() - } - } - } - - fun GameInstanceDataSource.updateInstance( - state: LaunchyState, - onSuccess: () -> Unit = {}, - ) { - screen = Screen.Default - enabled = false - val index = state.gameInstances.indexOf(this) - AppDispatchers.IO.launch { - state.runTask("updateInstance", InProgressTask("Updating instance: ${config.name}")) { - val cloudUrl = config.cloudInstanceURL - if (cloudUrl != null) { - val newCloudInstancePath = Dirs.createTempCloudInstanceFile() - Downloader.download( - cloudUrl, newCloudInstancePath, Downloader.Options( - saveModifyHeadersFor = this@updateInstance - ) - ) - instanceFile.copyTo(configDir / "instance-backup.yml", overwrite = true) - GameInstanceConfig.read(newCloudInstancePath).onSuccess { cloudConfig -> - instanceFile.deleteIfExists() - instanceFile.createFile() - config.copy( - description = cloudConfig.description, - backgroundURL = cloudConfig.backgroundURL, - logoURL = cloudConfig.logoURL, - hue = cloudConfig.hue, - source = cloudConfig.source, - ).saveTo(instanceFile) - } - } - - // Handle case where we just updated from cloud - val config = GameInstanceConfig.read(instanceFile).getOrElse { config } - - config.source.updateInstance(this@updateInstance) - .showDialogOnError("Failed to update instance ${config.name}") - .onFailure { it.printStackTrace() } - .onSuccess { - state.gameInstances[index] = it - onSuccess() - } - } - } - } -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/LocalInstancesDataSource.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/LocalInstancesDataSource.kt index f689ac7..f547cd1 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/LocalInstancesDataSource.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/LocalInstancesDataSource.kt @@ -1,19 +1,58 @@ package com.mineinabyss.launchy.instance_list.data -import com.mineinabyss.launchy.instance.data.GameInstanceDataSource +import com.charleskorn.kaml.decodeFromStream +import com.charleskorn.kaml.encodeToStream +import com.mineinabyss.launchy.downloads.data.Downloader +import com.mineinabyss.launchy.instance.data.InstanceModList +import com.mineinabyss.launchy.instance.data.InstanceModel +import com.mineinabyss.launchy.instance.data.storage.InstanceConfig +import com.mineinabyss.launchy.util.Formats import java.nio.file.Path -import kotlin.io.path.isDirectory -import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.* class LocalInstancesDataSource( val rootDir: Path, ) { - fun getInstanceList(): List = rootDir + fun getInstanceList(): List = rootDir .listDirectoryEntries() .filter { it.isDirectory() } - .mapNotNull { - runCatching { GameInstanceDataSource(it) } + .mapNotNull { dir -> + readInstance(dir / "instance.yml") .onFailure { it.printStackTrace() } .getOrNull() } + + fun readInstance(path: Path): Result = runCatching { + InstanceModel(Formats.yaml.decodeFromStream(path.inputStream()), path.parent) + } + + fun saveInstance(instance: InstanceModel) { + val file = instance.instanceFile + if (file.exists()) { + file.copyTo(instance.backupInstanceFile, overwrite = true) + } else { + file.createFile() + } + Formats.yaml.encodeToStream(instance, file.outputStream()) + } + + @OptIn(ExperimentalPathApi::class) + fun deleteInstance( + instance: InstanceModel, + deleteMinecraftDir: Boolean + ) { + if (deleteMinecraftDir) instance.minecraftDir.deleteRecursively() + instance.directory.deleteRecursively() + } + + fun loadModList(instance: InstanceModel): InstanceModList { + TODO() + } + + data class CloudInstanceWithHeaders( + val config: InstanceConfig, + val url: String, + val headers: Downloader.ModifyHeaders, + ) + } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/RemoteInstanceDataSource.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/RemoteInstanceDataSource.kt new file mode 100644 index 0000000..8b0a2d4 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/RemoteInstanceDataSource.kt @@ -0,0 +1,30 @@ +package com.mineinabyss.launchy.instance_list.data + +import com.mineinabyss.launchy.downloads.data.Downloader +import com.mineinabyss.launchy.instance.data.storage.InstanceConfig +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.http.* + +class RemoteInstanceDataSource { + suspend fun getRemoteInstance( + url: Url + ): Result = runCatching { + //TODO cache by headers + Downloader.httpClient.get(url).body() +// source.read(newCloudInstancePath).onSuccess { cloudConfig -> + } + + suspend fun fetchUpdatesForInstance( + instance: InstanceConfig, + url: Url, + ): Result = getRemoteInstance(url).mapCatching { remote -> + instance.copy( + description = remote.description, + backgroundURL = remote.backgroundURL, + logoURL = remote.logoURL, + hue = remote.hue, + source = remote.source, + ) + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/AddNewModpackCard.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/AddNewModpackCard.kt index 4bb2872..fd5eedc 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/AddNewModpackCard.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/AddNewModpackCard.kt @@ -15,8 +15,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.core.ui.Screen -import com.mineinabyss.launchy.core.ui.screen +import com.mineinabyss.launchy.core.ui.screens.Screen +import com.mineinabyss.launchy.core.ui.screens.screen @Composable fun AddNewModpackCard(modifier: Modifier = Modifier) { diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceCard.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceCard.kt index e918154..9ec6917 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceCard.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceCard.kt @@ -22,13 +22,13 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.core.ui.Screen import com.mineinabyss.launchy.core.ui.components.Tooltip -import com.mineinabyss.launchy.core.ui.screen +import com.mineinabyss.launchy.core.ui.screens.Screen +import com.mineinabyss.launchy.core.ui.screens.screen import com.mineinabyss.launchy.core.ui.theme.LaunchyColors import com.mineinabyss.launchy.core.ui.theme.currentHue -import com.mineinabyss.launchy.instance.data.GameInstanceConfig import com.mineinabyss.launchy.instance.data.GameInstanceDataSource +import com.mineinabyss.launchy.instance.data.storage.InstanceConfig import com.mineinabyss.launchy.instance.ui.components.SlightBackgroundTint import com.mineinabyss.launchy.instance.ui.components.buttons.PlayButton import com.mineinabyss.launchy.instance_list.ui.components.InstanceCardStyle.cardHeight @@ -45,7 +45,7 @@ object InstanceCardStyle { @Composable fun InstanceCard( - config: GameInstanceConfig, + config: InstanceConfig, instance: GameInstanceDataSource? = null, modifier: Modifier = Modifier ) = MaterialTheme( diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/Launcher.kt b/src/main/kotlin/com/mineinabyss/launchy/launcher/data/Launcher.kt similarity index 91% rename from src/main/kotlin/com/mineinabyss/launchy/instance/data/Launcher.kt rename to src/main/kotlin/com/mineinabyss/launchy/launcher/data/Launcher.kt index 8afde45..15ff898 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/Launcher.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/launcher/data/Launcher.kt @@ -1,10 +1,10 @@ -package com.mineinabyss.launchy.instance.data +package com.mineinabyss.launchy.launcher.data -import com.mineinabyss.launchy.auth.data.Auth -import com.mineinabyss.launchy.auth.ui.ProfileState +import com.mineinabyss.launchy.auth.data.ProfileRepository import com.mineinabyss.launchy.core.ui.Dialog -import com.mineinabyss.launchy.core.ui.LaunchyState -import com.mineinabyss.launchy.core.ui.dialog +import com.mineinabyss.launchy.core.ui.LaunchyUiState +import com.mineinabyss.launchy.core.ui.screens.dialog +import com.mineinabyss.launchy.instance.data.InstanceModLoaders import com.mineinabyss.launchy.instance.ui.GameInstanceState import com.mineinabyss.launchy.util.AppDispatchers import kotlinx.coroutines.Job @@ -31,7 +31,8 @@ import kotlin.io.path.notExists object Launcher { - suspend fun launch(state: LaunchyState, pack: GameInstanceState, profile: ProfileState): Unit = coroutineScope { + suspend fun launch(state: LaunchyUiState, pack: GameInstanceState, profile: ProfileRepository): Unit = + coroutineScope { val dir = MinecraftDirectory(pack.instance.minecraftDir.toFile()) val launcher = LauncherBuilder.buildDefault() val javaPath = state.jvm.javaPath @@ -42,7 +43,7 @@ object Launcher { state.lastPlayed[pack.instance.config.name] = Date().time // Auth or show dialog when (val session = profile.currentSession) { - null -> Auth.authOrShowDialog(state, profile) { + null -> Authenticator.authOrShowDialog(state, profile) { launch { launch(state, pack, profile) } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/launcher/data/ProcessRepository.kt b/src/main/kotlin/com/mineinabyss/launchy/launcher/data/ProcessRepository.kt new file mode 100644 index 0000000..2b2ef36 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/launcher/data/ProcessRepository.kt @@ -0,0 +1,13 @@ +package com.mineinabyss.launchy.launcher.data + +import com.mineinabyss.launchy.instance.data.GameInstanceDataSource + +class ProcessRepository { + private val launchedProcesses = mutableMapOf() + + fun processFor(instance: GameInstanceDataSource): Process? = launchedProcesses[instance.minecraftDir.toString()] + fun setProcessFor(instance: GameInstanceDataSource, process: Process?) { + if (process == null) launchedProcesses.remove(instance.minecraftDir.toString()) + else launchedProcesses[instance.minecraftDir.toString()] = process + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/launcher/ui/LauncherViewModel.kt b/src/main/kotlin/com/mineinabyss/launchy/launcher/ui/LauncherViewModel.kt new file mode 100644 index 0000000..137b5c7 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/launcher/ui/LauncherViewModel.kt @@ -0,0 +1,59 @@ +package com.mineinabyss.launchy.launcher.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mineinabyss.launchy.core.ui.Dialog +import com.mineinabyss.launchy.core.ui.screens.dialog +import com.mineinabyss.launchy.downloads.data.ModDownloader.startInstall +import com.mineinabyss.launchy.instance.ui.InstanceUiState +import com.mineinabyss.launchy.launcher.data.Launcher +import com.mineinabyss.launchy.util.AppDispatchers +import com.mineinabyss.launchy.util.AppDispatchers.launchOrShowDialog +import kotlinx.coroutines.launch + +class LauncherViewModel : ViewModel() { + fun launch(instance: InstanceUiState) { + viewModelScope.launch(AppDispatchers.IO) { + val packState = foundPackState ?: getModpackState() ?: return@launch + foundPackState = packState + val updatesAvailable = packState.instance.updatesAvailable + + if (process == null) { + when { + // Assume this means not launched before + packState.queued.userAgreedModLoaders == null -> { + AppDispatchers.profileLaunch.launchOrShowDialog { + packState.startInstall(state).getOrThrow() + Launcher.launch(state, packState, state.profile) + } + } + + updatesAvailable -> { + dialog = Dialog.Options( + title = "Update Available", + message = buildString { + appendLine("This cloud instance has updates available.") + appendLine("Would you like to download them now?") + }, + acceptText = "Download", + declineText = "Ignore", + onAccept = { packState.instance.updateInstance(state) }, + onDecline = { } + ) + } + + else -> { + AppDispatchers.profileLaunch.launchOrShowDialog { + packState.startInstall(state).getOrThrow() + println("Launching now!") + Launcher.launch(state, packState, state.profile) + } + } + } + } else { + process.destroyForcibly() + state.setProcessFor(packState.instance, null) + } + } + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/JvmState.kt b/src/main/kotlin/com/mineinabyss/launchy/settings/ui/JVMSettingsViewModel.kt similarity index 90% rename from src/main/kotlin/com/mineinabyss/launchy/core/ui/JvmState.kt rename to src/main/kotlin/com/mineinabyss/launchy/settings/ui/JVMSettingsViewModel.kt index 7a39ebb..ca7d8fe 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/core/ui/JvmState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/settings/ui/JVMSettingsViewModel.kt @@ -1,16 +1,17 @@ -package com.mineinabyss.launchy.core.ui +package com.mineinabyss.launchy.settings.ui import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel import com.mineinabyss.launchy.config.data.Config import com.mineinabyss.launchy.util.SuggestedJVMArgs import kotlin.io.path.Path -class JvmState( +class JVMSettingsViewModel( val config: Config -) { +) : ViewModel() { var javaPath by mutableStateOf(config.javaPath?.let { Path(it) }) var userMemoryAllocation by mutableStateOf(config.memoryAllocation) var userJvmArgs by mutableStateOf(config.jvmArguments) diff --git a/src/main/kotlin/com/mineinabyss/launchy/settings/ui/SettingsScreen.kt b/src/main/kotlin/com/mineinabyss/launchy/settings/ui/SettingsScreen.kt index cf4e31b..ab00165 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/settings/ui/SettingsScreen.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/settings/ui/SettingsScreen.kt @@ -14,16 +14,19 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.LocalLaunchyState +import com.mineinabyss.launchy.core.ui.LocalUiState import com.mineinabyss.launchy.core.ui.components.* import com.mineinabyss.launchy.util.DesktopHelpers import com.mineinabyss.launchy.util.Dirs import com.mineinabyss.launchy.util.SuggestedJVMArgs +import com.mineinabyss.launchy.util.koinViewModel @Composable @Preview -fun SettingsScreen() { - val state = LocalLaunchyState +fun SettingsScreen( + jvm: JVMSettingsViewModel = koinViewModel(), +) { + val ui = LocalUiState.current val scrollState = rememberScrollState() Column { ComfyTitle("Settings") @@ -38,16 +41,16 @@ fun SettingsScreen() { Column { Row(verticalAlignment = Alignment.CenterVertically) { Checkbox( - state.ui.fullscreen, - onCheckedChange = { state.ui.fullscreen = it } + ui.fullscreen, + onCheckedChange = { ui.fullscreen = it } ) Text("Fullscreen mode") } } Setting("Hue", icon = { Icon(Icons.Rounded.Colorize, contentDescription = "Hue") }) { Slider( - value = state.ui.preferHue, - onValueChange = { state.ui.preferHue = it }, + value = ui.preferHue, + onValueChange = { ui.preferHue = it }, valueRange = 0f..1f, modifier = Modifier.weight(1f) ) @@ -79,7 +82,7 @@ fun SettingsScreen() { title = "Choose java executable", onCloseRequest = { if (it != null) { - state.jvm.javaPath = it + jvm.javaPath = it } directoryPickerShown = false }, @@ -89,7 +92,7 @@ fun SettingsScreen() { Setting("Java path") { OutlinedTextField( - value = state.jvm.javaPath?.toString() ?: "No path selected", + value = jvm.javaPath?.toString() ?: "No path selected", readOnly = true, singleLine = true, leadingIcon = { Icon(Icons.Rounded.Folder, contentDescription = "Link") }, @@ -105,11 +108,11 @@ fun SettingsScreen() { Setting("Memory", icon = { Icon(Icons.Rounded.Memory, "Memory icon") }) { Row(verticalAlignment = Alignment.CenterVertically) { - val memory = state.jvm.userMemoryAllocation ?: SuggestedJVMArgs.memory + val memory = jvm.userMemoryAllocation ?: SuggestedJVMArgs.memory Slider( value = memory.toFloat(), - onValueChange = { state.jvm.userMemoryAllocation = it.toInt() }, + onValueChange = { jvm.userMemoryAllocation = it.toInt() }, valueRange = 1024f..8192f, steps = 13, modifier = Modifier.weight(1f) @@ -119,7 +122,7 @@ fun SettingsScreen() { keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Number ), - onValueChange = { state.jvm.userMemoryAllocation = it.toIntOrNull() ?: memory }, + onValueChange = { jvm.userMemoryAllocation = it.toIntOrNull() ?: memory }, label = { Text("Memory (MB)") }, modifier = Modifier.widthIn(120.dp) ) @@ -127,13 +130,13 @@ fun SettingsScreen() { } Setting("JVM arguments") { - AnimatedVisibility(!state.jvm.useRecommendedJvmArgs) { + AnimatedVisibility(!jvm.useRecommendedJvmArgs) { OutlinedTextField( - value = state.jvm.userJvmArgs ?: "", - enabled = !state.jvm.useRecommendedJvmArgs, + value = jvm.userJvmArgs ?: "", + enabled = !jvm.useRecommendedJvmArgs, singleLine = false, leadingIcon = { Icon(Icons.Rounded.Code, contentDescription = "") }, - onValueChange = { state.jvm.userJvmArgs = it }, + onValueChange = { jvm.userJvmArgs = it }, label = { Text("Custom JVM arguments") }, modifier = Modifier.fillMaxWidth() ) @@ -141,15 +144,15 @@ fun SettingsScreen() { Row(verticalAlignment = Alignment.CenterVertically) { Checkbox( - !state.jvm.useRecommendedJvmArgs, - onCheckedChange = { state.jvm.useRecommendedJvmArgs = !it }) + !jvm.useRecommendedJvmArgs, + onCheckedChange = { jvm.useRecommendedJvmArgs = !it }) Text("Use custom JVM arguments") } Spacer(Modifier.height(16.dp)) OutlinedTextField( - value = state.jvm.jvmArgs, + value = jvm.jvmArgs, enabled = false, singleLine = false, readOnly = true, diff --git a/src/main/kotlin/com/mineinabyss/launchy/util/AppDispatchers.kt b/src/main/kotlin/com/mineinabyss/launchy/util/AppDispatchers.kt index be00afa..eb6985b 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/util/AppDispatchers.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/AppDispatchers.kt @@ -1,7 +1,7 @@ package com.mineinabyss.launchy.util import com.mineinabyss.launchy.core.ui.Dialog -import com.mineinabyss.launchy.core.ui.dialog +import com.mineinabyss.launchy.core.ui.screens.dialog import kotlinx.coroutines.* import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext diff --git a/src/main/kotlin/com/mineinabyss/launchy/util/Helpers.kt b/src/main/kotlin/com/mineinabyss/launchy/util/Helpers.kt index a338ff8..ca77d49 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/util/Helpers.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/Helpers.kt @@ -1,11 +1,14 @@ package com.mineinabyss.launchy.util import com.mineinabyss.launchy.core.ui.Dialog -import com.mineinabyss.launchy.core.ui.dialog +import com.mineinabyss.launchy.core.ui.screens.dialog import java.util.* fun Result.showDialogOnError(title: String? = null): Result { - onFailure { dialog = Dialog.fromException(it, title) } + onFailure { + dialog = Dialog.fromException(it, title) + it.printStackTrace() + } return this } diff --git a/src/main/kotlin/com/mineinabyss/launchy/util/KoinViewModel.kt b/src/main/kotlin/com/mineinabyss/launchy/util/KoinViewModel.kt new file mode 100644 index 0000000..fb09018 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/util/KoinViewModel.kt @@ -0,0 +1,12 @@ +package com.mineinabyss.launchy.util + +import androidx.compose.runtime.Composable +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewmodel.compose.viewModel +import org.koin.compose.currentKoinScope + +@Composable +inline fun koinViewModel(): T { + val scope = currentKoinScope() + return viewModel { scope.get() } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/util/Tasks.kt b/src/main/kotlin/com/mineinabyss/launchy/util/Tasks.kt deleted file mode 100644 index c6ea1e5..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/util/Tasks.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.mineinabyss.launchy.util - -object Tasks { - val installModLoadersId = "installMCAndModLoaders" - val copyOverridesId = "copyOverrides" -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/util/Typealiases.kt b/src/main/kotlin/com/mineinabyss/launchy/util/Typealiases.kt index 182e325..8ec357f 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/util/Typealiases.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/Typealiases.kt @@ -1,5 +1,7 @@ package com.mineinabyss.launchy.util +import java.util.* + typealias ModName = String typealias GroupName = String typealias DownloadURL = String @@ -7,3 +9,7 @@ typealias ConfigURL = String typealias ModID = String + + +@JvmInline +value class InstanceKey(val key: UUID) From da413610d71f51f3d6744f171024ef90fb005d42 Mon Sep 17 00:00:00 2001 From: Danielle Voznyy Date: Sun, 2 Jun 2024 00:09:32 -0400 Subject: [PATCH 4/5] refactor: Migrate downloading and unpacking different modpack formats, lots of other components --- .../mineinabyss/launchy/auth/ui/AuthDialog.kt | 2 +- .../launchy/auth/ui/ProfileViewModel.kt | 3 +- .../mineinabyss/launchy/config/data/Config.kt | 3 +- .../launchy/config/data/ConfigRepository.kt | 21 ++++ .../launchy/core/ui/LaunchyViewModel.kt | 20 +--- .../{TopBar.kt => topbar/AppTopBar.kt} | 62 +--------- .../ui/components/topbar/LaunchyHeadline.kt | 31 +++++ .../core/ui/components/topbar/LaunchyTitle.kt | 27 +++++ .../core/ui/components/topbar/WindowButton.kt | 24 ++++ .../launchy/core/ui/screens/Screens.kt | 14 ++- .../downloads/data/DownloadQueueState.kt | 109 ++++++++---------- .../launchy/downloads/data/DownloadState.kt | 2 +- .../launchy/downloads/data/Downloader.kt | 12 +- .../launchy/downloads/data/ModDownloader.kt | 8 +- .../downloads/data/formats/ExtraInfoFormat.kt | 67 +++++++++++ .../launchy/downloads/data/formats/Hashes.kt | 9 ++ .../data/formats/LaunchyPackFormat.kt | 51 ++++++++ .../launchy/downloads/data/formats/Mod.kt | 19 +++ .../data/formats/ModDownloadPath.kt | 2 +- .../launchy/downloads/data/formats/Modpack.kt | 11 ++ .../downloads/data/formats/ModpackFormat.kt | 9 ++ .../data/formats/ModrinthPackFormat.kt | 69 +++++++++++ .../data/formats/SerializedPackFormat.kt | 27 +++++ .../downloads/data/source/PackSource.kt | 60 ---------- .../launchy/downloads/data/source/PackType.kt | 60 ---------- .../data/sources/LocalModpackDataSource.kt | 9 ++ .../data/sources/ModpackDataSource.kt | 10 ++ .../data/sources/SerializedDownloadSource.kt | 22 ++++ .../data/sources/URLModpackDataSource.kt | 19 +++ .../launchy/instance/data/InstanceModel.kt | 4 + .../mineinabyss/launchy/instance/data/Mod.kt | 23 ---- .../{InstanceModList.kt => ModListModel.kt} | 21 ++-- ...nstanceModLoaders.kt => ModLoaderModel.kt} | 2 +- .../launchy/instance/data/Modpack.kt | 9 -- .../instance/data/formats/ExtraInfoFormat.kt | 35 ------ .../instance/data/formats/ExtraPackInfo.kt | 16 --- .../data/formats/LaunchyPackFormat.kt | 40 ------- .../data/formats/ModrinthPackFormat.kt | 54 --------- .../instance/data/formats/PackFormat.kt | 13 --- .../instance/data/storage/InstanceConfig.kt | 6 +- .../data/storage/InstanceUserConfig.kt | 4 +- .../instance/data/storage/ModConfig.kt | 2 +- .../launchy/instance/ui/ButtonInteractions.kt | 5 + .../launchy/instance/ui/InstallState.kt | 2 +- .../launchy/instance/ui/InstanceUiState.kt | 6 + .../launchy/instance/ui/InstanceViewModel.kt | 33 +++--- .../ui/components/ImportSettingsDialog.kt | 59 ---------- .../ui/components/buttons/AuthButton.kt | 30 ----- .../ui/components/buttons/InstallButton.kt | 36 +----- .../ui/components/buttons/PlayButton.kt | 31 ++--- .../components/buttons/RetryFailedButton.kt | 23 ++++ .../ui/components/buttons/UpdateButton.kt | 11 +- .../ui/components/buttons/UpdateInfoButton.kt | 77 ------------- .../ui/components/settings/TripleSwitch.kt | 8 +- .../settings/infobar/ActionButton.kt | 1 + .../ui/components/settings/infobar/InfoBar.kt | 19 ++- .../instance/ui/screens/InstanceScreen.kt | 18 +-- .../data/InstanceCardInteractions.kt | 6 + .../instance_list/data/InstanceRepository.kt | 35 ++++-- .../data/LocalInstancesDataSource.kt | 10 +- .../data/RemoteInstanceDataSource.kt | 1 - .../instance_list/ui/InstanceListScreen.kt | 27 +---- .../instance_list/ui/InstanceListViewModel.kt | 42 ++++++- .../ui/components/InstanceCard.kt | 62 ++++------ .../ui/components/InstanceList.kt | 24 ++-- .../launchy/launcher/data/Launcher.kt | 4 +- .../com/mineinabyss/launchy/util/Dirs.kt | 4 +- .../mineinabyss/launchy/util/Typealiases.kt | 6 +- 68 files changed, 754 insertions(+), 837 deletions(-) create mode 100644 src/main/kotlin/com/mineinabyss/launchy/config/data/ConfigRepository.kt rename src/main/kotlin/com/mineinabyss/launchy/core/ui/components/{TopBar.kt => topbar/AppTopBar.kt} (66%) create mode 100644 src/main/kotlin/com/mineinabyss/launchy/core/ui/components/topbar/LaunchyHeadline.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/core/ui/components/topbar/LaunchyTitle.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/core/ui/components/topbar/WindowButton.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/ExtraInfoFormat.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/Hashes.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/LaunchyPackFormat.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/Mod.kt rename src/main/kotlin/com/mineinabyss/launchy/{instance => downloads}/data/formats/ModDownloadPath.kt (95%) create mode 100644 src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/Modpack.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/ModpackFormat.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/ModrinthPackFormat.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/SerializedPackFormat.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/downloads/data/source/PackSource.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/downloads/data/source/PackType.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/downloads/data/sources/LocalModpackDataSource.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/downloads/data/sources/ModpackDataSource.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/downloads/data/sources/SerializedDownloadSource.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/downloads/data/sources/URLModpackDataSource.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/data/Mod.kt rename src/main/kotlin/com/mineinabyss/launchy/instance/data/{InstanceModList.kt => ModListModel.kt} (60%) rename src/main/kotlin/com/mineinabyss/launchy/instance/data/{InstanceModLoaders.kt => ModLoaderModel.kt} (96%) delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/data/Modpack.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ExtraInfoFormat.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ExtraPackInfo.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/LaunchyPackFormat.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ModrinthPackFormat.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/PackFormat.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/ButtonInteractions.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/ImportSettingsDialog.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/AuthButton.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/RetryFailedButton.kt delete mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/UpdateInfoButton.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance_list/data/InstanceCardInteractions.kt diff --git a/src/main/kotlin/com/mineinabyss/launchy/auth/ui/AuthDialog.kt b/src/main/kotlin/com/mineinabyss/launchy/auth/ui/AuthDialog.kt index b2f1528..e23d0ef 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/auth/ui/AuthDialog.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/auth/ui/AuthDialog.kt @@ -54,7 +54,7 @@ fun AuthDialog( } pop() - append(" and enter the code ${state.profile.authCode}") + append(" and enter the code ${state.verification.code}") } } val inlineContent = mapOf( diff --git a/src/main/kotlin/com/mineinabyss/launchy/auth/ui/ProfileViewModel.kt b/src/main/kotlin/com/mineinabyss/launchy/auth/ui/ProfileViewModel.kt index 7a856af..489201b 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/auth/ui/ProfileViewModel.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/auth/ui/ProfileViewModel.kt @@ -25,6 +25,7 @@ import kotlin.io.path.inputStream class ProfileViewModel( private val profileRepository: ProfileRepository, private val tasks: TasksRepository, + private val downloader: Downloader, ) : ViewModel() { private val _profile = MutableStateFlow(null) @@ -63,7 +64,7 @@ class ProfileViewModel( private suspend fun getAvatar(uuid: UUID): Result = withContext(AppDispatchers.IO) { runCatching { - Downloader.downloadAvatar(uuid, Downloader.Options(overwrite = false)) + downloader.downloadAvatar(uuid, Downloader.Options(overwrite = false)) BitmapPainter( loadImageBitmap(Dirs.avatar(uuid).inputStream()), filterQuality = FilterQuality.None diff --git a/src/main/kotlin/com/mineinabyss/launchy/config/data/Config.kt b/src/main/kotlin/com/mineinabyss/launchy/config/data/Config.kt index 4b6cb39..a669f0a 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/config/data/Config.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/config/data/Config.kt @@ -1,6 +1,7 @@ package com.mineinabyss.launchy.config.data import com.mineinabyss.launchy.auth.data.ProfileModel +import com.mineinabyss.launchy.util.InstanceKey import kotlinx.serialization.Serializable @@ -15,5 +16,5 @@ data class Config( val useRecommendedJvmArguments: Boolean = true, val preferHue: Float? = null, val startInFullscreen: Boolean = false, - val lastPlayedMap: Map = mapOf(), + val lastPlayedMap: Map = mapOf(), ) diff --git a/src/main/kotlin/com/mineinabyss/launchy/config/data/ConfigRepository.kt b/src/main/kotlin/com/mineinabyss/launchy/config/data/ConfigRepository.kt new file mode 100644 index 0000000..c400fc5 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/config/data/ConfigRepository.kt @@ -0,0 +1,21 @@ +package com.mineinabyss.launchy.config.data + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class ConfigRepository( + val dataSource: ConfigDataSource +) { + private val _config = MutableStateFlow(Config()) + + val config = _config.asStateFlow() + + fun updateConfig(config: Config) { + _config.value = config + dataSource.saveConfig(config) + } + + fun tryLoadConfig() { + _config.value = dataSource.readConfig().getOrElse { return } + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyViewModel.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyViewModel.kt index 163fdb4..5d238fe 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyViewModel.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyViewModel.kt @@ -3,31 +3,23 @@ package com.mineinabyss.launchy.core.ui import androidx.compose.runtime.* import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.mineinabyss.launchy.config.data.Config -import com.mineinabyss.launchy.config.data.ConfigDataSource -import com.mineinabyss.launchy.instance.data.GameInstanceDataSource -import com.mineinabyss.launchy.util.AppDispatchers +import com.mineinabyss.launchy.config.data.ConfigRepository import com.mineinabyss.launchy.util.Dirs import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext class LaunchyViewModel( - val configDataSource: ConfigDataSource + val configRepo: ConfigRepository ) : ViewModel() { private val _uiState = MutableStateFlow(LaunchyUiState.Loading) - private val _config = MutableStateFlow(Config()) val uiState = _uiState.asStateFlow() - val config = _config.asStateFlow() init { viewModelScope.launch { setupFilesystem() - tryLoadConfig() - val instances = GameInstanceDataSource.readAll(Dirs.modpackConfigsDir) - val config = config.value + configRepo.tryLoadConfig() _uiState.emit( LaunchyUiState.Ready( UiState(config), @@ -68,10 +60,4 @@ class LaunchyViewModel( Dirs.createDirs() Dirs.createConfigFiles() } - - private suspend fun tryLoadConfig() = withContext(AppDispatchers.IO) { - configDataSource.readConfig().onSuccess { - _config.emit(it) - } - } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/TopBar.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/topbar/AppTopBar.kt similarity index 66% rename from src/main/kotlin/com/mineinabyss/launchy/core/ui/components/TopBar.kt rename to src/main/kotlin/com/mineinabyss/launchy/core/ui/components/topbar/AppTopBar.kt index a28b577..7596522 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/TopBar.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/topbar/AppTopBar.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.core.ui.components +package com.mineinabyss.launchy.core.ui.components.topbar import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.slideIn @@ -10,37 +10,18 @@ import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.CropSquare import androidx.compose.material.icons.rounded.Minimize -import androidx.compose.material.icons.rounded.RocketLaunch -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.window.WindowPlacement -import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.core.ui.Constants +import com.mineinabyss.launchy.core.ui.LocalUiState import com.mineinabyss.launchy.core.ui.TopBarState - -@Composable -fun WindowButton(icon: ImageVector, onClick: () -> Unit) { - Surface( - onClick = onClick, - modifier = Modifier.fillMaxHeight().width(44.dp), - contentColor = Color.White, - color = Color.Transparent - ) { - Icon(icon, "", Modifier.padding(10.dp)) - } -} +import com.mineinabyss.launchy.core.ui.components.BetterWindowDraggableArea @Composable fun AppTopBar( @@ -50,8 +31,8 @@ fun AppTopBar( showBackButton: Boolean, onBackButtonClicked: (() -> Unit), ) { - val appState = LocalLaunchyState - val forceFullscreen = appState.ui.fullscreen + val ui = LocalUiState.current + val forceFullscreen = ui.fullscreen LaunchedEffect(forceFullscreen) { when (forceFullscreen) { true -> state.windowState.placement = WindowPlacement.Fullscreen @@ -119,36 +100,3 @@ fun AppTopBar( } } -@Composable -fun LaunchyTitle() { - Row { - Icon( - Icons.Rounded.RocketLaunch, - contentDescription = "Launchy", - tint = MaterialTheme.colorScheme.primary - ) - Text( - "Launchy - ${Constants.APP_VERSION ?: "dev"}", - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.primary - ) - } -} - -@Composable -fun LaunchyHeadline() { - Row { - Icon( - Icons.Rounded.RocketLaunch, - contentDescription = "Launchy", - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(32.dp) - ) - Text( - "Launchy", - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.primary, - style = MaterialTheme.typography.headlineLarge - ) - } -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/topbar/LaunchyHeadline.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/topbar/LaunchyHeadline.kt new file mode 100644 index 0000000..ad79ad6 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/topbar/LaunchyHeadline.kt @@ -0,0 +1,31 @@ +package com.mineinabyss.launchy.core.ui.components.topbar + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.RocketLaunch +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@Composable +fun LaunchyHeadline() { + Row { + Icon( + Icons.Rounded.RocketLaunch, + contentDescription = "Launchy", + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(32.dp) + ) + Text( + "Launchy", + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.headlineLarge + ) + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/topbar/LaunchyTitle.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/topbar/LaunchyTitle.kt new file mode 100644 index 0000000..c91b20c --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/topbar/LaunchyTitle.kt @@ -0,0 +1,27 @@ +package com.mineinabyss.launchy.core.ui.components.topbar + +import androidx.compose.foundation.layout.Row +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.RocketLaunch +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.font.FontWeight +import com.mineinabyss.launchy.core.ui.Constants + +@Composable +fun LaunchyTitle() { + Row { + Icon( + Icons.Rounded.RocketLaunch, + contentDescription = "Launchy", + tint = MaterialTheme.colorScheme.primary + ) + Text( + "Launchy - ${Constants.APP_VERSION ?: "dev"}", + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.primary + ) + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/topbar/WindowButton.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/topbar/WindowButton.kt new file mode 100644 index 0000000..0d730eb --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/topbar/WindowButton.kt @@ -0,0 +1,24 @@ +package com.mineinabyss.launchy.core.ui.components.topbar + +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +@Composable +fun WindowButton(icon: ImageVector, onClick: () -> Unit) { + Surface( + onClick = onClick, + modifier = Modifier.fillMaxHeight().width(44.dp), + contentColor = Color.White, + color = Color.Transparent + ) { + Icon(icon, "", Modifier.padding(10.dp)) + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/screens/Screens.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/screens/Screens.kt index 293e8bc..fa314ed 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/core/ui/screens/Screens.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/screens/Screens.kt @@ -17,20 +17,22 @@ import com.mineinabyss.launchy.auth.ui.AuthDialog import com.mineinabyss.launchy.core.ui.Dialog import com.mineinabyss.launchy.core.ui.LocalUiState import com.mineinabyss.launchy.core.ui.TopBar -import com.mineinabyss.launchy.core.ui.components.AppTopBar import com.mineinabyss.launchy.core.ui.components.InProgressTasksIndicator import com.mineinabyss.launchy.core.ui.components.LaunchyDialog import com.mineinabyss.launchy.core.ui.components.LeftSidebar +import com.mineinabyss.launchy.core.ui.components.topbar.AppTopBar import com.mineinabyss.launchy.core.ui.dialogs.SelectJVMDialog import com.mineinabyss.launchy.core.ui.theme.currentHue +import com.mineinabyss.launchy.instance.ui.InstanceViewModel import com.mineinabyss.launchy.instance.ui.screens.InstanceScreen import com.mineinabyss.launchy.instance.ui.screens.InstanceSettingsScreen import com.mineinabyss.launchy.instance_creation.ui.NewInstance -import com.mineinabyss.launchy.instance_list.ui.HomeScreen +import com.mineinabyss.launchy.instance_list.ui.InstanceListScreen import com.mineinabyss.launchy.settings.ui.SettingsScreen import com.mineinabyss.launchy.updater.data.AppUpdateState import com.mineinabyss.launchy.updater.data.GithubUpdateChecker import com.mineinabyss.launchy.util.DesktopHelpers +import com.mineinabyss.launchy.util.koinViewModel var screen: Screen by mutableStateOf(Screen.Default) @@ -46,6 +48,7 @@ val snackbarHostState = SnackbarHostState() @Composable fun Screens( + instanceViewModel: InstanceViewModel = koinViewModel() ) = Scaffold( snackbarHost = { SnackbarHost(snackbarHostState) }, floatingActionButton = { @@ -63,9 +66,12 @@ fun Screens( } ) { val ui = LocalUiState.current - Screen(Screen.Instance) { InstanceScreen() } + Screen(Screen.Instance) { + val instance by instanceViewModel.instanceUiState.collectAsState() + InstanceScreen(instance ?: return@Screen) + } Screen(Screen.InstanceSettings, transition = Transitions.SlideUp) { InstanceSettingsScreen() } - Screen(Screen.Default) { HomeScreen() } + Screen(Screen.Default) { InstanceListScreen() } Screen(Screen.NewInstance) { NewInstance() } Screen(Screen.Settings) { SettingsScreen() } AnimatedVisibility( diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/DownloadQueueState.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/DownloadQueueState.kt index 1e2aa76..081e753 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/DownloadQueueState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/DownloadQueueState.kt @@ -1,60 +1,53 @@ package com.mineinabyss.launchy.downloads.data -import androidx.compose.runtime.* -import com.mineinabyss.launchy.instance.data.DownloadInfo -import com.mineinabyss.launchy.instance.data.Modpack -import com.mineinabyss.launchy.instance.data.storage.InstanceUserConfig -import com.mineinabyss.launchy.instance.ui.InstanceViewModel -import com.mineinabyss.launchy.util.ModID - -class DownloadQueueState( - private val userConfig: InstanceUserConfig, - val modpack: Modpack, - val toggles: InstanceViewModel -) { - /** Live mod download info, including mods that have been removed from the latest modpack version. */ - val modDownloadInfo = mutableStateMapOf().apply { - val availableIds = toggles.availableMods.map { it.modId } - putAll(userConfig.modDownloadInfo.filter { it.key in availableIds }) - } - - /** Mods whose download url matches a previously downloaded url and exist on the filesystem */ - val failures by derivedStateOf { - toggles.enabledMods.filter { - modDownloadInfo[it.modId]?.failed() == true - } - } - - /** Toggled mods that haven't been previously installed (are new to the instance) */ - val newDownloads by derivedStateOf { - toggles.enabledMods.filter { it.modId !in modDownloadInfo.keys } - } - - /** Toggled mods that have previously been downloaded but whose URL has changed */ - val updates by derivedStateOf { - toggles.enabledMods - .filter { mod -> - modDownloadInfo[mod.modId]?.let { mod.downloadUrl.toString() != it.url } == true - } - } - - /** Mods (currently listed in the Modpack) that were previously enabled, but no longer are */ - val deletions by derivedStateOf { - (modpack.mods.mods - toggles.enabledMods).filter { modDownloadInfo.contains(it.modId) } - } - - val areModLoaderUpdatesAvailable by derivedStateOf { - modpack.modLoaders != userAgreedModLoaders - } - - var userAgreedModLoaders by mutableStateOf(userConfig.userAgreedDeps) - - val needsInstall by derivedStateOf { updates + newDownloads + failures } - - val areUpdatesQueued by derivedStateOf { updates.isNotEmpty() } - val areNewDownloadsQueued by derivedStateOf { newDownloads.isNotEmpty() } - val areDeletionsQueued by derivedStateOf { deletions.isNotEmpty() } - val areOperationsQueued by derivedStateOf { - areUpdatesQueued || areNewDownloadsQueued || areDeletionsQueued || areModLoaderUpdatesAvailable - } -} +//class DownloadQueueState( +// private val userConfig: InstanceUserConfig, +// val modpack: Modpack, +// val toggles: InstanceViewModel +//) { +// /** Live mod download info, including mods that have been removed from the latest modpack version. */ +// val modDownloadInfo = mutableStateMapOf().apply { +// val availableIds = toggles.availableMods.map { it.modId } +// putAll(userConfig.modDownloadInfo.filter { it.key in availableIds }) +// } +// +// /** Mods whose download url matches a previously downloaded url and exist on the filesystem */ +// val failures by derivedStateOf { +// toggles.enabledMods.filter { +// modDownloadInfo[it.modId]?.failed() == true +// } +// } +// +// /** Toggled mods that haven't been previously installed (are new to the instance) */ +// val newDownloads by derivedStateOf { +// toggles.enabledMods.filter { it.modId !in modDownloadInfo.keys } +// } +// +// /** Toggled mods that have previously been downloaded but whose URL has changed */ +// val updates by derivedStateOf { +// toggles.enabledMods +// .filter { mod -> +// modDownloadInfo[mod.modId]?.let { mod.downloadUrl.toString() != it.url } == true +// } +// } +// +// /** Mods (currently listed in the Modpack) that were previously enabled, but no longer are */ +// val deletions by derivedStateOf { +// (modpack.mods.mods - toggles.enabledMods).filter { modDownloadInfo.contains(it.modId) } +// } +// +// val areModLoaderUpdatesAvailable by derivedStateOf { +// modpack.modLoaders != userAgreedModLoaders +// } +// +// var userAgreedModLoaders by mutableStateOf(userConfig.userAgreedDeps) +// +// val needsInstall by derivedStateOf { updates + newDownloads + failures } +// +// val areUpdatesQueued by derivedStateOf { updates.isNotEmpty() } +// val areNewDownloadsQueued by derivedStateOf { newDownloads.isNotEmpty() } +// val areDeletionsQueued by derivedStateOf { deletions.isNotEmpty() } +// val areOperationsQueued by derivedStateOf { +// areUpdatesQueued || areNewDownloadsQueued || areDeletionsQueued || areModLoaderUpdatesAvailable +// } +//} diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/DownloadState.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/DownloadState.kt index da7193d..bd0dbe1 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/DownloadState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/DownloadState.kt @@ -3,7 +3,7 @@ package com.mineinabyss.launchy.downloads.data import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateMapOf -import com.mineinabyss.launchy.instance.data.Mod +import com.mineinabyss.launchy.downloads.data.formats.Mod import com.mineinabyss.launchy.util.Progress class DownloadState { diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/Downloader.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/Downloader.kt index ca85c19..441686d 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/Downloader.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/Downloader.kt @@ -1,7 +1,7 @@ package com.mineinabyss.launchy.downloads.data import com.mineinabyss.launchy.core.ui.LaunchyUiState -import com.mineinabyss.launchy.instance.data.GameInstanceDataSource +import com.mineinabyss.launchy.instance.data.InstanceModel import com.mineinabyss.launchy.util.* import io.ktor.client.* import io.ktor.client.call.* @@ -22,7 +22,7 @@ import java.nio.file.Path import java.util.* import kotlin.io.path.* -object Downloader { +class Downloader { val httpClient = HttpClient(CIO) { install(HttpTimeout) install(ContentNegotiation) { @@ -44,12 +44,12 @@ object Downloader { headers["Content-Length"]?.toLongOrNull() ?: 0 ) - fun fileFor(instance: GameInstanceDataSource, url: String) = + fun fileFor(instance: InstanceModel, url: String) = Dirs.cacheDir(instance) / "${urlToFileName(url)}.header" } } - suspend fun checkUpdates(instance: GameInstanceDataSource, url: String): UpdateResult { + suspend fun checkUpdates(instance: InstanceModel, url: String): UpdateResult { val headers = ModifyHeaders.of(httpClient.head(url).headers) val cache = headers.toCacheString() val cacheFile = ModifyHeaders.fileFor(instance, url) @@ -60,7 +60,7 @@ object Downloader { } } - fun saveHeaders(instance: GameInstanceDataSource, url: String, headers: ModifyHeaders) { + fun saveHeaders(instance: InstanceModel, url: String, headers: ModifyHeaders) { ModifyHeaders.fileFor(instance, url).createParentDirectories().apply { deleteIfExists() createFile() @@ -194,6 +194,6 @@ object Downloader { val overwrite: Boolean = true, val whenChanged: () -> Unit = {}, val onProgressUpdate: (progress: Progress) -> Unit = {}, - val saveModifyHeadersFor: GameInstanceDataSource? = null, + val saveModifyHeadersFor: InstanceModel? = null, ) } diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/ModDownloader.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/ModDownloader.kt index a45fd5d..854753f 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/ModDownloader.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/ModDownloader.kt @@ -1,10 +1,10 @@ package com.mineinabyss.launchy.downloads.data import com.mineinabyss.launchy.core.ui.LaunchyUiState +import com.mineinabyss.launchy.downloads.data.formats.Mod import com.mineinabyss.launchy.instance.data.DownloadInfo import com.mineinabyss.launchy.instance.data.HashCheck -import com.mineinabyss.launchy.instance.data.InstanceModLoaders -import com.mineinabyss.launchy.instance.data.Mod +import com.mineinabyss.launchy.instance.data.ModLoaderModel import com.mineinabyss.launchy.instance.ui.GameInstanceState import com.mineinabyss.launchy.launcher.data.Launcher import com.mineinabyss.launchy.util.* @@ -17,7 +17,7 @@ import kotlin.io.path.* object ModDownloader { - suspend fun GameInstanceState.installMCAndModLoaders(state: LaunchyUiState, modLoaders: InstanceModLoaders) { + suspend fun GameInstanceState.installMCAndModLoaders(state: LaunchyUiState, modLoaders: ModLoaderModel) { state.runTask(Tasks.installModLoadersId, InProgressTask("Installing ${modLoaders.fullVersionName}")) { Launcher.download( modLoaders, @@ -36,7 +36,7 @@ object ModDownloader { data object Failed : DownloadResult } - suspend fun GameInstanceState.download(mod: Mod, overwrite: Boolean): DownloadResult { + suspend fun download(mod: Mod, overwrite: Boolean): DownloadResult { val name = mod.info.name try { println("Starting download of $name") diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/ExtraInfoFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/ExtraInfoFormat.kt new file mode 100644 index 0000000..9f17834 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/ExtraInfoFormat.kt @@ -0,0 +1,67 @@ +package com.mineinabyss.launchy.downloads.data.formats + +import com.charleskorn.kaml.decodeFromStream +import com.mineinabyss.launchy.instance.data.InstanceModel +import com.mineinabyss.launchy.instance.data.ModGroup +import com.mineinabyss.launchy.instance.data.ModListModel +import com.mineinabyss.launchy.instance.data.storage.ModConfig +import com.mineinabyss.launchy.util.Formats +import kotlinx.serialization.Serializable +import java.nio.file.Path +import kotlin.io.path.div +import kotlin.io.path.inputStream +import kotlin.io.path.isRegularFile + +data class ExtraInfoFormat( + val innerFormat: ModpackFormat, +) : ModpackFormat { + override suspend fun prepareSource(instance: InstanceModel, download: Path) { + innerFormat.prepareSource(instance, download) + } + + override suspend fun loadPackFor(instance: InstanceModel): Result { + val inner = innerFormat.loadPackFor(instance) + + val extraInfoFile = (instance.modpackFilesDir / "launchy.yml").takeIf { it.isRegularFile() } + val extraInfo = extraInfoFile?.runCatching { + Formats.yaml.decodeFromStream(extraInfoFile.inputStream()) + }?.getOrNull() ?: return inner + + return inner.map { pack -> + val originalMods = pack.modList.mods + val foundMods = mutableSetOf() + val mods: Map> = extraInfo.modGroups + .mapKeys { (name, _) -> pack.modList.groups.single { it.name == name } } + .mapValues { (_, mods) -> + mods.mapNotNull { ref -> + val found = originalMods.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 = pack.modList.modGroups.mapValues { + it.value.filterTo(mutableSetOf()) { mod -> mod !in foundMods } + } + + pack.copy( + modList = ModListModel((originalGroups + mods) + .filter { (_, mods) -> mods.isNotEmpty() }) + ) + } + } + + @Serializable + class ModReference( + val urlContains: String, + val info: ModConfig? = null, + ) + + @Serializable + class ExtraPackInfo( + val groups: List = listOf(), + val modGroups: Map> = mapOf(), + ) +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/Hashes.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/Hashes.kt new file mode 100644 index 0000000..76cb813 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/Hashes.kt @@ -0,0 +1,9 @@ +package com.mineinabyss.launchy.downloads.data.formats + +import kotlinx.serialization.Serializable + +@Serializable +data class Hashes( + val sha1: String, + val sha512: String, +) diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/LaunchyPackFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/LaunchyPackFormat.kt new file mode 100644 index 0000000..40b5964 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/LaunchyPackFormat.kt @@ -0,0 +1,51 @@ +package com.mineinabyss.launchy.downloads.data.formats + +import com.charleskorn.kaml.Yaml +import com.charleskorn.kaml.decodeFromStream +import com.mineinabyss.launchy.instance.data.InstanceModel +import com.mineinabyss.launchy.instance.data.ModGroup +import com.mineinabyss.launchy.instance.data.ModListModel +import com.mineinabyss.launchy.instance.data.ModLoaderModel +import com.mineinabyss.launchy.instance.data.storage.ModConfig +import com.mineinabyss.launchy.util.GroupName +import kotlinx.serialization.Serializable +import java.nio.file.Path +import kotlin.io.path.copyTo +import kotlin.io.path.div +import kotlin.io.path.inputStream + +class LaunchyPackFormat : ModpackFormat { + override suspend fun prepareSource(instance: InstanceModel, download: Path) { + download.copyTo(instance.modpackFilesDir / "pack.yml") + } + + override suspend fun loadPackFor(instance: InstanceModel): Result = runCatching { + val pack = Yaml.Companion.default.decodeFromStream( + (instance.modpackFilesDir / "pack.yml").inputStream() + ) + Modpack( + modLoader = ModLoaderModel(minecraft = pack.minecraftVersion, fabricLoader = pack.fabricVersion), + modList = ModListModel( + pack.modGroups + .mapKeys { (name, _) -> pack.groups.single { it.name == name } } + .mapValues { (_, mods) -> + mods.map { + Mod( + info = it, + modId = it.id ?: it.name, + desiredHashes = null, + ) + }.toSet() + }), + configSources = emptyList(), + ) + } + + @Serializable + data class SerializedLaunchyPack( + val fabricVersion: String? = null, + val minecraftVersion: String, + val groups: Set, + val modGroups: Map>, + ) +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/Mod.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/Mod.kt new file mode 100644 index 0000000..906a8a9 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/Mod.kt @@ -0,0 +1,19 @@ +package com.mineinabyss.launchy.downloads.data.formats + +import com.mineinabyss.launchy.instance.data.storage.ModConfig +import com.mineinabyss.launchy.util.ModID + +data class Mod( + val modId: ModID, + val info: ModConfig, + val desiredHashes: Hashes?, +) { + // val absoluteDownloadDest = +// if (info.downloadPath != null) downloadDir / info.downloadPath.validated +// else downloadDir / "mods" / "${info.id ?: info.name}.jar" +// +// val downloadUrl: Url = Url(info.url) +// + fun compatibleWith(other: Mod) = + other.info.name !in info.incompatibleWith && info.name !in other.info.incompatibleWith +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ModDownloadPath.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/ModDownloadPath.kt similarity index 95% rename from src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ModDownloadPath.kt rename to src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/ModDownloadPath.kt index 1de0025..bcd6671 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ModDownloadPath.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/ModDownloadPath.kt @@ -1,4 +1,4 @@ -package com.mineinabyss.launchy.instance.data.formats +package com.mineinabyss.launchy.downloads.data.formats import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/Modpack.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/Modpack.kt new file mode 100644 index 0000000..cac5fe3 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/Modpack.kt @@ -0,0 +1,11 @@ +package com.mineinabyss.launchy.downloads.data.formats + +import com.mineinabyss.launchy.instance.data.ModListModel +import com.mineinabyss.launchy.instance.data.ModLoaderModel +import java.nio.file.Path + +data class Modpack( + val modLoader: ModLoaderModel, + val modList: ModListModel, + val configSources: List = listOf(), +) diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/ModpackFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/ModpackFormat.kt new file mode 100644 index 0000000..3a1c64d --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/ModpackFormat.kt @@ -0,0 +1,9 @@ +package com.mineinabyss.launchy.downloads.data.formats + +import com.mineinabyss.launchy.instance.data.InstanceModel +import java.nio.file.Path + +interface ModpackFormat { + suspend fun prepareSource(instance: InstanceModel, download: Path) + suspend fun loadPackFor(instance: InstanceModel): Result +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/ModrinthPackFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/ModrinthPackFormat.kt new file mode 100644 index 0000000..55d49b4 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/ModrinthPackFormat.kt @@ -0,0 +1,69 @@ +package com.mineinabyss.launchy.downloads.data.formats + +import com.mineinabyss.launchy.instance.data.InstanceModel +import com.mineinabyss.launchy.instance.data.ModListModel +import com.mineinabyss.launchy.instance.data.ModLoaderModel +import com.mineinabyss.launchy.instance.data.storage.ModConfig +import com.mineinabyss.launchy.util.Formats +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.ExperimentalPathApi +import kotlin.io.path.deleteRecursively +import kotlin.io.path.div +import kotlin.io.path.inputStream + +class ModrinthPackFormat : ModpackFormat { + @OptIn(ExperimentalPathApi::class) + override suspend fun prepareSource(instance: InstanceModel, download: Path) { + val unzipDest = instance.modpackFilesDir + unzipDest.deleteRecursively() + ArchiverFactory.createArchiver("zip").extract(download.toFile(), unzipDest.toFile()) + } + + @OptIn(ExperimentalSerializationApi::class) + override suspend fun loadPackFor(instance: InstanceModel): Result { + val unzipDest = instance.modpackFilesDir + val index = unzipDest / "modrinth.index.json" + val mrpack = Formats.json.decodeFromStream(index.inputStream()) + + return runCatching { + Modpack( + modLoader = mrpack.dependencies, + modList = ModListModel.withSingleGroup(mrpack.files.map { it.toMod() }), + configSources = listOf(unzipDest / "overrides"), + ) + } + } + + @Serializable + data class SerializedModrinthPack( + val dependencies: ModLoaderModel, + val files: List, + val formatVersion: Int, + val name: String, + val versionId: String, + ) { + @Serializable + data class PackFile( + val downloads: List, + val fileSize: Long, + val path: ModDownloadPath, + val hashes: Hashes, + ) { + fun toMod() = Mod( + modId = downloads.single().removePrefix("https://cdn.modrinth.com/data/").substringBefore("/versions"), + ModConfig( + name = path.validated.toString().removePrefix("mods/").removeSuffix(".jar"), + desc = "", + url = downloads.single(), + downloadPath = path, + ), + desiredHashes = hashes, + ) + } + } +} + diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/SerializedPackFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/SerializedPackFormat.kt new file mode 100644 index 0000000..7c6cd3b --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/formats/SerializedPackFormat.kt @@ -0,0 +1,27 @@ +package com.mineinabyss.launchy.downloads.data.formats + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +sealed interface SerializedPackFormat { + fun getFormat(): ModpackFormat + + @Serializable + @SerialName("launchy") + data object Launchy : SerializedPackFormat { + override fun getFormat(): ModpackFormat = LaunchyPackFormat() + } + + @Serializable + @SerialName("modrinth") + data object Modrinth : SerializedPackFormat { + override fun getFormat(): ModpackFormat = ModrinthPackFormat() + } + + @Serializable + @SerialName("extras") + data class WithExtraInfo(val format: SerializedPackFormat) : SerializedPackFormat { + override fun getFormat(): ModpackFormat = ExtraInfoFormat(format.getFormat()) + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/source/PackSource.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/source/PackSource.kt deleted file mode 100644 index 15b802e..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/source/PackSource.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.mineinabyss.launchy.downloads.data.source - -import com.mineinabyss.launchy.downloads.data.Downloader -import com.mineinabyss.launchy.instance.data.GameInstanceDataSource -import com.mineinabyss.launchy.instance.data.Modpack -import com.mineinabyss.launchy.util.AppDispatchers -import com.mineinabyss.launchy.util.UpdateResult -import kotlinx.coroutines.launch -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlin.io.path.notExists - -@Serializable -sealed class PackSource { - abstract suspend fun loadInstance(instance: GameInstanceDataSource): Result - - abstract suspend fun updateInstance(instance: GameInstanceDataSource): Result - - @Serializable - @SerialName("localFile") - class LocalFile(val type: PackType) : PackSource() { - override suspend fun loadInstance(instance: GameInstanceDataSource): Result = runCatching { - val format = type.getFormat(instance.configDir).getOrThrow() - val mods = format.toGenericMods(instance.downloadsDir) - val modLoaders = format.getModLoaders() - Modpack(modLoaders, mods, format.getOverridesPaths(instance.configDir)) - } - - override suspend fun updateInstance(instance: GameInstanceDataSource): Result { - return runCatching { GameInstanceDataSource(instance.configDir) } - } - } - - @SerialName("downloadFromURL") - @Serializable - class DownloadFromURL(val url: String, val type: PackType) : PackSource() { - override suspend fun loadInstance(instance: GameInstanceDataSource): Result { - val downloadTo = type.getFilePath(instance.configDir) - if (downloadTo.notExists()) { - Downloader.download(url, downloadTo, options = Downloader.Options(saveModifyHeadersFor = instance)) - type.afterDownload(instance.configDir) - } else { - AppDispatchers.IO.launch { - val result = Downloader.checkUpdates(instance, url) - if (result !is UpdateResult.UpToDate) instance.updatesAvailable = true - } - } - return LocalFile(type).loadInstance(instance) - } - - override suspend fun updateInstance(instance: GameInstanceDataSource): Result { - return runCatching { - val downloadTo = type.getFilePath(instance.configDir) - Downloader.download(url, downloadTo, options = Downloader.Options(saveModifyHeadersFor = instance)) - type.afterDownload(instance.configDir) - GameInstanceDataSource(instance.configDir) - } - } - } -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/source/PackType.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/source/PackType.kt deleted file mode 100644 index bc58118..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/source/PackType.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.mineinabyss.launchy.downloads.data.source - -import com.charleskorn.kaml.decodeFromStream -import com.mineinabyss.launchy.instance.data.formats.* -import com.mineinabyss.launchy.util.Formats -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.* - -@Serializable -enum class PackType { - Launchy, Modrinth; - - fun getFilePath(configDir: Path): Path { - val ext = when (this) { - Launchy -> "yml" - Modrinth -> "zip" - } - return configDir / "pack.$ext" - } - - @OptIn(ExperimentalPathApi::class) - fun afterDownload(configDir: Path) { - val path = getFilePath(configDir) - if (this == Modrinth) { - val unzipDir = configDir / "mrpack" - unzipDir.deleteRecursively() - ArchiverFactory.createArchiver("zip").extract(path.toFile(), unzipDir.toFile()) - } - } - - @OptIn(ExperimentalSerializationApi::class) - fun getFormat(configDir: Path): Result { - val file = getFilePath(configDir) - return when (this) { - Launchy -> runCatching { - if (!file.isRegularFile()) return Result.failure(IllegalStateException("Could not find modpack file at $file")) - Formats.yaml.decodeFromStream(file.inputStream()) - } - - Modrinth -> runCatching { - val unzipDir = configDir / "mrpack" - val index = unzipDir / "modrinth.index.json" - if (unzipDir.notExists()) { - afterDownload(configDir) - } - 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/downloads/data/sources/LocalModpackDataSource.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/sources/LocalModpackDataSource.kt new file mode 100644 index 0000000..7f588f6 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/sources/LocalModpackDataSource.kt @@ -0,0 +1,9 @@ +package com.mineinabyss.launchy.downloads.data.sources + +import com.mineinabyss.launchy.instance.data.InstanceModel + +class LocalModpackDataSource : ModpackDataSource { + override suspend fun skip(instance: InstanceModel) = true + + override suspend fun fetchLatestModsFor(instance: InstanceModel) = null +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/sources/ModpackDataSource.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/sources/ModpackDataSource.kt new file mode 100644 index 0000000..16a00c0 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/sources/ModpackDataSource.kt @@ -0,0 +1,10 @@ +package com.mineinabyss.launchy.downloads.data.sources + +import com.mineinabyss.launchy.instance.data.InstanceModel +import java.nio.file.Path + +interface ModpackDataSource { + suspend fun skip(instance: InstanceModel): Boolean = false + + suspend fun fetchLatestModsFor(instance: InstanceModel): Path? +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/sources/SerializedDownloadSource.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/sources/SerializedDownloadSource.kt new file mode 100644 index 0000000..e1ebf90 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/sources/SerializedDownloadSource.kt @@ -0,0 +1,22 @@ +package com.mineinabyss.launchy.downloads.data.sources + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.koin.core.scope.Scope + +@Serializable +sealed interface SerializedDownloadSource { + fun getDataSource(scope: Scope): ModpackDataSource + + @Serializable + @SerialName("local") + class Local : SerializedDownloadSource { + override fun getDataSource(scope: Scope) = LocalModpackDataSource() + } + + @SerialName("downloadFromURL") + @Serializable + class DownloadFromURL(val url: String) : SerializedDownloadSource { + override fun getDataSource(scope: Scope) = URLModpackDataSource(scope.get(), url) + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/sources/URLModpackDataSource.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/sources/URLModpackDataSource.kt new file mode 100644 index 0000000..c0e3351 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/sources/URLModpackDataSource.kt @@ -0,0 +1,19 @@ +package com.mineinabyss.launchy.downloads.data.sources + +import com.mineinabyss.launchy.downloads.data.Downloader +import com.mineinabyss.launchy.instance.data.InstanceModel +import java.nio.file.Path + +class URLModpackDataSource( + val downloader: Downloader, + val resourceURL: String, +) : ModpackDataSource { + override suspend fun fetchLatestModsFor(instance: InstanceModel): Path? { + downloader.download( + resourceURL, + instance.packDownloadFile, + options = Downloader.Options(saveModifyHeadersFor = instance) + ) + return instance.packDownloadFile + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModel.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModel.kt index d5437dc..cd6c10e 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModel.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModel.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.mineinabyss.launchy.instance.data.storage.InstanceConfig import com.mineinabyss.launchy.util.Dirs +import com.mineinabyss.launchy.util.InstanceKey import kotlinx.coroutines.Dispatchers import java.nio.file.Path import kotlin.io.path.Path @@ -15,11 +16,13 @@ import kotlin.io.path.name data class InstanceModel( val config: InstanceConfig, val directory: Path, + val key: InstanceKey = InstanceKey(directory.name), ) { val instanceFile = directory / "instance.yml" val backupInstanceFile = directory / "instance-backup.yml" val overridesDir = directory / "overrides" val imageLoaderDispatcher = Dispatchers.IO.limitedParallelism(1) + val modpackFilesDir = directory / "modpack" val minecraftDir = config.overrideMinecraftDir?.let { Path(it) } ?: Dirs.modpackDir(directory.name) @@ -28,6 +31,7 @@ data class InstanceModel( val downloadsDir: Path = minecraftDir / "launchyDownloads" val userConfigFile = (directory / "config.yml") + val packDownloadFile = (downloadsDir / "pack") var updatesAvailable by mutableStateOf(false) var enabled: Boolean by mutableStateOf(true) diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/Mod.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/Mod.kt deleted file mode 100644 index 6774169..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/Mod.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.mineinabyss.launchy.instance.data - -import com.mineinabyss.launchy.instance.data.formats.ModrinthPackFormat -import com.mineinabyss.launchy.instance.data.storage.ModConfig -import io.ktor.http.* -import java.nio.file.Path -import kotlin.io.path.div - -data class Mod( - private val downloadDir: Path, - val info: ModConfig, - val modId: String, - val desiredHashes: ModrinthPackFormat.Hashes?, -) { - val absoluteDownloadDest = - if (info.downloadPath != null) downloadDir / info.downloadPath.validated - else downloadDir / "mods" / "${info.id ?: info.name}.jar" - - val downloadUrl: Url = Url(info.url) - - fun compatibleWith(other: Mod) = - other.info.name !in info.incompatibleWith && info.name !in other.info.incompatibleWith -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModList.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/ModListModel.kt similarity index 60% rename from src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModList.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/ModListModel.kt index a87cc30..d126f31 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModList.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/ModListModel.kt @@ -1,11 +1,12 @@ package com.mineinabyss.launchy.instance.data import androidx.compose.runtime.Immutable +import com.mineinabyss.launchy.downloads.data.formats.Mod import com.mineinabyss.launchy.util.GroupName import com.mineinabyss.launchy.util.ModID @Immutable -data class InstanceModList( +data class ModListModel( val modGroups: Map>, ) { val groups = modGroups.keys @@ -20,13 +21,13 @@ data class InstanceModList( fun getModById(id: ModID): Mod? = idToMod[id] fun getGroup(name: GroupName): ModGroup? = nameToGroup[name] -// companion object { -// const val VERSIONS_URL = "https://raw.githubusercontent.com/MineInAbyss/launchy/master/versions.yml" -// -// fun withSingleGroup(mods: Collection) = InstanceModList( -// modGroups = mapOf( -// ModGroup("Default", forceEnabled = true) to mods.toSet() -// ) -// ) -// } + companion object { + const val VERSIONS_URL = "https://raw.githubusercontent.com/MineInAbyss/launchy/master/versions.yml" + + fun withSingleGroup(mods: Collection) = ModListModel( + modGroups = mapOf( + ModGroup("Default", forceEnabled = true) to mods.toSet() + ) + ) + } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModLoaders.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/ModLoaderModel.kt similarity index 96% rename from src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModLoaders.kt rename to src/main/kotlin/com/mineinabyss/launchy/instance/data/ModLoaderModel.kt index f1047b8..a4a672d 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModLoaders.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/ModLoaderModel.kt @@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.Transient @Serializable -data class InstanceModLoaders( +data class ModLoaderModel( val minecraft: String, @SerialName("fabric-loader") val fabricLoader: String? = null, diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/Modpack.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/Modpack.kt deleted file mode 100644 index 8bc08ae..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/Modpack.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.mineinabyss.launchy.instance.data - -import java.nio.file.Path - -class Modpack( - val modLoaders: InstanceModLoaders, - val mods: InstanceModList, - val overridesPaths: List = listOf(), -) diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ExtraInfoFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ExtraInfoFormat.kt deleted file mode 100644 index 72a29bf..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ExtraInfoFormat.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.mineinabyss.launchy.instance.data.formats - -import com.mineinabyss.launchy.instance.data.InstanceModList -import com.mineinabyss.launchy.instance.data.Mod -import com.mineinabyss.launchy.instance.data.ModGroup -import java.nio.file.Path - - -data class ExtraInfoFormat( - val format: PackFormat, - val extraInfoPack: ExtraPackInfo, -) : PackFormat by format { - override fun toGenericMods(downloadsDir: Path): InstanceModList { - val originalMods = format.toGenericMods(downloadsDir) - 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 InstanceModList( - (originalGroups + mods) - .filter { (_, mods) -> mods.isNotEmpty() }) - } -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ExtraPackInfo.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ExtraPackInfo.kt deleted file mode 100644 index 73bdea4..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ExtraPackInfo.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.mineinabyss.launchy.instance.data.formats - -import com.mineinabyss.launchy.instance.data.ModGroup -import com.mineinabyss.launchy.instance.data.storage.ModConfig -import kotlinx.serialization.Serializable - -@Serializable -class ModReference( - val urlContains: String, - val info: ModConfig? = null, -) -@Serializable -class ExtraPackInfo( - val groups: List = listOf(), - val modGroups: Map> = mapOf(), -) diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/LaunchyPackFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/LaunchyPackFormat.kt deleted file mode 100644 index 7f2cca5..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/LaunchyPackFormat.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.mineinabyss.launchy.instance.data.formats - -import com.mineinabyss.launchy.instance.data.InstanceModList -import com.mineinabyss.launchy.instance.data.InstanceModLoaders -import com.mineinabyss.launchy.instance.data.Mod -import com.mineinabyss.launchy.instance.data.ModGroup -import com.mineinabyss.launchy.instance.data.storage.ModConfig -import com.mineinabyss.launchy.util.GroupName -import kotlinx.serialization.Serializable -import java.nio.file.Path - -@Serializable -data class LaunchyPackFormat( - val fabricVersion: String? = null, - val minecraftVersion: String, - val groups: Set, - private val modGroups: Map>, -) : PackFormat { - override fun toGenericMods(downloadsDir: Path): InstanceModList { - return InstanceModList( - modGroups - .mapKeys { (name, _) -> groups.single { it.name == name } } - .mapValues { (_, mods) -> - mods.map { - Mod( - downloadDir = downloadsDir, - info = it, - modId = it.id ?: it.name, - desiredHashes = null, - ) - }.toSet() - }) - } - - override fun getModLoaders(): InstanceModLoaders { - return InstanceModLoaders(minecraft = minecraftVersion, fabricLoader = fabricVersion) - } - - override fun getOverridesPaths(configDir: Path): List = emptyList() -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ModrinthPackFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ModrinthPackFormat.kt deleted file mode 100644 index da5c2ad..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/ModrinthPackFormat.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.mineinabyss.launchy.instance.data.formats - -import com.mineinabyss.launchy.instance.data.InstanceModList -import com.mineinabyss.launchy.instance.data.InstanceModLoaders -import com.mineinabyss.launchy.instance.data.Mod -import com.mineinabyss.launchy.instance.data.storage.ModConfig -import kotlinx.serialization.Serializable -import java.nio.file.Path -import kotlin.io.path.div - -@Serializable -data class ModrinthPackFormat( - val dependencies: InstanceModLoaders, - val files: List, - val formatVersion: Int, - val name: String, - val versionId: String, -) : PackFormat { - @Serializable - data class PackFile( - val downloads: List, - val fileSize: Long, - val path: ModDownloadPath, - val hashes: Hashes, - ) { - fun toMod(packDir: Path) = Mod( - packDir, - ModConfig( - name = path.validated.toString().removePrefix("mods/").removeSuffix(".jar"), - desc = "", - url = downloads.single(), - downloadPath = path, - ), - modId = downloads.single().removePrefix("https://cdn.modrinth.com/data/").substringBefore("/versions"), - desiredHashes = hashes, - ) - } - - @Serializable - data class Hashes( - val sha1: String, - val sha512: String, - ) - - override fun getModLoaders(): InstanceModLoaders { - return dependencies - } - - override fun toGenericMods(downloadsDir: Path) = - InstanceModList.withSingleGroup(files.map { it.toMod(downloadsDir) }) - - override fun getOverridesPaths(configDir: Path): List = listOf(configDir / "mrpack" / "overrides") -} - diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/PackFormat.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/PackFormat.kt deleted file mode 100644 index 751fd9b..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/formats/PackFormat.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.mineinabyss.launchy.instance.data.formats - -import com.mineinabyss.launchy.instance.data.InstanceModList -import com.mineinabyss.launchy.instance.data.InstanceModLoaders -import java.nio.file.Path - -sealed interface PackFormat { - fun toGenericMods(downloadsDir: Path): InstanceModList - - fun getModLoaders(): InstanceModLoaders - - fun getOverridesPaths(configDir: Path): List -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/storage/InstanceConfig.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/storage/InstanceConfig.kt index ff7a570..e3ee093 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/storage/InstanceConfig.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/storage/InstanceConfig.kt @@ -1,7 +1,8 @@ package com.mineinabyss.launchy.instance.data.storage import androidx.compose.runtime.Immutable -import com.mineinabyss.launchy.downloads.data.source.PackSource +import com.mineinabyss.launchy.downloads.data.formats.SerializedPackFormat +import com.mineinabyss.launchy.downloads.data.sources.SerializedDownloadSource import com.mineinabyss.launchy.util.Dirs import com.mineinabyss.launchy.util.urlToFileName import kotlinx.serialization.Serializable @@ -15,7 +16,8 @@ data class InstanceConfig( val description: String, val backgroundURL: String, val logoURL: String, - val source: PackSource, + val source: SerializedDownloadSource, + val pack: SerializedPackFormat, val hue: Float = 0f, val cloudInstanceURL: String? = null, val overrideMinecraftDir: String? = null, diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/storage/InstanceUserConfig.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/storage/InstanceUserConfig.kt index 9b394d7..ae65a63 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/storage/InstanceUserConfig.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/storage/InstanceUserConfig.kt @@ -2,7 +2,7 @@ package com.mineinabyss.launchy.instance.data.storage import com.charleskorn.kaml.decodeFromStream import com.mineinabyss.launchy.instance.data.DownloadInfo -import com.mineinabyss.launchy.instance.data.InstanceModLoaders +import com.mineinabyss.launchy.instance.data.ModLoaderModel import com.mineinabyss.launchy.util.Formats import com.mineinabyss.launchy.util.GroupName import com.mineinabyss.launchy.util.ModID @@ -13,7 +13,7 @@ import kotlin.io.path.* @Serializable data class InstanceUserConfig( - val userAgreedDeps: InstanceModLoaders? = null, + val userAgreedDeps: ModLoaderModel? = null, val fullEnabledGroups: Set = setOf(), val fullDisabledGroups: Set = setOf(), val toggledMods: Set = setOf(), diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/storage/ModConfig.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/storage/ModConfig.kt index 34a4aa4..a79e5cc 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/storage/ModConfig.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/storage/ModConfig.kt @@ -1,6 +1,6 @@ package com.mineinabyss.launchy.instance.data.storage -import com.mineinabyss.launchy.instance.data.formats.ModDownloadPath +import com.mineinabyss.launchy.downloads.data.formats.ModDownloadPath import com.mineinabyss.launchy.util.ModID import kotlinx.serialization.Serializable diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ButtonInteractions.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ButtonInteractions.kt new file mode 100644 index 0000000..6729d61 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ButtonInteractions.kt @@ -0,0 +1,5 @@ +package com.mineinabyss.launchy.instance.ui + +data class ButtonInteractions( + val onUpdate: () -> Unit, +) diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstallState.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstallState.kt index b1ae3f4..ad5fef2 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstallState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstallState.kt @@ -5,7 +5,7 @@ import com.mineinabyss.launchy.util.ModID sealed interface InstallState { data object InProgress : InstallState data class Queued( - val modLoaderUpdateAvailable: Boolean, + val modLoaderChange: String?, val install: List, val update: List, val remove: List, diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceUiState.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceUiState.kt index ebd2f5b..b2fc28c 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceUiState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceUiState.kt @@ -1,6 +1,7 @@ package com.mineinabyss.launchy.instance.ui import androidx.compose.ui.graphics.painter.BitmapPainter +import com.mineinabyss.launchy.util.InstanceKey data class InstanceUiState( val title: String, @@ -9,4 +10,9 @@ data class InstanceUiState( val logo: BitmapPainter?, val background: BitmapPainter?, val runningProcess: Process?, + val hue: Float, + val enabled: Boolean, + val updatesAvailable: Boolean, + val key: InstanceKey, + val installedModLoader: String? ) diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceViewModel.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceViewModel.kt index 5d2c4a4..1f032f5 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceViewModel.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceViewModel.kt @@ -1,34 +1,35 @@ package com.mineinabyss.launchy.instance.ui import androidx.lifecycle.ViewModel -import com.mineinabyss.launchy.instance.data.GameInstanceDataSource +import androidx.lifecycle.viewModelScope +import com.mineinabyss.launchy.instance.data.InstanceModel +import com.mineinabyss.launchy.instance_list.data.InstanceRepository import com.mineinabyss.launchy.util.AppDispatchers import com.mineinabyss.launchy.util.ModID -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.* import kotlinx.coroutines.withContext import org.to2mbn.jmccc.mcdownloader.download.Downloader class InstanceViewModel( val downloader: Downloader, + val instanceRepo: InstanceRepository, ) : ViewModel() { - private val currentInstance = MutableStateFlow(null) - private val _modsState = MutableStateFlow(ModListUiState.Loading) + private val currentInstance = MutableStateFlow(null) + private val _installState = MutableStateFlow(InstallState.InProgress) private val _instanceUiState = MutableStateFlow(null) - val modsState = _modsState.asStateFlow() - val instanceUiState = _instanceUiState.asStateFlow() - val installState = _installState.asStateFlow() - - @OptIn(ExperimentalCoroutinesApi::class) - val modList = currentInstance.mapLatest { + val modsState = currentInstance.mapLatest { instance -> + if (instance == null) return@mapLatest ModListUiState.Error("No instance selected") withContext(AppDispatchers.IO) { - it?.loadModList() + ModListUiState.Loaded( + instance.instanceFile + ) } - } + }.stateIn(viewModelScope, SharingStarted.Eagerly, ModListUiState.Loading) + + val instanceUiState = _instanceUiState.asStateFlow() + val installState = _installState.asStateFlow() //TODO read val userInstalledMods = MutableStateFlow(null) @@ -142,4 +143,6 @@ class InstanceViewModel( fun groupInteractionsFor(id: String): ModGroupInteractions = TODO() fun modInteractionsFor(id: ModID): ModInteractions = TODO() + + fun buttonInteractions(): ButtonInteractions = TODO() } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/ImportSettingsDialog.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/ImportSettingsDialog.kt deleted file mode 100644 index 68acece..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/ImportSettingsDialog.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.mineinabyss.launchy.instance.ui.components - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.core.ui.components.LaunchyDialog -import com.mineinabyss.launchy.core.ui.screens.Screen -import com.mineinabyss.launchy.core.ui.screens.screen -import com.mineinabyss.launchy.util.Dirs -import kotlin.io.path.copyTo -import kotlin.io.path.div - -//TODO needs to be updated for multiple instances -@Composable -fun HandleImportSettings() { - val state = LocalLaunchyState - AnimatedVisibility( - !state.handledImportOptions && state.onboardingComplete, - enter = fadeIn(), exit = fadeOut(), - ) { - ImportSettingsDialog( - onAccept = { - try { - (Dirs.minecraft / "options.txt").copyTo(Dirs.mineinabyss / "options.txt") - } catch (e: Exception) { - // TODO: Show error message - e.printStackTrace() - } - screen = Screen.InstanceSettings - state.handledImportOptions = true - }, - onDecline = { - screen = Screen.InstanceSettings - state.handledImportOptions = true - } - ) - } -} - -@Composable -fun ImportSettingsDialog( - onAccept: () -> Unit, - onDecline: () -> Unit, -) { - LaunchyDialog( - title = { Text("Import Settings") }, - onAccept = onAccept, - onDecline = onDecline, - onDismiss = onDecline, - acceptText = "Import", - declineText = "Skip", - content = { - Text("This will import the options.txt file from your .minecraft directory.") - } - ) -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/AuthButton.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/AuthButton.kt deleted file mode 100644 index 9c35c07..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/AuthButton.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.mineinabyss.launchy.instance.ui.components.buttons - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.Login -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.auth.data.identity.IdentityDataSource -import com.mineinabyss.launchy.core.ui.components.PrimaryButton -import kotlinx.coroutines.launch - -@Composable -fun AuthButton() { - val state = LocalLaunchyState - val coroutineScope = rememberCoroutineScope() - - PrimaryButton( - enabled = state.profile.currentSession == null, - onClick = { - coroutineScope.launch { - IdentityDataSource.authOrShowDialog(state, state.profile) - } - }, - ) { - Icon(Icons.AutoMirrored.Rounded.Login, "Login") - Text("Login") - } -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/InstallButton.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/InstallButton.kt index 082fad7..1ef56e7 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/InstallButton.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/InstallButton.kt @@ -8,54 +8,28 @@ import androidx.compose.material.icons.rounded.Download import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.core.ui.LocalGameInstanceState -import com.mineinabyss.launchy.core.ui.components.OutlinedRedButton import com.mineinabyss.launchy.core.ui.components.PrimaryButton -import com.mineinabyss.launchy.downloads.data.ModDownloader.startInstall import com.mineinabyss.launchy.instance.ui.InstallState -import com.mineinabyss.launchy.instance.ui.InstanceViewModel -import com.mineinabyss.launchy.util.AppDispatchers -import kotlinx.coroutines.launch - -@Composable -fun RetryFailedButton(enabled: Boolean) { - val state = LocalLaunchyState - val packState = LocalGameInstanceState - OutlinedRedButton( - enabled = enabled, - onClick = { - AppDispatchers.profileLaunch.launch { - packState.startInstall(state, ignoreCachedCheck = true) - } - }, - ) { - Text("Retry ${packState.queued.failures.size} failed downloads") - } -} @Composable fun InstallButton( + state: InstallState, modifier: Modifier = Modifier, - viewModel: InstanceViewModel = viewModel(), + onClick: () -> Unit = {} ) { - val state by viewModel.installState.collectAsState() PrimaryButton( - enabled = state == InstallState.Queued, - onClick = { viewModel.installMods() }, + enabled = state is InstallState.Queued, + onClick = onClick, modifier = modifier.width(150.dp) ) { Row(verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Rounded.Download, "Download") AnimatedVisibility(true, Modifier.animateContentSize()) { val text = when (state) { - InstallState.Queued, InstallState.Error -> "Install" + is InstallState.Queued, InstallState.Error -> "Install" InstallState.AllInstalled -> "Installed" InstallState.InProgress -> "Installing" } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/PlayButton.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/PlayButton.kt index 97248b0..fdb5ae2 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/PlayButton.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/PlayButton.kt @@ -6,55 +6,48 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.PlayArrow -import androidx.compose.material.icons.rounded.PlayDisabled import androidx.compose.material.icons.rounded.Stop import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.core.ui.components.PrimaryButtonColors import com.mineinabyss.launchy.core.ui.components.SecondaryButtonColors -import com.mineinabyss.launchy.instance.ui.GameInstanceState import com.mineinabyss.launchy.instance.ui.InstanceUiState -import com.mineinabyss.launchy.launcher.ui.LauncherViewModel -import com.mineinabyss.launchy.util.koinViewModel @Composable fun PlayButton( hideText: Boolean = false, instance: InstanceUiState, modifier: Modifier = Modifier, - launcher: LauncherViewModel = koinViewModel() + onClick: () -> Unit, ) { val buttonIcon by remember(instance, instance.runningProcess) { mutableStateOf( when { - state.profile.currentProfile == null -> Icons.Rounded.PlayDisabled - process == null -> Icons.Rounded.PlayArrow +// state.profile.currentProfile == null -> Icons.Rounded.PlayDisabled + instance.runningProcess == null -> Icons.Rounded.PlayArrow else -> Icons.Rounded.Stop } ) } - val buttonText by remember(process) { - mutableStateOf(if (process == null) "Play" else "Stop") + val buttonText by remember(instance.runningProcess) { + mutableStateOf(if (instance.runningProcess == null) "Play" else "Stop") } val buttonColors by mutableStateOf( - if (process == null) PrimaryButtonColors + if (instance.runningProcess == null) PrimaryButtonColors else SecondaryButtonColors ) - Box { - var foundPackState: GameInstanceState? by remember { mutableStateOf(null) } - val onClick: () -> Unit = { - launcher.launch(instance) - } - val enabled = state.profile.currentProfile != null - && foundPackState?.downloads?.isDownloading != true - && state.inProgressTasks.isEmpty() + val enabled = instance.enabled + Box { if (hideText) Button( enabled = enabled, onClick = onClick, diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/RetryFailedButton.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/RetryFailedButton.kt new file mode 100644 index 0000000..77d4ce4 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/RetryFailedButton.kt @@ -0,0 +1,23 @@ +package com.mineinabyss.launchy.instance.ui.components.buttons + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import com.mineinabyss.launchy.core.ui.components.OutlinedRedButton +import com.mineinabyss.launchy.util.AppDispatchers +import kotlinx.coroutines.launch + +@Composable +fun RetryFailedButton(enabled: Boolean) { + val state = LocalLaunchyState + val packState = LocalGameInstanceState + OutlinedRedButton( + enabled = enabled, + onClick = { + AppDispatchers.profileLaunch.launch { + packState.startInstall(state, ignoreCachedCheck = true) + } + }, + ) { + Text("Retry ${packState.queued.failures.size} failed downloads") + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/UpdateButton.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/UpdateButton.kt index dd18257..6c0d80c 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/UpdateButton.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/UpdateButton.kt @@ -7,18 +7,11 @@ import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.core.ui.LocalGameInstanceState -import com.mineinabyss.launchy.instance_list.data.InstanceRepository.updateInstance @Composable -fun UpdateButton() { - val state = LocalLaunchyState - val pack = LocalGameInstanceState +fun UpdateButton(onClick: () -> Unit = {}) { Box { - Button(onClick = { - pack.instance.updateInstance(state) - }) { + Button(onClick) { Icon(Icons.Rounded.Update, contentDescription = "Update") Text("Update Available") } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/UpdateInfoButton.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/UpdateInfoButton.kt deleted file mode 100644 index 3d81cd7..0000000 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/UpdateInfoButton.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.mineinabyss.launchy.instance.ui.components.buttons - -import androidx.compose.animation.* -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Delete -import androidx.compose.material.icons.rounded.Download -import androidx.compose.material.icons.rounded.Update -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.core.ui.LocalGameInstanceState - -@Composable -fun UpdateInfoButton() { - val state = LocalLaunchyState - val packState = LocalGameInstanceState - var toggled by remember { mutableStateOf(false) } - Button(onClick = { toggled = !toggled }, shape = RoundedCornerShape(20.dp)) { - Column { - val queued = packState.queued - - Row { - Icon(Icons.Rounded.Update, contentDescription = "Updates") - Text("${queued.newDownloads.size + queued.deletions.size} Updates") - } - - AnimatedVisibility( - toggled, - enter = expandIn(tween(200)) + fadeIn(tween(200, 100)), - exit = fadeOut() + shrinkOut(tween(200, 100)) - ) { - Column { - InfoText( - shown = queued.areUpdatesQueued, - icon = Icons.Rounded.Update, - desc = "Update", - extra = queued.updates.size.toString() - ) - InfoText( - shown = queued.areNewDownloadsQueued, - icon = Icons.Rounded.Download, - desc = "Download", - extra = queued.newDownloads.size.toString() - ) - InfoText( - shown = queued.areDeletionsQueued, - icon = Icons.Rounded.Delete, - desc = "Remove", - extra = queued.deletions.size.toString() - ) - } - } - } - } -} - -@Composable -fun InfoText(shown: Boolean, icon: ImageVector, desc: String, extra: String = "") { - if (shown) Row(verticalAlignment = Alignment.CenterVertically) { - Icon(icon, desc) - Text(desc, Modifier.padding(5.dp)) - Text(extra) - } -} - - diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/TripleSwitch.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/TripleSwitch.kt index d01ef1a..3ada6c4 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/TripleSwitch.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/TripleSwitch.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.core.ui.Constants -import com.mineinabyss.launchy.core.ui.LocalGameInstanceState import com.mineinabyss.launchy.instance.ui.ModGroupUiState import com.mineinabyss.launchy.instance.ui.ModUiState import com.mineinabyss.launchy.util.Option @@ -28,10 +27,9 @@ fun ToggleButtons( group: ModGroupUiState, mods: List, ) { - val state = LocalGameInstanceState val offColor = Color.Transparent val offTextColor = MaterialTheme.colorScheme.surface - val forced = group.forceEnabled || group.forceDisabled + val forced = group.force != Option.DEFAULT Surface(shape = RoundedCornerShape(20.0.dp)) { Surface( color = MaterialTheme.colorScheme.background, @@ -43,8 +41,8 @@ fun ToggleButtons( horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.width(Constants.SETTINGS_PRIMARY_BUTTON_WIDTH) ) { - val fullEnable = state.toggles.enabledMods.containsAll(mods) - val fullDisable = mods.none { it in state.toggles.enabledMods } + val fullEnable = mods.all { it.enabled } + val fullDisable = mods.all { !it.enabled } val disableColorContainer by animateColorAsState( if (fullDisable) MaterialTheme.colorScheme.errorContainer diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/ActionButton.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/ActionButton.kt index 393d6c6..a5696e6 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/ActionButton.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/ActionButton.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/InfoBar.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/InfoBar.kt index d52dcd5..fc93a1e 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/InfoBar.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/InfoBar.kt @@ -1,6 +1,6 @@ package com.mineinabyss.launchy.instance.ui.components.settings.infobar -import androidx.compose.animation.* +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -25,10 +25,10 @@ import com.mineinabyss.launchy.instance.ui.components.buttons.RetryFailedButton @Composable fun InfoBar( - instance: InstanceViewModel = viewModel(), + viewModel: InstanceViewModel = viewModel(), modifier: Modifier = Modifier ) { - val queuedState by instance._installQueueState.collectAsState() + val queuedState by viewModel.installState.collectAsState() val queue = when (queuedState) { is InstallState.Queued -> queuedState as InstallState.Queued else -> return @@ -46,15 +46,22 @@ fun InfoBar( .padding(horizontal = SETTINGS_HORIZONTAL_PADDING, vertical = 6.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - InstallButton(Modifier.width(Constants.SETTINGS_PRIMARY_BUTTON_WIDTH)) + val installState by viewModel.installState.collectAsState() + InstallButton( + installState, + Modifier.width(Constants.SETTINGS_PRIMARY_BUTTON_WIDTH), + onClick = { viewModel.installMods() } + ) val failures = queue.failures.isNotEmpty() AnimatedVisibility(failures) { RetryFailedButton(failures) } + val queued = installState as? InstallState.Queued + val instance by viewModel.instanceUiState.collectAsState() ActionButton( - shown = queue.modLoaderUpdateAvailable, + shown = queue.modLoaderChange != null, icon = Icons.Rounded.HistoryEdu, - desc = "Mod loader updates:\n${packState.queued.userAgreedModLoaders?.fullVersionName ?: "Not installed"} -> ${packState.modpack.modLoaders.fullVersionName}", + desc = "Mod loader updates:\n${instance?.installedModLoader ?: "Not installed"} -> ${queue.modLoaderChange}", count = 1 ) ActionButton( diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceScreen.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceScreen.kt index 8c67fb5..7eab22c 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceScreen.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceScreen.kt @@ -4,13 +4,12 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.core.ui.windowScope +import com.mineinabyss.launchy.instance.ui.InstanceUiState import com.mineinabyss.launchy.instance.ui.InstanceViewModel import com.mineinabyss.launchy.instance.ui.components.BackgroundImage import com.mineinabyss.launchy.instance.ui.components.LogoLarge @@ -23,12 +22,11 @@ import com.mineinabyss.launchy.util.koinViewModel @Preview @Composable fun InstanceScreen( + instance: InstanceUiState, viewModel: InstanceViewModel = koinViewModel(), ) { - val instance by viewModel.instanceUiState.collectAsState() - Box { - BackgroundImage(instance?.background, windowScope) + BackgroundImage(instance.background, windowScope) Column( modifier = @@ -38,14 +36,18 @@ fun InstanceScreen( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { - LogoLarge(instance?.logo, Modifier.weight(3f, false)) + LogoLarge(instance.logo, Modifier.weight(3f, false)) Row( horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().weight(1f, false), ) { - PlayButton(hideText = false, instance) { packState } - AnimatedVisibility(packState.instance.updatesAvailable) { + PlayButton( + hideText = false, + instance, + onClick = { viewModel.launch() }, + ) + AnimatedVisibility(instance.updatesAvailable) { UpdateButton() } SettingsButton() diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/InstanceCardInteractions.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/InstanceCardInteractions.kt new file mode 100644 index 0000000..f98bf4a --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/InstanceCardInteractions.kt @@ -0,0 +1,6 @@ +package com.mineinabyss.launchy.instance_list.data + +data class InstanceCardInteractions( + val onOpen: () -> Unit, + val onPlay: () -> Unit, +) diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/InstanceRepository.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/InstanceRepository.kt index 64c4648..0f68d30 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/InstanceRepository.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/InstanceRepository.kt @@ -1,9 +1,11 @@ package com.mineinabyss.launchy.instance_list.data +import com.mineinabyss.launchy.config.data.ConfigRepository import com.mineinabyss.launchy.core.data.FileSystemDataSource import com.mineinabyss.launchy.core.data.TasksRepository import com.mineinabyss.launchy.core.ui.screens.Screen import com.mineinabyss.launchy.core.ui.screens.screen +import com.mineinabyss.launchy.downloads.data.formats.Modpack import com.mineinabyss.launchy.instance.data.InstanceModel import com.mineinabyss.launchy.util.AppDispatchers import com.mineinabyss.launchy.util.InProgressTask @@ -12,18 +14,27 @@ import com.mineinabyss.launchy.util.showDialogOnError import io.ktor.http.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.withContext +import org.koin.compose.currentKoinScope class InstanceRepository( val local: LocalInstancesDataSource, val remote: RemoteInstanceDataSource, val tasks: TasksRepository, - val files: FileSystemDataSource + val files: FileSystemDataSource, + val configRepo: ConfigRepository, ) { private val _instances = MutableStateFlow(mapOf()) val instances = _instances.asStateFlow() + val lastPlayed = configRepo.config.map { it.lastPlayedMap } + + suspend fun loadLocalInstances() = withContext(AppDispatchers.IO) { + val instances = local.readInstances() + _instances.update { instances.associateBy { it.key } } + } suspend fun delete( key: InstanceKey, @@ -57,10 +68,20 @@ class InstanceRepository( } } -// suspend fun saveOnChanges() { -// instances.distinctUntilChanged { old, new -> }.collectLatest { instances -> -// instances -// files.scheduleWrite() -// } -// } + suspend fun fetchPackUpdates(key: InstanceKey) = withContext(AppDispatchers.IO) { + val instance = _instances.value[key] ?: error("Instance $key not found") + val source = instance.config.source.getDataSource(currentKoinScope()) + val packFormat = instance.config.pack.getFormat() + if (!source.skip(instance)) { + source.fetchLatestModsFor(instance)?.let { + packFormat.prepareSource(instance, it) + } + } + } + + suspend fun loadPack(key: InstanceKey): Result = withContext(AppDispatchers.IO) { + val instance = _instances.value[key] ?: error("Instance $key not found") + val packFormat = instance.config.pack.getFormat() + packFormat.loadPackFor(instance) + } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/LocalInstancesDataSource.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/LocalInstancesDataSource.kt index f547cd1..b772252 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/LocalInstancesDataSource.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/LocalInstancesDataSource.kt @@ -3,8 +3,8 @@ package com.mineinabyss.launchy.instance_list.data import com.charleskorn.kaml.decodeFromStream import com.charleskorn.kaml.encodeToStream import com.mineinabyss.launchy.downloads.data.Downloader -import com.mineinabyss.launchy.instance.data.InstanceModList import com.mineinabyss.launchy.instance.data.InstanceModel +import com.mineinabyss.launchy.instance.data.ModListModel import com.mineinabyss.launchy.instance.data.storage.InstanceConfig import com.mineinabyss.launchy.util.Formats import java.nio.file.Path @@ -13,7 +13,7 @@ import kotlin.io.path.* class LocalInstancesDataSource( val rootDir: Path, ) { - fun getInstanceList(): List = rootDir + fun readInstances(): List = rootDir .listDirectoryEntries() .filter { it.isDirectory() } .mapNotNull { dir -> @@ -22,8 +22,8 @@ class LocalInstancesDataSource( .getOrNull() } - fun readInstance(path: Path): Result = runCatching { - InstanceModel(Formats.yaml.decodeFromStream(path.inputStream()), path.parent) + fun readInstance(instanceFile: Path): Result = runCatching { + InstanceModel(Formats.yaml.decodeFromStream(instanceFile.inputStream()), instanceFile.parent) } fun saveInstance(instance: InstanceModel) { @@ -45,7 +45,7 @@ class LocalInstancesDataSource( instance.directory.deleteRecursively() } - fun loadModList(instance: InstanceModel): InstanceModList { + fun loadModList(instance: InstanceModel): ModListModel { TODO() } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/RemoteInstanceDataSource.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/RemoteInstanceDataSource.kt index 8b0a2d4..bcb11a1 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/RemoteInstanceDataSource.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/RemoteInstanceDataSource.kt @@ -12,7 +12,6 @@ class RemoteInstanceDataSource { ): Result = runCatching { //TODO cache by headers Downloader.httpClient.get(url).body() -// source.read(newCloudInstancePath).onSuccess { cloudConfig -> } suspend fun fetchUpdatesForInstance( diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListScreen.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListScreen.kt index a164577..1569a78 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListScreen.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListScreen.kt @@ -17,40 +17,19 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.mineinabyss.launchy.instance_list.ui.components.InstanceList @Composable -fun HomeScreen(viewModel: InstanceListViewModel = viewModel()) { +fun InstanceListScreen(viewModel: InstanceListViewModel = viewModel()) { Box { val scrollState = rememberLazyListState() BoxWithConstraints { Column(Modifier.padding(end = 20.dp).fillMaxSize()) { -// var searchQuery by remember { mutableStateOf("") } -// SearchBar( -// searchQuery, -// active = false, -// placeholder = { Text("Search for modpacks") }, -// onQueryChange = { searchQuery = it }, -// onSearch = {}, -// onActiveChange = {}, -// modifier = Modifier.fillMaxWidth(), -// leadingIcon = { -// Icon(Icons.Rounded.Search, contentDescription = "Search") -// } -// ) { -// } LazyColumn(state = scrollState, modifier = Modifier.fillMaxSize()) { item { Spacer(Modifier.height(16.dp)) } item { - val instances by viewModel.gameInstances.collectAsState() - val lastPlayed by viewModel.lastPlayed.collectAsState() - InstanceList( - "Instances", - instances.sortedByDescending { lastPlayed[it.config.name] } - ) + val instances by viewModel.instances.collectAsState() + InstanceList("Instances", instances) } -// item { -// ModpackGroup("Find more", state.downloadedModpacks) -// } } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListViewModel.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListViewModel.kt index c478b32..6f675b0 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListViewModel.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListViewModel.kt @@ -2,17 +2,47 @@ package com.mineinabyss.launchy.instance_list.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.mineinabyss.launchy.instance.data.GameInstanceDataSource -import kotlinx.coroutines.flow.MutableStateFlow +import com.mineinabyss.launchy.instance.ui.InstanceUiState +import com.mineinabyss.launchy.instance_list.data.InstanceCardInteractions +import com.mineinabyss.launchy.instance_list.data.InstanceRepository +import com.mineinabyss.launchy.util.InstanceKey +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -class InstanceListViewModel : ViewModel() { +class InstanceListViewModel( + val instanceRepo: InstanceRepository, +) : ViewModel() { + val instances = instanceRepo.instances.combine(instanceRepo.lastPlayed) { inst, lastPlayed -> + inst.values.sortedBy { lastPlayed[it.key] } + .map { (config, dir, key) -> + InstanceUiState( + title = config.name, + description = config.description, + isCloudInstance = config.cloudInstanceURL != null, + // TODO + logo = null, + background = null, + runningProcess = null, + enabled = true, + updatesAvailable = false, + hue = 0f, + key = key, + ) + } + }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) + init { + merge(instanceRepo.instances, instanceRepo.lastPlayed).map { + } viewModelScope.launch { - + instanceRepo.loadLocalInstances() } } - val gameInstances = MutableStateFlow(listOf()) - val lastPlayed = MutableStateFlow(mapOf()) + fun cardInteractionsFor(key: InstanceKey): InstanceCardInteractions { + return InstanceCardInteractions( + onOpen = { TODO() }, + onPlay = { TODO() } + ) + } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceCard.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceCard.kt index 9ec6917..4fa5e05 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceCard.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceCard.kt @@ -11,9 +11,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter @@ -21,21 +18,15 @@ import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.LocalLaunchyState import com.mineinabyss.launchy.core.ui.components.Tooltip -import com.mineinabyss.launchy.core.ui.screens.Screen -import com.mineinabyss.launchy.core.ui.screens.screen import com.mineinabyss.launchy.core.ui.theme.LaunchyColors -import com.mineinabyss.launchy.core.ui.theme.currentHue -import com.mineinabyss.launchy.instance.data.GameInstanceDataSource -import com.mineinabyss.launchy.instance.data.storage.InstanceConfig +import com.mineinabyss.launchy.instance.ui.InstanceUiState import com.mineinabyss.launchy.instance.ui.components.SlightBackgroundTint import com.mineinabyss.launchy.instance.ui.components.buttons.PlayButton +import com.mineinabyss.launchy.instance_list.data.InstanceCardInteractions import com.mineinabyss.launchy.instance_list.ui.components.InstanceCardStyle.cardHeight import com.mineinabyss.launchy.instance_list.ui.components.InstanceCardStyle.cardPadding import com.mineinabyss.launchy.instance_list.ui.components.InstanceCardStyle.cardWidth -import com.mineinabyss.launchy.util.InProgressTask -import kotlinx.coroutines.launch object InstanceCardStyle { val cardHeight = 256.dp @@ -45,37 +36,25 @@ object InstanceCardStyle { @Composable fun InstanceCard( - config: InstanceConfig, - instance: GameInstanceDataSource? = null, + instance: InstanceUiState, + interactions: InstanceCardInteractions, modifier: Modifier = Modifier -) = MaterialTheme( - colorScheme = LaunchyColors(config.hue).DarkColors -) { - val state = LocalLaunchyState - val coroutineScope = rememberCoroutineScope() - val background by remember(config) { config.getBackgroundAsState() } +) = MaterialTheme(colorScheme = LaunchyColors(instance.hue).DarkColors) { Card( - onClick = { - instance ?: return@Card - coroutineScope.launch { - state.instanceState = instance.createModpackState(state) - currentHue = instance.config.hue - screen = Screen.Instance - } - }, - enabled = instance?.enabled == true, + onClick = { interactions.onOpen() }, + enabled = instance.enabled, modifier = modifier.height(cardHeight).width(cardWidth), ) { Box(Modifier.fillMaxSize()) { androidx.compose.animation.AnimatedVisibility( - visible = background != null, + visible = instance.background != null, enter = fadeIn(), modifier = Modifier.fillMaxSize() ) { - if (background != null) Image( - painter = background!!, + if (instance.background != null) Image( + painter = instance.background, colorFilter = - if (instance?.enabled == false) ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }) + if (instance.enabled == false) ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }) else null, contentDescription = "Pack background image", contentScale = ContentScale.Crop, @@ -84,7 +63,7 @@ fun InstanceCard( } SlightBackgroundTint() - if (config.cloudInstanceURL != null) TooltipArea( + if (instance.isCloudInstance) TooltipArea( tooltip = { Tooltip("Cloud modpack") }, modifier = Modifier.align(Alignment.TopEnd).padding(cardPadding + 4.dp).size(24.dp), ) { @@ -98,24 +77,27 @@ fun InstanceCard( ) { Column(Modifier.weight(1f, true)) { Text( - config.name, + instance.title, style = MaterialTheme.typography.headlineMedium, overflow = TextOverflow.Ellipsis, maxLines = 1 ) Text( - config.description, + instance.description, style = MaterialTheme.typography.bodyMedium, overflow = TextOverflow.Ellipsis, maxLines = 1 ) } - if (instance?.enabled == true) - PlayButton(hideText = true, instance, Modifier.weight(1f, false)) { - state.runTask("modpackState", InProgressTask("Checking for pack updates...")) { - instance.createModpackState(state, awaitUpdatesCheck = true) + if (instance.enabled) + PlayButton( + hideText = true, + instance, + Modifier.weight(1f, false), + onClick = { + interactions.onPlay() } - } + ) } } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceList.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceList.kt index 5cead55..8a732b2 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceList.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/components/InstanceList.kt @@ -8,25 +8,28 @@ import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.instance.data.GameInstanceDataSource +import com.mineinabyss.launchy.instance.ui.InstanceUiState +import com.mineinabyss.launchy.instance_list.ui.InstanceListViewModel +import com.mineinabyss.launchy.util.koinViewModel @Composable -fun InstanceList(title: String, packs: List) { - val state = LocalLaunchyState +fun InstanceList( + title: String, + instances: List, + viewModel: InstanceListViewModel = koinViewModel() +) { Column { -// var showAll by remember { mutableStateOf(false) } - val visiblePacks = packs//.take(6) Row { Text(title, style = MaterialTheme.typography.headlineMedium) } Spacer(Modifier.height(8.dp)) - if (visiblePacks.isEmpty()) { + if (instances.isEmpty()) { Text("No instances installed yet, click the + button on the sidebar to add one!") } else BoxWithConstraints(Modifier) { - val total = packs.size + 1 + val total = instances.size + 1 val colums = ((maxWidth / InstanceCardStyle.cardWidth).toInt()).coerceAtMost(total).coerceAtLeast(1) val rows = (total / colums).coerceAtLeast(1) val lazyGridState = rememberLazyGridState() @@ -40,8 +43,9 @@ fun InstanceList(title: String, packs: List) { horizontalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp), ) { - items(visiblePacks) { pack -> - InstanceCard(pack.config, pack) + items(instances, key = { it.key }) { instance -> + val interactions = remember(instance.key) { viewModel.cardInteractionsFor(instance.key) } + InstanceCard(instance, interactions) } } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/launcher/data/Launcher.kt b/src/main/kotlin/com/mineinabyss/launchy/launcher/data/Launcher.kt index 15ff898..c2f8a27 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/launcher/data/Launcher.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/launcher/data/Launcher.kt @@ -4,7 +4,7 @@ import com.mineinabyss.launchy.auth.data.ProfileRepository import com.mineinabyss.launchy.core.ui.Dialog import com.mineinabyss.launchy.core.ui.LaunchyUiState import com.mineinabyss.launchy.core.ui.screens.dialog -import com.mineinabyss.launchy.instance.data.InstanceModLoaders +import com.mineinabyss.launchy.instance.data.ModLoaderModel import com.mineinabyss.launchy.instance.ui.GameInstanceState import com.mineinabyss.launchy.util.AppDispatchers import kotlinx.coroutines.Job @@ -92,7 +92,7 @@ object Launcher { } fun download( - modLoaders: InstanceModLoaders, + modLoaders: ModLoaderModel, minecraftDir: Path, onStartDownload: (String) -> Unit = {}, onFinishDownload: (String) -> Unit = {} diff --git a/src/main/kotlin/com/mineinabyss/launchy/util/Dirs.kt b/src/main/kotlin/com/mineinabyss/launchy/util/Dirs.kt index 58ad3fc..b7c85d0 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/util/Dirs.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/Dirs.kt @@ -1,6 +1,6 @@ package com.mineinabyss.launchy.util -import com.mineinabyss.launchy.instance.data.GameInstanceDataSource +import com.mineinabyss.launchy.instance.data.InstanceModel import java.util.* import kotlin.io.path.* @@ -25,7 +25,7 @@ object Dirs { OS.LINUX -> home / ".config" } / "mineinabyss" - fun cacheDir(instance: GameInstanceDataSource) = instance.configDir / "cache" + fun cacheDir(instance: InstanceModel) = instance.directory / "cache" val imageCache = config / "cache" / "images" diff --git a/src/main/kotlin/com/mineinabyss/launchy/util/Typealiases.kt b/src/main/kotlin/com/mineinabyss/launchy/util/Typealiases.kt index 8ec357f..1c2cbb8 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/util/Typealiases.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/util/Typealiases.kt @@ -1,6 +1,6 @@ package com.mineinabyss.launchy.util -import java.util.* +import kotlinx.serialization.Serializable typealias ModName = String typealias GroupName = String @@ -10,6 +10,6 @@ typealias ConfigURL = String typealias ModID = String - +@Serializable @JvmInline -value class InstanceKey(val key: UUID) +value class InstanceKey(val key: String) From a78c9cd49ef855659765dd501105bc356ea0c953 Mon Sep 17 00:00:00 2001 From: Danielle Voznyy Date: Sun, 2 Jun 2024 00:34:02 -0400 Subject: [PATCH 5/5] refactor: slowly going through project errors --- .../kotlin/com/mineinabyss/launchy/Main.kt | 2 +- .../launchy/core/ui/LaunchyViewModel.kt | 63 ++---- .../ui/components/InProgressTasksIndicator.kt | 1 + .../core/ui/dialogs/SelectJVMDialog.kt | 40 ++-- .../launchy/downloads/data/Downloader.kt | 67 +----- .../launchy/instance/data/InstanceModel.kt | 2 + .../launchy/instance/ui/InstanceViewModel.kt | 15 +- .../launchy/instance/ui/ModListUiState.kt | 4 +- .../launchy/instance/ui/ModUiState.kt | 32 ++- .../components/buttons/RetryFailedButton.kt | 18 +- .../ui/components/settings/ModInfoDisplay.kt | 4 +- .../ui/components/settings/infobar/InfoBar.kt | 8 +- .../instance/ui/screens/InstanceProperties.kt | 4 +- .../launchy/instance/ui/screens/OptionsTab.kt | 5 - .../instance_creation/ui/ConfirmImportTab.kt | 104 +++++++++ .../launchy/instance_creation/ui/ImportTab.kt | 103 +++++++++ .../instance_creation/ui/NewInstance.kt | 205 +----------------- .../instance_creation/ui/PopularInstances.kt | 25 +++ .../instance_list/data/InstanceRepository.kt | 1 + .../data/LocalInstancesDataSource.kt | 5 +- .../data/RemoteInstanceDataSource.kt | 6 +- .../instance_list/ui/InstanceListViewModel.kt | 3 +- .../launcher/data/ProcessRepository.kt | 13 +- .../settings/ui/JVMSettingsViewModel.kt | 98 ++++++++- 24 files changed, 431 insertions(+), 397 deletions(-) create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/ConfirmImportTab.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/ImportTab.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/PopularInstances.kt diff --git a/src/main/kotlin/com/mineinabyss/launchy/Main.kt b/src/main/kotlin/com/mineinabyss/launchy/Main.kt index 449c381..c427e23 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/Main.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/Main.kt @@ -40,7 +40,7 @@ fun main() = application { val viewModel = koinViewModel() val onClose: () -> Unit = { exitApplication() - viewModel.saveToConfig() +// viewModel.saveToConfig() TODO } Window( diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyViewModel.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyViewModel.kt index 5d238fe..3b64ba2 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyViewModel.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/LaunchyViewModel.kt @@ -1,13 +1,10 @@ package com.mineinabyss.launchy.core.ui -import androidx.compose.runtime.* import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.mineinabyss.launchy.config.data.ConfigRepository import com.mineinabyss.launchy.util.Dirs import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch class LaunchyViewModel( val configRepo: ConfigRepository @@ -17,44 +14,32 @@ class LaunchyViewModel( val uiState = _uiState.asStateFlow() init { - viewModelScope.launch { - setupFilesystem() - configRepo.tryLoadConfig() - _uiState.emit( - LaunchyUiState.Ready( - UiState(config), - instances, - ) - ) - } +// viewModelScope.launch { +// setupFilesystem() +// configRepo.tryLoadConfig() +// _uiState.emit( +// LaunchyUiState.Ready( +// UiState(config), +// instances, +// ) +// ) +// } } - val lastPlayed = mutableStateMapOf().apply { - putAll(config.lastPlayedMap) - } - - // If any state is true, we consider import handled and move on - var handledImportOptions by mutableStateOf( - config.handledImportOptions - ) - - var onboardingComplete by mutableStateOf(config.onboardingComplete) - - - fun saveToConfig() { - config.value.copy( - handledImportOptions = handledImportOptions, - onboardingComplete = onboardingComplete, - currentProfile = profile.currentProfile, - javaPath = jvm.javaPath?.toString(), - jvmArguments = jvm.userJvmArgs, - memoryAllocation = jvm.userMemoryAllocation, - useRecommendedJvmArguments = jvm.useRecommendedJvmArgs, - preferHue = ui.preferHue, - startInFullscreen = ui.fullscreen, - lastPlayedMap = lastPlayed - ).save() - } +// fun saveToConfig() { +// config.value.copy( +// handledImportOptions = handledImportOptions, +// onboardingComplete = onboardingComplete, +// currentProfile = profile.currentProfile, +// javaPath = jvm.javaPath?.toString(), +// jvmArguments = jvm.userJvmArgs, +// memoryAllocation = jvm.userMemoryAllocation, +// useRecommendedJvmArguments = jvm.useRecommendedJvmArgs, +// preferHue = ui.preferHue, +// startInFullscreen = ui.fullscreen, +// lastPlayedMap = lastPlayed +// ).save() +// } private fun setupFilesystem() { Dirs.createDirs() diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/InProgressTasksIndicator.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/InProgressTasksIndicator.kt index fae5424..2a0fd1e 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/InProgressTasksIndicator.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/components/InProgressTasksIndicator.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp diff --git a/src/main/kotlin/com/mineinabyss/launchy/core/ui/dialogs/SelectJVMDialog.kt b/src/main/kotlin/com/mineinabyss/launchy/core/ui/dialogs/SelectJVMDialog.kt index b1d5f23..08aeeb6 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/core/ui/dialogs/SelectJVMDialog.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/core/ui/dialogs/SelectJVMDialog.kt @@ -3,44 +3,30 @@ package com.mineinabyss.launchy.core.ui.dialogs import androidx.compose.material.LocalTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import com.mineinabyss.launchy.LocalLaunchyState import com.mineinabyss.launchy.core.ui.Dialog import com.mineinabyss.launchy.core.ui.components.LaunchyDialog import com.mineinabyss.launchy.core.ui.screens.Screen import com.mineinabyss.launchy.core.ui.screens.dialog import com.mineinabyss.launchy.core.ui.screens.screen -import com.mineinabyss.launchy.downloads.data.Downloader -import com.mineinabyss.launchy.util.AppDispatchers -import kotlinx.coroutines.launch +import com.mineinabyss.launchy.settings.ui.JVMSettingsViewModel +import com.mineinabyss.launchy.util.koinViewModel @Composable -fun SelectJVMDialog() { - val coroutineScope = rememberCoroutineScope() - val state = LocalLaunchyState +fun SelectJVMDialog( + jvm: JVMSettingsViewModel = koinViewModel() +) { LaunchyDialog( title = { Text("Install java", style = LocalTextStyle.current) }, onAccept = { dialog = Dialog.None - AppDispatchers.IO.launch { - val jdkPath = runCatching { - Downloader.installJDK(state) - }.getOrElse { - dialog = Dialog.Error( - "Failed to install Java", - it.stackTraceToString() - ) - return@launch - } - if (jdkPath != null) { - state.jvm.javaPath = jdkPath - state.saveToConfig() - } else { - dialog = Dialog.Error( - "Failed to install Java", - "Please install Java manually and select the path in settings." - ) - } + runCatching { + jvm.installJDK() + }.getOrElse { + dialog = Dialog.Error( + "Failed to install Java", + it.stackTraceToString() + ) + return@LaunchyDialog } }, onDecline = { dialog = Dialog.None; screen = Screen.Settings }, diff --git a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/Downloader.kt b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/Downloader.kt index 441686d..5d50b49 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/downloads/data/Downloader.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/downloads/data/Downloader.kt @@ -1,8 +1,10 @@ package com.mineinabyss.launchy.downloads.data -import com.mineinabyss.launchy.core.ui.LaunchyUiState import com.mineinabyss.launchy.instance.data.InstanceModel -import com.mineinabyss.launchy.util.* +import com.mineinabyss.launchy.util.Dirs +import com.mineinabyss.launchy.util.Progress +import com.mineinabyss.launchy.util.UpdateResult +import com.mineinabyss.launchy.util.urlToFileName import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.cio.* @@ -14,10 +16,7 @@ import io.ktor.serialization.kotlinx.json.* import io.ktor.utils.io.* import io.ktor.utils.io.core.* import kotlinx.serialization.json.Json -import org.rauschig.jarchivelib.ArchiveFormat import org.rauschig.jarchivelib.Archiver -import org.rauschig.jarchivelib.ArchiverFactory -import org.rauschig.jarchivelib.CompressionType import java.nio.file.Path import java.util.* import kotlin.io.path.* @@ -126,64 +125,6 @@ class Downloader { download("https://mc-heads.net/avatar/$uuid", Dirs.avatar(uuid), options) } - /** @return Path to java executable */ - @OptIn(ExperimentalPathApi::class) - suspend fun installJDK( - state: LaunchyUiState, - ): 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( - url, - "bin/java.exe", - ArchiverFactory.createArchiver(ArchiveFormat.ZIP) - ) - - OS.MAC -> JavaInstallation( - url, - "Contents/Home/bin/java", - ArchiverFactory.createArchiver(ArchiveFormat.TAR, CompressionType.GZIP) - ) - - OS.LINUX -> JavaInstallation( - url, - "bin/java", - ArchiverFactory.createArchiver(ArchiveFormat.TAR, CompressionType.GZIP) - ) - } - 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, Options( - onProgressUpdate = { - state.inProgressTasks["installJDK"] = - InProgressTask.bytes( - "Downloading Java environment", - it.bytesDownloaded, - 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 - 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") - } - } - class JavaInstallation( val url: String, val relativeJavaExecutable: String, diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModel.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModel.kt index cd6c10e..773aaf9 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModel.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/data/InstanceModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.mineinabyss.launchy.instance.data.storage.InstanceConfig +import com.mineinabyss.launchy.instance.data.storage.InstanceUserConfig import com.mineinabyss.launchy.util.Dirs import com.mineinabyss.launchy.util.InstanceKey import kotlinx.coroutines.Dispatchers @@ -15,6 +16,7 @@ import kotlin.io.path.name data class InstanceModel( val config: InstanceConfig, + val userConfig: InstanceUserConfig, val directory: Path, val key: InstanceKey = InstanceKey(directory.name), ) { diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceViewModel.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceViewModel.kt index 1f032f5..ca49b86 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceViewModel.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/InstanceViewModel.kt @@ -4,10 +4,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.mineinabyss.launchy.instance.data.InstanceModel import com.mineinabyss.launchy.instance_list.data.InstanceRepository -import com.mineinabyss.launchy.util.AppDispatchers import com.mineinabyss.launchy.util.ModID import kotlinx.coroutines.flow.* -import kotlinx.coroutines.withContext import org.to2mbn.jmccc.mcdownloader.download.Downloader class InstanceViewModel( @@ -21,11 +19,10 @@ class InstanceViewModel( val modsState = currentInstance.mapLatest { instance -> if (instance == null) return@mapLatest ModListUiState.Error("No instance selected") - withContext(AppDispatchers.IO) { - ModListUiState.Loaded( - instance.instanceFile - ) - } + val pack = instanceRepo + .loadPack(instance.key) + .getOrElse { return@mapLatest ModListUiState.Error(it.message ?: "Unknown error") } + ModListUiState.Loaded(pack) }.stateIn(viewModelScope, SharingStarted.Eagerly, ModListUiState.Loading) val instanceUiState = _instanceUiState.asStateFlow() @@ -92,7 +89,9 @@ class InstanceViewModel( else enabledMods.value -= mod } - fun installMods() { + fun installMods( + ignoreCachedCheck: Boolean = false, + ) { TODO() } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModListUiState.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModListUiState.kt index 0172c77..88c1bf8 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModListUiState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModListUiState.kt @@ -1,9 +1,11 @@ package com.mineinabyss.launchy.instance.ui +import com.mineinabyss.launchy.downloads.data.formats.Modpack + sealed interface ModListUiState { object Loading : ModListUiState data class Error(val message: String) : ModListUiState class Loaded( - val groups: List, + val groups: Modpack, ) : ModListUiState } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModUiState.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModUiState.kt index 8e4d440..966b47a 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModUiState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/ModUiState.kt @@ -6,6 +6,8 @@ import androidx.compose.material.icons.rounded.Download import androidx.compose.material.icons.rounded.Error import androidx.compose.material.icons.rounded.Update import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.graphics.vector.ImageVector import com.mineinabyss.launchy.instance.data.storage.ModConfig import com.mineinabyss.launchy.util.ModID @@ -32,20 +34,26 @@ enum class ModQueueState { NONE; companion object { - fun surfaceColor(state: ModQueueState) = when (state) { - RETRY_DOWNLOAD -> MaterialTheme.colorScheme.error - DELETE -> MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.25f) - INSTALL -> MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.25f) - UPDATE -> MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.1f) - NONE -> MaterialTheme.colorScheme.surface + @Composable + fun surfaceColor(state: ModQueueState) = remember(state) { + when (state) { + RETRY_DOWNLOAD -> MaterialTheme.colorScheme.error + DELETE -> MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.25f) + INSTALL -> MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.25f) + UPDATE -> MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.1f) + NONE -> MaterialTheme.colorScheme.surface + } } - fun infoIcon(state: ModQueueState): ImageVector? = when (state) { - RETRY_DOWNLOAD -> Icons.Rounded.Error - DELETE -> Icons.Rounded.Delete - INSTALL -> Icons.Rounded.Download - UPDATE -> Icons.Rounded.Update - NONE -> null + @Composable + fun infoIcon(state: ModQueueState): ImageVector? = remember(state) { + when (state) { + RETRY_DOWNLOAD -> Icons.Rounded.Error + DELETE -> Icons.Rounded.Delete + INSTALL -> Icons.Rounded.Download + UPDATE -> Icons.Rounded.Update + NONE -> null + } } } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/RetryFailedButton.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/RetryFailedButton.kt index 77d4ce4..8eb12a6 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/RetryFailedButton.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/buttons/RetryFailedButton.kt @@ -3,21 +3,15 @@ package com.mineinabyss.launchy.instance.ui.components.buttons import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.mineinabyss.launchy.core.ui.components.OutlinedRedButton -import com.mineinabyss.launchy.util.AppDispatchers -import kotlinx.coroutines.launch @Composable -fun RetryFailedButton(enabled: Boolean) { - val state = LocalLaunchyState - val packState = LocalGameInstanceState +fun RetryFailedButton( + failureCount: Int, + onClick: () -> Unit, +) { OutlinedRedButton( - enabled = enabled, - onClick = { - AppDispatchers.profileLaunch.launch { - packState.startInstall(state, ignoreCachedCheck = true) - } - }, + onClick = onClick, ) { - Text("Retry ${packState.queued.failures.size} failed downloads") + Text("Retry $failureCount failed downloads") } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModInfoDisplay.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModInfoDisplay.kt index 4cb05f9..bbde957 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModInfoDisplay.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/ModInfoDisplay.kt @@ -33,8 +33,8 @@ fun ModInfoDisplay( mod: ModUiState, interactions: ModInteractions, ) { - val surfaceColor = remember(mod.queueState) { ModQueueState.surfaceColor(mod.queueState) } - val infoIcon = remember(mod.queueState) { ModQueueState.infoIcon(mod.queueState) } + val surfaceColor = ModQueueState.surfaceColor(mod.queueState) + val infoIcon = ModQueueState.infoIcon(mod.queueState) Surface( modifier = Modifier.fillMaxWidth(), diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/InfoBar.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/InfoBar.kt index fc93a1e..b9b94f9 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/InfoBar.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/components/settings/infobar/InfoBar.kt @@ -52,9 +52,11 @@ fun InfoBar( Modifier.width(Constants.SETTINGS_PRIMARY_BUTTON_WIDTH), onClick = { viewModel.installMods() } ) - val failures = queue.failures.isNotEmpty() - AnimatedVisibility(failures) { - RetryFailedButton(failures) + AnimatedVisibility(queue.failures.isNotEmpty()) { + RetryFailedButton( + queue.failures.count(), + onClick = { viewModel.installMods(ignoreCachedCheck = true) } + ) } val queued = installState as? InstallState.Queued val instance by viewModel.instanceUiState.collectAsState() diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceProperties.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceProperties.kt index 9530c4c..991f27b 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceProperties.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/InstanceProperties.kt @@ -11,9 +11,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.mineinabyss.launchy.core.ui.components.DirectoryDialog diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/OptionsTab.kt b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/OptionsTab.kt index 3776df7..40eef1d 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/OptionsTab.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance/ui/screens/OptionsTab.kt @@ -11,8 +11,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.core.ui.LocalGameInstanceState import com.mineinabyss.launchy.core.ui.components.ComfyContent import com.mineinabyss.launchy.core.ui.components.TitleSmall import com.mineinabyss.launchy.core.ui.screens.Screen @@ -24,9 +22,6 @@ import kotlinx.coroutines.launch @Composable fun OptionsTab() { - val state = LocalLaunchyState - val pack = LocalGameInstanceState - ComfyContent(Modifier.padding(16.dp)) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { TitleSmall("Mods") diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/ConfirmImportTab.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/ConfirmImportTab.kt new file mode 100644 index 0000000..981605f --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/ConfirmImportTab.kt @@ -0,0 +1,104 @@ +package com.mineinabyss.launchy.instance_creation.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.TextFields +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.mineinabyss.launchy.core.ui.components.AnimatedTab +import com.mineinabyss.launchy.core.ui.components.ComfyContent +import com.mineinabyss.launchy.core.ui.components.ComfyTitle +import com.mineinabyss.launchy.core.ui.components.ComfyWidth +import com.mineinabyss.launchy.core.ui.screens.Screen +import com.mineinabyss.launchy.core.ui.screens.screen +import com.mineinabyss.launchy.instance.ui.screens.InstanceProperties +import com.mineinabyss.launchy.instance_list.data.LocalInstancesDataSource +import com.mineinabyss.launchy.instance_list.ui.components.InstanceCard +import com.mineinabyss.launchy.util.Dirs +import kotlin.io.path.exists + +@Composable +fun ConfirmImportTab( + visible: Boolean, + cloudInstance: LocalInstancesDataSource.CloudInstanceWithHeaders? +) { + if (cloudInstance == null) return + AnimatedTab(visible) { + val scrollState = rememberScrollState() + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.verticalScroll(scrollState) + ) { + ComfyTitle("Confirm import") + ComfyContent { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + var nameText by remember { mutableStateOf(cloudInstance.config.name) } + fun nameValid() = nameText.matches(validInstanceNameRegex) + fun instanceExists() = Dirs.modpackConfigDir(nameText).exists() + var nameValid by remember { mutableStateOf(nameValid()) } + var instanceExists by remember { mutableStateOf(instanceExists()) } + var minecraftDir: String? by remember { mutableStateOf(null) } + + TextField( + value = nameText, + onValueChange = { + nameText = it + instanceExists = false + }, + singleLine = true, + isError = !nameValid || instanceExists, + leadingIcon = { Icon(Icons.Rounded.TextFields, contentDescription = "Name") }, + supportingText = { + if (!nameValid) Text("Name must be alphanumeric") + else if (instanceExists) Text("An instance with this name already exists") + }, + label = { Text("Instance name") }, + modifier = Modifier.fillMaxWidth(), + ) + + InstanceProperties( + minecraftDir ?: nameText, + onChangeMinecraftDir = { minecraftDir = it } + ) + + TextButton( + onClick = { + nameValid = nameValid() + instanceExists = instanceExists() + if (!nameValid || instanceExists) return@TextButton + val editedConfig = cloudInstance.config.copy( + name = nameText, + overrideMinecraftDir = minecraftDir.takeIf { it?.isNotEmpty() == true } + ) + GameInstanceDataSource.createCloudInstance( + state, cloudInstance.copy(config = editedConfig) + ) + screen = Screen.Default + } + ) { + Text("Confirm", color = MaterialTheme.colorScheme.primary) + } + } + } + + ComfyWidth { + InstanceCard( + cloudInstance.config.copy(name = "Preview"), + modifier = Modifier.fillMaxWidth() + ) + } + } + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/ImportTab.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/ImportTab.kt new file mode 100644 index 0000000..b3cd357 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/ImportTab.kt @@ -0,0 +1,103 @@ +package com.mineinabyss.launchy.instance_creation.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Link +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.mineinabyss.launchy.core.ui.components.AnimatedTab +import com.mineinabyss.launchy.core.ui.components.ComfyContent +import com.mineinabyss.launchy.core.ui.components.ComfyTitle +import com.mineinabyss.launchy.downloads.data.Downloader +import com.mineinabyss.launchy.instance.data.storage.InstanceConfig +import com.mineinabyss.launchy.util.AppDispatchers +import com.mineinabyss.launchy.util.Dirs +import com.mineinabyss.launchy.util.InProgressTask +import kotlinx.coroutines.launch +import kotlin.io.path.deleteIfExists + +@Composable +fun ImportTab( + visible: Boolean, + onGetInstance: (GameInstanceDataSource.CloudInstanceWithHeaders) -> Unit = {} +) { + AnimatedTab(visible) { + Column { + ComfyTitle("Import from link") + + ComfyContent { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + var urlText by remember { mutableStateOf("") } + var urlValid by remember { mutableStateOf(true) } + fun urlValid() = urlText.startsWith("https://") || urlText.startsWith("http://") + var failMessage: String? by remember { mutableStateOf(null) } + + OutlinedTextField( + value = urlText, + singleLine = true, + isError = !urlValid || failMessage != null, + leadingIcon = { Icon(Icons.Rounded.Link, contentDescription = "Link") }, + onValueChange = { + urlText = it + failMessage = null + }, + label = { Text("Link") }, + supportingText = { + if (!urlValid) Text("Must be valid URL") + else if (failMessage != null) Text(failMessage!!) + }, + modifier = Modifier.fillMaxWidth() + ) + + TextButton(onClick = { + urlValid = urlValid() + if (!urlValid) return@TextButton + val taskKey = "importCloudInstance" + val downloadPath = Dirs.createTempCloudInstanceFile() + downloadPath.deleteIfExists() + AppDispatchers.IO.launch { + val cloudInstance = state.runTask(taskKey, InProgressTask("Importing cloud instance")) { + Downloader.download(urlText, downloadPath).mapCatching { + when (it) { + is Downloader.DownloadResult.AlreadyExists -> { + failMessage = "Instance already downloaded locally" + return@launch + } + + is Downloader.DownloadResult.Success -> { + GameInstanceDataSource.CloudInstanceWithHeaders( + config = InstanceConfig.read(downloadPath) + .showDialogOnError("Failed to read cloud instance") + .getOrThrow(), + url = urlText, + headers = it.modifyHeaders + ) + } + } + }.getOrElse { + failMessage = "URL is not a valid instance file" + return@launch + } + } + onGetInstance(cloudInstance) + } + }) { + Text("Import", color = MaterialTheme.colorScheme.primary) + } + } + } +// PopularInstances() + } + } + +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/NewInstance.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/NewInstance.kt index ca3c8b6..243bbc1 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/NewInstance.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/NewInstance.kt @@ -1,48 +1,20 @@ package com.mineinabyss.launchy.instance_creation.ui -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.TextButton -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Link -import androidx.compose.material.icons.rounded.TextFields -import androidx.compose.material3.* +import androidx.compose.material3.PrimaryTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.Text import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.core.ui.components.AnimatedTab -import com.mineinabyss.launchy.core.ui.components.ComfyContent -import com.mineinabyss.launchy.core.ui.components.ComfyTitle import com.mineinabyss.launchy.core.ui.components.ComfyWidth -import com.mineinabyss.launchy.core.ui.screens.Screen -import com.mineinabyss.launchy.core.ui.screens.screen -import com.mineinabyss.launchy.downloads.data.Downloader -import com.mineinabyss.launchy.instance.data.GameInstanceDataSource -import com.mineinabyss.launchy.instance.data.storage.InstanceConfig -import com.mineinabyss.launchy.instance.ui.screens.InstanceProperties -import com.mineinabyss.launchy.instance_list.ui.components.InstanceCard -import com.mineinabyss.launchy.util.AppDispatchers -import com.mineinabyss.launchy.util.Dirs -import com.mineinabyss.launchy.util.InProgressTask -import com.mineinabyss.launchy.util.showDialogOnError -import kotlinx.coroutines.launch -import kotlin.io.path.deleteIfExists -import kotlin.io.path.exists +import com.mineinabyss.launchy.instance_list.data.LocalInstancesDataSource val validInstanceNameRegex = Regex("^[a-zA-Z0-9_ ]+$") @Composable fun NewInstance() { - val state = LocalLaunchyState var selectedTabIndex by remember { mutableStateOf(0) } - var importingInstance: GameInstanceDataSource.CloudInstanceWithHeaders? by remember { mutableStateOf(null) } + var importingInstance: LocalInstancesDataSource.CloudInstanceWithHeaders? by remember { mutableStateOf(null) } Column { ComfyWidth { PrimaryTabRow(selectedTabIndex = selectedTabIndex) { @@ -53,7 +25,6 @@ fun NewInstance() { ) } } - val coroutineScope = rememberCoroutineScope() Box { ImportTab(selectedTabIndex == 0 && importingInstance == null, onGetInstance = { importingInstance = it @@ -63,169 +34,3 @@ fun NewInstance() { } } -@Composable -fun ImportTab(visible: Boolean, onGetInstance: (GameInstanceDataSource.CloudInstanceWithHeaders) -> Unit = {}) { - val state = LocalLaunchyState - AnimatedTab(visible) { - Column { - ComfyTitle("Import from link") - - ComfyContent { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - var urlText by remember { mutableStateOf("") } - var urlValid by remember { mutableStateOf(true) } - fun urlValid() = urlText.startsWith("https://") || urlText.startsWith("http://") - var failMessage: String? by remember { mutableStateOf(null) } - - OutlinedTextField( - value = urlText, - singleLine = true, - isError = !urlValid || failMessage != null, - leadingIcon = { Icon(Icons.Rounded.Link, contentDescription = "Link") }, - onValueChange = { - urlText = it - failMessage = null - }, - label = { Text("Link") }, - supportingText = { - if (!urlValid) Text("Must be valid URL") - else if (failMessage != null) Text(failMessage!!) - }, - modifier = Modifier.fillMaxWidth() - ) - - TextButton(onClick = { - urlValid = urlValid() - if (!urlValid) return@TextButton - val taskKey = "importCloudInstance" - val downloadPath = Dirs.createTempCloudInstanceFile() - downloadPath.deleteIfExists() - AppDispatchers.IO.launch { - val cloudInstance = state.runTask(taskKey, InProgressTask("Importing cloud instance")) { - Downloader.download(urlText, downloadPath).mapCatching { - when (it) { - is Downloader.DownloadResult.AlreadyExists -> { - failMessage = "Instance already downloaded locally" - return@launch - } - - is Downloader.DownloadResult.Success -> { - GameInstanceDataSource.CloudInstanceWithHeaders( - config = InstanceConfig.read(downloadPath) - .showDialogOnError("Failed to read cloud instance") - .getOrThrow(), - url = urlText, - headers = it.modifyHeaders - ) - } - } - }.getOrElse { - failMessage = "URL is not a valid instance file" - return@launch - } - } - onGetInstance(cloudInstance) - } - }) { - Text("Import", color = MaterialTheme.colorScheme.primary) - } - } - } -// PopularInstances() - } - } - -} - -@Composable -fun ConfirmImportTab(visible: Boolean, cloudInstance: GameInstanceDataSource.CloudInstanceWithHeaders?) { - if (cloudInstance == null) return - val state = LocalLaunchyState - AnimatedTab(visible) { - val scrollState = rememberScrollState() - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.verticalScroll(scrollState) - ) { - ComfyTitle("Confirm import") - ComfyContent { - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - var nameText by remember { mutableStateOf(cloudInstance.config.name) } - fun nameValid() = nameText.matches(validInstanceNameRegex) - fun instanceExists() = Dirs.modpackConfigDir(nameText).exists() - var nameValid by remember { mutableStateOf(nameValid()) } - var instanceExists by remember { mutableStateOf(instanceExists()) } - var minecraftDir: String? by remember { mutableStateOf(null) } - - OutlinedTextField( - value = nameText, - singleLine = true, - isError = !nameValid || instanceExists, - leadingIcon = { Icon(Icons.Rounded.TextFields, contentDescription = "Name") }, - supportingText = { - if (!nameValid) Text("Name must be alphanumeric") - else if (instanceExists) Text("An instance with this name already exists") - }, - onValueChange = { - nameText = it - instanceExists = false - }, - label = { Text("Instance name") }, - modifier = Modifier.fillMaxWidth() - ) - - InstanceProperties( - minecraftDir ?: nameText, - onChangeMinecraftDir = { minecraftDir = it } - ) - - TextButton( - onClick = { - nameValid = nameValid() - instanceExists = instanceExists() - if (!nameValid || instanceExists) return@TextButton - val editedConfig = cloudInstance.config.copy( - name = nameText, - overrideMinecraftDir = minecraftDir.takeIf { it?.isNotEmpty() == true } - ) - GameInstanceDataSource.createCloudInstance( - state, cloudInstance.copy(config = editedConfig) - ) - screen = Screen.Default - } - ) { - Text("Confirm", color = MaterialTheme.colorScheme.primary) - } - } - } - - ComfyWidth { - InstanceCard( - cloudInstance.config.copy(name = "Preview"), - modifier = Modifier.fillMaxWidth() - ) - } - } - } -} - -@Composable -fun PopularInstances() { - val state = LocalLaunchyState - val coroutineScope = rememberCoroutineScope() - val popularInstances = remember { - listOf( - "" - ) - } - ComfyTitle("Popular instances") - ComfyContent { - LazyRow { - items(popularInstances) { -// InstanceCard(it, modifier = Modifier.padding(8.dp)) - } - } - } -} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/PopularInstances.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/PopularInstances.kt new file mode 100644 index 0000000..e7d89ca --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_creation/ui/PopularInstances.kt @@ -0,0 +1,25 @@ +package com.mineinabyss.launchy.instance_creation.ui + +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.mineinabyss.launchy.core.ui.components.ComfyContent +import com.mineinabyss.launchy.core.ui.components.ComfyTitle + +@Composable +fun PopularInstances() { + val popularInstances = remember { + listOf( + "" + ) + } + ComfyTitle("Popular instances") + ComfyContent { + LazyRow { + items(popularInstances) { +// InstanceCard(it, modifier = Modifier.padding(8.dp)) + } + } + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/InstanceRepository.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/InstanceRepository.kt index 0f68d30..d7d3116 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/InstanceRepository.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/InstanceRepository.kt @@ -70,6 +70,7 @@ class InstanceRepository( suspend fun fetchPackUpdates(key: InstanceKey) = withContext(AppDispatchers.IO) { val instance = _instances.value[key] ?: error("Instance $key not found") + //TODO how to pass koin scope here? val source = instance.config.source.getDataSource(currentKoinScope()) val packFormat = instance.config.pack.getFormat() if (!source.skip(instance)) { diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/LocalInstancesDataSource.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/LocalInstancesDataSource.kt index b772252..62e6f8f 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/LocalInstancesDataSource.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/LocalInstancesDataSource.kt @@ -23,7 +23,10 @@ class LocalInstancesDataSource( } fun readInstance(instanceFile: Path): Result = runCatching { - InstanceModel(Formats.yaml.decodeFromStream(instanceFile.inputStream()), instanceFile.parent) + InstanceModel( + Formats.yaml.decodeFromStream(instanceFile.inputStream()), + instanceFile.parent + ) } fun saveInstance(instance: InstanceModel) { diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/RemoteInstanceDataSource.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/RemoteInstanceDataSource.kt index bcb11a1..9b4846f 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/RemoteInstanceDataSource.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/data/RemoteInstanceDataSource.kt @@ -6,12 +6,14 @@ import io.ktor.client.call.* import io.ktor.client.request.* import io.ktor.http.* -class RemoteInstanceDataSource { +class RemoteInstanceDataSource( + val downloader: Downloader +) { suspend fun getRemoteInstance( url: Url ): Result = runCatching { //TODO cache by headers - Downloader.httpClient.get(url).body() + downloader.httpClient.get(url).body() } suspend fun fetchUpdatesForInstance( diff --git a/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListViewModel.kt b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListViewModel.kt index 6f675b0..43bb399 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListViewModel.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/instance_list/ui/InstanceListViewModel.kt @@ -14,7 +14,7 @@ class InstanceListViewModel( ) : ViewModel() { val instances = instanceRepo.instances.combine(instanceRepo.lastPlayed) { inst, lastPlayed -> inst.values.sortedBy { lastPlayed[it.key] } - .map { (config, dir, key) -> + .map { (config, userConfig, dir, key) -> InstanceUiState( title = config.name, description = config.description, @@ -27,6 +27,7 @@ class InstanceListViewModel( updatesAvailable = false, hue = 0f, key = key, + installedModLoader = userConfig.userAgreedDeps?.fullVersionName, ) } }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) diff --git a/src/main/kotlin/com/mineinabyss/launchy/launcher/data/ProcessRepository.kt b/src/main/kotlin/com/mineinabyss/launchy/launcher/data/ProcessRepository.kt index 2b2ef36..45147d2 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/launcher/data/ProcessRepository.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/launcher/data/ProcessRepository.kt @@ -1,13 +1,14 @@ package com.mineinabyss.launchy.launcher.data -import com.mineinabyss.launchy.instance.data.GameInstanceDataSource +import com.mineinabyss.launchy.util.InstanceKey class ProcessRepository { - private val launchedProcesses = mutableMapOf() + private val launchedProcesses = mutableMapOf() - fun processFor(instance: GameInstanceDataSource): Process? = launchedProcesses[instance.minecraftDir.toString()] - fun setProcessFor(instance: GameInstanceDataSource, process: Process?) { - if (process == null) launchedProcesses.remove(instance.minecraftDir.toString()) - else launchedProcesses[instance.minecraftDir.toString()] = process + fun processFor(instance: InstanceKey): Process? = launchedProcesses[instance] + + fun setProcessFor(instance: InstanceKey, process: Process?) { + if (process == null) launchedProcesses.remove(instance) + else launchedProcesses[instance] = process } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/settings/ui/JVMSettingsViewModel.kt b/src/main/kotlin/com/mineinabyss/launchy/settings/ui/JVMSettingsViewModel.kt index ca7d8fe..a96e3bc 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/settings/ui/JVMSettingsViewModel.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/settings/ui/JVMSettingsViewModel.kt @@ -5,30 +5,106 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.mineinabyss.launchy.config.data.Config -import com.mineinabyss.launchy.util.SuggestedJVMArgs -import kotlin.io.path.Path +import com.mineinabyss.launchy.core.data.TasksRepository +import com.mineinabyss.launchy.downloads.data.Downloader +import com.mineinabyss.launchy.downloads.data.Downloader.JavaInstallation +import com.mineinabyss.launchy.downloads.data.Downloader.Options +import com.mineinabyss.launchy.util.* +import kotlinx.coroutines.launch +import org.rauschig.jarchivelib.ArchiveFormat +import org.rauschig.jarchivelib.ArchiverFactory +import org.rauschig.jarchivelib.CompressionType +import kotlin.io.path.* class JVMSettingsViewModel( - val config: Config + val config: Config, + val tasks: TasksRepository, + val downloader: Downloader, ) : ViewModel() { var javaPath by mutableStateOf(config.javaPath?.let { Path(it) }) var userMemoryAllocation by mutableStateOf(config.memoryAllocation) var userJvmArgs by mutableStateOf(config.jvmArguments) var useRecommendedJvmArgs by mutableStateOf(config.useRecommendedJvmArguments) - val suggestedArgs get() = buildString { - if("graalvm" in javaPath.toString()) { - append(SuggestedJVMArgs.graalVMBaseFlags) - } else { - append(SuggestedJVMArgs.baseFlags) + val suggestedArgs + get() = buildString { + if ("graalvm" in javaPath.toString()) { + append(SuggestedJVMArgs.graalVMBaseFlags) + } else { + append(SuggestedJVMArgs.baseFlags) + } + append(" ") + append(SuggestedJVMArgs.clientG1GC) } - append(" ") - append(SuggestedJVMArgs.clientG1GC) - } val jvmArgs by derivedStateOf { val memory = (userMemoryAllocation ?: SuggestedJVMArgs.memory).toString() "-Xms${memory}M -Xmx${memory}M ${userJvmArgs?.takeIf { !useRecommendedJvmArgs } ?: suggestedArgs}" } val memory get() = userMemoryAllocation ?: SuggestedJVMArgs.memory + + + /** @return Path to java executable */ + @OptIn(ExperimentalPathApi::class) + fun installJDK() = viewModelScope.launch(AppDispatchers.IO) { + try { + tasks.start("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( + url, + "bin/java.exe", + ArchiverFactory.createArchiver(ArchiveFormat.ZIP) + ) + + OS.MAC -> JavaInstallation( + url, + "Contents/Home/bin/java", + ArchiverFactory.createArchiver(ArchiveFormat.TAR, CompressionType.GZIP) + ) + + OS.LINUX -> JavaInstallation( + url, + "bin/java", + ArchiverFactory.createArchiver(ArchiveFormat.TAR, CompressionType.GZIP) + ) + } + 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@launch + downloader.download(javaInstallation.url, downloadTo, Options( + onProgressUpdate = { + tasks.start( + "installJDK", + InProgressTask.bytes( + "Downloading Java environment", + it.bytesDownloaded, + it.totalBytes + ) + ) + } + )) + tasks.start("installJDK", InProgressTask("Extracting Java environment")) + + // Handle a case where the extraction failed and the folder exists but not the java executable + 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() + + javaPath = jrePath / javaInstallation.relativeJavaExecutable +// dialog = Dialog.Error( +// "Failed to install Java", +// "Please install Java manually and select the path in settings." +// ) + } finally { + tasks.finish("installJDK") + } + } }