diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts
index 2dc5255..3ba22e2 100644
--- a/composeApp/build.gradle.kts
+++ b/composeApp/build.gradle.kts
@@ -29,6 +29,9 @@ kotlin {
api(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
+ // composable component.
+ implementation(libs.coil.compose)
+ implementation(libs.coil.network)
// common
implementation(projects.shared)
// network
diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_close_24dp.xml b/composeApp/src/commonMain/composeResources/drawable/ic_close_24dp.xml
new file mode 100644
index 0000000..dbc48c7
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/ic_close_24dp.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_heart_24dp.xml b/composeApp/src/commonMain/composeResources/drawable/ic_heart_24dp.xml
new file mode 100644
index 0000000..140a570
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/ic_heart_24dp.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_near_me.xml b/composeApp/src/commonMain/composeResources/drawable/ic_near_me.xml
new file mode 100644
index 0000000..0897cf4
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/ic_near_me.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_water_drop_24dp.xml b/composeApp/src/commonMain/composeResources/drawable/ic_water_drop_24dp.xml
new file mode 100644
index 0000000..e560f93
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/ic_water_drop_24dp.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt
index 0f54cc1..3287d4d 100644
--- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt
+++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt
@@ -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 {
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(
diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt
index 6e4165d..4ef1473 100644
--- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt
+++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt
@@ -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
diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/composable/blur/BlurContent.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/composable/blur/BlurContent.kt
index c743d14..19a4c08 100644
--- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/composable/blur/BlurContent.kt
+++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/composable/blur/BlurContent.kt
@@ -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(
diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetJsonFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetJsonFactory.kt
index 40a7576..2fd178c 100644
--- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetJsonFactory.kt
+++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetJsonFactory.kt
@@ -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,
diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/model/CharacterSheet.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/model/CharacterSheet.kt
index 254c58c..21bc30b 100644
--- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/model/CharacterSheet.kt
+++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/model/CharacterSheet.kt
@@ -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,
diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/model/CharacterSheetJsonV1.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/model/CharacterSheetJsonV1.kt
index d2e4550..c50034e 100644
--- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/model/CharacterSheetJsonV1.kt
+++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/model/CharacterSheetJsonV1.kt
@@ -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,
diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/campaign/CampaignScreen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/campaign/CampaignScreen.kt
new file mode 100644
index 0000000..68bb10b
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/campaign/CampaignScreen.kt
@@ -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,
+ val rightOverlay: State,
+)
+
+private fun IntSize.toDp(density: Density) = with(density) {
+ DpSize(
+ width = width.toDp(),
+ height = height.toDp(),
+ )
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/campaign/player/detail/CharacterDetail.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/campaign/player/detail/CharacterDetail.kt
new file mode 100644
index 0000000..f2de025
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/campaign/player/detail/CharacterDetail.kt
@@ -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,
+ 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,
+ 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",
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/campaign/player/detail/CharacterDetailViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/campaign/player/detail/CharacterDetailViewModel.kt
new file mode 100644
index 0000000..5fd3a29
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/campaign/player/detail/CharacterDetailViewModel.kt
@@ -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(null)
+
+ val detail: StateFlow = 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 {
+ 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
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/campaign/player/detail/CharacterDiminishedViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/campaign/player/detail/CharacterDiminishedViewModel.kt
new file mode 100644
index 0000000..2e85a85
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/campaign/player/detail/CharacterDiminishedViewModel.kt
@@ -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(null)
+ val diminishedDialog: State 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,
+ )
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/campaign/player/ribbon/PlayerPortrait.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/campaign/player/ribbon/PlayerPortrait.kt
new file mode 100644
index 0000000..da65d18
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/campaign/player/ribbon/PlayerPortrait.kt
@@ -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}",
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/campaign/player/ribbon/PlayerRibbon.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/campaign/player/ribbon/PlayerRibbon.kt
new file mode 100644
index 0000000..ee10f26
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/campaign/player/ribbon/PlayerRibbon.kt
@@ -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,
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/campaign/player/ribbon/PlayerRibbonViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/campaign/player/ribbon/PlayerRibbonViewModel.kt
new file mode 100644
index 0000000..d1a9cc6
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/campaign/player/ribbon/PlayerRibbonViewModel.kt
@@ -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()
+ )
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetViewModel.kt
index a8a3252..289c6a6 100644
--- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetViewModel.kt
+++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetViewModel.kt
@@ -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,
)
}
diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/dialog/DiminishedStatDialog.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/dialog/DiminishedStatDialog.kt
index 91b1e69..2063d84 100644
--- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/dialog/DiminishedStatDialog.kt
+++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/dialog/DiminishedStatDialog.kt
@@ -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,
diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEditFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEditFactory.kt
index eb94cc0..608e65e 100644
--- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEditFactory.kt
+++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEditFactory.kt
@@ -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,
diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/theme/LwaTheme.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/theme/LwaTheme.kt
index a67158b..ab00f0a 100644
--- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/theme/LwaTheme.kt
+++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/theme/LwaTheme.kt
@@ -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 {
+ 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,
+ )
+ }
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/theme/LwaColorPalette.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/theme/color/LwaColorPalette.kt
similarity index 73%
rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/theme/LwaColorPalette.kt
rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/theme/color/LwaColorPalette.kt
index ca2525e..691f062 100644
--- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/theme/LwaColorPalette.kt
+++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/theme/color/LwaColorPalette.kt
@@ -1,4 +1,4 @@
-package com.pixelized.desktop.lwa.theme
+package com.pixelized.desktop.lwa.theme.color
import androidx.compose.ui.graphics.Color
diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/theme/color/LwaColorTheme.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/theme/color/LwaColorTheme.kt
new file mode 100644
index 0000000..ff93249
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/theme/color/LwaColorTheme.kt
@@ -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)
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index b76ae76..60b1b57 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -9,7 +9,7 @@ ktor = "3.0.1"
koin = "4.0.0"
turtle = "0.5.0"
logback = "1.5.11"
-
+coil = "3.1.0"
[plugins]
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
@@ -42,4 +42,7 @@ ktor-server-netty = { group = 'io.ktor', name = "ktor-server-netty", version.ref
ktor-server-websockets = { group = 'io.ktor', name = "ktor-server-websockets", version.ref = "ktor" }
turtle = { group = "com.lordcodes.turtle", name = "turtle", version.ref = "turtle" }
-logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
\ No newline at end of file
+logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
+
+coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
+coil-network = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
\ No newline at end of file