Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ import com.flipcash.app.contact.verification.VerificationFlowScreen
import com.flipcash.app.currencycreator.CurrencyCreatorFlowScreen
import com.flipcash.app.core.AppRoute
import com.flipcash.app.currency.RegionSelectionScreen
import com.flipcash.app.deposit.DepositDestinationScreen
import com.flipcash.app.deposit.DepositFlowScreen
import com.flipcash.app.discovery.TokenDiscoveryScreen
import com.flipcash.app.internal.ui.navigation.decorators.rememberNavMessagingEntryDecorator
import com.flipcash.app.lab.LabsScreen
import com.flipcash.app.lab.NavBarSettingsScreen
import com.flipcash.app.lab.StandaloneLabsScreen
import com.flipcash.app.login.accesskey.AccessKeyScreen
import com.flipcash.app.login.accesskey.PhotoAccessKeyScreen
Expand Down Expand Up @@ -125,6 +125,7 @@ fun appEntryProvider(
// Menu
annotatedEntry<AppRoute.Menu.AppSettings> { AppSettingsScreen() }
annotatedEntry<AppRoute.Menu.Lab> { LabsScreen() }
annotatedEntry<AppRoute.Menu.NavBarSettings> { NavBarSettingsScreen() }
annotatedEntry<AppRoute.Menu.MyAccount> { MyAccountScreen() }
annotatedEntry<AppRoute.Menu.BackupKey> { BackupKeyScreen() }
annotatedEntry<AppRoute.Menu.AdvancedFeatures> { AdvancedFeaturesScreen() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ sealed interface AppRoute : NavKey, Parcelable {
data object DeviceLogs : Menu
@Serializable
data object Lab : Menu
@Serializable
data object NavBarSettings : Menu, com.getcode.navigation.Sheet, com.getcode.navigation.WrapContentSheet
}

@Serializable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.flipcash.app.core.navigation

import androidx.annotation.StringRes
import com.flipcash.core.R

enum class GiveButtonLabel(@StringRes val labelRes: Int) {
Give(R.string.action_give),
Cash(R.string.action_cash),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.flipcash.app.core.navigation

enum class NavBarButton {
Give,
Wallet,
Discover,
;

companion object {
val defaultOrder = listOf(Give, Wallet, Discover)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.flipcash.app.core.navigation

data class NavBarConfig(
val order: List<NavBarButton> = NavBarButton.defaultOrder,
val giveButtonLabel: GiveButtonLabel = GiveButtonLabel.Give,
) {
fun serialize(): String =
"${order.joinToString(",") { it.name }}|${giveButtonLabel.name}"

companion object {
val Default = NavBarConfig()

fun deserialize(value: String): NavBarConfig {
if (value.isBlank()) return Default
val parts = value.split("|")
val order = parts.getOrNull(0)
?.split(",")
?.mapNotNull { runCatching { NavBarButton.valueOf(it) }.getOrNull() }
?.ifEmpty { NavBarButton.defaultOrder }
?: NavBarButton.defaultOrder
val label = parts.getOrNull(1)
?.let { runCatching { GiveButtonLabel.valueOf(it) }.getOrNull() }
?: GiveButtonLabel.Give
return NavBarConfig(order, label)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package com.flipcash.app.core.ui

import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateIntAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.zIndex
import kotlin.math.roundToInt

class LongPressDraggableState internal constructor(
internal val itemCount: Int,
) {
internal val draggingIndex = mutableIntStateOf(-1)
internal val dragOffsetX = mutableFloatStateOf(0f)
internal val itemWidthPx = mutableIntStateOf(0)
internal var onReorder: (from: Int, to: Int) -> Unit = { _, _ -> }
}

/**
* @param key When this key changes the drag state resets. Pass the current order
* so that after a reorder propagates, the state clears atomically
* with the new layout positions.
*/
@Composable
fun rememberLongPressDraggableState(
itemCount: Int,
key: Any? = null,
onReorder: (from: Int, to: Int) -> Unit,
): LongPressDraggableState {
val state = remember(itemCount, key) { LongPressDraggableState(itemCount) }
state.onReorder = onReorder
return state
}

@Composable
fun Modifier.longPressDraggable(
state: LongPressDraggableState,
index: Int,
): Modifier {
val displacement by remember(state, index) {
derivedStateOf {
val currentDragging = state.draggingIndex.intValue
if (currentDragging == -1 || currentDragging == index) {
0
} else {
val w = state.itemWidthPx.intValue
if (w <= 0) 0
else {
val draggedVisualSlot = (currentDragging +
(state.dragOffsetX.floatValue / w).roundToInt())
.coerceIn(0, state.itemCount - 1)
when {
currentDragging < draggedVisualSlot &&
index in (currentDragging + 1)..draggedVisualSlot -> -w
currentDragging > draggedVisualSlot &&
index in draggedVisualSlot until currentDragging -> w
else -> 0
}
}
}
}
}
val animatedDisplacement by animateIntAsState(
targetValue = displacement,
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
)

return this
.zIndex(if (state.draggingIndex.intValue == index) 1f else 0f)
.offset {
val currentDragging = state.draggingIndex.intValue
val dx = when {
// Dragged item follows finger directly
currentDragging == index -> state.dragOffsetX.floatValue.roundToInt()
// Non-dragged items animate during an active drag
currentDragging != -1 -> animatedDisplacement
// No drag active — snap to natural position
else -> 0
}
IntOffset(dx, 0)
}
.onSizeChanged { state.itemWidthPx.intValue = it.width }
.pointerInput(state) {
detectDragGesturesAfterLongPress(
onDragStart = {
state.draggingIndex.intValue = index
state.dragOffsetX.floatValue = 0f
},
onDrag = { change, dragAmount ->
change.consume()
state.dragOffsetX.floatValue += dragAmount.x
},
onDragEnd = {
val w = state.itemWidthPx.intValue
if (w > 0) {
val from = state.draggingIndex.intValue
val to = (from + (state.dragOffsetX.floatValue / w).roundToInt())
.coerceIn(0, state.itemCount - 1)
if (from != to) {
// Snap offset to exact target so item stays visually
// in place until the reorder propagates and the state
// resets via key change.
state.dragOffsetX.floatValue = (to - from).toFloat() * w
state.onReorder(from, to)
return@detectDragGesturesAfterLongPress
}
}
state.draggingIndex.intValue = -1
state.dragOffsetX.floatValue = 0f
},
onDragCancel = {
state.draggingIndex.intValue = -1
state.dragOffsetX.floatValue = 0f
},
)
}
}
Loading
Loading