package com.pixelized.desktop.lwa import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.ButtonDefaults import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.ScaffoldState import androidx.compose.material.SnackbarData import androidx.compose.material.SnackbarDefaults import androidx.compose.material.SnackbarDuration import androidx.compose.material.SnackbarHost import androidx.compose.material.SnackbarHostState import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.min import androidx.compose.ui.window.ApplicationScope import androidx.compose.ui.window.Window import androidx.compose.ui.window.rememberWindowState import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Status import com.pixelized.desktop.lwa.ui.composable.blur.BlurContent import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController import com.pixelized.desktop.lwa.ui.composable.blur.rememberBlurContentController import com.pixelized.desktop.lwa.ui.composable.key.KeyEventHandler import com.pixelized.desktop.lwa.ui.composable.key.LocalKeyEventHandlers import com.pixelized.desktop.lwa.ui.navigation.screen.MainNavHost import com.pixelized.desktop.lwa.ui.navigation.screen.destination.CharacterSheetDestination import com.pixelized.desktop.lwa.ui.navigation.screen.destination.CharacterSheetEditDestination import com.pixelized.desktop.lwa.ui.navigation.window.LocalWindowState import com.pixelized.desktop.lwa.ui.navigation.window.WindowController import com.pixelized.desktop.lwa.ui.navigation.window.WindowsNavHost import com.pixelized.desktop.lwa.ui.navigation.window.destination.CharacterSheetEditWindow import com.pixelized.desktop.lwa.ui.navigation.window.destination.CharacterSheetWindow import com.pixelized.desktop.lwa.ui.navigation.window.destination.GameMasterWindow import com.pixelized.desktop.lwa.ui.navigation.window.destination.RollHistoryWindow import com.pixelized.desktop.lwa.ui.navigation.window.rememberMaxWindowHeight import com.pixelized.desktop.lwa.ui.overlay.roll.RollHostState import com.pixelized.desktop.lwa.ui.overlay.roll.RollOverlay import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterPortraitDefault import com.pixelized.desktop.lwa.ui.screen.characterSheet.CharacterSheetMainNavHost import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterScreen import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryPage import com.pixelized.desktop.lwa.ui.theme.LwaTheme import com.pixelized.desktop.lwa.utils.InstallCoil import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.app_name import lwacharactersheet.composeapp.generated.resources.network__connect__message import lwacharactersheet.composeapp.generated.resources.network__disconnect__message import lwacharactersheet.composeapp.generated.resources.network__message__action import org.jetbrains.compose.resources.getString import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel val LocalWindowController = compositionLocalOf { error("Local Window Controller is not yet ready") } val LocalSnackHost = compositionLocalOf { error("Local Snack Controller is not yet ready") } val LocalErrorSnackHost = compositionLocalOf { error("Local Snack Controller is not yet ready") } val LocalApplicationScope = compositionLocalOf { error("Local application scope is not yet ready") } val LocalRollHostState = compositionLocalOf { error("Local Roll Host State is not yet ready") } val LocalBlurController = compositionLocalOf { error("Local Blur Controller is not yet ready") } @Composable @Preview fun ApplicationScope.LwaApplication() { val maxWindowHeight = rememberMaxWindowHeight() val snackHostState = remember { SnackbarHostState() } val errorSnackHostState = remember { SnackbarHostState() } val windowController = remember { WindowController(maxWindowHeight) } val keyEventHandlers = remember { mutableStateListOf() } val rollHostState = remember { RollHostState() } val blurController = rememberBlurContentController() val windowsState = rememberWindowState( size = DpSize( width = 800.dp, height = min( a = 56.dp + CharacterPortraitDefault.size.height * 6 + 8.dp * 7 + 40.dp, b = maxWindowHeight, ), ), ) // Coil configuration. InstallCoil( crossfade = false, diskCachePath = { provider -> provider.imagesStorePath() } ) CompositionLocalProvider( LocalApplicationScope provides this, LocalSnackHost provides snackHostState, LocalErrorSnackHost provides errorSnackHostState, LocalWindowController provides windowController, LocalKeyEventHandlers provides keyEventHandlers, LocalRollHostState provides rollHostState, LocalBlurController provides blurController, LocalWindowState provides windowsState, ) { Window( onCloseRequest = ::exitApplication, state = windowsState, title = runBlocking { getString(Res.string.app_name) }, onKeyEvent = { event -> keyEventHandlers.reversed().any { it(event) } }, ) { MainWindowScreen() } } } @Composable private fun MainWindowScreen( dataSyncViewModel: DataSyncViewModel = koinViewModel(), ) { LaunchedEffect(Unit) { dataSyncViewModel.autoConnect() dataSyncViewModel.synchronise() } val snackHostState = LocalSnackHost.current val errorSnackHostState = LocalErrorSnackHost.current val windowController = LocalWindowController.current val rollHostState = LocalRollHostState.current val blurController = LocalBlurController.current LwaTheme { Surface( modifier = Modifier.fillMaxSize() ) { Scaffold( snackbarHost = { Column( modifier = Modifier.padding(all = 8.dp), verticalArrangement = Arrangement.spacedBy(space = 4.dp) ) { SnackbarHost( hostState = snackHostState, snackbar = { Snackbar( snackbarData = it, backgroundColor = MaterialTheme.colors.surface, contentColor = MaterialTheme.colors.onSurface, actionColor = MaterialTheme.colors.onSurface, ) } ) SnackbarHost( hostState = errorSnackHostState, snackbar = { Snackbar( snackbarData = it, backgroundColor = MaterialTheme.colors.error, contentColor = MaterialTheme.colors.onError, actionColor = MaterialTheme.colors.onError, ) } ) } }, content = { BlurContent( modifier = Modifier.fillMaxSize(), controller = blurController, ) { MainNavHost() } RollOverlay( modifier = Modifier.fillMaxSize(), hostState = rollHostState, ) } ) NetworkSnackHandler( snack = snackHostState, ) WindowsHandler( windowController = windowController, ) } } } @Composable private fun WindowsHandler( windowController: WindowController, ) { WindowsNavHost( controller = windowController, content = { window -> when (window) { is CharacterSheetWindow -> CharacterSheetMainNavHost( startDestination = CharacterSheetDestination.navigationRoute( characterInstanceId = window.characterId, ), ) is CharacterSheetEditWindow -> CharacterSheetMainNavHost( startDestination = CharacterSheetEditDestination.navigationRoute( id = window.sheetId, ), ) is RollHistoryWindow -> RollHistoryPage() is GameMasterWindow -> GameMasterScreen() } } ) } @Composable private fun NetworkSnackHandler( snack: SnackbarHostState, ) { val networkRepository = koinInject() LaunchedEffect(Unit) { launch { var ignoreInitial = true networkRepository.status.collect { if (ignoreInitial) { ignoreInitial = false } else { val message = when (it) { Status.CONNECTED -> getString(Res.string.network__connect__message) Status.DISCONNECTED -> getString(Res.string.network__disconnect__message) } snack.showSnackbar( message = message, actionLabel = getString(Res.string.network__message__action), duration = SnackbarDuration.Short, ) } } } } } /** * Material Design snackbar. * * Snackbars provide brief messages about app processes at the bottom of the screen. * * Snackbars inform users of a process that an app has performed or will perform. They appear * temporarily, towards the bottom of the screen. They shouldn’t interrupt the user experience, * and they don’t require user input to disappear. * * A Snackbar can contain a single action. Because they disappear automatically, the action * shouldn't be "Dismiss" or "Cancel". * * ![Snackbars image](https://developer.android.com/images/reference/androidx/compose/material/snackbars.png) * * This version of snackbar is designed to work with [SnackbarData] provided by the * [SnackbarHost], which is usually used inside of the [Scaffold]. * * This components provides only the visuals of the [Snackbar]. If you need to show a [Snackbar] * with defaults on the screen, use [ScaffoldState.snackbarHostState] and * [SnackbarHostState.showSnackbar]: * * @sample androidx.compose.material.samples.ScaffoldWithSimpleSnackbar * * If you want to customize appearance of the [Snackbar], you can pass your own version as a child * of the [SnackbarHost] to the [Scaffold]: * @sample androidx.compose.material.samples.ScaffoldWithCustomSnackbar * * @param snackbarData data about the current snackbar showing via [SnackbarHostState] * @param modifier modifiers for the Snackbar layout * @param actionOnNewLine whether or not action should be put on the separate line. Recommended * for action with long action text * @param shape Defines the Snackbar's shape as well as its shadow * @param backgroundColor background color of the Snackbar * @param contentColor color of the content to use inside the snackbar. Defaults to * either the matching content color for [backgroundColor], or, if it is not a color from * the theme, this will keep the same value set above this Surface. * @param actionColor color of the action * @param elevation The z-coordinate at which to place the SnackBar. This controls the size * of the shadow below the SnackBar */ @Composable fun Snackbar( snackbarData: SnackbarData, modifier: Modifier = Modifier, actionOnNewLine: Boolean = false, shape: Shape = MaterialTheme.shapes.small, backgroundColor: Color = SnackbarDefaults.backgroundColor, contentColor: Color = MaterialTheme.colors.surface, actionColor: Color = SnackbarDefaults.primaryActionColor, elevation: Dp = 6.dp, ) { val actionLabel = snackbarData.actionLabel val actionComposable: (@Composable () -> Unit)? = if (actionLabel != null) { @Composable { TextButton( colors = ButtonDefaults.textButtonColors(contentColor = actionColor), onClick = { snackbarData.performAction() }, content = { Text(actionLabel) } ) } } else { null } androidx.compose.material.Snackbar( modifier = modifier, content = { Text(snackbarData.message) }, action = actionComposable, actionOnNewLine = actionOnNewLine, shape = shape, backgroundColor = backgroundColor, contentColor = contentColor, elevation = elevation ) }