New UI.
This commit is contained in:
		
							parent
							
								
									d84bc9bbef
								
							
						
					
					
						commit
						0c613c5b72
					
				
					 25 changed files with 947 additions and 23 deletions
				
			
		| 
						 | 
				
			
			@ -25,16 +25,12 @@ 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.platform.LocalDensity
 | 
			
		||||
import androidx.compose.ui.unit.Dp
 | 
			
		||||
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.composable.key.KeyEventHandler
 | 
			
		||||
import com.pixelized.desktop.lwa.composable.key.LocalKeyEventHandlers
 | 
			
		||||
import com.pixelized.desktop.lwa.navigation.screen.MainNavHost
 | 
			
		||||
import com.pixelized.desktop.lwa.navigation.screen.destination.CharacterSheetDestination
 | 
			
		||||
import com.pixelized.desktop.lwa.navigation.screen.destination.CharacterSheetEditDestination
 | 
			
		||||
import com.pixelized.desktop.lwa.navigation.window.WindowController
 | 
			
		||||
| 
						 | 
				
			
			@ -45,6 +41,7 @@ import com.pixelized.desktop.lwa.navigation.window.destination.RollHistoryWindow
 | 
			
		|||
import com.pixelized.desktop.lwa.navigation.window.rememberMaxWindowHeight
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Status
 | 
			
		||||
import com.pixelized.desktop.lwa.screen.campaign.CampaignScreen
 | 
			
		||||
import com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheetMainNavHost
 | 
			
		||||
import com.pixelized.desktop.lwa.screen.rollhistory.RollHistoryPage
 | 
			
		||||
import com.pixelized.desktop.lwa.theme.LwaTheme
 | 
			
		||||
| 
						 | 
				
			
			@ -55,7 +52,6 @@ import lwacharactersheet.composeapp.generated.resources.network__disconnect__mes
 | 
			
		|||
import org.jetbrains.compose.resources.getString
 | 
			
		||||
import org.jetbrains.compose.ui.tooling.preview.Preview
 | 
			
		||||
import org.koin.compose.koinInject
 | 
			
		||||
import java.awt.Toolkit
 | 
			
		||||
 | 
			
		||||
val LocalWindowController = compositionLocalOf<WindowController> {
 | 
			
		||||
    error("Local Window Controller is not yet ready")
 | 
			
		||||
| 
						 | 
				
			
			@ -87,10 +83,10 @@ fun ApplicationScope.App() {
 | 
			
		|||
    ) {
 | 
			
		||||
        Window(
 | 
			
		||||
            onCloseRequest = ::exitApplication,
 | 
			
		||||
            state = rememberWindowState(
 | 
			
		||||
                width = 320.dp + 64.dp,
 | 
			
		||||
                height = 900.dp,
 | 
			
		||||
            ),
 | 
			
		||||
//            state = rememberWindowState(
 | 
			
		||||
//                width = 320.dp + 64.dp,
 | 
			
		||||
//                height = 900.dp,
 | 
			
		||||
//            ),
 | 
			
		||||
            title = "LwaCharacterSheet",
 | 
			
		||||
            onKeyEvent = { event ->
 | 
			
		||||
                keyEventHandlers.reversed().any { it(event) }
 | 
			
		||||
| 
						 | 
				
			
			@ -128,7 +124,8 @@ fun ApplicationScope.App() {
 | 
			
		|||
                            }
 | 
			
		||||
                        },
 | 
			
		||||
                        content = {
 | 
			
		||||
                            MainNavHost()
 | 
			
		||||
//                            MainNavHost()
 | 
			
		||||
                            CampaignScreen()
 | 
			
		||||
                        }
 | 
			
		||||
                    )
 | 
			
		||||
                    NetworkSnackHandler(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,6 +29,9 @@ import com.pixelized.desktop.lwa.screen.rollhistory.RollHistoryViewModel
 | 
			
		|||
import com.pixelized.desktop.lwa.parser.dice.DiceParser
 | 
			
		||||
import com.pixelized.desktop.lwa.parser.word.WordParser
 | 
			
		||||
import com.pixelized.desktop.lwa.parser.expression.ExpressionParser
 | 
			
		||||
import com.pixelized.desktop.lwa.screen.campaign.player.detail.CharacterDetailViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.screen.campaign.player.detail.CharacterDiminishedViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.screen.campaign.player.ribbon.PlayerRibbonViewModel
 | 
			
		||||
import kotlinx.serialization.json.Json
 | 
			
		||||
import org.koin.core.module.dsl.factoryOf
 | 
			
		||||
import org.koin.core.module.dsl.singleOf
 | 
			
		||||
| 
						 | 
				
			
			@ -86,6 +89,9 @@ val viewModelDependencies
 | 
			
		|||
        viewModelOf(::RollViewModel)
 | 
			
		||||
        viewModelOf(::RollHistoryViewModel)
 | 
			
		||||
        viewModelOf(::NetworkViewModel)
 | 
			
		||||
        viewModelOf(::PlayerRibbonViewModel)
 | 
			
		||||
        viewModelOf(::CharacterDetailViewModel)
 | 
			
		||||
        viewModelOf(::CharacterDiminishedViewModel)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
val parserDependencies
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,7 +16,7 @@ import androidx.compose.ui.draw.drawWithContent
 | 
			
		|||
import androidx.compose.ui.graphics.Color
 | 
			
		||||
import androidx.compose.ui.unit.Dp
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import com.pixelized.desktop.lwa.theme.LwaColorPalette
 | 
			
		||||
import com.pixelized.desktop.lwa.theme.color.LwaColorPalette
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
class BlurContentController(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,6 +16,8 @@ class CharacterSheetJsonFactory(
 | 
			
		|||
        val json = CharacterSheetJsonV1(
 | 
			
		||||
            id = sheet.id,
 | 
			
		||||
            name = sheet.name,
 | 
			
		||||
            thumbnail = sheet.thumbnail,
 | 
			
		||||
            portrait = sheet.portrait,
 | 
			
		||||
            strength = sheet.strength,
 | 
			
		||||
            dexterity = sheet.dexterity,
 | 
			
		||||
            constitution = sheet.constitution,
 | 
			
		||||
| 
						 | 
				
			
			@ -93,6 +95,8 @@ class CharacterSheetJsonFactory(
 | 
			
		|||
        CharacterSheet(
 | 
			
		||||
            id = json.id,
 | 
			
		||||
            name = json.name,
 | 
			
		||||
            portrait = json.portrait,
 | 
			
		||||
            thumbnail = json.thumbnail,
 | 
			
		||||
            strength = json.strength,
 | 
			
		||||
            dexterity = json.dexterity,
 | 
			
		||||
            constitution = json.constitution,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,8 @@ package com.pixelized.desktop.lwa.repository.characterSheet.model
 | 
			
		|||
data class CharacterSheet(
 | 
			
		||||
    val id: String,
 | 
			
		||||
    val name: String,
 | 
			
		||||
    val portrait: String?,
 | 
			
		||||
    val thumbnail: String?,
 | 
			
		||||
    // characteristics
 | 
			
		||||
    val strength: Int,
 | 
			
		||||
    val dexterity: Int,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,8 @@ import kotlinx.serialization.Serializable
 | 
			
		|||
data class CharacterSheetJsonV1(
 | 
			
		||||
    val id: String,
 | 
			
		||||
    val name: String,
 | 
			
		||||
    val portrait: String?,
 | 
			
		||||
    val thumbnail: String?,
 | 
			
		||||
    // characteristics
 | 
			
		||||
    val strength: Int,
 | 
			
		||||
    val dexterity: Int,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,177 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.screen.campaign
 | 
			
		||||
 | 
			
		||||
import androidx.compose.desktop.ui.tooling.preview.Preview
 | 
			
		||||
import androidx.compose.foundation.layout.Box
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxHeight
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxSize
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxWidth
 | 
			
		||||
import androidx.compose.foundation.layout.height
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.material.Surface
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.input.key.Key
 | 
			
		||||
import androidx.compose.ui.input.key.KeyEventType
 | 
			
		||||
import androidx.compose.ui.input.key.key
 | 
			
		||||
import androidx.compose.ui.input.key.type
 | 
			
		||||
import androidx.compose.ui.layout.onSizeChanged
 | 
			
		||||
import androidx.compose.ui.platform.LocalDensity
 | 
			
		||||
import androidx.compose.ui.unit.Density
 | 
			
		||||
import androidx.compose.ui.unit.DpSize
 | 
			
		||||
import androidx.compose.ui.unit.IntSize
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import com.pixelized.desktop.lwa.composable.key.KeyHandler
 | 
			
		||||
import com.pixelized.desktop.lwa.screen.campaign.player.detail.CharacterDetail
 | 
			
		||||
import com.pixelized.desktop.lwa.screen.campaign.player.detail.CharacterDetailViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.screen.campaign.player.detail.CharacterDiminishedViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.screen.campaign.player.ribbon.PlayerRibbon
 | 
			
		||||
import com.pixelized.desktop.lwa.screen.characterSheet.detail.dialog.DiminishedStatDialog
 | 
			
		||||
import org.koin.compose.viewmodel.koinViewModel
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun CampaignScreen(
 | 
			
		||||
    characterDetailViewModel: CharacterDetailViewModel = koinViewModel(),
 | 
			
		||||
    dismissedViewModel: CharacterDiminishedViewModel = koinViewModel(),
 | 
			
		||||
) {
 | 
			
		||||
    KeyHandler {
 | 
			
		||||
        when {
 | 
			
		||||
            it.type == KeyEventType.KeyUp && it.key == Key.Escape -> {
 | 
			
		||||
                characterDetailViewModel.hideCharacter()
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            else -> false
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Surface {
 | 
			
		||||
        CampaignScreenLayout(
 | 
			
		||||
            modifier = Modifier.fillMaxSize(),
 | 
			
		||||
            top = {
 | 
			
		||||
                Surface(
 | 
			
		||||
                    modifier = Modifier
 | 
			
		||||
                        .height(32.dp)
 | 
			
		||||
                        .fillMaxWidth(),
 | 
			
		||||
                    elevation = 1.dp,
 | 
			
		||||
                ) {
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            bottom = {
 | 
			
		||||
                Surface(
 | 
			
		||||
                    modifier = Modifier
 | 
			
		||||
                        .height(48.dp)
 | 
			
		||||
                        .fillMaxWidth(),
 | 
			
		||||
                    elevation = 1.dp,
 | 
			
		||||
                ) {
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            main = {
 | 
			
		||||
 | 
			
		||||
            },
 | 
			
		||||
            leftOverlay = {
 | 
			
		||||
                PlayerRibbon(
 | 
			
		||||
                    modifier = Modifier
 | 
			
		||||
                        .padding(all = 8.dp)
 | 
			
		||||
                        .fillMaxHeight(),
 | 
			
		||||
                    onCharacter = {
 | 
			
		||||
                        characterDetailViewModel.showCharacter(id = it)
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
            },
 | 
			
		||||
            rightOverlay = {
 | 
			
		||||
                CharacterDetail(
 | 
			
		||||
                    modifier = Modifier
 | 
			
		||||
                        .padding(all = 8.dp)
 | 
			
		||||
                        .fillMaxHeight(),
 | 
			
		||||
                    viewModel = characterDetailViewModel,
 | 
			
		||||
                    dismissedViewModel = dismissedViewModel,
 | 
			
		||||
                )
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        DiminishedStatDialog(
 | 
			
		||||
            dialog = dismissedViewModel.diminishedDialog,
 | 
			
		||||
            onConfirm = { diminished ->
 | 
			
		||||
                dismissedViewModel.changeDiminished(
 | 
			
		||||
                    dialog = diminished
 | 
			
		||||
                )
 | 
			
		||||
                dismissedViewModel.hideDiminishedDialog()
 | 
			
		||||
            },
 | 
			
		||||
            onDismissRequest = {
 | 
			
		||||
                dismissedViewModel.hideDiminishedDialog()
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
private fun CampaignScreenLayout(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    top: @Composable CampaignScreenScope.() -> Unit,
 | 
			
		||||
    bottom: @Composable CampaignScreenScope.() -> Unit,
 | 
			
		||||
    main: @Composable CampaignScreenScope.() -> Unit,
 | 
			
		||||
    leftOverlay: @Composable CampaignScreenScope.() -> Unit,
 | 
			
		||||
    rightOverlay: @Composable CampaignScreenScope.() -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    val density = LocalDensity.current
 | 
			
		||||
    val leftOverlayState = remember { mutableStateOf(DpSize.Unspecified) }
 | 
			
		||||
    val rightOverlayState = remember { mutableStateOf(DpSize.Unspecified) }
 | 
			
		||||
    val scope = remember {
 | 
			
		||||
        CampaignScreenScope(
 | 
			
		||||
            leftOverlay = leftOverlayState,
 | 
			
		||||
            rightOverlay = rightOverlayState,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
    with(scope) {
 | 
			
		||||
        Column(
 | 
			
		||||
            modifier = modifier,
 | 
			
		||||
        ) {
 | 
			
		||||
            top()
 | 
			
		||||
            Box(
 | 
			
		||||
                modifier = Modifier.weight(1f, fill = true),
 | 
			
		||||
            ) {
 | 
			
		||||
                Box(
 | 
			
		||||
                    modifier = Modifier
 | 
			
		||||
                        .align(alignment = Alignment.Center)
 | 
			
		||||
                        .fillMaxSize(),
 | 
			
		||||
                ) {
 | 
			
		||||
                    main()
 | 
			
		||||
                }
 | 
			
		||||
                Box(
 | 
			
		||||
                    modifier = Modifier
 | 
			
		||||
                        .align(alignment = Alignment.CenterStart)
 | 
			
		||||
                        .onSizeChanged { leftOverlayState.value = it.toDp(density) },
 | 
			
		||||
                ) {
 | 
			
		||||
                    leftOverlay()
 | 
			
		||||
                }
 | 
			
		||||
                Box(
 | 
			
		||||
                    modifier = Modifier
 | 
			
		||||
                        .align(alignment = Alignment.CenterEnd)
 | 
			
		||||
                        .onSizeChanged { rightOverlayState.value = it.toDp(density) },
 | 
			
		||||
                ) {
 | 
			
		||||
                    rightOverlay()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            bottom()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private class CampaignScreenScope(
 | 
			
		||||
    val leftOverlay: State<DpSize>,
 | 
			
		||||
    val rightOverlay: State<DpSize>,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
private fun IntSize.toDp(density: Density) = with(density) {
 | 
			
		||||
    DpSize(
 | 
			
		||||
        width = width.toDp(),
 | 
			
		||||
        height = height.toDp(),
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,279 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.screen.campaign.player.detail
 | 
			
		||||
 | 
			
		||||
import androidx.compose.animation.AnimatedContent
 | 
			
		||||
import androidx.compose.animation.fadeIn
 | 
			
		||||
import androidx.compose.animation.fadeOut
 | 
			
		||||
import androidx.compose.animation.slideInHorizontally
 | 
			
		||||
import androidx.compose.animation.togetherWith
 | 
			
		||||
import androidx.compose.foundation.clickable
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.Box
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxHeight
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxSize
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxWidth
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.layout.size
 | 
			
		||||
import androidx.compose.foundation.layout.width
 | 
			
		||||
import androidx.compose.foundation.shape.CircleShape
 | 
			
		||||
import androidx.compose.material.Icon
 | 
			
		||||
import androidx.compose.material.IconButton
 | 
			
		||||
import androidx.compose.material.MaterialTheme
 | 
			
		||||
import androidx.compose.material.Surface
 | 
			
		||||
import androidx.compose.material.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.Stable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.collectAsState
 | 
			
		||||
import androidx.compose.runtime.rememberCoroutineScope
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.draw.clip
 | 
			
		||||
import androidx.compose.ui.text.font.FontWeight
 | 
			
		||||
import androidx.compose.ui.text.style.TextOverflow
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import com.pixelized.desktop.lwa.screen.characterSheet.detail.dialog.DiminishedStatDialog
 | 
			
		||||
import com.pixelized.desktop.lwa.theme.lwa
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.Res
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.ic_close_24dp
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.ic_heart_24dp
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.ic_near_me
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.ic_skull_24dp
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.ic_water_drop_24dp
 | 
			
		||||
import org.jetbrains.compose.resources.painterResource
 | 
			
		||||
import org.koin.compose.viewmodel.koinViewModel
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
data class CharacterDetailUio(
 | 
			
		||||
    val id: String,
 | 
			
		||||
    val portrait: String?,
 | 
			
		||||
    val name: String,
 | 
			
		||||
    val hp: String,
 | 
			
		||||
    val pp: String,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
data class CharacterDynDetailUio(
 | 
			
		||||
    val hp: String,
 | 
			
		||||
    val pp: String,
 | 
			
		||||
    val mov: String,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun CharacterDetail(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    dismissedViewModel: CharacterDiminishedViewModel,
 | 
			
		||||
    viewModel: CharacterDetailViewModel = koinViewModel(),
 | 
			
		||||
) {
 | 
			
		||||
    val scope = rememberCoroutineScope()
 | 
			
		||||
    val detail = viewModel.detail.collectAsState()
 | 
			
		||||
 | 
			
		||||
    AnimatedContent(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        targetState = detail.value,
 | 
			
		||||
        transitionSpec = {
 | 
			
		||||
            (fadeIn() + slideInHorizontally { it / 2 }).togetherWith(fadeOut())
 | 
			
		||||
        }
 | 
			
		||||
    ) {
 | 
			
		||||
        when (it) {
 | 
			
		||||
            null -> Box(
 | 
			
		||||
                modifier = Modifier.fillMaxHeight(),
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            else -> {
 | 
			
		||||
                val dynDetail = viewModel.collectDynamicDetailAsState(id = it.id)
 | 
			
		||||
 | 
			
		||||
                CharacterDetailContent(
 | 
			
		||||
                    modifier = Modifier
 | 
			
		||||
                        .fillMaxHeight()
 | 
			
		||||
                        .width(width = 128.dp * 4),
 | 
			
		||||
                    character = it,
 | 
			
		||||
                    dynDetail = dynDetail,
 | 
			
		||||
                    onDismissRequest = { viewModel.hideCharacter() },
 | 
			
		||||
                    onDiminished = {
 | 
			
		||||
                        scope.launch {
 | 
			
		||||
                            dismissedViewModel.showDiminishedDialog(id = it.id)
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    onHp = { },
 | 
			
		||||
                    onPp = { },
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun CharacterDetailContent(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    character: CharacterDetailUio,
 | 
			
		||||
    dynDetail: State<CharacterDynDetailUio>,
 | 
			
		||||
    onDismissRequest: () -> Unit,
 | 
			
		||||
    onDiminished: () -> Unit,
 | 
			
		||||
    onHp: () -> Unit,
 | 
			
		||||
    onPp: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    Box(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
    ) {
 | 
			
		||||
        Background(
 | 
			
		||||
            character = character,
 | 
			
		||||
        )
 | 
			
		||||
        Column {
 | 
			
		||||
            CharacterHeader(
 | 
			
		||||
                modifier = Modifier.padding(start = 16.dp).fillMaxWidth(),
 | 
			
		||||
                character = character,
 | 
			
		||||
                onDismissRequest = onDismissRequest,
 | 
			
		||||
                dynDetail = dynDetail,
 | 
			
		||||
                onDiminished = onDiminished,
 | 
			
		||||
                onHp = onHp,
 | 
			
		||||
                onPp = onPp,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
private fun Background(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    character: CharacterDetailUio,
 | 
			
		||||
) {
 | 
			
		||||
    Surface(
 | 
			
		||||
        modifier = modifier.fillMaxSize(),
 | 
			
		||||
        color = MaterialTheme.lwa.color.elevatedSurface,
 | 
			
		||||
    ) {
 | 
			
		||||
//        Image(
 | 
			
		||||
//            modifier = Modifier.fillMaxSize().drawWithContent {
 | 
			
		||||
//                drawContent()
 | 
			
		||||
//                drawRect(
 | 
			
		||||
//                    brush = Brush.verticalGradient(
 | 
			
		||||
//                        listOfNotNull(
 | 
			
		||||
//                            color?.copy(alpha = 0.7f),
 | 
			
		||||
//                            color,
 | 
			
		||||
//                        )
 | 
			
		||||
//                    )
 | 
			
		||||
//                )
 | 
			
		||||
//            },
 | 
			
		||||
//            painter = rememberAsyncImagePainter(model = character.portrait),
 | 
			
		||||
//            contentDescription = null,
 | 
			
		||||
//            contentScale = ContentScale.Crop,
 | 
			
		||||
//            alignment = Alignment.TopCenter,
 | 
			
		||||
//        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
private fun CharacterHeader(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    character: CharacterDetailUio,
 | 
			
		||||
    dynDetail: State<CharacterDynDetailUio>,
 | 
			
		||||
    onDismissRequest: () -> Unit,
 | 
			
		||||
    onDiminished: () -> Unit,
 | 
			
		||||
    onHp: () -> Unit,
 | 
			
		||||
    onPp: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    Column(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
    ) {
 | 
			
		||||
        Row {
 | 
			
		||||
            Text(
 | 
			
		||||
                modifier = Modifier.weight(1f)
 | 
			
		||||
                    .align(alignment = Alignment.CenterVertically),
 | 
			
		||||
                style = MaterialTheme.typography.h5,
 | 
			
		||||
                text = character.name,
 | 
			
		||||
                overflow = TextOverflow.Ellipsis,
 | 
			
		||||
                maxLines = 1,
 | 
			
		||||
            )
 | 
			
		||||
            IconButton(
 | 
			
		||||
                onClick = onDiminished,
 | 
			
		||||
            ) {
 | 
			
		||||
                Icon(
 | 
			
		||||
                    modifier = Modifier.size(size = 24.dp),
 | 
			
		||||
                    painter = painterResource(Res.drawable.ic_skull_24dp),
 | 
			
		||||
                    tint = MaterialTheme.colors.primary,
 | 
			
		||||
                    contentDescription = null,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            IconButton(
 | 
			
		||||
                onClick = onDismissRequest,
 | 
			
		||||
            ) {
 | 
			
		||||
                Icon(
 | 
			
		||||
                    painter = painterResource(Res.drawable.ic_close_24dp),
 | 
			
		||||
                    tint = MaterialTheme.lwa.color.base.primary,
 | 
			
		||||
                    contentDescription = null,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Row(
 | 
			
		||||
            horizontalArrangement = Arrangement.spacedBy(space = 12.dp),
 | 
			
		||||
        ) {
 | 
			
		||||
            Row(
 | 
			
		||||
                modifier = Modifier.clip(shape = CircleShape).clickable { onHp() },
 | 
			
		||||
                verticalAlignment = Alignment.Bottom,
 | 
			
		||||
            ) {
 | 
			
		||||
                Icon(
 | 
			
		||||
                    modifier = Modifier.padding(bottom = 4.dp, end = 2.dp).size(12.dp),
 | 
			
		||||
                    painter = painterResource(Res.drawable.ic_heart_24dp),
 | 
			
		||||
                    contentDescription = null
 | 
			
		||||
                )
 | 
			
		||||
                Text(
 | 
			
		||||
                    modifier = Modifier.alignByBaseline(),
 | 
			
		||||
                    style = MaterialTheme.typography.h6,
 | 
			
		||||
                    color = MaterialTheme.lwa.color.base.primary,
 | 
			
		||||
                    fontWeight = FontWeight.Bold,
 | 
			
		||||
                    text = dynDetail.value.hp,
 | 
			
		||||
                )
 | 
			
		||||
                Text(
 | 
			
		||||
                    modifier = Modifier.alignByBaseline(),
 | 
			
		||||
                    style = MaterialTheme.typography.caption,
 | 
			
		||||
                    fontWeight = FontWeight.Thin,
 | 
			
		||||
                    text = "/${character.hp}",
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            Row(
 | 
			
		||||
                modifier = Modifier.clip(shape = CircleShape).clickable { onPp() },
 | 
			
		||||
                verticalAlignment = Alignment.Bottom,
 | 
			
		||||
            ) {
 | 
			
		||||
                Icon(
 | 
			
		||||
                    modifier = Modifier.padding(bottom = 4.dp, end = 2.dp).size(12.dp),
 | 
			
		||||
                    painter = painterResource(Res.drawable.ic_water_drop_24dp),
 | 
			
		||||
                    contentDescription = null
 | 
			
		||||
                )
 | 
			
		||||
                Text(
 | 
			
		||||
                    modifier = Modifier.alignByBaseline(),
 | 
			
		||||
                    style = MaterialTheme.typography.h6,
 | 
			
		||||
                    color = MaterialTheme.lwa.color.base.primary,
 | 
			
		||||
                    fontWeight = FontWeight.Bold,
 | 
			
		||||
                    text = dynDetail.value.pp,
 | 
			
		||||
                )
 | 
			
		||||
                Text(
 | 
			
		||||
                    modifier = Modifier.alignByBaseline(),
 | 
			
		||||
                    style = MaterialTheme.typography.caption,
 | 
			
		||||
                    fontWeight = FontWeight.Thin,
 | 
			
		||||
                    text = "/${character.pp}",
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            Row(
 | 
			
		||||
                verticalAlignment = Alignment.Bottom,
 | 
			
		||||
            ) {
 | 
			
		||||
                Icon(
 | 
			
		||||
                    modifier = Modifier.padding(bottom = 4.dp, end = 2.dp).size(12.dp),
 | 
			
		||||
                    painter = painterResource(Res.drawable.ic_near_me),
 | 
			
		||||
                    contentDescription = null,
 | 
			
		||||
                )
 | 
			
		||||
                Text(
 | 
			
		||||
                    modifier = Modifier.alignByBaseline(),
 | 
			
		||||
                    style = MaterialTheme.typography.h6,
 | 
			
		||||
                    text = dynDetail.value.mov,
 | 
			
		||||
                )
 | 
			
		||||
                Text(
 | 
			
		||||
                    modifier = Modifier.alignByBaseline(),
 | 
			
		||||
                    style = MaterialTheme.typography.caption,
 | 
			
		||||
                    text = "m",
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,76 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.screen.campaign.player.detail
 | 
			
		||||
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.Stable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.collectAsState
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.lifecycle.ViewModel
 | 
			
		||||
import androidx.lifecycle.viewModelScope
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
 | 
			
		||||
import kotlinx.coroutines.flow.MutableStateFlow
 | 
			
		||||
import kotlinx.coroutines.flow.SharingStarted
 | 
			
		||||
import kotlinx.coroutines.flow.StateFlow
 | 
			
		||||
import kotlinx.coroutines.flow.combine
 | 
			
		||||
import kotlinx.coroutines.flow.mapNotNull
 | 
			
		||||
import kotlinx.coroutines.flow.stateIn
 | 
			
		||||
 | 
			
		||||
class CharacterDetailViewModel(
 | 
			
		||||
    private val repository: CharacterSheetRepository,
 | 
			
		||||
    private val alteration: AlterationRepository,
 | 
			
		||||
) : ViewModel() {
 | 
			
		||||
 | 
			
		||||
    private val displayedCharacterId = MutableStateFlow<String?>(null)
 | 
			
		||||
 | 
			
		||||
    val detail: StateFlow<CharacterDetailUio?> = combine(
 | 
			
		||||
        displayedCharacterId,
 | 
			
		||||
        repository.characterSheetFlow(),
 | 
			
		||||
    ) { id, sheets ->
 | 
			
		||||
        val sheet = sheets.firstOrNull { it.id == id }
 | 
			
		||||
        if (sheet == null) return@combine null
 | 
			
		||||
        CharacterDetailUio(
 | 
			
		||||
            id = sheet.id,
 | 
			
		||||
            portrait = sheet.portrait,
 | 
			
		||||
            name = sheet.name,
 | 
			
		||||
            hp = "${sheet.maxHp}",
 | 
			
		||||
            pp = "${sheet.maxPp}",
 | 
			
		||||
        )
 | 
			
		||||
    }.stateIn(
 | 
			
		||||
        scope = viewModelScope,
 | 
			
		||||
        started = SharingStarted.Eagerly,
 | 
			
		||||
        initialValue = null,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @Composable
 | 
			
		||||
    @Stable
 | 
			
		||||
    fun collectDynamicDetailAsState(id: String): State<CharacterDynDetailUio> {
 | 
			
		||||
        val flow = remember(id) {
 | 
			
		||||
            repository.characterSheetFlow(id = id)
 | 
			
		||||
        }
 | 
			
		||||
        return remember(id) {
 | 
			
		||||
            flow.mapNotNull { sheet ->
 | 
			
		||||
                if (sheet == null) return@mapNotNull null
 | 
			
		||||
                CharacterDynDetailUio(
 | 
			
		||||
                    hp = sheet.currentHp.toString(),
 | 
			
		||||
                    pp = sheet.currentPp.toString(),
 | 
			
		||||
                    mov = sheet.movement.toString(),
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }.collectAsState(
 | 
			
		||||
            initial = CharacterDynDetailUio(
 | 
			
		||||
                hp = flow.value?.maxHp?.toString() ?: "",
 | 
			
		||||
                pp = flow.value?.maxPp?.toString() ?: "",
 | 
			
		||||
                mov = flow.value?.movement?.toString() ?: "",
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun showCharacter(id: String) {
 | 
			
		||||
        displayedCharacterId.value = id
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun hideCharacter() {
 | 
			
		||||
        displayedCharacterId.value = null
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,49 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.screen.campaign.player.detail
 | 
			
		||||
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.ui.text.TextRange
 | 
			
		||||
import androidx.compose.ui.text.input.TextFieldValue
 | 
			
		||||
import androidx.lifecycle.ViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
 | 
			
		||||
import com.pixelized.desktop.lwa.screen.characterSheet.detail.dialog.DiminishedStatDialogUio
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.Res
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.character_sheet__diminished__label
 | 
			
		||||
import org.jetbrains.compose.resources.getString
 | 
			
		||||
 | 
			
		||||
class CharacterDiminishedViewModel(
 | 
			
		||||
    private val repository: CharacterSheetRepository,
 | 
			
		||||
) : ViewModel() {
 | 
			
		||||
 | 
			
		||||
    private val _diminishedDialog = mutableStateOf<DiminishedStatDialogUio?>(null)
 | 
			
		||||
    val diminishedDialog: State<DiminishedStatDialogUio?> get() = _diminishedDialog
 | 
			
		||||
 | 
			
		||||
    suspend fun showDiminishedDialog(id: String) {
 | 
			
		||||
        val diminished = repository.characterDiminishedFlow(id = id).value
 | 
			
		||||
        val textFieldValue =
 | 
			
		||||
            mutableStateOf(TextFieldValue("$diminished", selection = TextRange(index = 0)))
 | 
			
		||||
        _diminishedDialog.value = DiminishedStatDialogUio(
 | 
			
		||||
            id = id,
 | 
			
		||||
            label = getString(resource = Res.string.character_sheet__diminished__label),
 | 
			
		||||
            value = { textFieldValue.value },
 | 
			
		||||
            onValueChange = { value ->
 | 
			
		||||
                textFieldValue.value = when (value.text.toIntOrNull()?.takeIf { it >= 0 }) {
 | 
			
		||||
                    null -> TextFieldValue("0", selection = TextRange(index = 0))
 | 
			
		||||
                    else -> value
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun hideDiminishedDialog() {
 | 
			
		||||
        _diminishedDialog.value = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun changeDiminished(dialog: DiminishedStatDialogUio) {
 | 
			
		||||
        val value = dialog.value().text.toIntOrNull() ?: 0
 | 
			
		||||
        repository.setDiminishedForCharacter(
 | 
			
		||||
            id = dialog.id,
 | 
			
		||||
            diminished = value,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,125 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.screen.campaign.player.ribbon
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.clickable
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.Box
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.foundation.layout.aspectRatio
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxSize
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.layout.size
 | 
			
		||||
import androidx.compose.foundation.layout.width
 | 
			
		||||
import androidx.compose.foundation.shape.RoundedCornerShape
 | 
			
		||||
import androidx.compose.material.Icon
 | 
			
		||||
import androidx.compose.material.MaterialTheme
 | 
			
		||||
import androidx.compose.material.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.Stable
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.draw.clip
 | 
			
		||||
import androidx.compose.ui.draw.drawWithContent
 | 
			
		||||
import androidx.compose.ui.graphics.Brush
 | 
			
		||||
import androidx.compose.ui.graphics.Color
 | 
			
		||||
import androidx.compose.ui.graphics.FilterQuality
 | 
			
		||||
import androidx.compose.ui.layout.ContentScale
 | 
			
		||||
import androidx.compose.ui.unit.DpSize
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import coil3.compose.AsyncImage
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.Res
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.ic_heart_24dp
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.ic_water_drop_24dp
 | 
			
		||||
import org.jetbrains.compose.resources.painterResource
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
data class PlayerPortraitUio(
 | 
			
		||||
    val id: String,
 | 
			
		||||
    val portrait: String?,
 | 
			
		||||
    val hp: Int,
 | 
			
		||||
    val maxHp: Int,
 | 
			
		||||
    val pp: Int,
 | 
			
		||||
    val maxPp: Int,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
object PlayerPortrait {
 | 
			
		||||
    object Default {
 | 
			
		||||
        val size = DpSize(96.dp, 128.dp)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun PlayerPortrait(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    size: DpSize = PlayerPortrait.Default.size,
 | 
			
		||||
    character: PlayerPortraitUio,
 | 
			
		||||
    onCharacter: (id: String) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    Box(
 | 
			
		||||
        modifier = modifier
 | 
			
		||||
            .size(size = size)
 | 
			
		||||
            .clip(shape = remember { RoundedCornerShape(8.dp) })
 | 
			
		||||
            .clickable { onCharacter(character.id) },
 | 
			
		||||
    ) {
 | 
			
		||||
        AsyncImage(
 | 
			
		||||
            modifier = Modifier.fillMaxSize(),
 | 
			
		||||
            model = character.portrait,
 | 
			
		||||
            contentScale = ContentScale.Crop,
 | 
			
		||||
            alignment = Alignment.TopCenter,
 | 
			
		||||
            filterQuality = FilterQuality.High,
 | 
			
		||||
            contentDescription = null,
 | 
			
		||||
        )
 | 
			
		||||
        Column(
 | 
			
		||||
            modifier = Modifier
 | 
			
		||||
                .fillMaxSize()
 | 
			
		||||
                .drawWithContent {
 | 
			
		||||
                    drawRect(
 | 
			
		||||
                        brush = Brush.verticalGradient(
 | 
			
		||||
                            listOf(
 | 
			
		||||
                                Color.Black.copy(alpha = 0.0f),
 | 
			
		||||
                                Color.Black.copy(alpha = 0.0f),
 | 
			
		||||
                                Color.Black.copy(alpha = 0.0f),
 | 
			
		||||
                                Color.Black.copy(alpha = 0.5f),
 | 
			
		||||
                                Color.Black.copy(alpha = 0.8f),
 | 
			
		||||
                            )
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                    drawContent()
 | 
			
		||||
                }
 | 
			
		||||
                .padding(all = 2.dp),
 | 
			
		||||
            verticalArrangement = Arrangement.aligned(alignment = Alignment.Bottom),
 | 
			
		||||
        ) {
 | 
			
		||||
            Row(
 | 
			
		||||
                verticalAlignment = Alignment.CenterVertically,
 | 
			
		||||
                horizontalArrangement = Arrangement.spacedBy(space = 2.dp),
 | 
			
		||||
            ) {
 | 
			
		||||
                Icon(
 | 
			
		||||
                    modifier = Modifier.size(12.dp),
 | 
			
		||||
                    painter = painterResource(Res.drawable.ic_heart_24dp),
 | 
			
		||||
                    contentDescription = null
 | 
			
		||||
                )
 | 
			
		||||
                Text(
 | 
			
		||||
                    modifier = Modifier.padding(bottom = 2.dp),
 | 
			
		||||
                    style = MaterialTheme.typography.caption,
 | 
			
		||||
                    text = "${character.hp}/${character.maxHp}",
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            Row(
 | 
			
		||||
                verticalAlignment = Alignment.CenterVertically,
 | 
			
		||||
                horizontalArrangement = Arrangement.spacedBy(space = 2.dp),
 | 
			
		||||
            ) {
 | 
			
		||||
                Icon(
 | 
			
		||||
                    modifier = Modifier.size(12.dp),
 | 
			
		||||
                    painter = painterResource(Res.drawable.ic_water_drop_24dp),
 | 
			
		||||
                    contentDescription = null
 | 
			
		||||
                )
 | 
			
		||||
                Text(
 | 
			
		||||
                    modifier = Modifier.padding(bottom = 2.dp),
 | 
			
		||||
                    style = MaterialTheme.typography.caption,
 | 
			
		||||
                    text = "${character.pp}/${character.maxPp}",
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,34 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.screen.campaign.player.ribbon
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.lazy.LazyColumn
 | 
			
		||||
import androidx.compose.foundation.lazy.items
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.collectAsState
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import org.koin.compose.viewmodel.koinViewModel
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun PlayerRibbon(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    playerRibbonViewModel: PlayerRibbonViewModel = koinViewModel(),
 | 
			
		||||
    onCharacter: (id: String) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    val characters = playerRibbonViewModel.characters.collectAsState()
 | 
			
		||||
 | 
			
		||||
    LazyColumn(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        verticalArrangement = Arrangement.spacedBy(space = 8.dp)
 | 
			
		||||
    ) {
 | 
			
		||||
        items(
 | 
			
		||||
            items = characters.value,
 | 
			
		||||
            key = { it.id },
 | 
			
		||||
        ) {
 | 
			
		||||
            PlayerPortrait(
 | 
			
		||||
                character = it,
 | 
			
		||||
                onCharacter = onCharacter,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.screen.campaign.player.ribbon
 | 
			
		||||
 | 
			
		||||
import androidx.lifecycle.ViewModel
 | 
			
		||||
import androidx.lifecycle.viewModelScope
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
 | 
			
		||||
import kotlinx.coroutines.flow.SharingStarted
 | 
			
		||||
import kotlinx.coroutines.flow.map
 | 
			
		||||
import kotlinx.coroutines.flow.stateIn
 | 
			
		||||
 | 
			
		||||
class PlayerRibbonViewModel(
 | 
			
		||||
    repository: CharacterSheetRepository,
 | 
			
		||||
) : ViewModel() {
 | 
			
		||||
 | 
			
		||||
    val characters = repository.characterSheetFlow()
 | 
			
		||||
        .map { sheets ->
 | 
			
		||||
            sheets.map { sheet ->
 | 
			
		||||
                PlayerPortraitUio(
 | 
			
		||||
                    id = sheet.id,
 | 
			
		||||
                    portrait = sheet.thumbnail,
 | 
			
		||||
                    hp = sheet.currentHp,
 | 
			
		||||
                    maxHp = sheet.maxHp,
 | 
			
		||||
                    pp = sheet.currentPp,
 | 
			
		||||
                    maxPp = sheet.maxPp,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }.stateIn(
 | 
			
		||||
            scope = viewModelScope,
 | 
			
		||||
            started = SharingStarted.Eagerly,
 | 
			
		||||
            initialValue = emptyList()
 | 
			
		||||
        )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -197,6 +197,7 @@ class CharacterSheetViewModel(
 | 
			
		|||
        val textFieldValue =
 | 
			
		||||
            mutableStateOf(TextFieldValue("$diminished", selection = TextRange(index = 0)))
 | 
			
		||||
        _diminishedDialog.value = DiminishedStatDialogUio(
 | 
			
		||||
            id = argument.id,
 | 
			
		||||
            label = getString(resource = Res.string.character_sheet__diminished__label),
 | 
			
		||||
            value = { textFieldValue.value },
 | 
			
		||||
            onValueChange = { value ->
 | 
			
		||||
| 
						 | 
				
			
			@ -215,7 +216,7 @@ class CharacterSheetViewModel(
 | 
			
		|||
    fun changeDiminished(dialog: DiminishedStatDialogUio) {
 | 
			
		||||
        val value = dialog.value().text.toIntOrNull() ?: 0
 | 
			
		||||
        repository.setDiminishedForCharacter(
 | 
			
		||||
            id = argument.id,
 | 
			
		||||
            id = dialog.id,
 | 
			
		||||
            diminished = value,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -49,6 +49,7 @@ import org.jetbrains.compose.resources.stringResource
 | 
			
		|||
 | 
			
		||||
@Stable
 | 
			
		||||
data class DiminishedStatDialogUio(
 | 
			
		||||
    val id: String,
 | 
			
		||||
    val label: String,
 | 
			
		||||
    val value: () -> TextFieldValue,
 | 
			
		||||
    val onValueChange: (TextFieldValue) -> Unit,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -67,6 +67,8 @@ class CharacterSheetEditFactory(
 | 
			
		|||
        return CharacterSheet(
 | 
			
		||||
            id = editedSheet.id,
 | 
			
		||||
            name = editedSheet.name.value.value,
 | 
			
		||||
            portrait = currentSheet?.portrait,
 | 
			
		||||
            thumbnail = currentSheet?.thumbnail,
 | 
			
		||||
            strength = editedSheet.strength.unpack()?.toIntOrNull()
 | 
			
		||||
                ?: currentSheet?.strength
 | 
			
		||||
                ?: 0,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,17 +1,47 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.theme
 | 
			
		||||
 | 
			
		||||
import androidx.compose.material.MaterialTheme
 | 
			
		||||
import androidx.compose.material.darkColors
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.CompositionLocalProvider
 | 
			
		||||
import androidx.compose.runtime.Stable
 | 
			
		||||
import androidx.compose.runtime.compositionLocalOf
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import com.pixelized.desktop.lwa.theme.color.LwaColorTheme
 | 
			
		||||
import com.pixelized.desktop.lwa.theme.color.darkLwaColorTheme
 | 
			
		||||
 | 
			
		||||
val LocalLwaTheme = compositionLocalOf<LwaTheme> {
 | 
			
		||||
    error("Local Snack Controller is not yet ready")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
val MaterialTheme.lwa: LwaTheme
 | 
			
		||||
    @Composable
 | 
			
		||||
    @Stable
 | 
			
		||||
    get() = LocalLwaTheme.current
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
data class LwaTheme(
 | 
			
		||||
    val color: LwaColorTheme,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun LwaTheme(
 | 
			
		||||
    content: @Composable () -> Unit
 | 
			
		||||
    content: @Composable () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    MaterialTheme(
 | 
			
		||||
        colors = darkColors(),
 | 
			
		||||
        typography = MaterialTheme.typography,
 | 
			
		||||
        shapes = MaterialTheme.shapes,
 | 
			
		||||
        content = content,
 | 
			
		||||
    )
 | 
			
		||||
    val lwaColorTheme = darkLwaColorTheme()
 | 
			
		||||
    val theme = remember {
 | 
			
		||||
        LwaTheme(
 | 
			
		||||
            color = lwaColorTheme,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    CompositionLocalProvider(
 | 
			
		||||
        LocalLwaTheme provides theme
 | 
			
		||||
    ) {
 | 
			
		||||
        MaterialTheme(
 | 
			
		||||
            colors = lwaColorTheme.base,
 | 
			
		||||
            typography = MaterialTheme.typography,
 | 
			
		||||
            shapes = MaterialTheme.shapes,
 | 
			
		||||
            content = content,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.theme
 | 
			
		||||
package com.pixelized.desktop.lwa.theme.color
 | 
			
		||||
 | 
			
		||||
import androidx.compose.ui.graphics.Color
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,50 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.theme.color
 | 
			
		||||
 | 
			
		||||
import androidx.compose.material.Colors
 | 
			
		||||
import androidx.compose.material.darkColors
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.ReadOnlyComposable
 | 
			
		||||
import androidx.compose.runtime.Stable
 | 
			
		||||
import androidx.compose.ui.graphics.Color
 | 
			
		||||
import androidx.compose.ui.graphics.compositeOver
 | 
			
		||||
import androidx.compose.ui.unit.Dp
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import kotlin.math.ln
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
data class LwaColorTheme(
 | 
			
		||||
    val base: Colors,
 | 
			
		||||
    val elevatedSurface: Color,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
@Stable
 | 
			
		||||
fun darkLwaColorTheme(
 | 
			
		||||
    base: Colors = darkColors(),
 | 
			
		||||
    elevatedSurface: Color = base.calculateElevatedColor(
 | 
			
		||||
        color = base.surface,
 | 
			
		||||
        onColor = base.onSurface,
 | 
			
		||||
        elevation = 1.dp,
 | 
			
		||||
    ),
 | 
			
		||||
): LwaColorTheme = LwaColorTheme(
 | 
			
		||||
    base = base,
 | 
			
		||||
    elevatedSurface = elevatedSurface,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ReadOnlyComposable
 | 
			
		||||
@Composable
 | 
			
		||||
private fun Colors.calculateElevatedColor(color: Color, onColor: Color, elevation: Dp): Color {
 | 
			
		||||
    return if (elevation > 0.dp && !isLight) {
 | 
			
		||||
        val foregroundColor = calculateForegroundColor(onColor, elevation)
 | 
			
		||||
        foregroundColor.compositeOver(color)
 | 
			
		||||
    } else {
 | 
			
		||||
        color
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ReadOnlyComposable
 | 
			
		||||
@Composable
 | 
			
		||||
private fun calculateForegroundColor(color: Color, elevation: Dp): Color {
 | 
			
		||||
    val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f
 | 
			
		||||
    return color.copy(alpha = alpha)
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue