Skip to content

Commit 20f7fa0

Browse files
committed
feat: add stack based undo system for bill playground
Signed-off-by: Brandon McAnsh <[email protected]>
1 parent f561d0f commit 20f7fa0

File tree

15 files changed

+405
-306
lines changed

15 files changed

+405
-306
lines changed

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,15 @@ fun BillPlaygroundScaffold(content: @Composable () -> Unit) {
6969

7070
val customizationsOptions by remember(
7171
state.selectedColors,
72-
state.bill?.token?.launchpadMetadata?.billCustomizations
72+
state.bill?.token?.billCustomizations
7373
) {
7474
derivedStateOf {
7575
val background = if (state.selectedColors.count() > 1) {
7676
BillBackground.Gradient.from(
77-
state.selectedColors.map { it.toAGColor() }
77+
state.selectedColors.map { it.color.toAGColor() }
7878
)
7979
} else {
80-
BillBackground.Solid.from(state.selectedColors.first().toAGColor())
80+
BillBackground.Solid.from(state.selectedColors.first().color.toAGColor())
8181
}
8282

8383

@@ -94,9 +94,7 @@ fun BillPlaygroundScaffold(content: @Composable () -> Unit) {
9494
if (bill !is Bill.Cash) return@derivedStateOf null
9595
bill.copy(
9696
token = bill.token.copy(
97-
launchpadMetadata = bill.token.launchpadMetadata?.copy(
98-
billCustomizations = customizationsOptions
99-
)
97+
billCustomizations = customizationsOptions
10098
)
10199
)
102100
}
@@ -142,7 +140,7 @@ fun BillPlaygroundScaffold(content: @Composable () -> Unit) {
142140
) {
143141
TopBar(
144142
modifier = Modifier.fillMaxWidth(),
145-
canUndo = state.canUndo,
143+
canUndo = controller.canUndo,
146144
onUndo = { controller.dispatchEvent(Event.Undo) },
147145
onBack = { controller.cancel() },
148146
onDone = { controller.cancel() }

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

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

33
import androidx.compose.animation.AnimatedContent
44
import androidx.compose.animation.ExperimentalSharedTransitionApi
5-
import androidx.compose.animation.animateBounds
6-
import androidx.compose.animation.animateColorAsState
75
import androidx.compose.animation.core.Spring
86
import androidx.compose.animation.core.VisibilityThreshold
9-
import androidx.compose.animation.core.animateFloatAsState
107
import androidx.compose.animation.core.spring
118
import androidx.compose.animation.fadeOut
129
import androidx.compose.animation.slideInHorizontally
@@ -18,61 +15,40 @@ import androidx.compose.foundation.layout.Arrangement
1815
import androidx.compose.foundation.layout.Box
1916
import androidx.compose.foundation.layout.Column
2017
import androidx.compose.foundation.layout.PaddingValues
21-
import androidx.compose.foundation.layout.Row
2218
import androidx.compose.foundation.layout.aspectRatio
23-
import androidx.compose.foundation.layout.fillMaxHeight
2419
import androidx.compose.foundation.layout.fillMaxSize
2520
import androidx.compose.foundation.layout.fillMaxWidth
2621
import androidx.compose.foundation.layout.height
2722
import androidx.compose.foundation.layout.navigationBarsPadding
2823
import androidx.compose.foundation.layout.padding
29-
import androidx.compose.foundation.layout.size
30-
import androidx.compose.foundation.layout.width
3124
import androidx.compose.foundation.lazy.grid.GridCells
3225
import androidx.compose.foundation.lazy.grid.GridItemSpan
3326
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
34-
import androidx.compose.material.ContentAlpha
35-
import androidx.compose.material.Icon
36-
import androidx.compose.material.IconButton
3727
import androidx.compose.runtime.Composable
3828
import androidx.compose.runtime.getValue
3929
import androidx.compose.runtime.remember
4030
import androidx.compose.runtime.rememberUpdatedState
4131
import androidx.compose.ui.Alignment
4232
import androidx.compose.ui.Modifier
4333
import androidx.compose.ui.composed
44-
import androidx.compose.ui.draw.clip
45-
import androidx.compose.ui.draw.drawWithContent
46-
import androidx.compose.ui.geometry.CornerRadius
47-
import androidx.compose.ui.geometry.Offset
48-
import androidx.compose.ui.graphics.Brush
4934
import androidx.compose.ui.graphics.Color
50-
import androidx.compose.ui.graphics.SolidColor
51-
import androidx.compose.ui.layout.LookaheadScope
5235
import androidx.compose.ui.platform.LocalContext
53-
import androidx.compose.ui.platform.LocalDensity
54-
import androidx.compose.ui.res.painterResource
5536
import androidx.compose.ui.tooling.preview.Preview
5637
import androidx.compose.ui.unit.Dp
5738
import androidx.compose.ui.unit.IntOffset
5839
import androidx.compose.ui.unit.dp
59-
import androidx.compose.ui.util.fastForEachIndexed
6040
import androidx.lifecycle.compose.collectAsStateWithLifecycle
61-
import com.flipcash.app.bill.customization.ColorChange
41+
import com.flipcash.app.bill.customization.ColorStore
6242
import com.flipcash.app.bill.customization.Event
6343
import com.flipcash.app.bill.customization.PlaygroundMode
6444
import com.flipcash.app.bill.customization.internal.InternalBillPlaygroundController
6545
import com.flipcash.app.theme.FlipcashDesignSystem
66-
import com.flipcash.features.bill.playground.R
6746
import com.getcode.opencode.compose.ExchangeStub
6847
import com.getcode.opencode.model.financial.BillBackground
6948
import com.getcode.opencode.model.financial.CurrencyCode
7049
import com.getcode.opencode.model.financial.Rate
7150
import com.getcode.theme.CodeTheme
7251
import com.getcode.ui.components.Pill
73-
import com.getcode.ui.core.addIf
74-
import com.getcode.ui.core.rememberedClickable
75-
import com.getcode.ui.utils.hexToColor
7652

7753
@OptIn(ExperimentalSharedTransitionApi::class)
7854
@Composable
@@ -81,7 +57,7 @@ internal fun BillPlayground(
8157
selectedSlot: Int,
8258
maxSlots: Int,
8359
colorOptions: List<BillBackground>,
84-
selectedColors: List<Color>,
60+
selectedColors: List<ColorStore>,
8561
dispatchEvent: (Event) -> Unit,
8662
) {
8763
Column(
@@ -101,79 +77,17 @@ internal fun BillPlayground(
10177
)
10278

10379
// color selections
104-
Row(
80+
ColorSlots(
10581
modifier = Modifier
10682
.fillMaxWidth()
10783
.padding(horizontal = CodeTheme.dimens.grid.x3),
108-
horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x1),
109-
verticalAlignment = Alignment.CenterVertically
110-
) {
111-
// remove slot
112-
IconButton(
113-
enabled = selectedColors.count() > 1,
114-
onClick = {
115-
dispatchEvent(Event.RemoveSlot)
116-
}
117-
) {
118-
val alpha by animateFloatAsState(
119-
if (selectedColors.count() > 1) 1f else ContentAlpha.disabled
120-
)
121-
Icon(
122-
modifier = Modifier.size(CodeTheme.dimens.staticGrid.x4),
123-
painter = painterResource(R.drawable.ic_minus),
124-
contentDescription = "Remove Color Slot",
125-
tint = Color.White.copy(alpha),
126-
)
127-
}
128-
129-
// slots
130-
LookaheadScope {
131-
Row(
132-
modifier = Modifier
133-
.weight(1f)
134-
.height(CodeTheme.dimens.grid.x10),
135-
horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x1),
136-
verticalAlignment = Alignment.CenterVertically,
137-
) {
138-
selectedColors.fastForEachIndexed { slot, color ->
139-
val borderColor by animateColorAsState(
140-
if (selectedSlot == slot) Color.White else Color.White.copy(0.30f)
141-
)
142-
143-
Box(
144-
modifier = Modifier
145-
.fillMaxHeight()
146-
.weight(1f)
147-
.animateBounds(this@LookaheadScope)
148-
.presenceBorder(3.dp, borderColor)
149-
.background(color = color, shape = CodeTheme.shapes.small)
150-
.rememberedClickable {
151-
dispatchEvent(Event.SelectSlot(slot))
152-
}
153-
)
154-
}
155-
}
156-
}
157-
// add slot
158-
IconButton(
159-
enabled = selectedColors.count() < maxSlots,
160-
onClick = {
161-
dispatchEvent(Event.AddSlot)
162-
}
163-
) {
164-
val alpha by animateFloatAsState(
165-
if (selectedColors.count() < maxSlots) 1f else ContentAlpha.disabled
166-
)
167-
Icon(
168-
modifier = Modifier.size(CodeTheme.dimens.staticGrid.x4),
169-
painter = painterResource(R.drawable.ic_plus),
170-
contentDescription = "Add Color Slot",
171-
tint = Color.White.copy(alpha),
172-
)
173-
}
174-
}
84+
selectedSlot = selectedSlot,
85+
maxSlots = maxSlots,
86+
selectedColors = selectedColors,
87+
dispatchEvent = dispatchEvent
88+
)
17589

176-
val selectedSlotColor by rememberUpdatedState(selectedColors[selectedSlot])
90+
val selectedSlotStore by rememberUpdatedState(selectedColors[selectedSlot])
17791

17892
// color options
17993
AnimatedContent(
@@ -205,13 +119,20 @@ internal fun BillPlayground(
205119
when (mode) {
206120
PlaygroundMode.ColorPanel -> {
207121
ColorPanel(
208-
selectedColor = selectedSlotColor,
122+
selectedColor = selectedSlotStore.color,
209123
modifier = Modifier
210124
.fillMaxWidth()
211125
.height(CodeTheme.dimens.grid.x14 * 2)
212126
.padding(horizontal = CodeTheme.dimens.grid.x5)
213127
.padding(vertical = CodeTheme.dimens.grid.x3),
214-
onChange = { dispatchEvent(Event.ChangeColor(it, ColorChange.Custom)) },
128+
onChange = { color, isDragging ->
129+
if (isDragging) {
130+
dispatchEvent(Event.PreviewColorChange(color))
131+
} else {
132+
dispatchEvent(Event.CommitColorChange(color))
133+
134+
}
135+
},
215136
onClose = {
216137
dispatchEvent(Event.CloseHueControls)
217138
}
@@ -231,28 +152,8 @@ internal fun BillPlayground(
231152
item(
232153
span = { GridItemSpan(maxLineSpan) }
233154
) {
234-
Box(
235-
modifier = Modifier
236-
.width(CodeTheme.dimens.grid.x10)
237-
.fillMaxHeight()
238-
.rainbowBackground()
239-
.padding(CodeTheme.dimens.thickBorder)
240-
.background(
241-
color = Color.Black.copy(0.50f),
242-
shape = CodeTheme.shapes.small
243-
)
244-
.clip(CodeTheme.shapes.small)
245-
.rememberedClickable {
246-
dispatchEvent(Event.OpenHueControls)
247-
}
248-
.padding(CodeTheme.dimens.grid.x3),
249-
contentAlignment = Alignment.Center
250-
) {
251-
Icon(
252-
painter = painterResource(R.drawable.ic_color_tune_hsl),
253-
contentDescription = "Color manipulation",
254-
tint = Color.White
255-
)
155+
HueControlButton {
156+
dispatchEvent(Event.OpenHueControls)
256157
}
257158
}
258159

@@ -268,67 +169,6 @@ internal fun BillPlayground(
268169
}
269170
}
270171

271-
@Composable
272-
private fun ColorOptionItem(
273-
colorOptions: List<BillBackground>,
274-
index: Int,
275-
dispatchEvent: (Event) -> Unit
276-
) {
277-
val numRows = 2
278-
val itemsPerRow = (colorOptions.size + numRows - 1) / numRows
279-
val col = index / numRows
280-
val row = index % numRows
281-
val newIndex = if (row == 0) {
282-
col
283-
} else {
284-
itemsPerRow + col
285-
}
286-
if (newIndex < colorOptions.size) {
287-
val option = colorOptions[newIndex]
288-
Box(
289-
modifier = Modifier
290-
.width(CodeTheme.dimens.grid.x10)
291-
.presenceBorder()
292-
.addIf(option is BillBackground.Solid) {
293-
Modifier.background(
294-
color = hexToColor((option as BillBackground.Solid).colorHex),
295-
shape = CodeTheme.shapes.small
296-
)
297-
}
298-
.addIf(option is BillBackground.Gradient) {
299-
val colors =
300-
(option as BillBackground.Gradient).colors.map {
301-
hexToColor(
302-
it
303-
)
304-
}
305-
Modifier.background(
306-
brush = Brush.verticalGradient(
307-
colors = colors,
308-
),
309-
shape = CodeTheme.shapes.small
310-
)
311-
}
312-
.rememberedClickable {
313-
when (option) {
314-
is BillBackground.Gradient -> dispatchEvent(
315-
Event.LoadBackground(
316-
option
317-
)
318-
)
319-
320-
is BillBackground.Solid -> dispatchEvent(
321-
Event.ChangeColor(
322-
hexToColor(option.colorHex),
323-
ColorChange.Preset
324-
)
325-
)
326-
}
327-
}
328-
)
329-
}
330-
}
331-
332172
internal fun Modifier.presenceBorder(
333173
width: Dp = 2.dp,
334174
color: Color = Color.White.copy(0.30f)
@@ -340,49 +180,6 @@ internal fun Modifier.presenceBorder(
340180
)
341181
}
342182

343-
private fun Modifier.rainbowBackground(): Modifier = composed {
344-
val small = CodeTheme.shapes.small
345-
val thickBorder = CodeTheme.dimens.thickBorder
346-
val density = LocalDensity.current
347-
this.drawWithContent {
348-
drawRoundRect(
349-
brush = Brush.sweepGradient(
350-
colorStops = arrayOf( // Starts at 3 o'clock
351-
0f to Color(0xFFBB3DFF), // Purple starts at 3 o'clock (right)
352-
0.16f to Color(0xFFFF3D3D), // Dark red
353-
0.22f to Color(0xFFFF7070), // Light red
354-
0.38f to Color(0xFFFFC23D), // Yellow
355-
0.58f to Color(0xFF54FF3D), // Green
356-
0.81f to Color(0xFF3DEFFF), // Cyan
357-
0.92f to Color(0xFF3DA8FF), // Blue
358-
1f to Color(0xFFBB3DFF) // Loops back to purple
359-
),
360-
center = center
361-
),
362-
cornerRadius = CornerRadius(
363-
small.topStart.toPx(size, density),
364-
small.topEnd.toPx(size, density)
365-
),
366-
)
367-
drawRoundRect(
368-
brush = SolidColor(Color.Black.copy(0.50f)),
369-
cornerRadius = CornerRadius(
370-
small.topStart.toPx(size, density),
371-
small.topEnd.toPx(size, density)
372-
),
373-
topLeft = Offset(
374-
x = thickBorder.toPx(),
375-
y = thickBorder.toPx()
376-
),
377-
size = size.copy(
378-
width = size.width - thickBorder.toPx() * 2,
379-
height = size.height - thickBorder.toPx() * 2
380-
)
381-
)
382-
drawContent()
383-
}
384-
}
385-
386183
private val usdToCadRate = Rate(fx = 1.37161, currency = CurrencyCode.CAD)
387184
private val cadToUsdRate = Rate(fx = 0.72894, currency = CurrencyCode.USD)
388185
private val rates = mapOf(

0 commit comments

Comments
 (0)