From eb77b05b328edc54d445607d7c69d7ebdd373655 Mon Sep 17 00:00:00 2001 From: Ankit Suda Date: Fri, 25 Nov 2022 17:30:44 +0530 Subject: [PATCH] Add workout duration adjuster --- .../build.gradle | 3 + .../WorkoutDurationEditorDialog.kt | 179 ++++++++++++++++++ modules/common-ui-components/build.gradle | 4 + .../components/datetimepicker/DatePicker.kt | 55 ++++++ .../datetimepicker/DateTimePicker.kt | 58 ++++++ .../components/datetimepicker/TimePicker.kt | 56 ++++++ .../ui/components/topbar/MenuActionItem.kt | 20 ++ .../ui/components/topbar/TopBarMenuAction.kt | 76 ++++++++ .../src/main/res/values/strings.xml | 3 + .../ui/workoutedit/WorkoutEditScreen.kt | 50 +++-- .../workoutedit/WorkoutEditScreenViewModel.kt | 13 ++ 11 files changed, 499 insertions(+), 18 deletions(-) create mode 100644 modules/common-ui-components-workout-editor/src/main/java/com/ankitsuda/rebound/ui/components/workouteditor/durationeditor/WorkoutDurationEditorDialog.kt create mode 100644 modules/common-ui-components/src/main/java/com/ankitsuda/rebound/ui/components/datetimepicker/DatePicker.kt create mode 100644 modules/common-ui-components/src/main/java/com/ankitsuda/rebound/ui/components/datetimepicker/DateTimePicker.kt create mode 100644 modules/common-ui-components/src/main/java/com/ankitsuda/rebound/ui/components/datetimepicker/TimePicker.kt create mode 100644 modules/common-ui-components/src/main/java/com/ankitsuda/rebound/ui/components/topbar/MenuActionItem.kt create mode 100644 modules/common-ui-components/src/main/java/com/ankitsuda/rebound/ui/components/topbar/TopBarMenuAction.kt diff --git a/modules/common-ui-components-workout-editor/build.gradle b/modules/common-ui-components-workout-editor/build.gradle index 9b717b32..15136c97 100644 --- a/modules/common-ui-components-workout-editor/build.gradle +++ b/modules/common-ui-components-workout-editor/build.gradle @@ -30,6 +30,7 @@ android { } compileOptions { + coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -56,6 +57,8 @@ dependencies { implementation project(":modules:navigation") implementation project(":modules:domain") + coreLibraryDesugaring Deps.Android.desugaring + implementation Deps.Dagger.hilt kapt Deps.Dagger.hiltCompiler } diff --git a/modules/common-ui-components-workout-editor/src/main/java/com/ankitsuda/rebound/ui/components/workouteditor/durationeditor/WorkoutDurationEditorDialog.kt b/modules/common-ui-components-workout-editor/src/main/java/com/ankitsuda/rebound/ui/components/workouteditor/durationeditor/WorkoutDurationEditorDialog.kt new file mode 100644 index 00000000..34dc17e5 --- /dev/null +++ b/modules/common-ui-components-workout-editor/src/main/java/com/ankitsuda/rebound/ui/components/workouteditor/durationeditor/WorkoutDurationEditorDialog.kt @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2022 Ankit Suda. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + */ + +package com.ankitsuda.rebound.ui.components.workouteditor.durationeditor + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.ankitsuda.base.utils.toString +import com.ankitsuda.rebound.ui.components.RButtonStyle2 +import com.ankitsuda.rebound.ui.components.datetimepicker.DateTimePicker +import com.ankitsuda.rebound.ui.components.workouteditor.R +import com.ankitsuda.rebound.ui.theme.ReboundTheme +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle + +@Composable +fun WorkoutDurationEditorDialog( + initialStartDateTime: LocalDateTime, + initialEndDateTime: LocalDateTime, + onDismissRequest: () -> Unit, + onSave: (startDateTime: LocalDateTime, endDateTime: LocalDateTime) -> Unit, +) { + var currentPickerActiveFor by remember { + mutableStateOf(0) + } + + var startDateTime by rememberSaveable { + mutableStateOf(initialStartDateTime) + } + + var endDateTime by rememberSaveable { + mutableStateOf(initialEndDateTime) + } + + fun getFormattedDateString(dateTime: LocalDateTime): String = + dateTime.format( + DateTimeFormatter.ofLocalizedDateTime( + FormatStyle.MEDIUM, + FormatStyle.SHORT + ) + ) + + fun handleSaveClick() { + onSave( + startDateTime, + endDateTime + ) + onDismissRequest() + } + + val formattedStartDateTime = remember(startDateTime) { + getFormattedDateString(startDateTime) + } + + val formattedEndDateTime = remember(endDateTime) { + getFormattedDateString(endDateTime) + } + + Dialog( + onDismissRequest = onDismissRequest, + ) { + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp), + color = ReboundTheme.colors.background, + shape = ReboundTheme.shapes.large + ) { + Column( + modifier = Modifier + .fillMaxWidth() + ) { + Column( + modifier = + Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = stringResource(R.string.adjust_duration), + style = ReboundTheme.typography.h6, + color = ReboundTheme.colors.onBackground + ) + + Text( + text = stringResource(R.string.start_time), + style = ReboundTheme.typography.caption, + color = ReboundTheme.colors.onBackground.copy(0.75f) + ) + + RButtonStyle2( + modifier = Modifier.fillMaxWidth(), + text = formattedStartDateTime, + onClick = { + currentPickerActiveFor = 1 + } + ) + + Text( + text = stringResource(R.string.end_time), + style = ReboundTheme.typography.caption, + color = ReboundTheme.colors.onBackground.copy(0.75f) + ) + + RButtonStyle2( + modifier = Modifier.fillMaxWidth(), + text = formattedEndDateTime, + onClick = { + currentPickerActiveFor = 2 + } + ) + + DialogButtonsRow( + onClickSave = ::handleSaveClick, + onClickCancel = onDismissRequest + ) + } + } + } + } + + if (currentPickerActiveFor == 1 || currentPickerActiveFor == 2) { + DateTimePicker( + value = when (currentPickerActiveFor) { + 2 -> endDateTime + else -> startDateTime + }, + onCancel = { + currentPickerActiveFor = 0 + }, + onValueChange = { + when (currentPickerActiveFor) { + 1 -> startDateTime = it + 2 -> endDateTime = it + } + currentPickerActiveFor = 0 + } + ) + } +} + +@Composable +private fun ColumnScope.DialogButtonsRow( + onClickSave: () -> Unit, + onClickCancel: () -> Unit +) { + Row( + modifier = Modifier + .align(Alignment.End), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + TextButton(onClick = onClickCancel) { + Text(stringResource(id = R.string.cancel)) + } + TextButton(onClick = onClickSave) { + Text(stringResource(id = R.string.save)) + } + } +} \ No newline at end of file diff --git a/modules/common-ui-components/build.gradle b/modules/common-ui-components/build.gradle index dca9f16f..8f478460 100644 --- a/modules/common-ui-components/build.gradle +++ b/modules/common-ui-components/build.gradle @@ -29,6 +29,8 @@ android { } compileOptions { + coreLibraryDesugaringEnabled true + sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -55,4 +57,6 @@ dependencies { api project(":modules:domain") api Deps.Android.Compose.collapsingToolbar kapt Deps.Dagger.hiltCompiler + coreLibraryDesugaring Deps.Android.desugaring + } diff --git a/modules/common-ui-components/src/main/java/com/ankitsuda/rebound/ui/components/datetimepicker/DatePicker.kt b/modules/common-ui-components/src/main/java/com/ankitsuda/rebound/ui/components/datetimepicker/DatePicker.kt new file mode 100644 index 00000000..4f4500bf --- /dev/null +++ b/modules/common-ui-components/src/main/java/com/ankitsuda/rebound/ui/components/datetimepicker/DatePicker.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022 Ankit Suda. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + */ + +package com.ankitsuda.rebound.ui.components.datetimepicker + +import android.app.DatePickerDialog +import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalContext +import java.time.LocalDate + +@Composable +fun DatePicker( + value: LocalDate, + onCancel: () -> Unit = {}, + onValueChange: (LocalDate) -> Unit = {}, +) { + val context = LocalContext.current + val dialog by remember { + mutableStateOf( + DatePickerDialog( + context, + { _, year, month, dayOfMonth -> + onValueChange(LocalDate.of(year, month + 1, dayOfMonth)) + }, + value.year, + value.monthValue - 1, + value.dayOfMonth, + ) + ) + } + + fun setupDialog() { + dialog.apply { + setOnCancelListener { + onCancel() + } + } + } + + LaunchedEffect(key1 = Unit) { + setupDialog() + dialog.show() + } +} \ No newline at end of file diff --git a/modules/common-ui-components/src/main/java/com/ankitsuda/rebound/ui/components/datetimepicker/DateTimePicker.kt b/modules/common-ui-components/src/main/java/com/ankitsuda/rebound/ui/components/datetimepicker/DateTimePicker.kt new file mode 100644 index 00000000..0eeddcf0 --- /dev/null +++ b/modules/common-ui-components/src/main/java/com/ankitsuda/rebound/ui/components/datetimepicker/DateTimePicker.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2022 Ankit Suda. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + */ + +package com.ankitsuda.rebound.ui.components.datetimepicker + +import androidx.compose.runtime.* +import java.time.LocalDateTime + +@Composable +fun DateTimePicker( + value: LocalDateTime, + onCancel: () -> Unit = {}, + onValueChange: (LocalDateTime) -> Unit = {}, +) { + var currentView by remember { + mutableStateOf(1) + } + + var pickedDate by remember(value) { + mutableStateOf(value.toLocalDate()) + } + + var pickedTime by remember(value) { + mutableStateOf(value.toLocalTime()) + } + + when (currentView) { + 1 -> DatePicker( + value = pickedDate, + onCancel = onCancel, + onValueChange = { + pickedDate = it + currentView = 2 + } + ) + 2 -> TimePicker( + value = pickedTime, + onCancel = onCancel, + onValueChange = { + pickedTime = it + currentView = 0 + onValueChange(LocalDateTime.of(pickedDate, pickedTime)) + } + ) + } + +} \ No newline at end of file diff --git a/modules/common-ui-components/src/main/java/com/ankitsuda/rebound/ui/components/datetimepicker/TimePicker.kt b/modules/common-ui-components/src/main/java/com/ankitsuda/rebound/ui/components/datetimepicker/TimePicker.kt new file mode 100644 index 00000000..d90587b6 --- /dev/null +++ b/modules/common-ui-components/src/main/java/com/ankitsuda/rebound/ui/components/datetimepicker/TimePicker.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2022 Ankit Suda. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + */ + +package com.ankitsuda.rebound.ui.components.datetimepicker + +import android.app.TimePickerDialog +import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalContext +import java.time.LocalTime + +@Composable +fun TimePicker( + value: LocalTime, + is24HourView: Boolean = false, + onCancel: () -> Unit = {}, + onValueChange: (LocalTime) -> Unit = {}, +) { + val context = LocalContext.current + val dialog by remember { + mutableStateOf( + TimePickerDialog( + context, + { _, hour, minute -> + onValueChange(LocalTime.of(hour, minute)) + }, + value.hour, + value.minute, + is24HourView, + ) + ) + } + + fun setupDialog() { + dialog.apply { + setOnCancelListener { + onCancel() + } + } + } + + LaunchedEffect(key1 = Unit) { + setupDialog() + dialog.show() + } +} \ No newline at end of file diff --git a/modules/common-ui-components/src/main/java/com/ankitsuda/rebound/ui/components/topbar/MenuActionItem.kt b/modules/common-ui-components/src/main/java/com/ankitsuda/rebound/ui/components/topbar/MenuActionItem.kt new file mode 100644 index 00000000..7888e032 --- /dev/null +++ b/modules/common-ui-components/src/main/java/com/ankitsuda/rebound/ui/components/topbar/MenuActionItem.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022 Ankit Suda. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + */ + +package com.ankitsuda.rebound.ui.components.topbar + +data class MenuActionItem( + val title: String, + val onClick: () -> Unit, +) diff --git a/modules/common-ui-components/src/main/java/com/ankitsuda/rebound/ui/components/topbar/TopBarMenuAction.kt b/modules/common-ui-components/src/main/java/com/ankitsuda/rebound/ui/components/topbar/TopBarMenuAction.kt new file mode 100644 index 00000000..ff5fc5da --- /dev/null +++ b/modules/common-ui-components/src/main/java/com/ankitsuda/rebound/ui/components/topbar/TopBarMenuAction.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2022 Ankit Suda. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + */ + +package com.ankitsuda.rebound.ui.components.topbar + +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.MoreVert +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import com.ankitsuda.rebound.ui.components.R +import com.ankitsuda.rebound.ui.components.TopBarIconButton + +@Composable +fun TopBarMenuAction( + modifier: Modifier = Modifier, + icon: ImageVector = Icons.Outlined.MoreVert, + title: String = stringResource(id = R.string.open_menu), + enabled: Boolean = true, + customTint: Color? = null, + actions: List +) { + var isMenuExpanded by remember { + mutableStateOf(false) + } + + fun openMenu() { + isMenuExpanded = true + } + + fun closeMenu() { + isMenuExpanded = false + } + + TopBarIconButton( + modifier = modifier, + icon = icon, + title = title, + enabled = enabled, + customTint = customTint, + onClick = ::openMenu + ) + + DropdownMenu( + modifier = modifier, + expanded = isMenuExpanded, + onDismissRequest = ::closeMenu + ) { + for (action in actions) { + DropdownMenuItem( + onClick = { + closeMenu() + action.onClick() + } + ) { + Text(action.title) + } + } + } +} \ No newline at end of file diff --git a/modules/common-ui-resources/src/main/res/values/strings.xml b/modules/common-ui-resources/src/main/res/values/strings.xml index a50e0d68..4b66fb4c 100644 --- a/modules/common-ui-resources/src/main/res/values/strings.xml +++ b/modules/common-ui-resources/src/main/res/values/strings.xml @@ -248,4 +248,7 @@ No more reps possible Insert Personal records + Adjust duration + Start time + End time \ No newline at end of file diff --git a/modules/ui-workout-edit/src/main/java/com/ankitsuda/rebound/ui/workoutedit/WorkoutEditScreen.kt b/modules/ui-workout-edit/src/main/java/com/ankitsuda/rebound/ui/workoutedit/WorkoutEditScreen.kt index 241fc732..ad1984ba 100644 --- a/modules/ui-workout-edit/src/main/java/com/ankitsuda/rebound/ui/workoutedit/WorkoutEditScreen.kt +++ b/modules/ui-workout-edit/src/main/java/com/ankitsuda/rebound/ui/workoutedit/WorkoutEditScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -36,14 +37,10 @@ import com.ankitsuda.navigation.Navigator import com.ankitsuda.rebound.ui.components.TopBar2 import com.ankitsuda.rebound.ui.components.TopBarBackIconButton import com.ankitsuda.rebound.ui.components.TopBarIconButton +import com.ankitsuda.rebound.ui.components.topbar.MenuActionItem +import com.ankitsuda.rebound.ui.components.topbar.TopBarMenuAction import com.ankitsuda.rebound.ui.components.workouteditor.WorkoutEditorComponent -import com.ankitsuda.rebound.ui.keyboard.LocalReboundSetKeyboard -import com.ankitsuda.rebound.ui.keyboard.ReboundSetKeyboard -import com.ankitsuda.rebound.ui.keyboard.ReboundSetKeyboardComponent -import com.ankitsuda.rebound.ui.keyboard.models.ClearNumKey -import com.ankitsuda.rebound.ui.keyboard.models.DecimalNumKey -import com.ankitsuda.rebound.ui.keyboard.models.NumKey -import com.ankitsuda.rebound.ui.keyboard.models.NumberNumKey +import com.ankitsuda.rebound.ui.components.workouteditor.durationeditor.WorkoutDurationEditorDialog import com.ankitsuda.rebound.ui.theme.ReboundTheme import me.onebone.toolbar.CollapsingToolbarScaffold import me.onebone.toolbar.ScrollStrategy @@ -59,11 +56,11 @@ fun WorkoutEditScreen( val isTemplate = viewModel.isTemplate val logEntriesWithJunction by viewModel.logEntriesWithExerciseJunction.collectAsState() - val workoutName = workout?.name - val workoutNote = workout?.note - val collapsingState = rememberCollapsingToolbarScaffoldState() + var isDurationEditorDialogOpen by rememberSaveable { + mutableStateOf(false) + } Column() { CollapsingToolbarScaffold( @@ -85,12 +82,16 @@ fun WorkoutEditScreen( } }, actions = { - TopBarIconButton( - icon = Icons.Outlined.MoreVert, - title = stringResource(R.string.open_menu) - ) { - - } + TopBarMenuAction( + actions = listOf( + MenuActionItem( + title = stringResource(id = R.string.adjust_duration), + onClick = { + isDurationEditorDialogOpen = true + } + ) + ) + ) }) }) @@ -98,8 +99,8 @@ fun WorkoutEditScreen( WorkoutEditorComponent( navController = navController, navigator = navigator, - workoutName = workoutName, - workoutNote = workoutNote, + workoutName = workout?.name, + workoutNote = workout?.note, useReboundKeyboard = true, cancelWorkoutButtonVisible = false, logEntriesWithJunction = logEntriesWithJunction, @@ -138,5 +139,18 @@ fun WorkoutEditScreen( onRemoveFromSuperset = viewModel::removeFromSuperset, ) } + + if (isDurationEditorDialogOpen && workout?.startAt != null && workout?.completedAt != null) { + WorkoutDurationEditorDialog( + initialStartDateTime = workout?.startAt!!, + initialEndDateTime = workout?.completedAt!!, + onDismissRequest = { + isDurationEditorDialogOpen = false + }, + onSave = { startDateTime, endDateTime -> + viewModel.updateStartAndCompletedAt(startDateTime, endDateTime) + }, + ) + } } } \ No newline at end of file diff --git a/modules/ui-workout-edit/src/main/java/com/ankitsuda/rebound/ui/workoutedit/WorkoutEditScreenViewModel.kt b/modules/ui-workout-edit/src/main/java/com/ankitsuda/rebound/ui/workoutedit/WorkoutEditScreenViewModel.kt index 025972db..67ecbc88 100644 --- a/modules/ui-workout-edit/src/main/java/com/ankitsuda/rebound/ui/workoutedit/WorkoutEditScreenViewModel.kt +++ b/modules/ui-workout-edit/src/main/java/com/ankitsuda/rebound/ui/workoutedit/WorkoutEditScreenViewModel.kt @@ -222,4 +222,17 @@ class WorkoutEditScreenViewModel @Inject constructor( } } + fun updateStartAndCompletedAt(startAt: LocalDateTime, completedAt: LocalDateTime) { + viewModelScope.launch { + mWorkout?.let { + workoutsRepository.updateWorkout( + it.copy( + startAt = startAt, + completedAt = completedAt + ) + ) + } + } + } + } \ No newline at end of file