LwaApplication/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt

349 lines
No EOL
14 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<WindowController> {
error("Local Window Controller is not yet ready")
}
val LocalSnackHost = compositionLocalOf<SnackbarHostState> {
error("Local Snack Controller is not yet ready")
}
val LocalErrorSnackHost = compositionLocalOf<SnackbarHostState> {
error("Local Snack Controller is not yet ready")
}
val LocalApplicationScope = compositionLocalOf<ApplicationScope> {
error("Local application scope is not yet ready")
}
val LocalRollHostState = compositionLocalOf<RollHostState> {
error("Local Roll Host State is not yet ready")
}
val LocalBlurController = compositionLocalOf<BlurContentController> {
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<KeyEventHandler>() }
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<NetworkRepository>()
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,
)
}
}
}
}
}
/**
* <a href="https://material.io/components/snackbars" class="external" target="_blank">Material Design snackbar</a>.
*
* 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 shouldnt interrupt the user experience,
* and they dont 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
)
}