@@ -26,41 +26,33 @@ import androidx.activity.compose.setContent
2626import androidx.appcompat.app.AppCompatActivity
2727import androidx.compose.foundation.ExperimentalFoundationApi
2828import androidx.compose.foundation.layout.Arrangement
29+ import androidx.compose.foundation.layout.Box
2930import androidx.compose.foundation.layout.Column
3031import androidx.compose.foundation.layout.fillMaxHeight
3132import androidx.compose.foundation.layout.fillMaxWidth
32- import androidx.compose.foundation.layout.padding
3333import androidx.compose.material.BackdropScaffold
34+ import androidx.compose.material.BackdropValue
3435import androidx.compose.material.ExperimentalMaterialApi
35- import androidx.compose.material.FloatingActionButton
36- import androidx.compose.material.Icon
3736import androidx.compose.material.MaterialTheme
3837import androidx.compose.material.Surface
39- import androidx.compose.material.Text
40- import androidx.compose.material.icons.Icons
41- import androidx.compose.material.icons.twotone.PlayArrow
38+ import androidx.compose.material.rememberBackdropScaffoldState
4239import androidx.compose.runtime.Composable
43- import androidx.compose.runtime.LaunchedEffect
4440import androidx.compose.runtime.getValue
4541import androidx.compose.runtime.livedata.observeAsState
46- import androidx.compose.runtime.mutableStateOf
47- import androidx.compose.runtime.remember
48- import androidx.compose.runtime.setValue
42+ import androidx.compose.runtime.rememberCoroutineScope
4943import androidx.compose.ui.Alignment
5044import androidx.compose.ui.Modifier
51- import androidx.compose.ui.res.stringResource
52- import androidx.compose.ui.unit.dp
5345import androidx.lifecycle.viewmodel.compose.viewModel
54- import kotlinx.coroutines.delay
55- import kotlinx.coroutines.isActive
46+ import kotlinx.coroutines.coroutineScope
47+ import kotlinx.coroutines.launch
5648import net.opatry.countdowntimer.ui.component.TimerCircle
5749import net.opatry.countdowntimer.ui.component.TimerControls
5850import net.opatry.countdowntimer.ui.component.TimerLabel
5951import net.opatry.countdowntimer.ui.component.TimerList
6052import net.opatry.countdowntimer.ui.theme.MyTheme
6153import kotlin.math.roundToInt
54+ import kotlin.time.Duration
6255import kotlin.time.ExperimentalTime
63- import kotlin.time.milliseconds
6456
6557class MainActivity : AppCompatActivity () {
6658 @ExperimentalTime
@@ -97,108 +89,81 @@ fun MyApp() {
9789fun CountDownTimerDispatcher () {
9890 val viewModel = viewModel<CounterViewModel >()
9991 val timers by viewModel.timers.observeAsState(listOf ())
100- val state by viewModel.state.observeAsState(TimerState .Reset (null ))
92+ val state by viewModel.state.observeAsState(null )
93+ val coroutineScope = rememberCoroutineScope()
10194
102- state.let { uiState ->
103- when (uiState) {
104- is TimerState .Reset -> CountDownTimerReset {
105- // TODO TODO handle no timer and ask for a duration & name
106- viewModel.start(timers[0 ])
95+ val scaffoldState = rememberBackdropScaffoldState(if (state == null ) BackdropValue .Revealed else BackdropValue .Concealed )
96+
97+ BackdropScaffold (
98+ appBar = { },
99+ scaffoldState = scaffoldState,
100+ backLayerContent = {
101+ Column (
102+ Modifier .fillMaxWidth()
103+ ) {
104+ // TODO if list is empty, allow to create one
105+ TimerList (state?.timer, timers) { timer ->
106+ coroutineScope.launch {
107+ scaffoldState.conceal()
108+ }
109+ viewModel.start(timer)
110+ }
107111 }
108- is TimerState .Running -> {
109- val activeTimer = uiState.timer
112+ },
113+ frontLayerContent = {
114+ state?.let { uiState ->
110115 CountDownTimerLayout (
111- activeTimer,
112- timers,
113- onTimerClicked = {
114- viewModel.start(it)
115- },
116+ uiState.remaining,
116117 onFABClicked = {
117- viewModel.reset()
118- // viewModel.pause()
118+ if (uiState.remaining.isPositive()) {
119+ coroutineScope.launch {
120+ scaffoldState.reveal()
121+ }
122+ viewModel.stop()
123+ } else {
124+ coroutineScope.launch {
125+ scaffoldState.conceal()
126+ }
127+ viewModel.start(uiState.timer)
128+ }
119129 }
120130 )
121- }
122- // is TimerState.Paused -> CountDownTimerLayout(
123- // onTimerClicked = {}
124- // ) {
125- // viewModel.resume()
126- // }
127- // is TimerState.Done -> CountDownTimerLayout(
128- // onTimerClicked = {}
129- // ) {
130- // viewModel.reset()
131- // }
131+ } ? : Box {}
132132 }
133- }
134- }
135-
136- @Composable
137- fun CountDownTimerReset (onFABClicked : () -> Unit ) {
138- Column {
139- Text (" TODO CREATE NEW TIMER" , Modifier .padding(48 .dp))
140- FloatingActionButton (onClick = onFABClicked) {
141- Icon (Icons .TwoTone .PlayArrow , stringResource(R .string.timer_start))
142- }
143- }
133+ )
144134}
145135
146136@Composable
147137@ExperimentalTime
148138@ExperimentalMaterialApi
149139@ExperimentalFoundationApi
150140fun CountDownTimerLayout (
151- activeTimer : Timer ,
152- timers : List <Timer >,
153- onTimerClicked : (Timer ) -> Unit ,
141+ remainingDuration : Duration ,
154142 onFABClicked : () -> Unit
155143) {
156- // FIXME there is a bug when finishing the first progress for the first time, progress sticks to max
157- var remainingDuration by remember/* Saveable TODO Duration to bundle*/ { mutableStateOf(activeTimer.duration) }
158- LaunchedEffect (activeTimer/* .name*/ ) {
159- // FIXME how to smoothly animate the progress and update remaining time accordingly
160- // FIXME not stopped even when key change :(
161- while (! remainingDuration.isNegative() && isActive) {
162- remainingDuration = remainingDuration - 16 .milliseconds
163- delay(16 )
164- }
165- }
166-
167- val hours = (remainingDuration.inHours % 12 ).coerceAtLeast(.0 )
144+ val hours = (remainingDuration.inHours % 24 ).coerceAtLeast(.0 )
168145 val minutes = (remainingDuration.inMinutes % 60 ).coerceAtLeast(.0 )
169146 val seconds = (remainingDuration.inSeconds % 60 ).coerceAtLeast(.0 )
170147
171- BackdropScaffold (
172- appBar = { },
173- backLayerContent = {
174- Column (
175- Modifier .fillMaxWidth()
176- ) {
177- TimerList (activeTimer, timers, onTimerClicked)
178- }
179- },
180- frontLayerContent = {
181- Column (
182- Modifier
183- .fillMaxWidth()
184- .fillMaxHeight(.8f ),
185- horizontalAlignment = Alignment .CenterHorizontally ,
186- verticalArrangement = Arrangement .SpaceEvenly
187- ) {
188- // FIXME values rounding is incorrect
189- // 1. when switching from 35min 30sec to 35min 29sec, it displays 34min 29sec
190- // 2. when seconds is almost 0 but not yet, digit changes to 0 before the progress reach 12O'clock
191- // (especially visible for last second)
192- val hoursI = hours.roundToInt()
193- val minutesI = minutes.roundToInt()
194- val secondsI = seconds.roundToInt()
195- TimerLabel (hoursI, minutesI, secondsI)
196- val hoursP = if (hoursI == 0 ) 0f else (hours / 12 ).toFloat()
197- val minutesP = if (minutesI == 0 ) 0f else (minutes / 60 ).toFloat()
198- val secondsP = if (secondsI == 0 ) 0f else (seconds / 60 ).toFloat()
199- TimerCircle (hoursP, minutesP, secondsP, onFABClicked)
200- TimerControls (onClose = {}, onDelete = {})
201- }
202- }
203- )
148+ Column (
149+ Modifier
150+ .fillMaxWidth()
151+ .fillMaxHeight(.8f ),
152+ horizontalAlignment = Alignment .CenterHorizontally ,
153+ verticalArrangement = Arrangement .SpaceEvenly
154+ ) {
155+ // FIXME values rounding is incorrect
156+ // 1. when switching from 35min 30sec to 35min 29sec, it displays 34min 29sec
157+ // 2. when seconds is almost 0 but not yet, digit changes to 0 before the progress reach 12 O'clock
158+ // (especially visible for last second)
159+ val hoursI = hours.roundToInt()
160+ val minutesI = minutes.roundToInt()
161+ val secondsI = seconds.roundToInt()
162+ TimerLabel (hoursI, minutesI, secondsI)
163+ val hoursP = if (hoursI == 0 ) 0f else (hours / 24 ).toFloat()
164+ val minutesP = if (minutesI == 0 ) 0f else (minutes / 60 ).toFloat()
165+ val secondsP = if (secondsI == 0 ) 0f else (seconds / 60 ).toFloat()
166+ TimerCircle (hoursP, minutesP, secondsP, onFABClicked)
167+ TimerControls (onClose = { /* TODO */ }, onDelete = { /* TODO */ })
168+ }
204169}
0 commit comments