diff --git a/build.gradle.kts b/build.gradle.kts index f2a477d..dea9a9f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,7 +34,7 @@ dependencies { implementation(kotlin("reflect")) implementation(files("deps/BrowserLauncher2-all-1_3.jar")) implementation(compose.desktop.currentOs) { - exclude(group = "org.jetbrains.compose.material", module ="material") + exclude(group = "org.jetbrains.compose.material", module = "material") } @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) implementation(compose.material3) @@ -54,7 +54,11 @@ tasks.withType { kotlinOptions.freeCompilerArgs = listOf("-opt-in=androidx.compose.material3.ExperimentalMaterial3Api") } -val appName = "MineInAbyss_Launcher" +val appName = "MineInAbyss_Launcher-" + when { + Os.isFamily(Os.FAMILY_MAC) -> "macOS" + Os.isFamily(Os.FAMILY_WINDOWS) -> "windows" + else -> "linux" +} compose.desktop { application { @@ -120,6 +124,7 @@ tasks { val executeAppImageBuilder by registering(Exec::class) { dependsOn(downloadAppImageBuilder) dependsOn(copyBuildToPackaging) + environment("ARCH", "x86_64") commandLine(appImageTool, linuxAppDir, "releases/$appName-${project.version}.AppImage") } diff --git a/gradle.properties b/gradle.properties index 7784249..2f11da0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ group=com.mineinabyss -version=1.0.4 +version=1.1.0 idofrontConventions=1.6.10-51 kotlinVersion=1.6.10 diff --git a/packaging/appimage/Mine in Abyss.AppDir/AppRun b/packaging/appimage/Mine in Abyss.AppDir/AppRun index 40b5360..4e08bc1 100755 --- a/packaging/appimage/Mine in Abyss.AppDir/AppRun +++ b/packaging/appimage/Mine in Abyss.AppDir/AppRun @@ -1,5 +1,5 @@ #!/bin/bash HERE="$(dirname $"$(readlink -f "${0}")")" -EXEC="${HERE}/usr/bin/MineInAbyss_Launcher" +EXEC="${HERE}/usr/bin/MineInAbyss_Launcher-linux" exec "${EXEC}" diff --git a/src/main/kotlin/com/mineinabyss/launchy/ColorSchemeGenerator.kt b/src/main/kotlin/com/mineinabyss/launchy/ColorSchemeGenerator.kt new file mode 100644 index 0000000..af0647b --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/ColorSchemeGenerator.kt @@ -0,0 +1,28 @@ +package com.mineinabyss.launchy + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import kotlin.reflect.KMutableProperty1 +import kotlin.reflect.full.memberProperties + +@Composable +fun rememberMIAColorScheme(): ColorScheme = remember { + darkColorScheme().also { scheme -> + ColorScheme::class.memberProperties.filterIsInstance>() + .filter { "error" !in it.name.toLowerCase() } + .map { prop -> + val col = (prop.get(scheme)) + val hsbVals = FloatArray(3) + val javaCol = java.awt.Color(col.red, col.green, col.blue, col.alpha) + java.awt.Color.RGBtoHSB(javaCol.red, javaCol.green, javaCol.blue, hsbVals) + val shiftedColor = Color(java.awt.Color.HSBtoRGB(0.02f, hsbVals[1], hsbVals[2])) + prop.set( + scheme, + col.copy(red = shiftedColor.red, blue = shiftedColor.blue, green = shiftedColor.green) + ) + } + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/Main.kt b/src/main/kotlin/com/mineinabyss/launchy/Main.kt index 1da42d7..1d328e3 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/Main.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/Main.kt @@ -3,108 +3,38 @@ package com.mineinabyss.launchy import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.window.WindowDraggableArea -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Close -import androidx.compose.material.icons.rounded.CropSquare -import androidx.compose.material.icons.rounded.Minimize -import androidx.compose.material3.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text 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.vector.ImageVector import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.* +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.Config import com.mineinabyss.launchy.data.Dirs import com.mineinabyss.launchy.data.Versions import com.mineinabyss.launchy.logic.LaunchyState -import com.mineinabyss.launchy.ui.screens.MainScreen -import kotlin.reflect.KMutableProperty1 -import kotlin.reflect.full.memberProperties +import com.mineinabyss.launchy.ui.screens.Content -//private val LocalConfigProvider = compositionLocalOf { error("No local config provided") } -//val LocalConfig: Config -// @Composable -// get() = LocalConfigProvider.current -// -//private val LocalVersionsProvider = compositionLocalOf { error("No local versions provided") } -//val LocalVersions: Versions -// @Composable -// get() = LocalVersionsProvider.current private val LaunchyStateProvider = compositionLocalOf { error("No local versions provided") } val LocalLaunchyState: LaunchyState @Composable get() = LaunchyStateProvider.current -@Composable -fun WindowScope.AppWindowTitleBar( - app: ApplicationScope, - state: WindowState, - onCloseRequest: () -> Unit, -) = WindowDraggableArea { - Surface( - Modifier.fillMaxWidth().height(40.dp), - tonalElevation = 1.dp - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceAround, - ) { - Spacer(Modifier.width(15.dp)) - Row( - Modifier.weight(1f), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - "Mine in Abyss", - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.primary - ) - } - Row { - WindowButton(Icons.Rounded.Minimize) { - state.isMinimized = true - } - WindowButton(Icons.Rounded.CropSquare) { - if (state.placement != WindowPlacement.Maximized) - state.placement = WindowPlacement.Maximized - else state.placement = WindowPlacement.Floating - } - WindowButton(Icons.Rounded.Close) { - onCloseRequest() - app.exitApplication() - } - } - } - } -} - -@Composable -fun WindowButton(icon: ImageVector, onClick: () -> Unit) { - Surface( - onClick = onClick, - modifier = Modifier.fillMaxHeight().width(44.dp), - contentColor = MaterialTheme.colorScheme.primary, - ) { - Icon(icon, "", Modifier.padding(10.dp)) - } - -} - fun main() { application { val windowState = rememberWindowState(placement = WindowPlacement.Floating) val icon = painterResource("mia_profile_icon.png") -// val scaffoldState = rememberScaffoldState() val launchyState by produceState(null) { val config = Config.read() val versions = Versions.readLatest(config.downloadUpdates) - value = LaunchyState(config, versions/*, scaffoldState*/) + value = LaunchyState(config, versions) } val onClose: () -> Unit = { exitApplication() @@ -117,41 +47,24 @@ fun main() { onCloseRequest = onClose, undecorated = true, ) { - + val topBarState = remember { TopBarState(onClose, windowState, this) } val ready = launchyState != null - val scheme = darkColorScheme() - ColorScheme::class.memberProperties.filterIsInstance>() - .filter { "error" !in it.name.toLowerCase() } - .map { prop -> - val col = (prop.get(scheme)) - val hsbVals = FloatArray(3) - val javaCol = java.awt.Color(col.red, col.green, col.blue, col.alpha) - java.awt.Color.RGBtoHSB(javaCol.red, javaCol.green, javaCol.blue, hsbVals) - val shiftedColor = Color(java.awt.Color.HSBtoRGB(0.02f, hsbVals[1], hsbVals[2])) - prop.set( - scheme, - col.copy(red = shiftedColor.red, blue = shiftedColor.blue, green = shiftedColor.green) - ) - } - MaterialTheme( - colorScheme = scheme - ) { - Scaffold( - topBar = { - AppWindowTitleBar(this@application, windowState, onClose) - } - ) { - AnimatedVisibility(!ready, exit = fadeOut()) { - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("Getting latest plugin versions...") + val scheme = rememberMIAColorScheme() + MaterialTheme(colorScheme = scheme) { + CompositionLocalProvider(TopBarProvider provides topBarState) { + Scaffold { + AnimatedVisibility(!ready, exit = fadeOut()) { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text("Getting latest plugin versions...") + } } - } - AnimatedVisibility(ready, enter = fadeIn()) { - CompositionLocalProvider( - LaunchyStateProvider provides launchyState!!, - ) { - Dirs.createDirs() - MainScreen() + AnimatedVisibility(ready, enter = fadeIn()) { + CompositionLocalProvider( + LaunchyStateProvider provides launchyState!!, + ) { + Dirs.createDirs() + Content() + } } } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/TopBar.kt b/src/main/kotlin/com/mineinabyss/launchy/TopBar.kt new file mode 100644 index 0000000..1835af2 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/TopBar.kt @@ -0,0 +1,93 @@ +package com.mineinabyss.launchy + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.slideIn +import androidx.compose.animation.slideOut +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.window.WindowDraggableArea +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.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.material3.* +import androidx.compose.runtime.Composable +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.text.font.FontWeight +import androidx.compose.ui.unit.IntOffset +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)) + } +} + +@Composable +fun AppTopBar( + state: TopBarState, + transparent: Boolean, + showBackButton: Boolean, + onBackButtonClicked: (() -> Unit), +) = state.windowScope.WindowDraggableArea( +// Modifier.combinedClickable(onDoubleClick = { toggleMaximized(state) }, onClick = {}) +) { + Box( + Modifier.fillMaxWidth().height(40.dp) + ) { + AnimatedVisibility( + !transparent, + enter = slideIn(initialOffset = { IntOffset(0, -40) }), + exit = slideOut(targetOffset = { IntOffset(0, -40) }) + ) { + Surface(tonalElevation = 1.dp, modifier = Modifier.fillMaxSize()) {} + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceAround, + ) { + Spacer(Modifier.width(5.dp)) + Row( + Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically, + ) { + AnimatedVisibility(showBackButton/*, enter = fadeIn(animationSpec = tween(300, 300))*/) { + IconButton(onClick = onBackButtonClicked) { + Icon(Icons.Rounded.ArrowBack, contentDescription = "Back button") + } + Spacer(Modifier.width(5.dp)) + } + AnimatedVisibility(!transparent) { + Text( + "Mine in Abyss", + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.primary + ) + } + } + 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/TopBarState.kt b/src/main/kotlin/com/mineinabyss/launchy/TopBarState.kt new file mode 100644 index 0000000..929f691 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/launchy/TopBarState.kt @@ -0,0 +1,26 @@ +package com.mineinabyss.launchy + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.window.ApplicationScope +import androidx.compose.ui.window.WindowPlacement +import androidx.compose.ui.window.WindowScope +import androidx.compose.ui.window.WindowState +import com.mineinabyss.launchy.logic.LaunchyState + +val TopBarProvider = compositionLocalOf { error("No top bar provided") } +val TopBar: TopBarState + @Composable + get() = TopBarProvider.current + +class TopBarState( + val onClose: () -> Unit, + val windowState: WindowState, + val windowScope: WindowScope, +) { + fun toggleMaximized() { + if (windowState.placement != WindowPlacement.Maximized) + windowState.placement = WindowPlacement.Maximized + else windowState.placement = WindowPlacement.Floating + } +} diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/MainScreen.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/MainScreen.kt index bbe1281..892c079 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/MainScreen.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/MainScreen.kt @@ -1,55 +1,251 @@ package com.mineinabyss.launchy.ui.screens -import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.* +import androidx.compose.animation.core.tween import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.Image import androidx.compose.foundation.VerticalScrollbar +import androidx.compose.foundation.background 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.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.window.WindowDraggableArea import androidx.compose.material.icons.Icons 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.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.ApplicationScope import androidx.compose.ui.window.WindowScope -import androidx.compose.ui.window.WindowState -import com.mineinabyss.launchy.AppWindowTitleBar +import androidx.compose.ui.zIndex +import com.mineinabyss.launchy.AppTopBar import com.mineinabyss.launchy.LocalLaunchyState +import com.mineinabyss.launchy.TopBar import com.mineinabyss.launchy.ui.ModGroup import kotlinx.coroutines.launch +sealed class Screen(val transparentTopBar: Boolean = false) { + object Default : Screen(transparentTopBar = true) + object Settings : Screen() +} + +@Composable +fun Content() { + var screen: Screen by remember { mutableStateOf(Screen.Default) } + AnimatedVisibility(screen == Screen.Default, enter = fadeIn(), exit = fadeOut()) { + MainScreen(TopBar.windowScope, onSettings = { screen = Screen.Settings }) + } + Column { + AnimatedVisibility(!screen.transparentTopBar, enter = fadeIn(), exit = fadeOut()) { + Spacer(Modifier.height(40.dp)) + } + Box { + Animate(screen == Screen.Settings) { + SettingsScreen() + } + } + } + + AppTopBar( + TopBar, + screen.transparentTopBar, + showBackButton = screen != Screen.Default, + onBackButtonClicked = { screen = Screen.Default }) +} + + +@Composable +fun Animate(enabled: Boolean, content: @Composable () -> Unit) { + AnimatedVisibility( + enabled, + enter = fadeIn() + slideIn(initialOffset = { IntOffset(0, 100) }), + exit = fadeOut() + slideOut(targetOffset = { IntOffset(0, 100) }), + ) { + content() + } +} + + @Composable @Preview -fun MainScreen() { +fun SettingsScreen() { val state = LocalLaunchyState - val coroutineScope = rememberCoroutineScope() Scaffold( bottomBar = { InfoBar() }, ) { paddingValues -> - Box(Modifier.padding(paddingValues).padding(start = 10.dp, top = 5.dp)) { - val lazyListState = rememberLazyListState() - LazyColumn(Modifier.fillMaxSize().padding(end = 12.dp), lazyListState) { - items(state.versions.modGroups.toList()) { (group, mods) -> - ModGroup(group, mods) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Surface( + shape = RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp), + modifier = Modifier.padding(5.dp) + ) { + Box( + Modifier.padding(paddingValues) + .padding(start = 10.dp, top = 5.dp) + ) { + val lazyListState = rememberLazyListState() + LazyColumn(Modifier.fillMaxSize().padding(end = 12.dp), lazyListState) { + + items(state.versions.modGroups.toList()) { (group, mods) -> + ModGroup(group, mods) + } + } + VerticalScrollbar( + modifier = Modifier.fillMaxHeight().align(Alignment.CenterEnd), + adapter = rememberScrollbarAdapter(lazyListState) + ) } } - VerticalScrollbar( - modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(), - adapter = rememberScrollbarAdapter(lazyListState) + } + } +} + +@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) + } +} + +@Preview +@Composable +fun MainScreen(windowScope: WindowScope, onSettings: () -> Unit) { + val colors = listOf( + Color.Transparent, + MaterialTheme.colorScheme.background, + ) + Box { + windowScope.WindowDraggableArea { + Image( + painter = painterResource("mia_render.jpg"), + contentDescription = "Main render", + modifier = Modifier + .fillMaxSize(), + contentScale = ContentScale.Crop + ) + } + Column( + Modifier.align(Alignment.Center) + .heightIn(0.dp, 500.dp) + .fillMaxSize() + .zIndex(4f), + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource("mia_profile_icon.png"), + contentDescription = "Mine in Abyss logo", + modifier = Modifier + .widthIn(0.dp, 500.dp) + .fillMaxSize() + .weight(3f), + contentScale = ContentScale.FillWidth + ) + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.Top, + modifier = Modifier + .fillMaxWidth() + .weight(1f), + ) { + val state = LocalLaunchyState + InstallButton() + Spacer(Modifier.width(10.dp)) + var toggled by remember { mutableStateOf(false) } + AnimatedVisibility(state.operationsQueued) { + Button(onClick = { toggled = !toggled }) { + Column() { + Row { + Icon(Icons.Rounded.Update, contentDescription = "Updates") + Text("${state.queuedDownloads.size + state.queuedDeletions.size} Updates") + } + + androidx.compose.animation.AnimatedVisibility( + toggled, + enter = expandIn(tween(200)) + fadeIn(tween(200, 100)), + exit = fadeOut() + shrinkOut(tween(200, 100)) + ) { + Column { + InfoText( + shown = !state.fabricUpToDate, + icon = Icons.Rounded.HistoryEdu, + desc = "Install fabric", + ) + InfoText( + shown = state.updatesQueued, + icon = Icons.Rounded.Update, + desc = "Update", + extra = state.queuedUpdates.size.toString() + ) + InfoText( + shown = state.installsQueued, + icon = Icons.Rounded.Download, + desc = "Download", + extra = state.queuedInstalls.size.toString() + ) + InfoText( + shown = state.deletionsQueued, + icon = Icons.Rounded.Delete, + desc = "Remove", + extra = state.queuedDeletions.size.toString() + ) + } + } + } + } + } + Spacer(Modifier.width(10.dp)) +// NewsButton(hasUpdates = true) +// Spacer(Modifier.width(10.dp)) + Button(onClick = onSettings) { + Icon(Icons.Rounded.Settings, contentDescription = "Settings") + Text("Settings") + } + } + } + BoxWithConstraints( + Modifier + .align(Alignment.BottomCenter) + ) { + Spacer( + Modifier + .fillMaxWidth() + .height(maxHeight / 2) + .background(Brush.verticalGradient(colors)) ) } } } +@Composable +fun NewsButton(hasUpdates: Boolean) { + Box { + Button(onClick = {}) { + Icon(Icons.Rounded.Feed, contentDescription = "Settings") + Text("News") + } + if (hasUpdates) Surface( + Modifier.size(12.dp).align(Alignment.TopEnd).offset((-2).dp, (2).dp), + shape = CircleShape, + color = Color(255, 138, 128) + ) {} + } +} + @Composable fun ActionButton(shown: Boolean, icon: ImageVector, desc: String, extra: String = "") { AnimatedVisibility(shown) { @@ -67,10 +263,40 @@ fun ActionButton(shown: Boolean, icon: ImageVector, desc: String, extra: String } @Composable -fun InfoBar(modifier: Modifier = Modifier) { +fun InstallButton() { val state = LocalLaunchyState - val coroutineScope = rememberCoroutineScope() + Button( + enabled = !state.isDownloading && state.operationsQueued && state.minecraftValid, + onClick = { + coroutineScope.launch { state.install() } + }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.primary + ) + ) { + Icon(Icons.Rounded.Download, "Download") + AnimatedVisibility(!state.minecraftValid) { + Text("Invalid Minecraft") + } + AnimatedVisibility(state.minecraftValid) { + AnimatedVisibility(state.operationsQueued && !state.isDownloading) { + Text("Install") + } + AnimatedVisibility(!state.operationsQueued) { + Text("Installed") + } + AnimatedVisibility(state.isDownloading) { + Text("Installing...") + } + } + } +} + +@Composable +fun InfoBar(modifier: Modifier = Modifier) { + val state = LocalLaunchyState Surface( tonalElevation = 2.dp, shape = RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp), @@ -81,28 +307,7 @@ fun InfoBar(modifier: Modifier = Modifier) { verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(6.dp) ) { - println(state.operationsQueued && state.minecraftValid) - Button( - enabled = !state.isDownloading && state.operationsQueued && state.minecraftValid, - onClick = { - coroutineScope.launch { state.install() } - }, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.primary - ) - ) { - Icon(Icons.Rounded.Download, "Download") - AnimatedVisibility(state.operationsQueued && !state.isDownloading) { - Text("Install") - } - AnimatedVisibility(!state.operationsQueued) { - Text("Installed") - } - AnimatedVisibility(state.isDownloading) { - Text("Installing...") - } - } + InstallButton() Spacer(Modifier.width(10.dp)) ActionButton( diff --git a/src/main/resources/mia_render.jpg b/src/main/resources/mia_render.jpg new file mode 100644 index 0000000..e5c55fc Binary files /dev/null and b/src/main/resources/mia_render.jpg differ