Skip to content

Commit 4a66d5b

Browse files
committed
feat(bill/playground): add undo support
Signed-off-by: Brandon McAnsh <[email protected]>
1 parent cc10db0 commit 4a66d5b

File tree

5 files changed

+146
-63
lines changed

5 files changed

+146
-63
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="960"
5+
android:viewportHeight="960">
6+
<path
7+
android:fillColor="#FFFFFFFF"
8+
android:pathData="M280,760v-80h284q63,0 109.5,-40T720,540t-46.5,-100T564,400L312,400l104,104 -56,56 -200,-200 200,-200 56,56 -104,104h252q97,0 166.5,63T800,540t-69.5,157T564,760z"/>
9+
</vector>

apps/flipcash/features/bill-customization/src/main/kotlin/com/flipcash/app/bill/customization/BillCustomizationScaffold.kt

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package com.flipcash.app.bill.customization
22

33
import androidx.activity.compose.BackHandler
44
import androidx.compose.animation.AnimatedVisibility
5+
import androidx.compose.animation.core.animateFloatAsState
56
import androidx.compose.animation.fadeIn
67
import androidx.compose.animation.fadeOut
78
import androidx.compose.animation.slideInVertically
89
import androidx.compose.animation.slideOutVertically
910
import androidx.compose.animation.togetherWith
11+
import androidx.compose.foundation.Image
1012
import androidx.compose.foundation.background
1113
import androidx.compose.foundation.layout.Arrangement
1214
import androidx.compose.foundation.layout.Box
@@ -17,6 +19,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
1719
import androidx.compose.foundation.layout.padding
1820
import androidx.compose.foundation.layout.statusBarsPadding
1921
import androidx.compose.foundation.shape.CircleShape
22+
import androidx.compose.material.ContentAlpha
2023
import androidx.compose.material.DismissState
2124
import androidx.compose.material.DismissValue
2225
import androidx.compose.material.ExperimentalMaterialApi
@@ -31,17 +34,21 @@ import androidx.compose.ui.Alignment
3134
import androidx.compose.ui.Modifier
3235
import androidx.compose.ui.draw.clip
3336
import androidx.compose.ui.graphics.Color
37+
import androidx.compose.ui.graphics.ColorFilter
38+
import androidx.compose.ui.res.painterResource
3439
import androidx.compose.ui.unit.dp
3540
import androidx.lifecycle.compose.collectAsStateWithLifecycle
3641
import com.flipcash.app.bill.customization.components.BillPlayground
3742
import com.flipcash.app.bills.AnimatedBill
3843
import com.flipcash.app.core.bill.Bill
44+
import com.flipcash.features.bill.playground.R
3945
import com.getcode.opencode.model.financial.BillBackground
4046
import com.getcode.opencode.model.financial.TokenBillCustomizations
4147
import com.getcode.theme.CodeTheme
4248
import com.getcode.ui.components.AppBarDefaults
4349
import com.getcode.ui.core.measured
4450
import com.getcode.ui.core.rememberedClickable
51+
import com.getcode.ui.core.unboundedClickable
4552
import com.getcode.ui.utils.AnimationUtils
4653
import com.getcode.ui.utils.toAGColor
4754

@@ -135,6 +142,8 @@ fun BillPlaygroundScaffold(content: @Composable () -> Unit) {
135142
) {
136143
TopBar(
137144
modifier = Modifier.fillMaxWidth(),
145+
canUndo = state.canUndo,
146+
onUndo = { controller.dispatchEvent(Event.Undo) },
138147
onBack = { controller.cancel() },
139148
onDone = { controller.cancel() }
140149
)
@@ -171,6 +180,8 @@ fun BillPlaygroundScaffold(content: @Composable () -> Unit) {
171180
private fun TopBar(
172181
modifier: Modifier = Modifier,
173182
onBack: () -> Unit,
183+
canUndo: Boolean,
184+
onUndo: () -> Unit,
174185
onDone: () -> Unit,
175186
) {
176187
Row(
@@ -183,18 +194,37 @@ private fun TopBar(
183194
) {
184195
AppBarDefaults.UpNavigation { onBack() }
185196

186-
Text(
187-
modifier = Modifier
188-
.background(Color.Black.copy(0.19f), CircleShape)
189-
.clip(CircleShape)
190-
.rememberedClickable { onDone() }
191-
.padding(
192-
horizontal = CodeTheme.dimens.grid.x2,
193-
vertical = CodeTheme.dimens.grid.x1
194-
),
195-
text = "Done",
196-
style = CodeTheme.typography.textMedium,
197-
color = CodeTheme.colors.textMain,
198-
)
197+
Row(
198+
horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2),
199+
verticalAlignment = Alignment.CenterVertically
200+
) {
201+
val undoAlpha by animateFloatAsState(
202+
targetValue = if (canUndo) 1f else ContentAlpha.disabled,
203+
)
204+
Image(
205+
modifier = Modifier
206+
.background(Color.Black.copy(0.19f), CircleShape)
207+
.clip(CircleShape)
208+
.rememberedClickable(enabled = canUndo) { onUndo() }
209+
.padding(2.dp),
210+
painter = painterResource(R.drawable.ic_undo),
211+
colorFilter = ColorFilter.tint(Color.White.copy(undoAlpha)),
212+
contentDescription = null
213+
)
214+
215+
Text(
216+
modifier = Modifier
217+
.background(Color.Black.copy(0.19f), CircleShape)
218+
.clip(CircleShape)
219+
.rememberedClickable { onDone() }
220+
.padding(
221+
horizontal = CodeTheme.dimens.grid.x2,
222+
vertical = CodeTheme.dimens.grid.x1
223+
),
224+
text = "Done",
225+
style = CodeTheme.typography.textMedium,
226+
color = CodeTheme.colors.textMain,
227+
)
228+
}
199229
}
200230
}

apps/flipcash/features/bill-customization/src/main/kotlin/com/flipcash/app/bill/customization/components/BillPlayground.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import androidx.compose.ui.unit.IntOffset
5858
import androidx.compose.ui.unit.dp
5959
import androidx.compose.ui.util.fastForEachIndexed
6060
import androidx.lifecycle.compose.collectAsStateWithLifecycle
61+
import com.flipcash.app.bill.customization.ColorChange
6162
import com.flipcash.app.bill.customization.Event
6263
import com.flipcash.app.bill.customization.PlaygroundMode
6364
import com.flipcash.app.bill.customization.internal.InternalBillPlaygroundController
@@ -210,7 +211,7 @@ internal fun BillPlayground(
210211
.height(CodeTheme.dimens.grid.x14 * 2)
211212
.padding(horizontal = CodeTheme.dimens.grid.x5)
212213
.padding(vertical = CodeTheme.dimens.grid.x3),
213-
onChange = { dispatchEvent(Event.ChangeColor(it)) },
214+
onChange = { dispatchEvent(Event.ChangeColor(it, ColorChange.Custom)) },
214215
onClose = {
215216
dispatchEvent(Event.CloseHueControls)
216217
}
@@ -318,7 +319,8 @@ private fun ColorOptionItem(
318319

319320
is BillBackground.Solid -> dispatchEvent(
320321
Event.ChangeColor(
321-
hexToColor(option.colorHex)
322+
hexToColor(option.colorHex),
323+
ColorChange.Preset
322324
)
323325
)
324326
}

apps/flipcash/shared/bill-customization/src/main/kotlin/com/flipcash/app/bill/customization/BillPlaygroundController.kt

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,27 @@ enum class PlaygroundMode {
2222
ColorPanel, Presets
2323
}
2424

25+
enum class ColorChange {
26+
Preset, Custom
27+
}
28+
2529
data class State(
2630
val bill: Bill? = null,
31+
val presetsOpen: Boolean = false,
2732
val mode: PlaygroundMode = PlaygroundMode.Presets,
2833
val selectedSlot: Int = 0,
2934
val maxSlots: Int = MaxGradientColors,
3035
val selectedColors: List<Color> = buildGradient(),
31-
val colorOptions: List<BillBackground> = DefaultColorOptions
36+
val colorOptions: List<BillBackground.Solid> = PresetColorOptions,
37+
val gradientOptions: List<BillBackground.Gradient> = PresetGradients,
38+
val previousState: State? = null,
3239
) {
33-
3440
val isCustomizing: Boolean
3541
get() = bill != null
3642

43+
val canUndo: Boolean
44+
get() = previousState != null
45+
3746
val brush: Brush
3847
get() {
3948
if (selectedColors.size == 1) return Brush.verticalGradient(listOf(selectedColors.first(), selectedColors.first()))
@@ -48,16 +57,17 @@ sealed interface Event {
4857
data object AddSlot: Event
4958
data object RemoveSlot: Event
5059
data class SelectSlot(val slot: Int): Event
51-
data class ChangeColor(val color: Color): Event
60+
data class ChangeColor(val color: Color, val from: ColorChange): Event
5261
data class LoadBackground(val background: BillBackground): Event
5362
data object OpenHueControls: Event
5463
data object CloseHueControls: Event
64+
data object Undo: Event
5565
}
5666

5767
private const val MaxGradientColors = 3
5868

5969
@OptIn(ExperimentalStdlibApi::class)
60-
private val DefaultColorOptions = listOf(
70+
private val PresetColorOptions: List<BillBackground.Solid> = listOf(
6171
BillBackground.Solid("#FFFF453A"), // Red
6272
BillBackground.Solid("#FFFF9F0A"), // Orange
6373
BillBackground.Solid("#FFFFD60A"), // Yellow
@@ -70,16 +80,19 @@ private val DefaultColorOptions = listOf(
7080
BillBackground.Solid("#FFFF4500"), // Coral Red
7181
BillBackground.Solid("#FF00FF7F"), // Spring Green
7282
BillBackground.Solid("#FF8B4513"), // Brown
73-
// BillBackground.Gradient(listOf("#FFE2EAF3", "#FF5487C1")),
74-
// BillBackground.Gradient(listOf("#FFCDB3FF", "#FFECE0E5", "#FFFB9655")),
75-
// BillBackground.Gradient(listOf("#FFFFD5E7", "#FF31D9AA")),
76-
// BillBackground.Gradient(listOf("#FFE4307B", "#FF6123FF", "#FF8A02CE")),
77-
// BillBackground.Gradient(listOf("#FFCCCC31", "#FFC65A24")),
78-
// BillBackground.Gradient(listOf("#FF4F63FC", "#FF31D9AA"))
83+
)
84+
85+
private val PresetGradients: List<BillBackground.Gradient> = listOf(
86+
BillBackground.Gradient(listOf("#FFE2EAF3", "#FF5487C1")),
87+
BillBackground.Gradient(listOf("#FFCDB3FF", "#FFECE0E5", "#FFFB9655")),
88+
BillBackground.Gradient(listOf("#FFFFD5E7", "#FF31D9AA")),
89+
BillBackground.Gradient(listOf("#FFE4307B", "#FF6123FF", "#FF8A02CE")),
90+
BillBackground.Gradient(listOf("#FFCCCC31", "#FFC65A24")),
91+
BillBackground.Gradient(listOf("#FF4F63FC", "#FF31D9AA"))
7992
)
8093

8194
private fun buildGradient(): List<Color> {
82-
val swatches = DefaultColorOptions.filterIsInstance<BillBackground.Solid>()
95+
val swatches = PresetColorOptions
8396

8497
// return a random 3 color gradient
8598
return listOf(

apps/flipcash/shared/bill-customization/src/main/kotlin/com/flipcash/app/bill/customization/internal/InternalBillPlaygroundController.kt

Lines changed: 66 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.flipcash.app.bill.customization.internal
22

33
import androidx.compose.ui.graphics.Color
44
import com.flipcash.app.bill.customization.BillPlaygroundController
5+
import com.flipcash.app.bill.customization.ColorChange
56
import com.flipcash.app.bill.customization.Event
67
import com.flipcash.app.bill.customization.PlaygroundMode
78
import com.flipcash.app.bill.customization.State
@@ -28,7 +29,7 @@ import kotlinx.coroutines.flow.update
2829

2930
class InternalBillPlaygroundController(
3031
private val exchange: Exchange,
31-
): BillPlaygroundController {
32+
) : BillPlaygroundController {
3233

3334
private val _state: MutableStateFlow<State> = MutableStateFlow(State())
3435
override val state: StateFlow<State>
@@ -65,57 +66,45 @@ class InternalBillPlaygroundController(
6566
override fun dispatchEvent(event: Event) {
6667
when (event) {
6768
Event.AddSlot -> addSlot()
68-
is Event.ChangeColor -> changeColorForSlot(event.color)
69+
is Event.ChangeColor -> changeColorForSlot(event.color, event.from)
6970
Event.CloseHueControls -> closeHueControls()
7071
Event.OpenHueControls -> openHueControls()
7172
Event.RemoveSlot -> removeSlot()
7273
is Event.SelectSlot -> selectSlot(event.slot)
73-
is Event.LoadBackground -> {
74-
when (val bg = event.background) {
75-
is BillBackground.Gradient -> {
76-
val colors = bg.colors.map { hexToColor(it) }
77-
_state.update {
78-
it.copy(
79-
selectedColors = colors,
80-
selectedSlot = colors.lastIndex,
81-
)
82-
}
83-
}
84-
is BillBackground.Solid -> {
85-
_state.update {
86-
it.copy(
87-
selectedColors = listOf(hexToColor(bg.colorHex)),
88-
selectedSlot = 0
89-
)
90-
}
91-
}
92-
}
93-
}
74+
is Event.LoadBackground -> loadBackground(event.background)
75+
Event.Undo -> undoLastChange()
9476
}
9577
}
9678

9779
private fun addSlot() {
9880
if (_state.value.selectedColors.count() < _state.value.maxSlots) {
99-
_state.update {
100-
val lastSlotColor = it.selectedColors[it.selectedSlot]
101-
val insertIndex = it.selectedSlot + 1
102-
val colors = it.selectedColors.toMutableList().apply {
81+
_state.update { s ->
82+
val previousState = s.copy()
83+
val lastSlotColor = s.selectedColors[s.selectedSlot]
84+
val insertIndex = s.selectedSlot + 1
85+
val colors = s.selectedColors.toMutableList().apply {
10386
add(insertIndex, lastSlotColor)
10487
}.toList()
10588

106-
it.copy(selectedColors = colors, selectedSlot = insertIndex)
89+
s.copy(
90+
selectedColors = colors,
91+
selectedSlot = insertIndex,
92+
previousState = previousState,
93+
)
10794
}
10895
}
10996
}
11097

11198
private fun removeSlot() {
11299
if (_state.value.selectedColors.count() > 1) {
113-
_state.update {
114-
val indexToRemove = it.selectedSlot
115-
val newColors = it.selectedColors.toMutableList().apply { removeAt(indexToRemove) }
116-
it.copy(
100+
_state.update { s ->
101+
val previousState = s.copy()
102+
val indexToRemove = s.selectedSlot
103+
val newColors = s.selectedColors.toMutableList().apply { removeAt(indexToRemove) }
104+
s.copy(
117105
selectedColors = newColors,
118-
selectedSlot = it.selectedSlot.coerceAtMost(newColors.size - 1)
106+
selectedSlot = s.selectedSlot.coerceAtMost(newColors.size - 1),
107+
previousState = previousState,
119108
)
120109
}
121110
}
@@ -127,15 +116,21 @@ class InternalBillPlaygroundController(
127116
}
128117
}
129118

130-
private fun changeColorForSlot(color: Color) {
119+
private fun changeColorForSlot(color: Color, reason: ColorChange) {
131120
_state.update { s ->
121+
val previousState = s.copy()
132122
val slotIndex = s.selectedSlot
133123
val updatedColors = s.selectedColors.toMutableList().apply {
134124
set(slotIndex, color)
135125
}.toList()
136-
s.copy(
137-
selectedColors = updatedColors
138-
)
126+
if (reason == ColorChange.Preset) {
127+
s.copy(
128+
selectedColors = updatedColors,
129+
previousState = previousState,
130+
)
131+
} else {
132+
s.copy(selectedColors = updatedColors,)
133+
}
139134
}
140135
}
141136

@@ -151,6 +146,40 @@ class InternalBillPlaygroundController(
151146
}
152147
}
153148

149+
private fun loadBackground(background: BillBackground) {
150+
when (val bg = background) {
151+
is BillBackground.Gradient -> {
152+
val colors = bg.colors.map { hexToColor(it) }
153+
_state.update { s ->
154+
val previousState = s.copy()
155+
s.copy(
156+
selectedColors = colors,
157+
selectedSlot = colors.lastIndex,
158+
previousState = previousState
159+
)
160+
}
161+
}
162+
163+
is BillBackground.Solid -> {
164+
_state.update { s ->
165+
val previousState = s.copy()
166+
s.copy(
167+
selectedColors = listOf(hexToColor(bg.colorHex)),
168+
selectedSlot = 0,
169+
previousState = previousState
170+
)
171+
}
172+
}
173+
}
174+
}
175+
176+
private fun undoLastChange() {
177+
_state.update {
178+
val previous = it.previousState ?: return
179+
previous
180+
}
181+
}
182+
154183
override fun cancel() {
155184
_state.update { State() }
156185
}

0 commit comments

Comments
 (0)