From ef8c560406c6da1276949832dd854ca07a9514bb Mon Sep 17 00:00:00 2001 From: Danielle Voznyy Date: Tue, 12 Mar 2024 18:13:44 -0400 Subject: [PATCH] Feat: Allow creating instance with custom .minecraft directory Feat: Fullscreen mode Feat: Buttons to open some common directories Improvement: Use trailing icon button for choosing jvm path Fix: Play button on home screen getting squished when title or desc too long Feat: List user mods as a category --- gradle.properties | 2 +- .../com/mineinabyss/launchy/data/Dirs.kt | 3 +- .../mineinabyss/launchy/data/config/Config.kt | 1 + .../launchy/data/config/GameInstanceConfig.kt | 1 - .../com/mineinabyss/launchy/logic/Auth.kt | 2 +- .../logic/{Browser.kt => DesktopHelpers.kt} | 15 +- .../launchy/logic/ModDownloader.kt | 7 +- .../mineinabyss/launchy/state/LaunchyState.kt | 5 +- .../com/mineinabyss/launchy/state/UIState.kt | 11 + .../com/mineinabyss/launchy/ui/TopBar.kt | 42 +++- .../launchy/ui/dialogs/AuthDialog.kt | 4 +- .../launchy/ui/elements/SingleFileDialog.kt | 89 +++++++ .../launchy/ui/elements/Typography.kt | 30 +++ .../mineinabyss/launchy/ui/screens/Screens.kt | 4 +- .../ui/screens/home/AddNewModpackCard.kt | 2 +- .../launchy/ui/screens/home/ModpackCard.kt | 30 ++- .../launchy/ui/screens/home/ModpackGroup.kt | 6 +- .../screens/home/newinstance/NewInstance.kt | 8 + .../screens/home/settings/SettingsScreen.kt | 228 +++++++++--------- .../screens/modpack/main/FirstLaunchDialog.kt | 10 +- .../modpack/main/buttons/PlayButton.kt | 4 +- .../settings/InstanceSettingsScreen.kt | 71 +++++- .../modpack/settings/ModInfoDisplay.kt | 4 +- 23 files changed, 407 insertions(+), 172 deletions(-) rename src/main/kotlin/com/mineinabyss/launchy/logic/{Browser.kt => DesktopHelpers.kt} (56%) create mode 100644 src/main/kotlin/com/mineinabyss/launchy/state/UIState.kt create mode 100644 src/main/kotlin/com/mineinabyss/launchy/ui/elements/SingleFileDialog.kt diff --git a/gradle.properties b/gradle.properties index 92e207e..78aa71d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ group=com.mineinabyss -version=2.0.0-alpha.12 +version=2.0.0-alpha.13 idofrontVersion=0.22.3 diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/Dirs.kt b/src/main/kotlin/com/mineinabyss/launchy/data/Dirs.kt index c202b49..5154a96 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/Dirs.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/Dirs.kt @@ -43,7 +43,8 @@ object Dirs { val modpackConfigsDir = (config / "modpacks") - fun modpackDir(string: String) = mineinabyss / "modpacks" / string + val modpacksDir = mineinabyss / "modpacks" + fun modpackDir(string: String) = modpacksDir / string fun modpackConfigDir(name: String) = modpackConfigsDir / name fun createDirs() { diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/config/Config.kt b/src/main/kotlin/com/mineinabyss/launchy/data/config/Config.kt index 08b9f2b..d742c34 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/config/Config.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/config/Config.kt @@ -18,6 +18,7 @@ data class Config( val memoryAllocation: Int? = null, val useRecommendedJvmArguments: Boolean = true, val preferHue: Float? = null, + val startInFullscreen: Boolean = false, ) { fun save() { Dirs.configFile.writeText(Formats.yaml.encodeToString(this)) diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/config/GameInstanceConfig.kt b/src/main/kotlin/com/mineinabyss/launchy/data/config/GameInstanceConfig.kt index 6366cd8..b3414be 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/config/GameInstanceConfig.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/config/GameInstanceConfig.kt @@ -23,7 +23,6 @@ import kotlin.io.path.inputStream import kotlin.io.path.outputStream @Serializable -@OptIn(ExperimentalStdlibApi::class) data class GameInstanceConfig( val name: String, val description: String, diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/Auth.kt b/src/main/kotlin/com/mineinabyss/launchy/logic/Auth.kt index 0da78ec..882c422 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/Auth.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/logic/Auth.kt @@ -30,7 +30,7 @@ object Auth { profile, onVerificationRequired = { state.inProgressTasks.remove("auth") - Browser.browse(it.redirectTo) + DesktopHelpers.browse(it.redirectTo) profile.authCode = it.code dialog = Dialog.Auth println(profile.authCode) diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/Browser.kt b/src/main/kotlin/com/mineinabyss/launchy/logic/DesktopHelpers.kt similarity index 56% rename from src/main/kotlin/com/mineinabyss/launchy/logic/Browser.kt rename to src/main/kotlin/com/mineinabyss/launchy/logic/DesktopHelpers.kt index 6e4cf90..9715889 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/Browser.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/logic/DesktopHelpers.kt @@ -3,8 +3,9 @@ package com.mineinabyss.launchy.logic import com.mineinabyss.launchy.util.OS import java.awt.Desktop import java.net.URI +import java.nio.file.Path -object Browser { +object DesktopHelpers { val desktop = Desktop.getDesktop() fun browse(url: String): Result<*> = synchronized(desktop) { val os = OS.get() @@ -17,4 +18,16 @@ object Browser { } } } + + fun openDirectory(path: Path) { + val os = OS.get() + runCatching { + when { + Desktop.isDesktopSupported() && desktop.isSupported(Desktop.Action.OPEN) -> desktop.open(path.toFile()) + os == OS.LINUX -> Runtime.getRuntime().exec("xdg-open $path") + os == OS.MAC -> Runtime.getRuntime().exec("open $path") + else -> error("Unsupported OS") + } + } + } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/ModDownloader.kt b/src/main/kotlin/com/mineinabyss/launchy/logic/ModDownloader.kt index 0a2d3eb..6275280 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/ModDownloader.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/logic/ModDownloader.kt @@ -143,7 +143,8 @@ object ModDownloader { /** * Updates mod loader versions and mods to latest modpack definition. */ - suspend fun ModpackState.startInstall(state: LaunchyState, ignoreCachedCheck: Boolean = false) = coroutineScope { + suspend fun ModpackState.startInstall(state: LaunchyState, ignoreCachedCheck: Boolean = false): Result<*> = + coroutineScope { userAgreedDeps = modpack.modLoaders ensureDependenciesReady(state) copyOverrides(state) @@ -190,11 +191,13 @@ object ModDownloader { saveToConfig() if (queued.modDownloadInfo.any { it.value.hashCheck != HashCheck.VERIFIED }) { - error("Hash check failed on one or more downloads downloads, please re-run the installer!") + return@coroutineScope Result.failure(Exception("Failed to verify hashes")) } copyMods() saveToConfig() + + return@coroutineScope Result.success(Unit) } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/state/LaunchyState.kt b/src/main/kotlin/com/mineinabyss/launchy/state/LaunchyState.kt index 20e75d8..30d42c9 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/state/LaunchyState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/state/LaunchyState.kt @@ -15,7 +15,7 @@ class LaunchyState( var modpackState: ModpackState? by mutableStateOf(null) private val launchedProcesses = mutableStateMapOf() val jvm = JvmState(config) - var preferHue: Float by mutableStateOf(config.preferHue ?: 0f) + val ui = UIState(config) val gameInstances = mutableStateListOf().apply { addAll(instances) @@ -45,7 +45,8 @@ class LaunchyState( jvmArguments = jvm.userJvmArgs, memoryAllocation = jvm.userMemoryAllocation, useRecommendedJvmArguments = jvm.useRecommendedJvmArgs, - preferHue = preferHue, + preferHue = ui.preferHue, + startInFullscreen = ui.fullscreen ).save() } diff --git a/src/main/kotlin/com/mineinabyss/launchy/state/UIState.kt b/src/main/kotlin/com/mineinabyss/launchy/state/UIState.kt new file mode 100644 index 0000000..58831f8 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/state/UIState.kt @@ -0,0 +1,11 @@ +package com.mineinabyss.launchy.state + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import com.mineinabyss.launchy.data.config.Config + +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/ui/TopBar.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/TopBar.kt index 92df2df..aee00f9 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/TopBar.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/TopBar.kt @@ -16,6 +16,7 @@ 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 @@ -24,6 +25,8 @@ 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.ui.elements.BetterWindowDraggableArea import com.mineinabyss.launchy.ui.state.TopBarState @@ -46,13 +49,26 @@ fun AppTopBar( showTitle: Boolean, showBackButton: Boolean, onBackButtonClicked: (() -> Unit), -) = state.windowScope.BetterWindowDraggableArea( - Modifier.pointerInput(Unit) { - detectTapGestures(onDoubleTap = { - state.toggleMaximized() - }) - } ) { + val appState = LocalLaunchyState + val forceFullscreen = appState.ui.fullscreen + LaunchedEffect(forceFullscreen) { + when (forceFullscreen) { + true -> state.windowState.placement = WindowPlacement.Fullscreen + false -> state.windowState.placement = WindowPlacement.Floating + } + } + + if (!forceFullscreen) state.windowScope.BetterWindowDraggableArea( + Modifier.pointerInput(Unit) { + detectTapGestures(onDoubleTap = { + state.toggleMaximized() + }) + } + ) { + Box(Modifier.fillMaxWidth().height(40.dp)) + } + Box( Modifier.fillMaxWidth().height(40.dp) ) { @@ -85,11 +101,15 @@ fun AppTopBar( } } Row { - WindowButton(Icons.Rounded.Minimize) { - state.windowState.isMinimized = true - } - WindowButton(Icons.Rounded.CropSquare) { - state.toggleMaximized() + AnimatedVisibility(!forceFullscreen) { + Row { + WindowButton(Icons.Rounded.Minimize) { + state.windowState.isMinimized = true + } + WindowButton(Icons.Rounded.CropSquare) { + state.toggleMaximized() + } + } } WindowButton(Icons.Rounded.Close) { state.onClose() diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/dialogs/AuthDialog.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/dialogs/AuthDialog.kt index 1938bcd..9bf4291 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/dialogs/AuthDialog.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/dialogs/AuthDialog.kt @@ -15,7 +15,7 @@ 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.Browser +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 @@ -84,7 +84,7 @@ fun AuthDialog( onClick = { annotatedText.getUrlAnnotations(it, it) .firstOrNull() - ?.let { Browser.browse(it.item.url) } + ?.let { DesktopHelpers.browse(it.item.url) } }, ) } diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/SingleFileDialog.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/elements/SingleFileDialog.kt new file mode 100644 index 0000000..a68671a --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/elements/SingleFileDialog.kt @@ -0,0 +1,89 @@ +package com.mineinabyss.launchy.ui.elements + +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.OS +import java.awt.FileDialog +import java.awt.Frame +import java.io.FilenameFilter +import java.nio.file.Path +import kotlin.io.path.Path +import kotlin.io.path.isDirectory + + +@Composable +fun DirectoryDialog( + shown: Boolean, + title: String, + fallbackTitle: String? = null, + parent: Frame? = null, + onCloseRequest: (result: Path?) -> Unit, +) { + when { + OS.get() == OS.WINDOWS || OS.get() == OS.MAC -> DirectoryPicker( + shown, + initialDirectory = Dirs.jdks.toString(), + title = title, + onFileSelected = { dir -> + onCloseRequest(dir?.let { Path(it) }) + }) + + shown -> AwtWindow( + create = { + object : FileDialog(parent, fallbackTitle ?: title, LOAD) { + override fun setVisible(value: Boolean) { + super.setVisible(value) + if (value) { + val path = files.firstOrNull()?.toPath() + if (path?.isDirectory() == true) + onCloseRequest(path) + else onCloseRequest(path?.parent) + } + } + } + }, + dispose = FileDialog::dispose + ) + } +} + +@Composable +fun SingleFileDialog( + shown: Boolean, + title: String, + fallbackTitle: String? = null, + parent: Frame? = null, + onCloseRequest: (result: Path?) -> Unit, + fileExtensions: () -> List, + fallbackFilter: FilenameFilter +) { + when { + OS.get() == OS.WINDOWS || OS.get() == OS.MAC -> FilePicker( + shown, + initialDirectory = Dirs.jdks.toString(), + title = title, + fileExtensions = fileExtensions(), + onFileSelected = { file -> + onCloseRequest(file?.let { Path(it.path) }) + }) + + shown -> AwtWindow( + create = { + object : FileDialog(parent, fallbackTitle ?: title, LOAD) { + override fun setVisible(value: Boolean) { + super.setVisible(value) + if (value) { + onCloseRequest(files.firstOrNull()?.toPath()) + } + } + }.apply { + setFilenameFilter(fallbackFilter) + } + }, + dispose = FileDialog::dispose + ) + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/Typography.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/elements/Typography.kt index 35ebc69..d696e58 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/elements/Typography.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/elements/Typography.kt @@ -1,6 +1,8 @@ package com.mineinabyss.launchy.ui.elements 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.material3.MaterialTheme import androidx.compose.material3.Text @@ -14,3 +16,31 @@ fun TitleSmall(text: String) { Text(text, style = MaterialTheme.typography.titleSmall) } } + +@Composable +fun TitleMedium(text: String) { + Box(Modifier.padding(top = 12.dp, bottom = 8.dp)) { + Text(text, style = MaterialTheme.typography.titleMedium) + } +} + +@Composable +fun TitleLarge(text: String) { + Box(Modifier.padding(top = 8.dp, bottom = 4.dp)) { + Text(text, style = MaterialTheme.typography.titleLarge) + } +} + + +@Composable +fun Setting(title: String, icon: @Composable () -> Unit = {}, content: @Composable () -> Unit) { + Column { + TitleSmall(title) + Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) { + icon() + Column { + content() + } + } + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/Screens.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/Screens.kt index 6ce7e0a..4d1b435 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/Screens.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/Screens.kt @@ -63,8 +63,8 @@ fun Screens() = Scaffold( val isDefault = screen is Screen.OnLeftSidebar - LaunchedEffect(isDefault, state.preferHue) { - if (isDefault) currentHue = state.preferHue + LaunchedEffect(isDefault, state.ui.preferHue) { + if (isDefault) currentHue = state.ui.preferHue } AppTopBar( diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/AddNewModpackCard.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/AddNewModpackCard.kt index c40793f..6e7cc7e 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/AddNewModpackCard.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/AddNewModpackCard.kt @@ -24,7 +24,7 @@ fun AddNewModpackCard(modifier: Modifier = Modifier) { Surface( border = BorderStroke(3.dp, highlightColor), shape = MaterialTheme.shapes.medium, - modifier = modifier.height(ModpackCardStyle.cardHeight).clickable { screen = Screen.NewInstance } + modifier = modifier.height(InstanceCardStyle.cardHeight).clickable { screen = Screen.NewInstance } ) { Box { Row(Modifier.align(Alignment.Center), verticalAlignment = Alignment.CenterVertically) { diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/ModpackCard.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/ModpackCard.kt index f838870..05e5cd4 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/ModpackCard.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/ModpackCard.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter 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.data.config.GameInstance @@ -27,15 +28,15 @@ 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.ModpackCardStyle.cardHeight -import com.mineinabyss.launchy.ui.screens.home.ModpackCardStyle.cardPadding -import com.mineinabyss.launchy.ui.screens.home.ModpackCardStyle.cardWidth +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 kotlinx.coroutines.launch -object ModpackCardStyle { +object InstanceCardStyle { val cardHeight = 256.dp val cardPadding = 12.dp val cardWidth = 400.dp @@ -92,15 +93,24 @@ fun InstanceCard( Row( Modifier.align(Alignment.BottomStart).padding(cardPadding), verticalAlignment = Alignment.Bottom, - horizontalArrangement = Arrangement.SpaceAround + horizontalArrangement = Arrangement.SpaceBetween ) { - Column { - Text(config.name, style = MaterialTheme.typography.headlineMedium) - Text(config.description, style = MaterialTheme.typography.bodyMedium) + Column(Modifier.weight(1f, true)) { + Text( + config.name, + style = MaterialTheme.typography.headlineMedium, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + Text( + config.description, + style = MaterialTheme.typography.bodyMedium, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) } - Spacer(Modifier.weight(1f)) if (instance?.enabled == true) - PlayButton(hideText = true, instance) { + PlayButton(hideText = true, instance, Modifier.weight(1f, false)) { state.inProgressTasks["modpackState"] = InProgressTask("Reading modpack configuration") try { instance.createModpackState(state) diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/ModpackGroup.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/ModpackGroup.kt index bf421ec..26f31db 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/ModpackGroup.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/ModpackGroup.kt @@ -25,7 +25,7 @@ fun ModpackGroup(title: String, packs: List) { Text("No instances installed yet, click the + button on the sidebar to add one!") } else BoxWithConstraints(Modifier) { val total = packs.size + 1 - val colums = ((maxWidth / ModpackCardStyle.cardWidth).toInt()).coerceAtMost(total).coerceAtLeast(1) + val colums = ((maxWidth / InstanceCardStyle.cardWidth).toInt()).coerceAtMost(total).coerceAtLeast(1) val rows = (total / colums).coerceAtLeast(1) val lazyGridState = rememberLazyGridState() LazyVerticalGrid( @@ -33,8 +33,8 @@ fun ModpackGroup(title: String, packs: List) { state = lazyGridState, columns = GridCells.Fixed(colums), modifier = Modifier - .width((16.dp + ModpackCardStyle.cardWidth) * total) - .heightIn(max = (16.dp + ModpackCardStyle.cardHeight) * rows), + .width((16.dp + InstanceCardStyle.cardWidth) * total) + .heightIn(max = (16.dp + InstanceCardStyle.cardHeight) * rows), horizontalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp), ) { diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/newinstance/NewInstance.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/newinstance/NewInstance.kt index ca4f595..db13800 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/newinstance/NewInstance.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/newinstance/NewInstance.kt @@ -28,6 +28,7 @@ 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 kotlinx.coroutines.launch import kotlin.collections.set @@ -146,6 +147,7 @@ fun ConfirmImportTab(visible: Boolean, importingInstance: GameInstanceConfig?) { 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, @@ -164,6 +166,11 @@ fun ConfirmImportTab(visible: Boolean, importingInstance: GameInstanceConfig?) { modifier = Modifier.fillMaxWidth() ) + InstanceProperties( + minecraftDir ?: Dirs.modpackDir(nameText).toString(), + onChangeMinecraftDir = { minecraftDir = it } + ) + TextButton( enabled = importingInstance != null, onClick = { @@ -174,6 +181,7 @@ fun ConfirmImportTab(visible: Boolean, importingInstance: GameInstanceConfig?) { GameInstance.create( state, instance.copy( name = nameText, + overrideMinecraftDir = minecraftDir.takeIf { it?.isNotEmpty() == true } ) ) screen = Screen.Default diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/settings/SettingsScreen.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/settings/SettingsScreen.kt index 906d35b..2fae540 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/settings/SettingsScreen.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/home/settings/SettingsScreen.kt @@ -1,33 +1,24 @@ package com.mineinabyss.launchy.ui.screens.home.settings +import androidx.compose.animation.AnimatedVisibility import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material.TextButton import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Code -import androidx.compose.material.icons.rounded.FileOpen +import androidx.compose.material.icons.rounded.* import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.AwtWindow -import com.darkrockstudios.libraries.mpfilepicker.FilePicker 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.ComfyContent -import com.mineinabyss.launchy.ui.elements.ComfyTitle -import com.mineinabyss.launchy.ui.elements.TitleSmall -import com.mineinabyss.launchy.util.OS -import java.awt.FileDialog -import java.awt.Frame -import java.nio.file.Path -import kotlin.io.path.Path +import com.mineinabyss.launchy.ui.elements.* @Composable @Preview @@ -40,134 +31,137 @@ fun SettingsScreen() { var directoryPickerShown by remember { mutableStateOf(false) } Column( Modifier.verticalScroll(scrollState), - verticalArrangement = Arrangement.spacedBy(16.dp) + verticalArrangement = Arrangement.spacedBy(32.dp) ) { Column { - TitleSmall("Hue") - Row { + TitleLarge("Interface") + Column { + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + state.ui.fullscreen, + onCheckedChange = { state.ui.fullscreen = it } + ) + Text("Fullscreen mode") + } + } + Setting("Hue", icon = { Icon(Icons.Rounded.Colorize, contentDescription = "Hue") }) { Slider( - value = state.preferHue, - onValueChange = { state.preferHue = it }, + value = state.ui.preferHue, + onValueChange = { state.ui.preferHue = it }, valueRange = 0f..1f, modifier = Modifier.weight(1f) ) } } - if (directoryPickerShown) FileDialog(onCloseRequest = { - if (it != null) { - state.jvm.javaPath = it - } - directoryPickerShown = false - }) Column { - TitleSmall("Java path") - OutlinedTextField( - value = state.jvm.javaPath?.toString() ?: "No path selected", - readOnly = true, - singleLine = true, - leadingIcon = { Icon(Icons.Rounded.FileOpen, contentDescription = "Link") }, - onValueChange = {}, - modifier = Modifier.fillMaxWidth() - ) - - TextButton(onClick = { directoryPickerShown = true }) { - Text("Select Java Path", color = MaterialTheme.colorScheme.primary) + TitleLarge("Quick access") + @OptIn(ExperimentalLayoutApi::class) + FlowRow( + maxItemsInEachRow = 2, + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + OutlinedButton(onClick = { DesktopHelpers.openDirectory(Dirs.config) }) { + Text("Open launchy config dir") + } + OutlinedButton(onClick = { DesktopHelpers.openDirectory(Dirs.modpacksDir) }) { + Text("Open modpacks dir") + } } } Column { - TitleSmall("Memory") - Row { - val memory = state.jvm.userMemoryAllocation ?: SuggestedJVMArgs.memory - Slider( - value = memory.toFloat(), - onValueChange = { state.jvm.userMemoryAllocation = it.toInt() }, - valueRange = 1024f..8192f, - steps = 13, - modifier = Modifier.weight(1f) - ) - TextField( - value = memory.toString(), - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Number - ), - onValueChange = { state.jvm.userMemoryAllocation = it.toIntOrNull() ?: memory }, - label = { Text("Memory (MB)") }, - modifier = Modifier.widthIn(120.dp) - ) - } - } + TitleLarge("Java") - Column { - TitleSmall("JVM arguments") - OutlinedTextField( - value = state.jvm.userJvmArgs ?: "", - enabled = !state.jvm.useRecommendedJvmArgs, - singleLine = false, - leadingIcon = { Icon(Icons.Rounded.Code, contentDescription = "") }, - onValueChange = { state.jvm.userJvmArgs = it }, - label = { Text("Override JVM arguments") }, - modifier = Modifier.fillMaxWidth() + SingleFileDialog( + directoryPickerShown, + title = "Choose java executable", + onCloseRequest = { + if (it != null) { + state.jvm.javaPath = it + } + directoryPickerShown = false + }, + fileExtensions = { listOf("exe") }, + fallbackFilter = { dir, name -> name == "java.exe" || name == "java" } ) - Row(verticalAlignment = Alignment.CenterVertically) { - Checkbox( - state.jvm.useRecommendedJvmArgs, - onCheckedChange = { state.jvm.useRecommendedJvmArgs = it }) - Text("Use recommended JVM arguments") + Setting("Java path") { + OutlinedTextField( + value = state.jvm.javaPath?.toString() ?: "No path selected", + readOnly = true, + singleLine = true, + leadingIcon = { Icon(Icons.Rounded.Folder, contentDescription = "Link") }, + trailingIcon = { + IconButton(onClick = { directoryPickerShown = true }) { + Icon(Icons.Rounded.FileOpen, contentDescription = "Choose") + } + }, + onValueChange = {}, + modifier = Modifier.fillMaxWidth() + ) } - Spacer(Modifier.height(16.dp)) + Setting("Memory", icon = { Icon(Icons.Rounded.Memory, "Memory icon") }) { + Row(verticalAlignment = Alignment.CenterVertically) { + val memory = state.jvm.userMemoryAllocation ?: SuggestedJVMArgs.memory - OutlinedTextField( - value = state.jvm.jvmArgs, - enabled = false, - singleLine = false, - readOnly = true, - leadingIcon = { Icon(Icons.Rounded.Code, contentDescription = "") }, - onValueChange = { }, - label = { Text("Full arguments") }, - modifier = Modifier.fillMaxWidth() - ) - } - } - } - } -} + Slider( + value = memory.toFloat(), + onValueChange = { state.jvm.userMemoryAllocation = it.toInt() }, + valueRange = 1024f..8192f, + steps = 13, + modifier = Modifier.weight(1f) + ) + TextField( + value = memory.toString(), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number + ), + onValueChange = { state.jvm.userMemoryAllocation = it.toIntOrNull() ?: memory }, + label = { Text("Memory (MB)") }, + modifier = Modifier.widthIn(120.dp) + ) + } + } -@Composable -fun FileDialog( - parent: Frame? = null, - onCloseRequest: (result: Path?) -> Unit -) { - when (OS.get()) { - OS.WINDOWS -> FilePicker( - true, - initialDirectory = Dirs.jdks.toString(), - title = "Choose java executable", - fileExtensions = listOf("exe"), - ) { file -> - onCloseRequest(file?.let { Path(it.path) }) - } + Setting("JVM arguments") { + AnimatedVisibility(!state.jvm.useRecommendedJvmArgs) { + OutlinedTextField( + value = state.jvm.userJvmArgs ?: "", + enabled = !state.jvm.useRecommendedJvmArgs, + singleLine = false, + leadingIcon = { Icon(Icons.Rounded.Code, contentDescription = "") }, + onValueChange = { state.jvm.userJvmArgs = it }, + label = { Text("Custom JVM arguments") }, + modifier = Modifier.fillMaxWidth() + ) + } - else -> AwtWindow( - create = { - object : FileDialog(parent, "Choose a file", LOAD) { - override fun setVisible(value: Boolean) { - super.setVisible(value) - if (value) { - onCloseRequest(files.firstOrNull()?.toPath()) + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + !state.jvm.useRecommendedJvmArgs, + onCheckedChange = { state.jvm.useRecommendedJvmArgs = !it }) + Text("Use custom JVM arguments") } - } - }.apply { - setFilenameFilter { dir, name -> - name == "java.exe" || name == "java" + + Spacer(Modifier.height(16.dp)) + + OutlinedTextField( + value = state.jvm.jvmArgs, + enabled = false, + singleLine = false, + readOnly = true, + leadingIcon = { Icon(Icons.Rounded.Code, contentDescription = "") }, + onValueChange = { }, + label = { Text("Full arguments") }, + modifier = Modifier.fillMaxWidth() + ) } } - }, - dispose = FileDialog::dispose - ) + } + } } - } + 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 index ad85084..5d01be8 100644 --- 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 @@ -1,20 +1,16 @@ package com.mineinabyss.launchy.ui.screens.modpack.main import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape +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.Surface import androidx.compose.material.Text -import androidx.compose.material.TextButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.WindowScope import com.mineinabyss.launchy.LocalLaunchyState -import com.mineinabyss.launchy.logic.Browser import com.mineinabyss.launchy.ui.elements.LaunchyDialog import com.mineinabyss.launchy.ui.state.windowScope diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/PlayButton.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/PlayButton.kt index f4cace7..7617f06 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/PlayButton.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/PlayButton.kt @@ -2,7 +2,6 @@ package com.mineinabyss.launchy.ui.screens.modpack.main.buttons import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -35,6 +34,7 @@ import kotlinx.coroutines.launch fun PlayButton( hideText: Boolean = false, instance: GameInstance, + modifier: Modifier = Modifier, getModpackState: suspend () -> ModpackState?, ) { val state = LocalLaunchyState @@ -115,7 +115,7 @@ fun PlayButton( if (hideText) Button( enabled = enabled, onClick = onClick, - modifier = Modifier.size(52.dp).defaultMinSize(minWidth = 1.dp, minHeight = 1.dp), + modifier = Modifier.size(52.dp).then(modifier), contentPadding = PaddingValues(0.dp), colors = buttonColors, shape = MaterialTheme.shapes.medium diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/InstanceSettingsScreen.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/InstanceSettingsScreen.kt index 2b1ebff..3e1b55f 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/InstanceSettingsScreen.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/InstanceSettingsScreen.kt @@ -7,6 +7,9 @@ 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 @@ -15,15 +18,17 @@ 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.DesktopHelpers import com.mineinabyss.launchy.logic.Instances.delete import com.mineinabyss.launchy.logic.Instances.updateInstance -import com.mineinabyss.launchy.ui.elements.AnimatedTab -import com.mineinabyss.launchy.ui.elements.ComfyContent -import com.mineinabyss.launchy.ui.elements.ComfyWidth -import com.mineinabyss.launchy.ui.elements.TitleSmall +import com.mineinabyss.launchy.ui.elements.* import com.mineinabyss.launchy.ui.screens.LocalModpackState import com.mineinabyss.launchy.ui.screens.Screen import com.mineinabyss.launchy.ui.screens.screen +import kotlin.io.path.listDirectoryEntries @Composable @Preview @@ -56,6 +61,40 @@ fun InstanceSettingsScreen() { } } +@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 @@ -64,8 +103,13 @@ fun OptionsTab() { ComfyContent(Modifier.padding(16.dp)) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { TitleSmall("Mods") - OutlinedButton(onClick = { pack.instance.updateInstance(state) }) { - Text("Force update Instance") + 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") + } } TitleSmall("Danger zone") @@ -100,6 +144,18 @@ fun ModManagement() { 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() @@ -108,6 +164,9 @@ fun ModManagement() { items(state.modpack.mods.modGroups.toList()) { (group, mods) -> ModGroup(group, mods) } + if (userMods.isNotEmpty()) item { + ModGroup(Group("User mods", forceEnabled = true), userMods) + } } VerticalScrollbar( modifier = Modifier.fillMaxHeight().align(Alignment.CenterEnd).padding(vertical = 2.dp), diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/ModInfoDisplay.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/ModInfoDisplay.kt index 371b5b0..631db18 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/ModInfoDisplay.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/ModInfoDisplay.kt @@ -20,7 +20,7 @@ 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.Browser +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 @@ -145,7 +145,7 @@ fun ModInfoDisplay(group: Group, mod: Mod) { } } ) { - IconButton(onClick = { Browser.browse(mod.info.homepage) }) { + IconButton(onClick = { DesktopHelpers.browse(mod.info.homepage) }) { Icon( imageVector = Icons.Rounded.OpenInNew, contentDescription = "Homepage"