298 lines
No EOL
12 KiB
Kotlin
298 lines
No EOL
12 KiB
Kotlin
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.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.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.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.NetworkWindows
|
||
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.screen.campaign.CampaignViewModel
|
||
import com.pixelized.desktop.lwa.ui.screen.characterSheet.CharacterSheetMainNavHost
|
||
import com.pixelized.desktop.lwa.ui.screen.network.NetworkPage
|
||
import com.pixelized.desktop.lwa.ui.screen.network.NetworkViewModel
|
||
import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryPage
|
||
import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryViewModel
|
||
import com.pixelized.desktop.lwa.ui.theme.LwaTheme
|
||
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 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")
|
||
}
|
||
|
||
@Composable
|
||
@Preview
|
||
fun ApplicationScope.App() {
|
||
|
||
val maxWindowHeight = rememberMaxWindowHeight()
|
||
val snackHostState = remember { SnackbarHostState() }
|
||
val errorSnackHostState = remember { SnackbarHostState() }
|
||
val windowController = remember { WindowController(maxWindowHeight) }
|
||
val keyEventHandlers = remember { mutableStateListOf<KeyEventHandler>() }
|
||
|
||
CompositionLocalProvider(
|
||
LocalSnackHost provides snackHostState,
|
||
LocalErrorSnackHost provides errorSnackHostState,
|
||
LocalWindowController provides windowController,
|
||
LocalKeyEventHandlers provides keyEventHandlers,
|
||
) {
|
||
Window(
|
||
onCloseRequest = ::exitApplication,
|
||
state = rememberWindowState(size = DpSize(width = 800.dp, height = maxWindowHeight)),
|
||
title = runBlocking { getString(Res.string.app_name) },
|
||
onKeyEvent = { event ->
|
||
keyEventHandlers.reversed().any { it(event) }
|
||
},
|
||
) {
|
||
MainWindowScreen()
|
||
}
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
private fun MainWindowScreen(
|
||
campaignViewModel: CampaignViewModel = koinViewModel(),
|
||
networkViewModel: NetworkViewModel = koinViewModel(),
|
||
rollViewModel: RollHistoryViewModel = koinViewModel(),
|
||
) {
|
||
LaunchedEffect(Unit) {
|
||
networkViewModel.connect()
|
||
campaignViewModel.init()
|
||
}
|
||
|
||
val snackHostState = LocalSnackHost.current
|
||
val errorSnackHostState = LocalErrorSnackHost.current
|
||
val windowController = LocalWindowController.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,
|
||
)
|
||
}
|
||
)
|
||
SnackbarHost(
|
||
hostState = errorSnackHostState,
|
||
snackbar = {
|
||
Snackbar(
|
||
snackbarData = it,
|
||
backgroundColor = MaterialTheme.colors.error,
|
||
contentColor = MaterialTheme.colors.onError,
|
||
actionColor = MaterialTheme.colors.onError,
|
||
)
|
||
}
|
||
)
|
||
}
|
||
},
|
||
content = {
|
||
MainNavHost()
|
||
}
|
||
)
|
||
NetworkSnackHandler(
|
||
snack = snackHostState,
|
||
)
|
||
WindowsHandler(
|
||
windowController = windowController,
|
||
rollViewModel = rollViewModel,
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
private fun WindowsHandler(
|
||
windowController: WindowController,
|
||
rollViewModel: RollHistoryViewModel = koinViewModel(),
|
||
) {
|
||
WindowsNavHost(
|
||
controller = windowController,
|
||
content = { window ->
|
||
when (window) {
|
||
is CharacterSheetWindow -> CharacterSheetMainNavHost(
|
||
startDestination = CharacterSheetDestination.navigationRoute(
|
||
id = window.characterId,
|
||
),
|
||
)
|
||
|
||
is CharacterSheetEditWindow -> CharacterSheetMainNavHost(
|
||
startDestination = CharacterSheetEditDestination.navigationRoute(
|
||
id = window.sheetId,
|
||
),
|
||
)
|
||
|
||
is RollHistoryWindow -> RollHistoryPage(
|
||
viewModel = rollViewModel,
|
||
)
|
||
|
||
is NetworkWindows -> NetworkPage()
|
||
}
|
||
}
|
||
)
|
||
}
|
||
|
||
@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,
|
||
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 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".
|
||
*
|
||
* 
|
||
*
|
||
* 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
|
||
)
|
||
} |