From 531e4bea9816e3c14d5754689b7192cf24c368ea Mon Sep 17 00:00:00 2001 From: "Andres Gomez, Thomas (ITDV RL)" Date: Thu, 11 Jul 2024 11:12:07 +0200 Subject: [PATCH] New Inventory component. --- .../com/pixelized/rplexicon/MainActivity.kt | 4 +- .../edit/HandleHitPointEditDialog.kt | 5 +- .../composable/edit/HandleSkillEditDialog.kt | 4 +- .../composable/edit/HandleSpellEditDialog.kt | 4 +- .../composable/actions/AttackItem.kt | 3 - .../character/composable/character/Stat.kt | 5 +- .../dialogs/AlterationDetailDialog.kt | 5 +- .../composable/dialogs/SkillDetailDialog.kt | 4 +- .../composable/dialogs/SpellDetailDialog.kt | 5 +- .../pages/inventory/InventoryItem.kt | 176 +++++++++++ .../character/pages/inventory/Pouet.kt | 254 ++++++++++++++++ .../pages/proficiency/ProficiencyPage.kt | 14 +- .../ui/screens/rolls/composable/ThrowsCard.kt | 4 +- .../summary/composable/AttributesSummary.kt | 4 +- .../composable/CharacteristicsSummary.kt | 4 +- .../summary/composable/PassivesSummary.kt | 4 +- .../summary/composable/ProficiencySummary.kt | 4 +- .../summary/composable/SavingThrowsSummary.kt | 4 +- .../summary/composable/SpellSummary.kt | 4 +- .../summary/composable/StatusSummary.kt | 4 +- .../ui/theme/colors/LexiconColors.kt | 2 +- .../utilitary/LazyGrid+DragAndDrop.kt | 287 ++++++++++++++++++ .../extentions/modifier/ModifierEx.kt | 60 ++-- .../detectDragGesturesAfterLongPress.kt | 55 ++++ .../res/drawable/icbg_pouch_a_unfaded.webp | Bin 0 -> 12600 bytes 25 files changed, 842 insertions(+), 77 deletions(-) create mode 100644 app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryItem.kt create mode 100644 app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/Pouet.kt create mode 100644 app/src/main/java/com/pixelized/rplexicon/utilitary/LazyGrid+DragAndDrop.kt create mode 100644 app/src/main/java/com/pixelized/rplexicon/utilitary/lazyGridDragAndDrop/detectDragGesturesAfterLongPress.kt create mode 100644 app/src/main/res/drawable/icbg_pouch_a_unfaded.webp diff --git a/app/src/main/java/com/pixelized/rplexicon/MainActivity.kt b/app/src/main/java/com/pixelized/rplexicon/MainActivity.kt index 4b4883b..e65af57 100644 --- a/app/src/main/java/com/pixelized/rplexicon/MainActivity.kt +++ b/app/src/main/java/com/pixelized/rplexicon/MainActivity.kt @@ -38,7 +38,7 @@ import com.pixelized.rplexicon.ui.screens.rolls.RollOverlay import com.pixelized.rplexicon.ui.screens.rolls.RollOverlayViewModel import com.pixelized.rplexicon.ui.screens.rolls.rememberBlurredRollOverlayHostState import com.pixelized.rplexicon.ui.theme.LexiconTheme -import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder +import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder import dagger.hilt.android.AndroidEntryPoint val NO_WINDOW_INSETS = WindowInsets(0, 0, 0, 0) @@ -128,7 +128,7 @@ class MainActivity : ComponentActivity() { Snackbar( modifier = Modifier .padding(horizontal = 16.dp) - .ddBorder( + .doubleBorder( inner = remember { RoundedCornerShape(size = 8.dp) }, outline = remember { CutCornerShape(size = 16.dp) }, ), diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/composable/edit/HandleHitPointEditDialog.kt b/app/src/main/java/com/pixelized/rplexicon/ui/composable/edit/HandleHitPointEditDialog.kt index 51d952f..daa431e 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/composable/edit/HandleHitPointEditDialog.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/composable/edit/HandleHitPointEditDialog.kt @@ -4,7 +4,6 @@ import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -35,7 +34,7 @@ import androidx.compose.ui.window.DialogProperties import com.pixelized.rplexicon.R import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.utilitary.extentions.lexicon -import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder +import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder import com.pixelized.rplexicon.utilitary.extentions.toLabel @Stable @@ -66,7 +65,7 @@ fun HandleHitPointEditDialog( onDismissRequest = onDismissRequest, ) { Surface( - modifier = Modifier.ddBorder( + modifier = Modifier.doubleBorder( inner = remember { RoundedCornerShape(size = 8.dp) }, outline = remember { CutCornerShape(size = 16.dp) }, ) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/composable/edit/HandleSkillEditDialog.kt b/app/src/main/java/com/pixelized/rplexicon/ui/composable/edit/HandleSkillEditDialog.kt index 495ad76..6de36c1 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/composable/edit/HandleSkillEditDialog.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/composable/edit/HandleSkillEditDialog.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.pixelized.rplexicon.utilitary.extentions.lexicon -import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder +import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder @Stable data class SkillEditDialogUio( @@ -48,7 +48,7 @@ fun HandleSkillEditDialog( onDismissRequest = onDismissRequest, ) { Surface( - modifier = Modifier.ddBorder( + modifier = Modifier.doubleBorder( inner = remember { RoundedCornerShape(size = 8.dp) }, outline = remember { CutCornerShape(size = 16.dp) }, ) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/composable/edit/HandleSpellEditDialog.kt b/app/src/main/java/com/pixelized/rplexicon/ui/composable/edit/HandleSpellEditDialog.kt index 2b3e755..84e6eba 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/composable/edit/HandleSpellEditDialog.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/composable/edit/HandleSpellEditDialog.kt @@ -26,7 +26,7 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.pixelized.rplexicon.R import com.pixelized.rplexicon.utilitary.extentions.lexicon -import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder +import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder @Stable data class SpellEditDialogUio( @@ -49,7 +49,7 @@ fun HandleSpellEditDialog( onDismissRequest = onDismissRequest, ) { Surface( - modifier = Modifier.ddBorder( + modifier = Modifier.doubleBorder( inner = remember { RoundedCornerShape(size = 8.dp) }, outline = remember { CutCornerShape(size = 16.dp) }, ) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/actions/AttackItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/actions/AttackItem.kt index 98ee643..e644ca6 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/actions/AttackItem.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/actions/AttackItem.kt @@ -2,7 +2,6 @@ package com.pixelized.rplexicon.ui.screens.character.composable.actions import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_YES -import android.net.Uri import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.foundation.layout.Arrangement @@ -27,14 +26,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp -import androidx.core.net.toUri import com.pixelized.rplexicon.R import com.pixelized.rplexicon.data.model.Attack import com.pixelized.rplexicon.ui.composable.images.AsyncImage import com.pixelized.rplexicon.ui.screens.character.composable.common.DiceButton import com.pixelized.rplexicon.ui.screens.character.composable.common.FlatValue import com.pixelized.rplexicon.ui.theme.LexiconTheme -import com.pixelized.rplexicon.utilitary.extentions.uri @Stable data class AttackUio( diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/character/Stat.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/character/Stat.kt index e040acf..212bfe7 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/character/Stat.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/character/Stat.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CutCornerShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Divider import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -26,7 +25,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.pixelized.rplexicon.R import com.pixelized.rplexicon.ui.theme.LexiconTheme -import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder +import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder import com.pixelized.rplexicon.utilitary.extentions.toLabel @Stable @@ -88,7 +87,7 @@ private fun StatPreview() { Stat( modifier = Modifier .padding(all = 8.dp) - .ddBorder( + .doubleBorder( inner = remember { RoundedCornerShape(size = 8.dp) }, outline = remember { CutCornerShape(size = 16.dp) }, ), diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/dialogs/AlterationDetailDialog.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/dialogs/AlterationDetailDialog.kt index df4a21f..8b13892 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/dialogs/AlterationDetailDialog.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/dialogs/AlterationDetailDialog.kt @@ -2,7 +2,6 @@ package com.pixelized.rplexicon.ui.screens.character.composable.dialogs import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_YES -import android.net.Uri import androidx.compose.foundation.ScrollState import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -37,7 +36,7 @@ import com.pixelized.rplexicon.ui.composable.images.BackgroundImage import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.utilitary.annotateWithDropCap import com.pixelized.rplexicon.utilitary.extentions.lexicon -import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder +import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder @Stable data class AlterationDialogDetailUio( @@ -75,7 +74,7 @@ fun AlterationDetailDialog( enabled = false, onClick = { }, ) - .ddBorder( + .doubleBorder( inner = remember { RoundedCornerShape(size = 8.dp) }, outline = remember { CutCornerShape(size = 16.dp) }, ), diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/dialogs/SkillDetailDialog.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/dialogs/SkillDetailDialog.kt index 0314326..ae62f70 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/dialogs/SkillDetailDialog.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/dialogs/SkillDetailDialog.kt @@ -34,7 +34,7 @@ import com.pixelized.rplexicon.ui.composable.images.BackgroundImage import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.utilitary.annotateWithDropCap import com.pixelized.rplexicon.utilitary.extentions.lexicon -import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder +import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder @Stable data class SkillDialogDetailUio( @@ -70,7 +70,7 @@ fun SkillDetailDialog( enabled = false, onClick = { }, ) - .ddBorder( + .doubleBorder( inner = remember { RoundedCornerShape(size = 8.dp) }, outline = remember { CutCornerShape(size = 16.dp) }, ), diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/dialogs/SpellDetailDialog.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/dialogs/SpellDetailDialog.kt index 56aff8d..eaabbd8 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/dialogs/SpellDetailDialog.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/dialogs/SpellDetailDialog.kt @@ -1,7 +1,6 @@ package com.pixelized.rplexicon.ui.screens.character.composable.dialogs import android.content.res.Configuration -import android.net.Uri import androidx.annotation.StringRes import androidx.compose.foundation.ScrollState import androidx.compose.foundation.clickable @@ -40,7 +39,7 @@ import com.pixelized.rplexicon.ui.composable.images.BackgroundImage import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.utilitary.annotateWithDropCap import com.pixelized.rplexicon.utilitary.extentions.lexicon -import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder +import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder @Stable data class SpellDialogDetailUio( @@ -83,7 +82,7 @@ fun SpellDetailDialog( enabled = false, onClick = { }, ) - .ddBorder( + .doubleBorder( inner = remember { RoundedCornerShape(size = 8.dp) }, outline = remember { CutCornerShape(size = 16.dp) }, ), diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryItem.kt new file mode 100644 index 0000000..3829e92 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryItem.kt @@ -0,0 +1,176 @@ +package com.pixelized.rplexicon.ui.screens.character.pages.inventory + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.SizeTransform +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.pixelized.rplexicon.R +import com.pixelized.rplexicon.ui.composable.images.AsyncImage +import com.pixelized.rplexicon.ui.theme.LexiconTheme + +@Stable +data class InventoryItemUio( + val id: Int, + val name: String, + val amount: Int, + val container: Boolean, + val icon: Any?, + val items: List = emptyList(), +) + +@Composable +fun InventoryItem( + modifier: Modifier = Modifier, + item: InventoryItemUio, +) { + Box( + modifier = Modifier + .then(other = modifier) + .aspectRatio(ratio = 1f), + ) { + AsyncImage( + modifier = Modifier.matchParentSize(), + model = item.icon, + contentScale = ContentScale.Fit, + ) + AnimatedContent( + modifier = Modifier + .align(alignment = Alignment.BottomEnd) + .offset(x = 0.dp, y = 4.dp) + .padding(horizontal = 2.dp), + targetState = item.amount, + transitionSpec = { + // Compare the incoming number with the previous number. + if (targetState > initialState) { + // If the target number is larger, it slides up and fades in + // while the initial (smaller) number slides up and fades out. + slideInVertically { height -> height / 2 } + fadeIn() togetherWith + slideOutVertically { height -> -height / 2 } + fadeOut() + } else { + // If the target number is smaller, it slides down and fades in + // while the initial number slides down and fades out. + slideInVertically { height -> -height / 2 } + fadeIn() togetherWith + slideOutVertically { height -> height / 2 } + fadeOut() + }.using( + // Disable clipping since the faded slide-in/out should + // be displayed out of bounds. + SizeTransform(clip = false) + ) + }, + label = "Container count size", + ) { amount -> + Text( + modifier = Modifier.graphicsLayer { this.alpha = if (amount == 1) 0f else 1f }, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.End, + text = amount.takeIf { it > 0 }?.let { "$it" } ?: " ", //   + ) + } + AnimatedContent( + modifier = Modifier + .align(alignment = Alignment.TopEnd) + .offset(x = 0.dp, y = (-4).dp) + .padding(horizontal = 2.dp), + targetState = item.items.size, + transitionSpec = { + // Compare the incoming number with the previous number. + if (targetState > initialState) { + // If the target number is larger, it slides up and fades in + // while the initial (smaller) number slides up and fades out. + slideInVertically { height -> height / 2 } + fadeIn() togetherWith + slideOutVertically { height -> -height / 2 } + fadeOut() + } else { + // If the target number is smaller, it slides down and fades in + // while the initial number slides down and fades out. + slideInVertically { height -> -height / 2 } + fadeIn() togetherWith + slideOutVertically { height -> height / 2 } + fadeOut() + }.using( + // Disable clipping since the faded slide-in/out should + // be displayed out of bounds. + SizeTransform(clip = false) + ) + }, + label = "Container count size", + ) { amount -> + Text( + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.End, + text = amount.takeIf { it > 0 }?.let { "$it" } ?: " ", //   + ) + } + } +} + + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, widthDp = 64) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 64) +private fun InventoryItemPreview( + @PreviewParameter(ClassInventoryItemProvider::class) preview: InventoryItemUio, +) { + LexiconTheme { + Surface { + InventoryItem( + item = preview, + ) + } + } +} + +private class ClassInventoryItemProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + InventoryItemUio( + id = 0, + name = "Pouch", + amount = 1, + container = true, + icon = R.drawable.icbg_pouch_a_unfaded, + + ), + InventoryItemUio( + id = 1, + name = "Scroll of blessing", + amount = 1, + container = false, + icon = R.drawable.icbg_scroll_of_bless_unfaded, + + ), + InventoryItemUio( + id = 2, + name = "Potion of blessing", + amount = 2, + container = false, + icon = R.drawable.icbg_pot_potion_of_healing_unfaded, + ), + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/Pouet.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/Pouet.kt new file mode 100644 index 0000000..bcf8e51 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/Pouet.kt @@ -0,0 +1,254 @@ +package com.pixelized.rplexicon.ui.screens.character.pages.inventory + +import android.content.res.Configuration +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.itemsIndexed +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.pixelized.rplexicon.R +import com.pixelized.rplexicon.ui.theme.LexiconTheme +import com.pixelized.rplexicon.utilitary.DraggableItem +import com.pixelized.rplexicon.utilitary.dragContainer +import com.pixelized.rplexicon.utilitary.extentions.lexicon +import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder +import com.pixelized.rplexicon.utilitary.rememberGridDragDropState + + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +fun Pouet() { + val items = remember { + mutableStateOf( + listOf( + InventoryItemUio( + id = 0, + name = "Pouch", + amount = 1, + container = true, + icon = R.drawable.icbg_pouch_a_unfaded, + ), + InventoryItemUio( + id = 1, + name = "Scroll of blessing", + amount = 1, + container = false, + icon = R.drawable.icbg_scroll_of_bless_unfaded, + ), + InventoryItemUio( + id = 2, + name = "Scroll of spirit weapon", + amount = 1, + container = false, + icon = R.drawable.icbg_book_signedtradebisa_unfaded, + ), + InventoryItemUio( + id = 3, + name = "Potion of healing", + amount = 2, + container = false, + icon = R.drawable.icbg_potion_of_superior_healing_unfaded, + ), + InventoryItemUio( + id = 4, + name = "Potion of supĂ©rior healing", + amount = 2, + container = false, + icon = R.drawable.icbg_pot_potion_of_healing_unfaded, + ), + InventoryItemUio( + id = 5, + name = "Potion of holy water", + amount = 2, + container = false, + icon = R.drawable.icbg_grn_holy_water_unfaded, + ), + InventoryItemUio( + id = 6, + name = "Leather armor", + amount = 1, + container = false, + icon = R.drawable.icbg_leather_armour_rogue_unfaded, + ), + InventoryItemUio( + id = 7, + name = "Silver battleaxe", + amount = 0, + container = false, + icon = R.drawable.icbg_battleaxe_plus_one_unfaded, + ), + InventoryItemUio( + id = 8, + name = "Hand crossbow", + amount = 0, + container = false, + icon = R.drawable.icbg_hand_crossbow_unfaded, + ), + InventoryItemUio( + id = 9, + name = "Goodberry", + amount = 0, + container = false, + icon = R.drawable.icbg_food_goodberry_unfaded, + ), + InventoryItemUio( + id = 10, + name = "Goodberry", + amount = 0, + container = false, + icon = R.drawable.icbg_worg_fang_unfaded, + ), + InventoryItemUio( + id = 11, + name = "Lantern of revealing", + amount = 0, + container = false, + icon = R.drawable.icbg_lantern_of_revealing, + ), + InventoryItemUio( + id = 12, + name = "Dust of disappearance", + amount = 0, + container = false, + icon = R.drawable.icbg_haste_spore_grenade_unfaded, + ), + InventoryItemUio( + id = 13, + name = "Pouch", + amount = 1, + container = true, + icon = R.drawable.icbg_pouch_a_unfaded, + ), + ) + ) + } + + val overIndex = remember { mutableIntStateOf(-1) } + val contentPadding = remember { PaddingValues(16.dp) } + val gridState = rememberLazyGridState() + val dragDropState = rememberGridDragDropState( + contentPadding = contentPadding, + gridState = gridState, + onMove = { fromIndex, toIndex -> + items.value = items.value.toMutableList().apply { + add(toIndex, removeAt(fromIndex)) + } + }, + onOver = { _, toIndex -> + val receiver = items.value[toIndex] + if (receiver.container) { + overIndex.intValue = toIndex + false + } else { + true + } + }, + onDrop = { fromIndex, toIndex -> + if (fromIndex != toIndex) { + val receiver = items.value[toIndex] + if (receiver.container) { + items.value = items.value.toMutableList().apply { + val item = removeAt(fromIndex) + val receiverCopy = receiver.copy( + items = receiver.items.toMutableList().also { + it.add(item) + } + ) + val receiverIndex = indexOf(receiver) + removeAt(receiverIndex) + add(receiverIndex, receiverCopy) + } + } + } + } + ) + + LexiconTheme { + Surface { + LazyVerticalGrid( + columns = GridCells.Fixed(5), + modifier = Modifier + .dragContainer(dragDropState) + .fillMaxSize(), + state = gridState, + contentPadding = contentPadding, + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + itemsIndexed( + items = items.value, + key = { _, item -> item.id }, + ) { index, item -> + DraggableItem( + dragDropState = dragDropState, + index = index + ) { isDragging, isOvering -> + val colorScheme = MaterialTheme.lexicon.colorScheme + val transition = updateTransition( + targetState = isDragging || isOvering , + label = "Dragging transition", + ) + val backgroundColor = transition.animateColor( + label = "Draggable item background color", + ) { + val surface = colorScheme.base.surfaceColorAtElevation(2.dp) + when (it) { + true -> colorScheme.base.primary.copy(alpha = 0.15f) + .compositeOver(surface) + + else -> surface + } + } + val outlineColor = transition.animateColor( + label = "Draggable item outline color", + ) { + when (it) { + true -> colorScheme.base.primary + else -> colorScheme.characterSheet.outlineBorder + } + } + val innerColor = transition.animateColor( + label = "Draggable item inline color", + ) { + when (it) { + true -> colorScheme.base.primary + else -> colorScheme.characterSheet.innerBorder + } + } + Box( + modifier = Modifier.doubleBorder( + backgroundColor = backgroundColor.value, + outline = remember { RoundedCornerShape(8.dp) }, + outlineColor = outlineColor.value, + inner = remember { RoundedCornerShape(6.dp) }, + innerColor = innerColor.value, + ) + ) { + InventoryItem( + item = item, + ) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/proficiency/ProficiencyPage.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/proficiency/ProficiencyPage.kt index a58aedc..7e943d3 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/proficiency/ProficiencyPage.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/proficiency/ProficiencyPage.kt @@ -55,7 +55,7 @@ import com.pixelized.rplexicon.ui.screens.character.composable.character.StatUio import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberCharacterSheetStatePreview import com.pixelized.rplexicon.ui.screens.character.composable.dialogs.SkillDetailDialog import com.pixelized.rplexicon.ui.theme.LexiconTheme -import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder +import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder import kotlinx.coroutines.launch @Stable @@ -146,7 +146,7 @@ fun ProficiencyPageContent( Column( modifier = Modifier .sizeIn(minWidth = 100.dp, minHeight = 116.dp) - .ddBorder(outline = outline, inner = inner) + .doubleBorder(outline = outline, inner = inner) .padding(all = 8.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(4.dp), @@ -163,7 +163,7 @@ fun ProficiencyPageContent( } sheet.stats.forEach { Stat( - modifier = Modifier.ddBorder(inner = inner, outline = outline), + modifier = Modifier.doubleBorder(inner = inner, outline = outline), stat = it, onClick = onStats, ) @@ -266,28 +266,28 @@ private fun ProficiencyLayout( Column( modifier = Modifier .layoutId("SavingThrowsId") - .ddBorder(inner = inner, outline = outline), + .doubleBorder(inner = inner, outline = outline), horizontalAlignment = Alignment.CenterHorizontally, content = savingThrows, ) Column( modifier = Modifier .layoutId("ProficienciesId") - .ddBorder(inner = inner, outline = outline), + .doubleBorder(inner = inner, outline = outline), horizontalAlignment = Alignment.CenterHorizontally, content = proficiencies, ) Column( modifier = Modifier .layoutId("SpeedId") - .ddBorder(inner = inner, outline = outline), + .doubleBorder(inner = inner, outline = outline), content = speed, horizontalAlignment = Alignment.CenterHorizontally, ) Column( modifier = Modifier .layoutId("PassivesId") - .ddBorder(inner = inner, outline = outline), + .doubleBorder(inner = inner, outline = outline), content = passives, horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceBetween, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/ThrowsCard.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/ThrowsCard.kt index 6a48f5b..82f335a 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/ThrowsCard.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/ThrowsCard.kt @@ -55,7 +55,7 @@ import com.pixelized.rplexicon.isInDarkTheme import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.utilitary.extentions.annotatedSpan import com.pixelized.rplexicon.utilitary.extentions.lexicon -import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder +import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder import com.pixelized.rplexicon.utilitary.highlightRegex import java.util.UUID @@ -118,7 +118,7 @@ fun ThrowsCard( Surface( modifier = modifier .fillMaxWidth() - .ddBorder( + .doubleBorder( inner = inner, outline = remember { CutCornerShape(size = 16.dp) }, ) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/AttributesSummary.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/AttributesSummary.kt index 4664327..27b98ff 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/AttributesSummary.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/AttributesSummary.kt @@ -25,7 +25,7 @@ import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRow import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRowUio import com.pixelized.rplexicon.ui.screens.summary.composable.preview.statistic.rememberAttributesSummary import com.pixelized.rplexicon.ui.theme.LexiconTheme -import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder +import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder @Stable data class AttributesSummaryUio( @@ -45,7 +45,7 @@ fun AttributesSummary( ) { Column( modifier = Modifier - .ddBorder( + .doubleBorder( inner = remember { RoundedCornerShape(size = 8.dp) }, outline = remember { CutCornerShape(size = 16.dp) }, ) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/CharacteristicsSummary.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/CharacteristicsSummary.kt index da58e0f..daa83af 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/CharacteristicsSummary.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/CharacteristicsSummary.kt @@ -26,7 +26,7 @@ import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRow import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRowUio import com.pixelized.rplexicon.ui.screens.summary.composable.preview.statistic.rememberCharacteristicsSummary import com.pixelized.rplexicon.ui.theme.LexiconTheme -import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder +import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder @Stable data class CharacteristicsSummaryUio( @@ -48,7 +48,7 @@ fun CharacteristicsSummary( ) { Column( modifier = Modifier - .ddBorder( + .doubleBorder( inner = remember { RoundedCornerShape(size = 8.dp) }, outline = remember { CutCornerShape(size = 16.dp) }, ) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/PassivesSummary.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/PassivesSummary.kt index 1c0253f..e772f80 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/PassivesSummary.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/PassivesSummary.kt @@ -25,7 +25,7 @@ import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRow import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRowUio import com.pixelized.rplexicon.ui.screens.summary.composable.preview.statistic.rememberPassivesSummary import com.pixelized.rplexicon.ui.theme.LexiconTheme -import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder +import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder @Stable data class PassivesSummaryUio( @@ -43,7 +43,7 @@ fun PassivesSummary( ) { Column( modifier = Modifier - .ddBorder( + .doubleBorder( inner = remember { RoundedCornerShape(size = 8.dp) }, outline = remember { CutCornerShape(size = 16.dp) }, ) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/ProficiencySummary.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/ProficiencySummary.kt index 98d1b3f..009c131 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/ProficiencySummary.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/ProficiencySummary.kt @@ -25,7 +25,7 @@ import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRow import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRowUio import com.pixelized.rplexicon.ui.screens.summary.composable.preview.statistic.rememberProficienciesSummary import com.pixelized.rplexicon.ui.theme.LexiconTheme -import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder +import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder @Stable data class ProficiencySummaryUio( @@ -58,7 +58,7 @@ fun ProficiencySummary( ) { Column( modifier = Modifier - .ddBorder( + .doubleBorder( inner = remember { RoundedCornerShape(size = 8.dp) }, outline = remember { CutCornerShape(size = 16.dp) }, ) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/SavingThrowsSummary.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/SavingThrowsSummary.kt index 192bf6e..134937f 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/SavingThrowsSummary.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/SavingThrowsSummary.kt @@ -25,7 +25,7 @@ import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRow import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRowUio import com.pixelized.rplexicon.ui.screens.summary.composable.preview.statistic.rememberSavingThrowsSummary import com.pixelized.rplexicon.ui.theme.LexiconTheme -import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder +import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder @Stable data class SavingThrowsSummaryUio( @@ -46,7 +46,7 @@ fun SavingThrowsSummary( ) { Column( modifier = Modifier - .ddBorder( + .doubleBorder( inner = remember { RoundedCornerShape(size = 8.dp) }, outline = remember { CutCornerShape(size = 16.dp) }, ) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/SpellSummary.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/SpellSummary.kt index 3a92087..27d284c 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/SpellSummary.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/SpellSummary.kt @@ -26,7 +26,7 @@ import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRow import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRowUio import com.pixelized.rplexicon.ui.screens.summary.composable.preview.statistic.rememberSpellsSummary import com.pixelized.rplexicon.ui.theme.LexiconTheme -import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder +import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder @Stable data class SpellSummaryUio( @@ -53,7 +53,7 @@ fun SpellSummary( ) { Column( modifier = Modifier - .ddBorder( + .doubleBorder( inner = remember { RoundedCornerShape(size = 8.dp) }, outline = remember { CutCornerShape(size = 16.dp) }, ) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/StatusSummary.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/StatusSummary.kt index 62280e3..d852645 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/StatusSummary.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/summary/composable/StatusSummary.kt @@ -39,7 +39,7 @@ import com.pixelized.rplexicon.R import com.pixelized.rplexicon.ui.screens.summary.composable.preview.statistic.rememberStatusSummary import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.utilitary.extentions.lexicon -import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder +import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder import com.pixelized.rplexicon.utilitary.extentions.modifier.verticalDivider @Stable @@ -80,7 +80,7 @@ fun StatusSummary( ) { Column( modifier = Modifier - .ddBorder( + .doubleBorder( inner = remember { RoundedCornerShape(size = 8.dp) }, outline = remember { CutCornerShape(size = 16.dp) }, ) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt b/app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt index 45db50d..9468ed0 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt @@ -83,7 +83,7 @@ fun lightColorScheme( secondary = BaseColorPalette.Teal80, tertiary = BaseColorPalette.Teal40, onPrimary = Color.White, - surfaceTint = BaseColorPalette.Purple40, + surfaceTint = Color.Gray, ), placeholder: Color = Color(red = 230, green = 225, blue = 229), sheet: LexiconColors.CharacterSheet = LexiconColors.CharacterSheet( diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/LazyGrid+DragAndDrop.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/LazyGrid+DragAndDrop.kt new file mode 100644 index 0000000..8eccf52 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/LazyGrid+DragAndDrop.kt @@ -0,0 +1,287 @@ +package com.pixelized.rplexicon.utilitary + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.VectorConverter +import androidx.compose.animation.core.VisibilityThreshold +import androidx.compose.animation.core.spring +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.grid.LazyGridItemInfo +import androidx.compose.foundation.lazy.grid.LazyGridItemScope +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.toOffset +import androidx.compose.ui.unit.toSize +import androidx.compose.ui.zIndex +import com.pixelized.rplexicon.utilitary.lazyGridDragAndDrop.detectDragGesturesAfterLongPress +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +/*** + * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyGridDragAndDropDemo.kt + */ +@Composable +fun rememberGridDragDropState( + contentPadding: PaddingValues, + gridState: LazyGridState, + onMove: (Int, Int) -> Unit, + onOver: (Int, Int) -> Boolean, + onDrop: (Int, Int) -> Unit, +): GridDragDropState { + val density = LocalDensity.current + val layoutDirection = LocalLayoutDirection.current + val scope = rememberCoroutineScope() + val currentOnMove = rememberUpdatedState(newValue = onMove) + val currentOnOver = rememberUpdatedState(newValue = onOver) + val currentOnDrop = rememberUpdatedState(newValue = onDrop) + + val state = remember(gridState, scope, currentOnMove) { + GridDragDropState( + contentOffset = with(density) { + Offset( + contentPadding.calculateTopPadding().toPx(), + contentPadding.calculateLeftPadding(layoutDirection).toPx() + ) + }, + scope = scope, + state = gridState, + onMove = currentOnMove.value, + onOver = currentOnOver.value, + onDrop = currentOnDrop.value, + ) + } + + LaunchedEffect(state) { + while (isActive) { + val diff = state.scrollChannel.receive() + gridState.scrollBy(diff) + } + } + + return state +} + +class GridDragDropState internal constructor( + private val contentOffset: Offset, + private val scope: CoroutineScope, + private val state: LazyGridState, + private val onMove: (Int, Int) -> Unit, + private val onOver: (Int, Int) -> Boolean, + private val onDrop: (Int, Int) -> Unit, +) { + internal val scrollChannel = Channel() + + var draggingItemIndex by mutableStateOf(null) + private set + + var overItemIndex by mutableStateOf(null) + private set + + private var draggingItemInitialOffset by mutableStateOf(Offset.Zero) + + private var draggingItemDraggedDelta by mutableStateOf(Offset.Zero) + + internal val draggingItemOffset: Offset + get() = draggingItemLayoutInfo + ?.let { item -> draggingItemInitialOffset + draggingItemDraggedDelta - item.offset.toOffset() } + ?: Offset.Zero + + private val draggingItemLayoutInfo: LazyGridItemInfo? + get() = state.layoutInfo.visibleItemsInfo + .firstOrNull { it.index == draggingItemIndex } + + internal var previousIndexOfDraggedItem by mutableStateOf(null) + private set + + internal var previousItemOffset = Animatable(Offset.Zero, Offset.VectorConverter) + private set + + internal fun onDragStart(offset: Offset) { + state.layoutInfo.visibleItemsInfo + .firstItemWithOffsetOrNull(offset = offset) + ?.let { + draggingItemIndex = it.index + draggingItemInitialOffset = it.offset.toOffset() + } + } + + internal fun onDragInterrupted(offset: Offset) { + val localDraggingItemIndex = draggingItemIndex + if (localDraggingItemIndex != null) { + state.layoutInfo.visibleItemsInfo + .firstItemWithOffsetOrNull(offset) + ?.let { onDrop(localDraggingItemIndex, it.index) } + + previousIndexOfDraggedItem = draggingItemIndex + val startOffset = draggingItemOffset + scope.launch { + previousItemOffset.snapTo(startOffset) + previousItemOffset.animateTo( + Offset.Zero, + spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = Offset.VisibilityThreshold + ) + ) + previousIndexOfDraggedItem = null + } + } + overItemIndex = null + draggingItemDraggedDelta = Offset.Zero + draggingItemIndex = null + draggingItemInitialOffset = Offset.Zero + } + + internal fun onDrag(offset: Offset) { + draggingItemDraggedDelta += offset + + val draggingItem = draggingItemLayoutInfo ?: return + val startOffset = draggingItem.offset.toOffset() + draggingItemOffset + val endOffset = startOffset + draggingItem.size.toSize() + + val middleOffset = startOffset + (endOffset - startOffset) / 2f + val middleXOffset = middleOffset.x.toInt() + val middleYOffset = middleOffset.y.toInt() + + val targetItem = state.layoutInfo.visibleItemsInfo.find { item -> + draggingItem.index != item.index && + item.offset.x <= middleXOffset && middleXOffset <= item.offsetEnd.x && + item.offset.y <= middleYOffset && middleYOffset <= item.offsetEnd.y + } + + overItemIndex = targetItem?.index + + if (targetItem != null) { + if (onOver.invoke(draggingItem.index, targetItem.index)) { + onMove.invoke(draggingItem.index, targetItem.index) + draggingItemIndex = targetItem.index + } + } else { + val overscroll = when { + draggingItemDraggedDelta.y > 0 -> + (endOffset.y - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f) + + draggingItemDraggedDelta.y < 0 -> + (startOffset.y - state.layoutInfo.viewportStartOffset).coerceAtMost(0f) + + else -> 0f + } + if (overscroll != 0f) { + scrollChannel.trySend(overscroll) + } + } + } + + private fun List.firstItemWithOffsetOrNull( + offset: Offset, + ): LazyGridItemInfo? { + val offsetX = offset.x.toInt() - contentOffset.x.toInt() + val offsetY = offset.y.toInt() - contentOffset.y.toInt() + return this.firstOrNull { item -> + item.offset.x <= offsetX && offsetX <= item.offsetEnd.x && + item.offset.y <= offsetY && offsetY <= item.offsetEnd.y + } + } + + private val LazyGridItemInfo.offsetEnd: IntOffset + get() = this.offset + this.size +} + +private operator fun IntOffset.plus(size: IntSize): IntOffset { + return IntOffset(x + size.width, y + size.height) +} + +private operator fun Offset.plus(size: Size): Offset { + return Offset(x + size.width, y + size.height) +} + +fun Modifier.dragContainer(dragDropState: GridDragDropState): Modifier { + return this then Modifier.pointerInput(dragDropState) { + detectDragGesturesAfterLongPress( + onDrag = { change, offset -> + change.consume() + dragDropState.onDrag(offset = offset) + }, + onDragStart = { offset -> + dragDropState.onDragStart(offset) + }, + onDragEnd = { offset -> + dragDropState.onDragInterrupted(offset) + }, + onDragCancel = { + dragDropState.onDragInterrupted(Offset.Zero) + } + ) + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun LazyGridItemScope.DraggableItem( + dragDropState: GridDragDropState, + index: Int, + modifier: Modifier = Modifier, + content: @Composable (isDragging: Boolean, isOver: Boolean) -> Unit +) { + val dragging by remember(index, dragDropState) { + derivedStateOf { + index == dragDropState.draggingItemIndex + } + } + val overing by remember(index, dragDropState) { + derivedStateOf { + index == dragDropState.overItemIndex + } + } + val draggingModifier = when { + dragging -> { + Modifier + .zIndex(1f) + .graphicsLayer { + translationX = dragDropState.draggingItemOffset.x + translationY = dragDropState.draggingItemOffset.y + } + } + + index == dragDropState.previousIndexOfDraggedItem -> { + Modifier + .zIndex(1f) + .graphicsLayer { + translationX = dragDropState.previousItemOffset.value.x + translationY = dragDropState.previousItemOffset.value.y + } + } + + else -> { + Modifier.animateItemPlacement() + } + } + Box( + modifier = modifier.then(draggingModifier), + propagateMinConstraints = true, + ) { + content(dragging, overing) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/modifier/ModifierEx.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/modifier/ModifierEx.kt index 96b7f8b..b504c1a 100644 --- a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/modifier/ModifierEx.kt +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/modifier/ModifierEx.kt @@ -19,7 +19,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf @@ -106,7 +105,7 @@ fun Modifier.clickableInterceptor(): Modifier = composed { ) } -fun Modifier.ddBorder( +fun Modifier.doubleBorder( horizontalSpacing: Dp = 3.dp, verticalSpacing: Dp = 3.dp, outline: Shape, @@ -114,37 +113,38 @@ fun Modifier.ddBorder( inner: Shape, innerWidth: Dp = 1.dp, ): Modifier = composed { - val isDarkTheme = isInDarkTheme() - val elevation = remember { derivedStateOf { if (isDarkTheme) 2.dp else 0.dp } } val colorScheme = MaterialTheme.lexicon.colorScheme - this then Modifier - .border( - width = outlineWidth, - color = colorScheme.characterSheet.outlineBorder, - shape = outline, - ) - .background( - shape = outline, - color = colorScheme.base.surfaceColorAtElevation(elevation.value) - ) - .padding( - horizontal = horizontalSpacing, - vertical = verticalSpacing, - ) - .border( - width = innerWidth, - color = colorScheme.characterSheet.innerBorder, - shape = inner, - ) - .background( - shape = inner, - color = colorScheme.base.surfaceColorAtElevation(elevation.value) - ) - .clip( - shape = inner, - ) + this then Modifier.doubleBorder( + horizontalSpacing = horizontalSpacing, + verticalSpacing = verticalSpacing, + backgroundColor = colorScheme.base.surfaceColorAtElevation(1.dp), + outline = outline, + outlineWidth = outlineWidth, + outlineColor = colorScheme.characterSheet.outlineBorder, + inner = inner, + innerWidth = innerWidth, + innerColor = colorScheme.characterSheet.innerBorder, + ) } +fun Modifier.doubleBorder( + horizontalSpacing: Dp = 3.dp, + verticalSpacing: Dp = 3.dp, + backgroundColor: Color, + outline: Shape, + outlineWidth: Dp = 1.dp, + outlineColor: Color, + inner: Shape, + innerWidth: Dp = 1.dp, + innerColor: Color, +): Modifier = this then Modifier + .border(width = outlineWidth, color = outlineColor, shape = outline) + .background(shape = outline, color = backgroundColor) + .padding(horizontal = horizontalSpacing, vertical = verticalSpacing) + .border(width = innerWidth, color = innerColor, shape = inner) + .background(shape = inner, color = backgroundColor) + .clip(shape = inner) + fun Modifier.lexiconShadow(): Modifier { return this then composed { val isDarkTheme = isInDarkTheme() diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/lazyGridDragAndDrop/detectDragGesturesAfterLongPress.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/lazyGridDragAndDrop/detectDragGesturesAfterLongPress.kt new file mode 100644 index 0000000..8fc447c --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/lazyGridDragAndDrop/detectDragGesturesAfterLongPress.kt @@ -0,0 +1,55 @@ +package com.pixelized.rplexicon.utilitary.lazyGridDragAndDrop + +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.awaitLongPressOrCancellation +import androidx.compose.foundation.gestures.drag +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.input.pointer.changedToUp +import androidx.compose.ui.input.pointer.positionChange +import androidx.compose.ui.util.fastForEach +import kotlinx.coroutines.CancellationException + +suspend fun PointerInputScope.detectDragGesturesAfterLongPress( + onDragStart: (Offset) -> Unit = { }, + onDragEnd: (Offset) -> Unit = { }, + onDragCancel: () -> Unit = { }, + onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit +) { + awaitEachGesture { + try { + // await for a press + val down = awaitFirstDown(requireUnconsumed = false) + // await for a long press or a cancellation + val drag = awaitLongPressOrCancellation(pointerId = down.id) + // if the user did a long press + if (drag != null) { + // callback onDragStart with the initial position. + onDragStart.invoke(drag.position) + var lastDragInput: PointerInputChange = drag + val dragEnded = drag( + pointerId = drag.id, + onDrag = { input -> + lastDragInput = input + onDrag(input, input.positionChange()) + input.consume() + } + ) + if (dragEnded) { + // consume up if we quit drag gracefully with the up + currentEvent.changes.fastForEach { + if (it.changedToUp()) it.consume() + } + onDragEnd(lastDragInput.position) + } else { + onDragCancel() + } + } + } catch (c: CancellationException) { + onDragCancel() + throw c + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/icbg_pouch_a_unfaded.webp b/app/src/main/res/drawable/icbg_pouch_a_unfaded.webp new file mode 100644 index 0000000000000000000000000000000000000000..ccc8ea620fa4e3f97667ade1ccb3e2d37830a547 GIT binary patch literal 12600 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 Us`~%`%r4|`GAwm6JHfyJ00S#b00000 literal 0 HcmV?d00001