Skip to content

Commit 043ec81

Browse files
committed
UX improvements to show assist timer
1 parent f9eab8a commit 043ec81

2 files changed

Lines changed: 227 additions & 46 deletions

File tree

storyboard-easel/src/commonMain/kotlin/dev/bnorm/storyboard/easel/notes/ShowAssist.kt

Lines changed: 1 addition & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,20 @@ package dev.bnorm.storyboard.easel.notes
33
import androidx.compose.foundation.background
44
import androidx.compose.foundation.clickable
55
import androidx.compose.foundation.layout.*
6-
import androidx.compose.foundation.shape.CircleShape
76
import androidx.compose.material.*
8-
import androidx.compose.material.icons.Icons
9-
import androidx.compose.material.icons.filled.PlayArrow
107
import androidx.compose.runtime.*
118
import androidx.compose.ui.Alignment
129
import androidx.compose.ui.Modifier
1310
import androidx.compose.ui.graphics.Color
1411
import androidx.compose.ui.input.pointer.PointerIcon
1512
import androidx.compose.ui.input.pointer.pointerHoverIcon
16-
import androidx.compose.ui.text.font.FontFamily
1713
import androidx.compose.ui.unit.dp
18-
import androidx.compose.ui.unit.sp
1914
import dev.bnorm.storyboard.core.AdvanceDirection
2015
import dev.bnorm.storyboard.core.Storyboard
2116
import dev.bnorm.storyboard.easel.internal.aspectRatio
2217
import dev.bnorm.storyboard.easel.internal.requestFocus
2318
import dev.bnorm.storyboard.easel.onStoryboardNavigation
2419
import dev.bnorm.storyboard.ui.SlidePreview
25-
import kotlinx.coroutines.delay
26-
import kotlin.time.Duration.Companion.milliseconds
27-
import kotlin.time.TimeMark
28-
import kotlin.time.TimeSource
2920

3021
@Composable
3122
fun StoryboardNotes(storyboard: Storyboard, notes: StoryboardNotes, modifier: Modifier = Modifier) {
@@ -36,7 +27,7 @@ fun StoryboardNotes(storyboard: Storyboard, notes: StoryboardNotes, modifier: Mo
3627
) {
3728
Column(modifier.padding(16.dp)) {
3829
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
39-
StoryboardClock()
30+
StoryboardTimer()
4031
}
4132

4233
Row(modifier = Modifier.fillMaxWidth().padding(top = 16.dp)) {
@@ -67,10 +58,6 @@ fun StoryboardNotes(storyboard: Storyboard, notes: StoryboardNotes, modifier: Mo
6758
}
6859
}
6960

70-
// TODO how can we do a preview of the next slide?
71-
// - we would really like to have a preview of the next **advancement**
72-
// - can we render the show like an export to create the previews without animations?
73-
7461
if (notes.tabs.isNotEmpty()) {
7562
var state by remember { mutableStateOf(0) }
7663
Scaffold(
@@ -132,35 +119,3 @@ private fun SlideAnimationProgressIndicator(storyboard: Storyboard) {
132119
}
133120
}
134121

135-
@Composable
136-
fun StoryboardClock(timeSource: TimeSource = TimeSource.Monotonic) {
137-
var start by remember { mutableStateOf<TimeMark?>(null) }
138-
var display by remember { mutableStateOf("00h 00m 00s") }
139-
140-
fun Long.pad(): String = toString().padStart(2, padChar = '0')
141-
142-
LaunchedEffect(start) {
143-
val mark = start ?: return@LaunchedEffect
144-
while (true) {
145-
delay(250.milliseconds)
146-
val d = mark.elapsedNow()
147-
display = "${d.inWholeHours.pad()}h ${(d.inWholeMinutes % 60).pad()}m ${(d.inWholeSeconds % 60).pad()}s"
148-
}
149-
}
150-
151-
Row(verticalAlignment = Alignment.CenterVertically) {
152-
Text(display, fontSize = 32.sp, fontFamily = FontFamily.Monospace)
153-
154-
IconButton(
155-
onClick = {
156-
start = timeSource.markNow()
157-
display = "00h 00m 00s"
158-
},
159-
modifier = Modifier
160-
.padding(start = 16.dp)
161-
.background(MaterialTheme.colors.primary, shape = CircleShape)
162-
) {
163-
Icon(Icons.Filled.PlayArrow, tint = MaterialTheme.colors.onPrimary, contentDescription = "")
164-
}
165-
}
166-
}
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
package dev.bnorm.storyboard.easel.notes
2+
3+
import androidx.compose.animation.AnimatedContent
4+
import androidx.compose.animation.SharedTransitionLayout
5+
import androidx.compose.animation.core.Transition
6+
import androidx.compose.animation.core.animateFloat
7+
import androidx.compose.animation.core.updateTransition
8+
import androidx.compose.foundation.background
9+
import androidx.compose.foundation.layout.Box
10+
import androidx.compose.foundation.layout.Row
11+
import androidx.compose.foundation.layout.padding
12+
import androidx.compose.foundation.layout.size
13+
import androidx.compose.foundation.shape.CircleShape
14+
import androidx.compose.material.Icon
15+
import androidx.compose.material.IconButton
16+
import androidx.compose.material.MaterialTheme
17+
import androidx.compose.material.Text
18+
import androidx.compose.material.icons.Icons
19+
import androidx.compose.material.icons.filled.Refresh
20+
import androidx.compose.runtime.*
21+
import androidx.compose.ui.Alignment
22+
import androidx.compose.ui.Modifier
23+
import androidx.compose.ui.draw.scale
24+
import androidx.compose.ui.graphics.Color
25+
import androidx.compose.ui.graphics.SolidColor
26+
import androidx.compose.ui.graphics.vector.Path
27+
import androidx.compose.ui.graphics.vector.PathData
28+
import androidx.compose.ui.graphics.vector.VectorPainter
29+
import androidx.compose.ui.graphics.vector.rememberVectorPainter
30+
import androidx.compose.ui.input.pointer.PointerIcon
31+
import androidx.compose.ui.input.pointer.pointerHoverIcon
32+
import androidx.compose.ui.text.font.FontFamily
33+
import androidx.compose.ui.unit.dp
34+
import androidx.compose.ui.unit.sp
35+
import kotlinx.coroutines.delay
36+
import kotlin.time.Duration.Companion.milliseconds
37+
import kotlin.time.TimeMark
38+
import kotlin.time.TimeSource
39+
40+
@Stable
41+
class Timer(
42+
val timeSource: TimeSource = TimeSource.Monotonic,
43+
) {
44+
enum class State {
45+
Stopped,
46+
Running,
47+
Paused,
48+
}
49+
50+
var state: State by mutableStateOf(State.Stopped)
51+
private set
52+
53+
private var start by mutableStateOf<TimeMark?>(null)
54+
private var duration by mutableStateOf(0.milliseconds)
55+
56+
fun onPlayPause() {
57+
if (start == null) {
58+
start = timeSource.markNow() - duration
59+
state = State.Running
60+
} else {
61+
start?.let { duration = it.elapsedNow() }
62+
start = null
63+
state = State.Paused
64+
}
65+
}
66+
67+
fun onReset() {
68+
duration = 0.milliseconds
69+
if (start != null) {
70+
start = timeSource.markNow()
71+
} else {
72+
state = State.Stopped
73+
}
74+
}
75+
76+
suspend fun await() {
77+
// TODO there has to be a better way to achieve this state update...
78+
val mark = start ?: return
79+
while (true) {
80+
delay(250.milliseconds)
81+
duration = mark.elapsedNow()
82+
}
83+
}
84+
85+
override fun toString(): String {
86+
fun Long.pad(): String = toString().padStart(2, padChar = '0')
87+
return "${duration.inWholeHours.pad()}h ${(duration.inWholeMinutes % 60).pad()}m ${(duration.inWholeSeconds % 60).pad()}s"
88+
}
89+
}
90+
91+
@Composable
92+
fun StoryboardTimer(timeSource: TimeSource = TimeSource.Monotonic) {
93+
val timer = remember(timeSource) { Timer(timeSource) }
94+
LaunchedEffect(timer.state) { timer.await() }
95+
96+
Row(verticalAlignment = Alignment.CenterVertically) {
97+
Text(
98+
timer.toString(),
99+
fontSize = 32.sp,
100+
fontFamily = FontFamily.Monospace
101+
)
102+
103+
TimerButtons(timer)
104+
}
105+
}
106+
107+
@Composable
108+
private fun TimerButtons(timer: Timer) {
109+
SharedTransitionLayout {
110+
val transition = updateTransition(timer.state)
111+
transition.AnimatedContent {
112+
if (it != Timer.State.Stopped) {
113+
Row {
114+
PlayPause(
115+
pause = transition,
116+
onClick = { timer.onPlayPause() },
117+
modifier = Modifier.sharedElement(
118+
rememberSharedContentState("play-pause"),
119+
animatedVisibilityScope = this@AnimatedContent
120+
),
121+
)
122+
Reset(
123+
onClick = { timer.onReset() },
124+
modifier = Modifier.sharedElement(
125+
rememberSharedContentState("reset"),
126+
animatedVisibilityScope = this@AnimatedContent
127+
),
128+
)
129+
}
130+
} else {
131+
Box {
132+
Reset(
133+
onClick = { timer.onReset() },
134+
modifier = Modifier.sharedElement(
135+
rememberSharedContentState("reset"),
136+
animatedVisibilityScope = this@AnimatedContent
137+
),
138+
)
139+
PlayPause(
140+
pause = transition,
141+
onClick = { timer.onPlayPause() },
142+
modifier = Modifier.sharedElement(
143+
rememberSharedContentState("play-pause"),
144+
animatedVisibilityScope = this@AnimatedContent
145+
),
146+
)
147+
}
148+
}
149+
}
150+
}
151+
}
152+
153+
@Composable
154+
private fun PlayPause(
155+
pause: Transition<Timer.State>,
156+
onClick: () -> Unit,
157+
modifier: Modifier = Modifier,
158+
) {
159+
IconButton(
160+
onClick = onClick,
161+
modifier = modifier
162+
.padding(start = 16.dp)
163+
.pointerHoverIcon(PointerIcon.Hand)
164+
.background(MaterialTheme.colors.primary, shape = CircleShape)
165+
) {
166+
Icon(
167+
PlayPauseIcon(pause),
168+
tint = MaterialTheme.colors.onPrimary,
169+
contentDescription = "",
170+
modifier = Modifier.size(32.dp)
171+
)
172+
}
173+
}
174+
175+
@Composable
176+
private fun Reset(
177+
onClick: () -> Unit,
178+
modifier: Modifier = Modifier,
179+
) {
180+
IconButton(
181+
onClick = onClick,
182+
modifier = modifier
183+
.padding(start = 16.dp)
184+
.pointerHoverIcon(PointerIcon.Hand)
185+
.background(MaterialTheme.colors.primary, shape = CircleShape)
186+
) {
187+
Icon(
188+
Icons.Filled.Refresh,
189+
tint = MaterialTheme.colors.onPrimary,
190+
contentDescription = "",
191+
modifier = Modifier.size(32.dp).scale(scaleX = -1f, scaleY = 1f)
192+
)
193+
}
194+
}
195+
196+
@Composable
197+
private fun PlayPauseIcon(transition: Transition<Timer.State>): VectorPainter {
198+
return rememberVectorPainter(
199+
defaultWidth = 24.dp,
200+
defaultHeight = 24.dp,
201+
viewportWidth = 24f,
202+
viewportHeight = 24f,
203+
autoMirror = false,
204+
) { _, _ ->
205+
val fraction by transition.animateFloat { if (it == Timer.State.Running) 1f else 0f }
206+
207+
Path(
208+
pathData = PathData {
209+
moveTo(6f, 5f)
210+
horizontalLineTo(6f + 4f * fraction)
211+
verticalLineTo(19f)
212+
horizontalLineTo(6f)
213+
close()
214+
215+
moveTo(8f + 6f * fraction, 5f)
216+
lineTo(8f + 10f * fraction, 5f)
217+
lineTo(19f - 1f * fraction, 12f)
218+
lineTo(8f + 10f * fraction, 19f)
219+
lineTo(8f + 6f * fraction, 19f)
220+
close()
221+
},
222+
223+
fill = SolidColor(Color.White)
224+
)
225+
}
226+
}

0 commit comments

Comments
 (0)