Intial scene commit

This commit is contained in:
Thomas Andres Gomez 2025-05-10 14:32:29 +02:00
parent fc06e3ef95
commit 76fc199d5e
20 changed files with 583 additions and 233 deletions

View file

@ -34,6 +34,7 @@ kotlin {
// injection // injection
implementation(libs.koin.compose) implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel) implementation(libs.koin.compose.viewmodel)
implementation(libs.engawapg.zoomable)
// composable component. // composable component.
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(libs.coil.network.ktor) implementation(libs.coil.network.ktor)

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M480,640Q414,640 367,593Q320,546 320,480Q320,414 367,367Q414,320 480,320Q546,320 593,367Q640,414 640,480Q640,546 593,593Q546,640 480,640ZM480,560Q513,560 536.5,536.5Q560,513 560,480Q560,447 536.5,423.5Q513,400 480,400Q447,400 423.5,423.5Q400,447 400,480Q400,513 423.5,536.5Q447,560 480,560ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM200,840Q167,840 143.5,816.5Q120,793 120,760L120,600L200,600L200,760Q200,760 200,760Q200,760 200,760L360,760L360,840L200,840ZM600,840L600,760L760,760Q760,760 760,760Q760,760 760,760L760,600L840,600L840,760Q840,793 816.5,816.5Q793,840 760,840L600,840ZM120,360L120,200Q120,167 143.5,143.5Q167,120 200,120L360,120L360,200L200,200Q200,200 200,200Q200,200 200,200L200,360L120,360ZM760,360L760,200Q760,200 760,200Q760,200 760,200L600,200L600,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,360L760,360Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M156,860L100,804L224,680L120,680L120,600L360,600L360,840L280,840L280,736L156,860ZM804,860L680,736L680,840L600,840L600,600L840,600L840,680L736,680L860,804L804,860ZM120,360L120,280L224,280L100,156L156,100L280,224L280,120L360,120L360,360L120,360ZM600,360L600,120L680,120L680,224L804,100L860,156L736,280L840,280L840,360L600,360Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M120,840L120,600L200,600L200,704L324,580L380,636L256,760L360,760L360,840L120,840ZM600,840L600,760L704,760L580,636L636,580L760,704L760,600L840,600L840,840L600,840ZM324,380L200,256L200,360L120,360L120,120L360,120L360,200L256,200L380,324L324,380ZM636,380L580,324L704,200L600,200L600,120L840,120L840,360L760,360L760,256L636,380Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

View file

@ -40,8 +40,8 @@ import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.Characte
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanelViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanelViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeaderFactory import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeaderFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.CharacterDetailInventoryFactory import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.CharacterDetailInventoryFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.npc.NpcRibbonViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.NpcRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player.PlayerRibbonViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.text.CampaignChatViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.text.CampaignChatViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.text.TextMessageFactory import com.pixelized.desktop.lwa.ui.screen.campaign.text.TextMessageFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbarViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbarViewModel

View file

@ -0,0 +1,101 @@
package com.pixelized.desktop.lwa.ui.composable.scene
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.VectorConverter
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastRoundToInt
@Stable
class Camera(
private val initialZoom: Float = 2f,
private val initialOffset: IntOffset = IntOffset.Zero,
) {
private var _zoom = Animatable(
initialValue = initialZoom,
typeConverter = Float.VectorConverter,
)
val zoom: Float get() = _zoom.value
private var _offset = Animatable(
initialValue = initialOffset,
typeConverter = IntOffset.VectorConverter,
)
val offset: IntOffset by derivedStateOf {
_offset.value + IntOffset(
x = (_sceneSize.width - cameraSizeZoomed.width) / 2,
y = (_sceneSize.height - cameraSizeZoomed.height) / 2,
)
}
private var _sceneSize: IntSize by mutableStateOf(IntSize.Zero)
private var _cameraSize: IntSize by mutableStateOf(IntSize.Zero)
val cameraSize: IntSize get() = _cameraSize
val cameraSizeZoomed: IntSize by derivedStateOf {
IntSize(
width = (cameraSize.width * zoom).fastRoundToInt(),
height = (cameraSize.height * zoom).fastRoundToInt(),
)
}
fun changeSizes(
sceneSize: IntSize,
cameraSize: IntSize,
) {
_cameraSize = cameraSize
_sceneSize = sceneSize
}
suspend fun handlePanning(
delta: Offset,
snap: Boolean,
) {
val value = _offset.value - IntOffset(
x = (delta.x * zoom).fastRoundToInt(),
y = (delta.y * zoom).fastRoundToInt(),
)
when {
snap -> _offset.snapTo(targetValue = value)
else -> _offset.animateTo(targetValue = value)
}
}
suspend fun handleZoom(
zoomIn: Boolean,
power: Float,
snap: Boolean = false,
) {
val value = _zoom.value * when {
zoomIn -> 1f - power
else -> 1f + power
}
when {
snap -> _zoom.snapTo(targetValue = value)
else -> _zoom.animateTo(targetValue = value)
}
}
suspend fun resetPosition(
snap: Boolean = false,
) {
when (snap) {
true -> _offset.snapTo(targetValue = initialOffset)
else -> _offset.animateTo(targetValue = initialOffset)
}
}
suspend fun resetZoom(
snap: Boolean = false,
) {
when (snap) {
true -> _zoom.snapTo(targetValue = initialZoom)
else -> _zoom.animateTo(targetValue = initialZoom)
}
}
}

View file

@ -0,0 +1,9 @@
package com.pixelized.desktop.lwa.ui.composable.scene
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.Color
@Stable
data class FogOfWar(
val color: Color = Color.Black.copy(alpha = 0.5f),
)

View file

@ -0,0 +1,41 @@
package com.pixelized.desktop.lwa.ui.composable.scene
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.VectorConverter
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
@Stable
class Layout(
val texture: ImageBitmap,
val offset: IntOffset = IntOffset.Zero,
val size: IntSize = IntSize(texture.width, texture.height),
private val initialAlpha: Float = 1f,
) {
private val _alpha = Animatable(
initialValue = initialAlpha,
typeConverter = Float.VectorConverter,
)
val alpha get() = _alpha.value
suspend fun alpha(
alpha: Float,
snap: Boolean = false,
) {
when (snap) {
true -> _alpha.snapTo(targetValue = alpha)
else -> _alpha.animateTo(targetValue = alpha)
}
}
suspend fun resetAlpha(
snap: Boolean = false,
) {
when (snap) {
true -> _alpha.snapTo(targetValue = initialAlpha)
else -> _alpha.animateTo(targetValue = initialAlpha)
}
}
}

View file

@ -0,0 +1,278 @@
package com.pixelized.desktop.lwa.ui.composable.scene
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.isAltPressed
import androidx.compose.ui.input.pointer.isCtrlPressed
import androidx.compose.ui.input.pointer.isPrimaryPressed
import androidx.compose.ui.input.pointer.isTertiaryPressed
import androidx.compose.ui.input.pointer.onPointerEvent
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.screen.campaign.LocalCampaignLayoutScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_center_focus_weak_24dp
import lwacharactersheet.composeapp.generated.resources.ic_zoom_in_map_24dp
import lwacharactersheet.composeapp.generated.resources.ic_zoom_out_map_24dp
import lwacharactersheet.composeapp.generated.resources.ic_visibility_24dp
import lwacharactersheet.composeapp.generated.resources.icon_d100
import lwacharactersheet.composeapp.generated.resources.image_dahome_maps
import lwacharactersheet.composeapp.generated.resources.image_dahome_regions
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.imageResource
import org.jetbrains.compose.resources.painterResource
import kotlin.math.sign
@Stable
data class Scene(
val camera: Camera,
val layouts: List<Layout>,
val fogOfWar: FogOfWar,
) {
val size: IntSize = IntSize(
width = layouts.maxOf { it.size.width },
height = layouts.maxOf { it.size.height },
)
}
@Composable
fun Scene(
modifier: Modifier,
) {
val campaign = LocalCampaignLayoutScope.current
val scope = rememberCoroutineScope()
val scene = rememberScene(
camera = Camera(
initialZoom = 1f,
initialOffset = IntOffset(x = -150, y = -120),
),
fogOfWar = FogOfWar(),
rememberLayoutFromResource(
resource = Res.drawable.image_dahome_maps,
),
rememberLayoutFromResource(
resource = Res.drawable.image_dahome_regions,
),
rememberLayoutFromResource(
resource = Res.drawable.icon_d100,
offset = IntOffset(x = 1740, y = 910),
),
)
Box(
modifier = modifier
.graphicsLayer { clip = true }
.onCameraControl(scope = scope, scene = scene)
.drawScene(scene = scene)
.fogOfWar(scene = scene)
) {
Column(
modifier = Modifier
.align(alignment = Alignment.BottomEnd)
.padding(end = campaign.rightPanel.value.width)
.padding(all = 8.dp)
) {
IconButton(
onClick = {
scope.launch {
scene.camera.handleZoom(
zoomIn = true,
power = 0.3f,
)
}
}
) {
Icon(
painter = painterResource(Res.drawable.ic_zoom_in_map_24dp),
contentDescription = null
)
}
IconButton(
onClick = {
scope.launch {
scene.camera.handleZoom(
zoomIn = false,
power = 0.3f,
)
}
}
) {
Icon(
painter = painterResource(Res.drawable.ic_zoom_out_map_24dp),
contentDescription = null
)
}
IconButton(
onClick = {
scope.launch {
scene.camera.resetPosition()
}
scope.launch {
scene.camera.resetZoom()
}
}
) {
Icon(
painter = painterResource(Res.drawable.ic_center_focus_weak_24dp),
contentDescription = null
)
}
IconButton(
onClick = {
scope.launch {
scene.layouts.getOrNull(1)?.let {
it.alpha(alpha = if (it.alpha == 0f) 1f else 0f)
}
}
}
) {
Icon(
painter = painterResource(Res.drawable.ic_visibility_24dp),
contentDescription = null
)
}
}
}
}
@Composable
@Stable
fun rememberLayoutFromResource(
resource: DrawableResource,
offset: IntOffset = IntOffset.Zero,
): Layout {
val texture = imageResource(
resource = resource,
)
return remember(resource) {
Layout(
texture = texture,
offset = offset,
)
}
}
@Composable
@Stable
fun rememberScene(
camera: Camera,
fogOfWar: FogOfWar,
vararg layouts: Layout,
): Scene {
return remember {
Scene(
camera = camera,
layouts = layouts.toList(),
fogOfWar = fogOfWar,
)
}
}
@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
fun Modifier.onCameraControl(
scope: CoroutineScope,
scene: Scene,
): Modifier {
val offsetDelta = CursorDelta()
return this
.onSizeChanged {
scene.camera.changeSizes(
sceneSize = scene.size,
cameraSize = it,
)
}
.onPointerEvent(PointerEventType.Move) { event: PointerEvent ->
scope.launch {
offsetDelta.handlePositionChange(
event = event,
) { delta ->
when {
event.buttons.isTertiaryPressed || (event.keyboardModifiers.isCtrlPressed && event.buttons.isPrimaryPressed) -> scene.camera.handlePanning(
delta = delta,
snap = true,
)
event.keyboardModifiers.isAltPressed -> scene.camera.handleZoom(
zoomIn = delta.y.sign < 0f,
power = 0.025f,
snap = true,
)
}
}
}
}
.onPointerEvent(PointerEventType.Scroll) { event: PointerEvent ->
scope.launch {
scene.camera.handleZoom(
zoomIn = event.changes.first().scrollDelta.y.sign < 0f,
power = 0.15f,
snap = false,
)
}
}
}
fun Modifier.drawScene(
scene: Scene,
): Modifier = this.drawWithCache {
onDrawBehind {
scene.layouts.forEach { layout ->
drawImage(
image = layout.texture,
srcOffset = scene.camera.offset - layout.offset,
srcSize = scene.camera.cameraSizeZoomed,
dstSize = scene.camera.cameraSize,
alpha = layout.alpha,
)
}
}
}
fun Modifier.fogOfWar(
scene: Scene,
): Modifier = this.drawWithCache {
onDrawBehind {
drawRect(color = scene.fogOfWar.color)
}
}
private data class CursorDelta(
var lastDeltaTimestamp: Long = System.currentTimeMillis(),
var previousPosition: Offset = Offset.Zero,
var currentPosition: Offset = Offset.Zero,
) {
suspend inline fun handlePositionChange(
event: PointerEvent,
delay: Float = 10f,
crossinline block: suspend (delta: Offset) -> Unit,
) {
val currentTimestamp = System.currentTimeMillis()
if (currentTimestamp - lastDeltaTimestamp > delay) {
lastDeltaTimestamp = currentTimestamp
previousPosition = currentPosition
currentPosition = event.changes.first().position
block(currentPosition - previousPosition)
}
}
}

View file

@ -5,7 +5,7 @@ import androidx.compose.foundation.TooltipPlacement
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface import androidx.compose.material.Surface
@ -56,7 +56,7 @@ fun BasicTooltipLayout(
tips = tooltip, tips = tooltip,
tooltip = { tooltip = {
BasicTooltip( BasicTooltip(
modifier = Modifier.width(width = 448.dp), modifier = Modifier.widthIn(max = 448.dp),
elevation = elevation, elevation = elevation,
tooltip = it, tooltip = it,
) )
@ -72,7 +72,9 @@ private fun BasicTooltip(
tooltip: BasicTooltipUio, tooltip: BasicTooltipUio,
) { ) {
Surface( Surface(
modifier = Modifier.padding(16.dp).then(other = modifier), modifier = Modifier
.padding(16.dp)
.then(other = modifier),
color = MaterialTheme.colors.surface, color = MaterialTheme.colors.surface,
elevation = elevation, elevation = elevation,
shape = remember { RoundedCornerShape(4.dp) } shape = remember { RoundedCornerShape(4.dp) }

View file

@ -2,11 +2,11 @@ package com.pixelized.desktop.lwa.ui.screen.campaign
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
@ -20,6 +20,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.isAltPressed import androidx.compose.ui.input.key.isAltPressed
import androidx.compose.ui.input.key.isCtrlPressed import androidx.compose.ui.input.key.isCtrlPressed
@ -42,6 +43,7 @@ import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterShe
import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterSheetDiminishedDialogViewModel import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterSheetDiminishedDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler
import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler
import com.pixelized.desktop.lwa.ui.composable.scene.Scene
import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToLevelScreen import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToLevelScreen
import com.pixelized.desktop.lwa.ui.overlay.portrait.PortraitOverlay import com.pixelized.desktop.lwa.ui.overlay.portrait.PortraitOverlay
@ -49,13 +51,14 @@ import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetai
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanelViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanelViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.DetailPanelUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.DetailPanelUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.rememberTransitionAnimation import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.rememberTransitionAnimation
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.npc.NpcRibbon import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.CharacterRibbon
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player.PlayerRibbon import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.NpcRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player.PlayerRibbonViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.text.CampaignChat import com.pixelized.desktop.lwa.ui.screen.campaign.text.CampaignChat
import com.pixelized.desktop.lwa.ui.screen.campaign.text.CampaignChatViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.text.CampaignChatViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbar import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbar
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbarViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbarViewModel
import com.pixelized.desktop.lwa.ui.theme.lwa
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
@ -67,6 +70,7 @@ val LocalCampaignLayoutScope = compositionLocalOf<CampaignLayoutScope> {
fun CampaignScreen( fun CampaignScreen(
playerRibbonViewModel: PlayerRibbonViewModel = koinViewModel(), playerRibbonViewModel: PlayerRibbonViewModel = koinViewModel(),
playerDetailViewModel: CharacterDetailPanelViewModel = koinViewModel(key = "player"), playerDetailViewModel: CharacterDetailPanelViewModel = koinViewModel(key = "player"),
npcRibbonViewModel: NpcRibbonViewModel = koinViewModel(),
npcDetailViewModel: CharacterDetailPanelViewModel = koinViewModel(key = "npc"), npcDetailViewModel: CharacterDetailPanelViewModel = koinViewModel(key = "npc"),
characteristicDialogViewModel: CharacterSheetCharacteristicDialogViewModel = koinViewModel(), characteristicDialogViewModel: CharacterSheetCharacteristicDialogViewModel = koinViewModel(),
dismissedViewModel: CharacterSheetDiminishedDialogViewModel = koinViewModel(), dismissedViewModel: CharacterSheetDiminishedDialogViewModel = koinViewModel(),
@ -96,7 +100,9 @@ fun CampaignScreen(
}, },
main = { main = {
Scene(
modifier = Modifier.matchParentSize(),
)
}, },
chat = { chat = {
CampaignChat( CampaignChat(
@ -115,9 +121,10 @@ fun CampaignScreen(
} }
}, },
leftPanel = { leftPanel = {
PlayerRibbon( CharacterRibbon(
modifier = Modifier.fillMaxHeight(), modifier = Modifier.fillMaxHeight(),
viewModel = playerRibbonViewModel, viewModel = playerRibbonViewModel,
layoutDirection = LayoutDirection.Ltr,
onCharacterLeftClick = { onCharacterLeftClick = {
scope.launch { scope.launch {
playerDetailViewModel.showCharacter( playerDetailViewModel.showCharacter(
@ -140,8 +147,10 @@ fun CampaignScreen(
) )
}, },
rightPanel = { rightPanel = {
NpcRibbon( CharacterRibbon(
modifier = Modifier.fillMaxHeight(), modifier = Modifier.fillMaxHeight(),
viewModel = npcRibbonViewModel,
layoutDirection = LayoutDirection.Rtl,
onCharacterLeftClick = { onCharacterLeftClick = {
scope.launch { scope.launch {
npcDetailViewModel.showCharacter( npcDetailViewModel.showCharacter(
@ -167,10 +176,11 @@ fun CampaignScreen(
CharacterDetailPanel( CharacterDetailPanel(
modifier = Modifier modifier = Modifier
.padding(all = 8.dp) .padding(all = 8.dp)
.padding(start = MaterialTheme.lwa.size.portrait.minimized.width + 8.dp)
.width(width = 128.dp * 4) .width(width = 128.dp * 4)
.fillMaxHeight(), .fillMaxHeight(),
transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Ltr), transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Ltr),
detailPanelViewModel = npcDetailViewModel, detailPanelViewModel = playerDetailViewModel,
characterDiminishedViewModel = dismissedViewModel, characterDiminishedViewModel = dismissedViewModel,
characteristicDialogViewModel = characteristicDialogViewModel, characteristicDialogViewModel = characteristicDialogViewModel,
alterationViewModel = alterationViewModel, alterationViewModel = alterationViewModel,
@ -183,10 +193,11 @@ fun CampaignScreen(
CharacterDetailPanel( CharacterDetailPanel(
modifier = Modifier modifier = Modifier
.padding(all = 8.dp) .padding(all = 8.dp)
.padding(end = MaterialTheme.lwa.size.portrait.minimized.width + 8.dp)
.width(width = 128.dp * 4) .width(width = 128.dp * 4)
.fillMaxHeight(), .fillMaxHeight(),
transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Rtl), transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Rtl),
detailPanelViewModel = playerDetailViewModel, detailPanelViewModel = npcDetailViewModel,
characterDiminishedViewModel = dismissedViewModel, characterDiminishedViewModel = dismissedViewModel,
characteristicDialogViewModel = characteristicDialogViewModel, characteristicDialogViewModel = characteristicDialogViewModel,
alterationViewModel = alterationViewModel, alterationViewModel = alterationViewModel,
@ -205,7 +216,7 @@ fun CampaignScreen(
characteristicDialogViewModel.changeSubCharacteristic( characteristicDialogViewModel.changeSubCharacteristic(
characterSheetId = dialog.characterSheetId, characterSheetId = dialog.characterSheetId,
characteristic = dialog.characteristic, characteristic = dialog.characteristic,
useArmor= dialog.enableArmor?.checked?.value == true, useArmor = dialog.enableArmor?.checked?.value == true,
value = dialog.value.valueFlow.value.toIntOrNull() ?: 0, value = dialog.value.valueFlow.value.toIntOrNull() ?: 0,
) )
characteristicDialogViewModel.hideSubCharacteristicDialog() characteristicDialogViewModel.hideSubCharacteristicDialog()
@ -307,6 +318,7 @@ private fun CampaignLayout(
) { ) {
val density = LocalDensity.current val density = LocalDensity.current
val mainState = remember { mutableStateOf(DpSize.Unspecified) }
val leftOverlayState = remember { mutableStateOf(DpSize.Unspecified) } val leftOverlayState = remember { mutableStateOf(DpSize.Unspecified) }
val leftPanelState = remember { mutableStateOf(DpSize.Unspecified) } val leftPanelState = remember { mutableStateOf(DpSize.Unspecified) }
val rightOverlayState = remember { mutableStateOf(DpSize.Unspecified) } val rightOverlayState = remember { mutableStateOf(DpSize.Unspecified) }
@ -314,6 +326,7 @@ private fun CampaignLayout(
val chatOverlayState = remember { mutableStateOf(DpSize.Unspecified) } val chatOverlayState = remember { mutableStateOf(DpSize.Unspecified) }
val scope = remember { val scope = remember {
CampaignLayoutScope( CampaignLayoutScope(
main = mainState,
leftOverlay = leftOverlayState, leftOverlay = leftOverlayState,
leftPanel = leftPanelState, leftPanel = leftPanelState,
rightOverlay = rightOverlayState, rightOverlay = rightOverlayState,
@ -334,30 +347,35 @@ private fun CampaignLayout(
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.align(alignment = Alignment.Center) .onSizeChanged { mainState.value = it.toDp(density) }
.fillMaxSize(), .matchParentSize(),
) { ) {
main() main()
} }
Row { Box(
Box( modifier = Modifier
modifier = Modifier.onSizeChanged { leftPanelState.value = it.toDp(density) }, .align(alignment = Alignment.BottomStart)
) { .padding(
leftPanel() start = MaterialTheme.lwa.size.portrait.minimized.width * 2 + 20.dp,
} end = MaterialTheme.lwa.size.portrait.minimized.width * 2 + 20.dp + 56.dp,
Box( )
modifier = Modifier .onSizeChanged { chatOverlayState.value = it.toDp(density) },
.align(alignment = Alignment.Bottom) ) {
.weight(weight = 1f) chat()
.onSizeChanged { chatOverlayState.value = it.toDp(density) }, }
) { Box(
chat() modifier = Modifier
} .align(alignment = Alignment.CenterStart)
Box( .onSizeChanged { leftPanelState.value = it.toDp(density) },
modifier = Modifier.onSizeChanged { rightPanelState.value = it.toDp(density) }, ) {
) { leftPanel()
rightPanel() }
} Box(
modifier = Modifier
.align(alignment = Alignment.CenterEnd)
.onSizeChanged { rightPanelState.value = it.toDp(density) },
) {
rightPanel()
} }
Box( Box(
modifier = Modifier modifier = Modifier
@ -392,24 +410,31 @@ private fun CampaignKeyHandler(
onPlayerNumber: (index: Int) -> Unit, onPlayerNumber: (index: Int) -> Unit,
onAltPLayerNumber: (index: Int) -> Unit, onAltPLayerNumber: (index: Int) -> Unit,
) { ) {
fun KeyEvent.callback(index: Int) {
if (isAltPressed) onAltPLayerNumber(index) else onPlayerNumber(index)
}
KeyHandler { KeyHandler {
if (it.type != KeyEventType.KeyDown) return@KeyHandler false if (it.type != KeyEventType.KeyDown) {
return@KeyHandler false
}
if (it.key == Key.Escape) { if (it.key == Key.Escape) {
onDismissRequest() onDismissRequest()
return@KeyHandler true return@KeyHandler true
} }
if (it.isCtrlPressed.not()) return@KeyHandler false if (it.isCtrlPressed.not()) {
return@KeyHandler false
}
when (it.key) { when (it.key) {
Key.Escape -> onDismissRequest() Key.Escape -> onDismissRequest()
Key.One, Key.NumPad1 -> if (it.isAltPressed) onAltPLayerNumber(0) else onPlayerNumber(0) Key.One, Key.NumPad1 -> it.callback(index = 0)
Key.Two, Key.NumPad2 -> if (it.isAltPressed) onAltPLayerNumber(1) else onPlayerNumber(1) Key.Two, Key.NumPad2 -> it.callback(index = 1)
Key.Three, Key.NumPad3 -> if (it.isAltPressed) onAltPLayerNumber(2) else onPlayerNumber(2) Key.Three, Key.NumPad3 -> it.callback(index = 2)
Key.Four, Key.NumPad4 -> if (it.isAltPressed) onAltPLayerNumber(3) else onPlayerNumber(3) Key.Four, Key.NumPad4 -> it.callback(index = 3)
Key.Five, Key.NumPad5 -> if (it.isAltPressed) onAltPLayerNumber(4) else onPlayerNumber(4) Key.Five, Key.NumPad5 -> it.callback(index = 4)
Key.Six, Key.NumPad6 -> if (it.isAltPressed) onAltPLayerNumber(5) else onPlayerNumber(5) Key.Six, Key.NumPad6 -> it.callback(index = 5)
Key.Seven, Key.NumPad7 -> if (it.isAltPressed) onAltPLayerNumber(6) else onPlayerNumber(6) Key.Seven, Key.NumPad7 -> it.callback(index = 6)
Key.Eight, Key.NumPad8 -> if (it.isAltPressed) onAltPLayerNumber(7) else onPlayerNumber(7) Key.Eight, Key.NumPad8 -> it.callback(index = 7)
Key.Nine, Key.NumPad9 -> if (it.isAltPressed) onAltPLayerNumber(8) else onPlayerNumber(8) Key.Nine, Key.NumPad9 -> it.callback(index = 8)
else -> return@KeyHandler false else -> return@KeyHandler false
} }
return@KeyHandler true return@KeyHandler true
@ -425,6 +450,7 @@ private fun IntSize.toDp(density: Density) = with(density) {
@Stable @Stable
data class CampaignLayoutScope( data class CampaignLayoutScope(
val main: State<DpSize>,
val leftOverlay: State<DpSize>, val leftOverlay: State<DpSize>,
val leftPanel: State<DpSize>, val leftPanel: State<DpSize>,
val rightOverlay: State<DpSize>, val rightOverlay: State<DpSize>,

View file

@ -17,14 +17,12 @@ import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonPortrait import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonPortrait
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonRoll import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonRoll
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonAlteration import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonAlteration
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player.PlayerRibbonViewModel
import org.koin.compose.viewmodel.koinViewModel
@Composable @Composable
fun CharacterRibbon( fun CharacterRibbon(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
layoutDirection: LayoutDirection, layoutDirection: LayoutDirection,
viewModel: PlayerRibbonViewModel = koinViewModel(), viewModel: CharacterRibbonViewModel,
padding: PaddingValues = PaddingValues(all = 8.dp), padding: PaddingValues = PaddingValues(all = 8.dp),
onCharacterLeftClick: (characterSheetId: String) -> Unit, onCharacterLeftClick: (characterSheetId: String) -> Unit,
onCharacterRightClick: (characterSheetId: String) -> Unit, onCharacterRightClick: (characterSheetId: String) -> Unit,

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.npc package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
@ -6,8 +6,6 @@ import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetReposit
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.repository.settings.model.Settings import com.pixelized.desktop.lwa.repository.settings.model.Settings
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.CharacterRibbonFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.CharacterRibbonViewModel
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.Campaign
class NpcRibbonViewModel( class NpcRibbonViewModel(

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
@ -6,8 +6,6 @@ import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetReposit
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.repository.settings.model.Settings import com.pixelized.desktop.lwa.repository.settings.model.Settings
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.CharacterRibbonFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.CharacterRibbonViewModel
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.Campaign
class PlayerRibbonViewModel( class PlayerRibbonViewModel(

View file

@ -32,6 +32,7 @@ import com.pixelized.desktop.lwa.ui.theme.lwa
@Stable @Stable
data class CharacterRibbonAlterationUio( data class CharacterRibbonAlterationUio(
val icon: String, val icon: String,
val tooltips: BasicTooltipUio?, val tooltips: BasicTooltipUio?,
) )
@ -44,60 +45,56 @@ fun CharacterRibbonAlteration(
direction: LayoutDirection, direction: LayoutDirection,
status: List<List<CharacterRibbonAlterationUio>>, status: List<List<CharacterRibbonAlterationUio>>,
) { ) {
val currentDirection: LayoutDirection = LocalLayoutDirection.current val currentDirection = LocalLayoutDirection.current
CompositionLocalProvider( Row(
LocalLayoutDirection provides direction modifier = Modifier
.animateContentSize()
.size(size = size)
.then(other = modifier),
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) { ) {
Row( status.forEach { columns ->
modifier = Modifier Column(
.animateContentSize() modifier = Modifier.animateContentSize(),
.size(size = size) verticalArrangement = Arrangement.spacedBy(space = 2.dp),
.then(other = modifier),
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
CompositionLocalProvider(
LocalLayoutDirection provides currentDirection
) { ) {
status.forEach { columns -> CompositionLocalProvider(
Column( LocalLayoutDirection provides LayoutDirection.Ltr
modifier = Modifier.animateContentSize(), ) {
verticalArrangement = Arrangement.spacedBy(space = 2.dp), columns.forEach {
) { BasicTooltipLayout(
columns.forEach { delayMillis = 0,
BasicTooltipLayout( tooltip = it.tooltips,
delayMillis = 0, tooltipPlacement = remember(currentDirection) {
tooltip = it.tooltips, TooltipPlacement.ComponentRect(
tooltipPlacement = remember(currentDirection) { anchor = when (direction) {
TooltipPlacement.ComponentRect( LayoutDirection.Ltr -> Alignment.CenterEnd
anchor = when (direction) { LayoutDirection.Rtl -> Alignment.CenterStart
LayoutDirection.Ltr -> Alignment.TopStart },
LayoutDirection.Rtl -> Alignment.TopEnd alignment = when (direction) {
}, LayoutDirection.Ltr -> Alignment.CenterEnd
alignment = when (direction) { LayoutDirection.Rtl -> Alignment.CenterStart
LayoutDirection.Ltr -> Alignment.BottomEnd },
LayoutDirection.Rtl -> Alignment.BottomStart )
}, },
content = {
AnimatedContent(
targetState = it.icon,
transitionSpec = { fadeIn() togetherWith fadeOut() },
) { icon ->
AsyncImage(
modifier = Modifier.size(24.dp),
model = ImageRequest.Builder(context = PlatformContext.INSTANCE)
.data(data = icon)
.size(size = 48)
.build(),
filterQuality = FilterQuality.High,
contentDescription = null,
) )
},
content = {
AnimatedContent(
targetState = it.icon,
transitionSpec = { fadeIn() togetherWith fadeOut() },
) { icon ->
AsyncImage(
modifier = Modifier.size(24.dp),
model = ImageRequest.Builder(context = PlatformContext.INSTANCE)
.data(data = icon)
.size(size = 48)
.build(),
filterQuality = FilterQuality.High,
contentDescription = null,
)
}
} }
) }
} )
} }
} }
} }

View file

@ -1,64 +0,0 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.npc
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
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.graphics.graphicsLayer
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonPortrait
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonRoll
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonAlteration
import org.koin.compose.viewmodel.koinViewModel
@Composable
fun NpcRibbon(
modifier: Modifier = Modifier,
viewModel: NpcRibbonViewModel = koinViewModel(),
padding: PaddingValues = PaddingValues(all = 8.dp),
onCharacterLeftClick: (characterSheetId: String) -> Unit,
onCharacterRightClick: (characterSheetId: String) -> Unit,
onLevelUp: (characterSheetId: String) -> Unit,
) {
val characters = viewModel.characters.collectAsState()
LazyColumn(
modifier = modifier,
contentPadding = padding,
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) {
items(
items = characters.value,
key = { it.characterSheetId },
) {
Row(
modifier = Modifier
.animateItem()
.graphicsLayer { if (it.hideOverruled) this.alpha = 0.3f },
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
CharacterRibbonAlteration(
status = it.status,
direction = LayoutDirection.Rtl,
)
Box {
CharacterRibbonPortrait(
character = it.portrait,
onCharacterLeftClick = { onCharacterLeftClick(it.characterSheetId) },
onCharacterRightClick = { onCharacterRightClick(it.characterSheetId) },
onLevelUp = { onLevelUp(it.characterSheetId) },
)
CharacterRibbonRoll(
value = viewModel.roll(characterSheetId = it.characterSheetId).value,
)
}
}
}
}
}

View file

@ -1,64 +0,0 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
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.graphics.graphicsLayer
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonPortrait
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonRoll
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonAlteration
import org.koin.compose.viewmodel.koinViewModel
@Composable
fun PlayerRibbon(
modifier: Modifier = Modifier,
viewModel: PlayerRibbonViewModel = koinViewModel(),
padding: PaddingValues = PaddingValues(all = 8.dp),
onCharacterLeftClick: (characterSheetId: String) -> Unit,
onCharacterRightClick: (characterSheetId: String) -> Unit,
onLevelUp: (characterSheetId: String) -> Unit,
) {
val characters = viewModel.characters.collectAsState()
LazyColumn(
modifier = modifier,
contentPadding = padding,
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) {
items(
items = characters.value,
key = { it.characterSheetId },
) {
Row(
modifier = Modifier
.animateItem()
.graphicsLayer { if (it.hideOverruled) this.alpha = 0.3f },
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Box {
CharacterRibbonPortrait(
character = it.portrait,
onCharacterLeftClick = { onCharacterLeftClick(it.characterSheetId) },
onCharacterRightClick = { onCharacterRightClick(it.characterSheetId) },
onLevelUp = { onLevelUp(it.characterSheetId) },
)
CharacterRibbonRoll(
value = viewModel.roll(characterSheetId = it.characterSheetId).value,
)
}
CharacterRibbonAlteration(
status = it.status,
direction = LayoutDirection.Ltr,
)
}
}
}
}

View file

@ -10,6 +10,7 @@ koin = "4.0.0"
turtle = "0.10.0" turtle = "0.10.0"
logback = "1.5.17" logback = "1.5.17"
coil = "3.1.0" coil = "3.1.0"
zoomable = "2.7.0"
ui-graphics-android = "1.7.8" ui-graphics-android = "1.7.8"
buildkonfig = "0.17.0" buildkonfig = "0.17.0"
@ -35,6 +36,7 @@ kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-
# UI. # UI.
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
engawapg-zoomable = { module = "net.engawapg.lib:zoomable", version.ref = "zoomable" }
# Injection with Koin # Injection with Koin
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" } koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }