diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d712bc27e..c906cf26f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -231,6 +231,7 @@ dependencies { implementation("com.google.android.material:compose-theme-adapter:${Versions.compose_theme_adapter}") implementation("androidx.activity:activity-compose:1.4.0") implementation("androidx.compose.material:material-icons-extended:${Versions.compose}") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1") releaseCompileOnly("androidx.compose.ui:ui-tooling:${Versions.compose}") implementation("com.google.accompanist:accompanist-flowlayout:${Versions.accompanist}") diff --git a/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.kt b/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.kt index 3088bee43..916acbabe 100644 --- a/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.kt +++ b/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.kt @@ -1,37 +1,27 @@ package com.todoroo.astrid.ui +import android.Manifest import android.app.Activity import android.content.DialogInterface import android.content.Intent import android.os.Bundle -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.material.ripple.rememberRipple -import androidx.compose.runtime.Composable +import android.view.View +import android.view.ViewGroup +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.unit.dp +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.viewModels import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionStatus import com.google.accompanist.permissions.rememberPermissionState +import com.google.android.material.composethemeadapter.MdcTheme import com.todoroo.andlib.utility.AndroidUtilities -import com.todoroo.andlib.utility.AndroidUtilities.atLeastTiramisu import dagger.hilt.android.AndroidEntryPoint import org.tasks.R import org.tasks.activities.DateAndTimePickerActivity -import org.tasks.compose.AddReminderDialog -import org.tasks.compose.AlarmRow -import org.tasks.compose.DisabledText import org.tasks.compose.collectAsStateLifecycleAware +import org.tasks.compose.edit.AlarmRow import org.tasks.data.Alarm import org.tasks.data.Alarm.Companion.TYPE_DATE_TIME import org.tasks.data.Alarm.Companion.TYPE_REL_END @@ -42,8 +32,8 @@ import org.tasks.data.Alarm.Companion.whenStarted import org.tasks.date.DateTimeUtils import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.MyTimePickerDialog -import org.tasks.reminders.AlarmToString import org.tasks.ui.TaskEditControlFragment +import java.util.* import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -51,7 +41,7 @@ import javax.inject.Inject class ReminderControlSet : TaskEditControlFragment() { @Inject lateinit var activity: Activity @Inject lateinit var dialogBuilder: DialogBuilder - @Inject lateinit var alarmToString: AlarmToString + @Inject lateinit var locale: Locale data class ViewState( val showCustomDialog: Boolean = false, @@ -92,7 +82,7 @@ class ReminderControlSet : TaskEditControlFragment() { } private fun addAlarm(selected: String) { - val id = viewModel.task?.id ?: 0 + val id = viewModel.task.id when (selected) { getString(R.string.when_started) -> addAlarmRow(whenStarted(id)) @@ -124,112 +114,38 @@ class ReminderControlSet : TaskEditControlFragment() { } } - @OptIn(ExperimentalComposeUiApi::class, ExperimentalPermissionsApi::class) - @Composable - override fun Body() { - val viewState = vm.viewState.collectAsStateLifecycleAware() - val current: ViewState = viewState.value - val notificationPermissions = if (atLeastTiramisu()) { - rememberPermissionState( - android.Manifest.permission.POST_NOTIFICATIONS - ) - } else { - null - } - when (notificationPermissions?.status ?: PermissionStatus.Granted) { - PermissionStatus.Granted -> - Alarms() - is PermissionStatus.Denied -> { - Column( - modifier = Modifier.clickable { - notificationPermissions?.launchPermissionRequest() - } - ) { - Spacer(modifier = Modifier.height(20.dp)) - Text( - text = stringResource(id = R.string.enable_reminders), - color = colorResource(id = R.color.red_500), - ) - Text( - text = stringResource(id = R.string.enable_reminders_description), - style = MaterialTheme.typography.caption, - color = colorResource(id = R.color.red_500), - ) - Spacer(modifier = Modifier.height(20.dp)) - } - } - } - - AddReminderDialog.AddCustomReminderDialog( - openDialog = current.showCustomDialog, - addAlarm = this::addAlarmRow, - closeDialog = { - vm.showCustomDialog(visible = false) - AndroidUtilities.hideKeyboard(activity) - } - ) - - AddReminderDialog.AddRandomReminderDialog( - openDialog = current.showRandomDialog, - addAlarm = this::addAlarmRow, - closeDialog = { - vm.showRandomDialog(visible = false) - AndroidUtilities.hideKeyboard(activity) - } - ) - } - - @Composable - fun Alarms() { - Column { - val alarms = viewModel.selectedAlarms.collectAsStateLifecycleAware() - Spacer(modifier = Modifier.height(8.dp)) - alarms.value.forEach { alarm -> - AlarmRow(alarmToString.toString(alarm)) { - viewModel.selectedAlarms.value = - viewModel.selectedAlarms.value.minus(alarm) - } - } - Row(modifier = Modifier.fillMaxWidth()) { - DisabledText( - text = stringResource(id = R.string.add_reminder), - modifier = Modifier - .padding(vertical = 12.dp) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false), - onClick = { addAlarm() } + @OptIn(ExperimentalPermissionsApi::class) + override fun bind(parent: ViewGroup?): View = + (parent as ComposeView).apply { + setContent { + MdcTheme { + val ringMode by remember { this@ReminderControlSet.ringMode } + val notificationPermissions = if (AndroidUtilities.atLeastTiramisu()) { + rememberPermissionState( + Manifest.permission.POST_NOTIFICATIONS ) - ) - Spacer(modifier = Modifier.weight(1f)) - val ringMode = remember { this@ReminderControlSet.ringMode } - if (alarms.value.isNotEmpty()) { - Text( - text = stringResource( - id = when (ringMode.value) { - 2 -> R.string.ring_nonstop - 1 -> R.string.ring_five_times - else -> R.string.ring_once - } - ), - style = MaterialTheme.typography.body1.copy( - textDecoration = TextDecoration.Underline - ), - modifier = Modifier - .padding(vertical = 12.dp, horizontal = 16.dp) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false), - onClick = { onClickRingType() } - ) + } else { + null + } + AlarmRow( + locale = locale, + alarms = viewModel.selectedAlarms.collectAsStateLifecycleAware().value, + permissionStatus = notificationPermissions?.status + ?: PermissionStatus.Granted, + launchPermissionRequest = { + notificationPermissions?.launchPermissionRequest() + }, + ringMode = ringMode, + newAlarm = this@ReminderControlSet::addAlarm, + addAlarm = this@ReminderControlSet::addAlarmRow, + openRingType = this@ReminderControlSet::onClickRingType, + deleteAlarm = { + viewModel.selectedAlarms.value = viewModel.selectedAlarms.value.minus(it) + } ) } } - Spacer(modifier = Modifier.height(8.dp)) } - } - - override val icon = R.drawable.ic_outline_notifications_24px override fun controlId() = TAG @@ -284,4 +200,3 @@ class ReminderControlSet : TaskEditControlFragment() { private const val REQUEST_NEW_ALARM = 12152 } } - diff --git a/app/src/main/java/org/tasks/compose/edit/AlarmRow.kt b/app/src/main/java/org/tasks/compose/edit/AlarmRow.kt new file mode 100644 index 000000000..7a44d090c --- /dev/null +++ b/app/src/main/java/org/tasks/compose/edit/AlarmRow.kt @@ -0,0 +1,196 @@ +package org.tasks.compose.edit + +import android.content.res.Configuration +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionStatus +import com.google.android.material.composethemeadapter.MdcTheme +import com.todoroo.astrid.ui.ReminderControlSet +import com.todoroo.astrid.ui.ReminderControlSetViewModel +import org.tasks.R +import org.tasks.compose.AddReminderDialog +import org.tasks.compose.DisabledText +import org.tasks.compose.TaskEditRow +import org.tasks.compose.collectAsStateLifecycleAware +import org.tasks.data.Alarm +import org.tasks.reminders.AlarmToString +import java.util.* + +@OptIn(ExperimentalPermissionsApi::class, ExperimentalComposeUiApi::class) +@Composable +fun AlarmRow( + vm: ReminderControlSetViewModel = viewModel(), + permissionStatus: PermissionStatus, + launchPermissionRequest: () -> Unit, + alarms: List, + ringMode: Int, + locale: Locale, + newAlarm: () -> Unit, + addAlarm: (Alarm) -> Unit, + deleteAlarm: (Alarm) -> Unit, + openRingType: () -> Unit, +) { + TaskEditRow( + iconRes = R.drawable.ic_outline_notifications_24px, + content = { + val viewState = vm.viewState.collectAsStateLifecycleAware() + val current: ReminderControlSet.ViewState = viewState.value + + when (permissionStatus) { + PermissionStatus.Granted -> { + Alarms( + alarms = alarms, + ringMode = ringMode, + locale = locale, + addAlarm = newAlarm, + deleteAlarm = deleteAlarm, + openRingType = openRingType, + ) + } + is PermissionStatus.Denied -> { + Column( + modifier = Modifier + .padding(end = 16.dp) + .clickable { + launchPermissionRequest() + } + ) { + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = stringResource(id = R.string.enable_reminders), + color = colorResource(id = R.color.red_500), + ) + Text( + text = stringResource(id = R.string.enable_reminders_description), + style = MaterialTheme.typography.caption, + color = colorResource(id = R.color.red_500), + ) + Spacer(modifier = Modifier.height(20.dp)) + } + } + } + + AddReminderDialog.AddCustomReminderDialog( + openDialog = current.showCustomDialog, + addAlarm = addAlarm, + closeDialog = { vm.showCustomDialog(visible = false) } + ) + + AddReminderDialog.AddRandomReminderDialog( + openDialog = current.showRandomDialog, + addAlarm = addAlarm, + closeDialog = { vm.showRandomDialog(visible = false) } + ) + }, + ) +} + +@Composable +fun Alarms( + alarms: List, + ringMode: Int, + locale: Locale, + addAlarm: () -> Unit, + deleteAlarm: (Alarm) -> Unit, + openRingType: () -> Unit, +) { + Column { + Spacer(modifier = Modifier.height(8.dp)) + alarms.forEach { alarm -> + org.tasks.compose.AlarmRow(AlarmToString(LocalContext.current, locale).toString(alarm)) { + deleteAlarm(alarm) + } + } + Row(modifier = Modifier.fillMaxWidth()) { + DisabledText( + text = stringResource(id = R.string.add_reminder), + modifier = Modifier + .padding(vertical = 12.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + onClick = { addAlarm() } + ) + ) + Spacer(modifier = Modifier.weight(1f)) + if (alarms.isNotEmpty()) { + Text( + text = stringResource( + id = when (ringMode) { + 2 -> R.string.ring_nonstop + 1 -> R.string.ring_five_times + else -> R.string.ring_once + } + ), + style = MaterialTheme.typography.body1.copy( + textDecoration = TextDecoration.Underline + ), + modifier = Modifier + .padding(vertical = 12.dp, horizontal = 16.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + onClick = openRingType + ) + ) + } + } + Spacer(modifier = Modifier.height(8.dp)) + } +} + +@OptIn(ExperimentalPermissionsApi::class) +@Preview(showBackground = true, widthDp = 320) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320) +@Composable +fun NoAlarms() { + MdcTheme { + AlarmRow( + alarms = emptyList(), + ringMode = 0, + locale = Locale.getDefault(), + newAlarm = {}, + addAlarm = {}, + deleteAlarm = {}, + openRingType = {}, + permissionStatus = PermissionStatus.Granted, + launchPermissionRequest = {} + ) + } +} + +@OptIn(ExperimentalPermissionsApi::class) +@Preview(showBackground = true, widthDp = 320) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320) +@Composable +fun PermissionDenied() { + MdcTheme { + AlarmRow( + alarms = emptyList(), + ringMode = 0, + locale = Locale.getDefault(), + newAlarm = {}, + addAlarm = {}, + deleteAlarm = {}, + openRingType = {}, + permissionStatus = PermissionStatus.Denied(true), + launchPermissionRequest = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/reminders/AlarmToString.kt b/app/src/main/java/org/tasks/reminders/AlarmToString.kt index e4fdf3fdb..0b4800087 100644 --- a/app/src/main/java/org/tasks/reminders/AlarmToString.kt +++ b/app/src/main/java/org/tasks/reminders/AlarmToString.kt @@ -3,18 +3,13 @@ package org.tasks.reminders import android.content.Context import android.content.res.Resources import com.todoroo.andlib.utility.DateUtilities -import dagger.hilt.android.qualifiers.ApplicationContext import org.tasks.R import org.tasks.data.Alarm import java.util.* import java.util.concurrent.TimeUnit -import javax.inject.Inject import kotlin.math.absoluteValue -class AlarmToString @Inject constructor( - @ApplicationContext context: Context, - var locale: Locale, -) { +class AlarmToString constructor(context: Context, var locale: Locale) { private val resources = context.resources fun toString(alarm: Alarm): String { diff --git a/deps_fdroid.txt b/deps_fdroid.txt index 81febf895..3389ec0dc 100644 --- a/deps_fdroid.txt +++ b/deps_fdroid.txt @@ -567,6 +567,11 @@ +| +--- androidx.compose.material:material-icons-core:1.2.0-rc02 (*) +| +--- androidx.compose.runtime:runtime:1.2.0-rc02 (*) +| \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.6.21 +++--- androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1 ++| +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0 -> 2.5.0-rc01 (*) ++| +--- androidx.compose.runtime:runtime:1.0.1 -> 1.2.0-rc02 (*) ++| +--- androidx.compose.ui:ui:1.0.1 -> 1.2.0-rc02 (*) ++| \--- org.jetbrains.kotlin:kotlin-stdlib:1.5.30 -> 1.6.21 (*) ++--- com.google.accompanist:accompanist-flowlayout:0.24.12-rc +| +--- androidx.compose.foundation:foundation:1.2.0-rc02 (*) +| \--- androidx.compose.ui:ui-util:1.2.0-rc02 (*) diff --git a/deps_googleplay.txt b/deps_googleplay.txt index 315d84444..f9e65757c 100644 --- a/deps_googleplay.txt +++ b/deps_googleplay.txt @@ -703,6 +703,11 @@ +| +--- androidx.compose.material:material-icons-core:1.2.0-rc02 (*) +| +--- androidx.compose.runtime:runtime:1.2.0-rc02 (*) +| \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.6.21 +++--- androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1 ++| +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0 -> 2.5.0-rc01 (*) ++| +--- androidx.compose.runtime:runtime:1.0.1 -> 1.2.0-rc02 (*) ++| +--- androidx.compose.ui:ui:1.0.1 -> 1.2.0-rc02 (*) ++| \--- org.jetbrains.kotlin:kotlin-stdlib:1.5.30 -> 1.6.21 (*) ++--- com.google.accompanist:accompanist-flowlayout:0.24.12-rc +| +--- androidx.compose.foundation:foundation:1.2.0-rc02 (*) +| \--- androidx.compose.ui:ui-util:1.2.0-rc02 (*)