From ec3ce74af8d6f16b58b368e78acf72f929f3f9e6 Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Sat, 25 Feb 2023 03:22:23 +0700 Subject: [PATCH] TrackDateSelectorScreen: Use M3 date picker (#9138) --- .../track/TrackInfoDialogSelector.kt | 84 +++++----- .../ui/manga/track/TrackInfoDialog.kt | 97 +++++++----- .../core/components/material/AlertDialog.kt | 144 +++++++++++------- 3 files changed, 185 insertions(+), 140 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt index 919c9cf2c..0f0d9593a 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt @@ -3,6 +3,7 @@ package eu.kanade.presentation.track import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer @@ -14,34 +15,27 @@ import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.DatePicker import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.minimumInteractiveComponentSize +import androidx.compose.material3.rememberDatePickerState import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import eu.kanade.tachiyomi.R import tachiyomi.presentation.core.components.ScrollbarLazyColumn -import tachiyomi.presentation.core.components.WheelDatePicker import tachiyomi.presentation.core.components.WheelTextPicker import tachiyomi.presentation.core.components.material.AlertDialogContent import tachiyomi.presentation.core.components.material.Divider import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.util.isScrolledToEnd import tachiyomi.presentation.core.util.isScrolledToStart -import java.time.LocalDate -import java.time.format.TextStyle -import java.util.Locale @Composable fun TrackStatusSelector( @@ -140,53 +134,47 @@ fun TrackScoreSelector( @Composable fun TrackDateSelector( title: String, - minDate: LocalDate?, - maxDate: LocalDate?, - selection: LocalDate, - onSelectionChange: (LocalDate) -> Unit, - onConfirm: () -> Unit, + initialSelectedDateMillis: Long, + dateValidator: (Long) -> Boolean, + onConfirm: (Long) -> Unit, onRemove: (() -> Unit)?, onDismissRequest: () -> Unit, ) { - BaseSelector( - title = title, + val pickerState = rememberDatePickerState( + initialSelectedDateMillis = initialSelectedDateMillis, + ) + AlertDialogContent( + modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars), content = { - Row( - modifier = Modifier.align(Alignment.Center), - verticalAlignment = Alignment.CenterVertically, - ) { - var internalSelection by remember { mutableStateOf(selection) } - Text( + Column { + DatePicker( + state = pickerState, + title = { Text(text = title) }, + dateValidator = dateValidator, + showModeToggle = false, + ) + + Row( modifier = Modifier - .weight(1f) - .padding(end = 16.dp), - text = internalSelection.dayOfWeek - .getDisplayName(TextStyle.SHORT, Locale.getDefault()), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleMedium, - ) - WheelDatePicker( - startDate = selection, - minDate = minDate, - maxDate = maxDate, - onSelectionChanged = { - internalSelection = it - onSelectionChange(it) - }, - ) - } - }, - thirdButton = if (onRemove != null) { - { - TextButton(onClick = onRemove) { - Text(text = stringResource(R.string.action_remove)) + .fillMaxWidth() + .padding(start = 12.dp, top = 8.dp, end = 12.dp, bottom = 24.dp), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small, Alignment.End), + ) { + if (onRemove != null) { + TextButton(onClick = onRemove) { + Text(text = stringResource(R.string.action_remove)) + } + Spacer(modifier = Modifier.weight(1f)) + } + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(android.R.string.cancel)) + } + TextButton(onClick = { onConfirm(pickerState.selectedDateMillis!!) }) { + Text(text = stringResource(android.R.string.ok)) + } } } - } else { - null }, - onConfirm = onConfirm, - onDismissRequest = onDismissRequest, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt index 9ed5bc594..a117411ac 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt @@ -77,7 +77,9 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.time.Instant import java.time.LocalDate +import java.time.LocalDateTime import java.time.ZoneId +import java.time.ZoneOffset data class TrackInfoDialogHomeScreen( private val mangaId: Long, @@ -432,7 +434,6 @@ private data class TrackDateSelectorScreen( start = start, ) } - val state by sm.state.collectAsState() val canRemove = if (start) { track.started_reading_date > 0 @@ -445,22 +446,35 @@ private data class TrackDateSelectorScreen( } else { stringResource(R.string.track_finished_reading_date) }, - minDate = if (!start && track.started_reading_date > 0) { - // Disallow end date to be set earlier than start date - Instant.ofEpochMilli(track.started_reading_date).atZone(ZoneId.systemDefault()).toLocalDate() - } else { - null + initialSelectedDateMillis = sm.initialSelection, + dateValidator = { utcMillis -> + val dateToCheck = Instant.ofEpochMilli(utcMillis) + .atZone(ZoneOffset.systemDefault()) + .toLocalDate() + + if (dateToCheck > LocalDate.now()) { + // Disallow future dates + return@TrackDateSelector false + } + + if (start && track.finished_reading_date > 0) { + // Disallow start date to be set later than finish date + val dateFinished = Instant.ofEpochMilli(track.finished_reading_date) + .atZone(ZoneId.systemDefault()) + .toLocalDate() + dateToCheck <= dateFinished + } else if (!start && track.started_reading_date > 0) { + // Disallow end date to be set earlier than start date + val dateStarted = Instant.ofEpochMilli(track.started_reading_date) + .atZone(ZoneId.systemDefault()) + .toLocalDate() + dateToCheck >= dateStarted + } else { + // Nothing set before + true + } }, - maxDate = if (start && track.finished_reading_date > 0) { - // Disallow start date to be set later than finish date - Instant.ofEpochMilli(track.finished_reading_date).atZone(ZoneId.systemDefault()).toLocalDate() - } else { - // Disallow future dates - LocalDate.now() - }, - selection = state.selection, - onSelectionChange = sm::setSelection, - onConfirm = { sm.setDate(); navigator.pop() }, + onConfirm = { sm.setDate(it); navigator.pop() }, onRemove = { sm.confirmRemoveDate(navigator) }.takeIf { canRemove }, onDismissRequest = navigator::pop, ) @@ -470,32 +484,26 @@ private data class TrackDateSelectorScreen( private val track: Track, private val service: TrackService, private val start: Boolean, - ) : StateScreenModel( - State( - (if (start) track.started_reading_date else track.finished_reading_date) - .takeIf { it != 0L } - ?.let { - Instant.ofEpochMilli(it) - .atZone(ZoneId.systemDefault()) - .toLocalDate() - } - ?: LocalDate.now(), - ), - ) { + ) : ScreenModel { - fun setSelection(selection: LocalDate) { - mutableState.update { it.copy(selection = selection) } - } + // In UTC + val initialSelection: Long + get() { + val millis = (if (start) track.started_reading_date else track.finished_reading_date) + .takeIf { it != 0L } + ?: Instant.now().toEpochMilli() + return convertEpochMillisZone(millis, ZoneOffset.systemDefault(), ZoneOffset.UTC) + } - fun setDate() { + // In UTC + fun setDate(millis: Long) { + // Convert to local time + val localMillis = convertEpochMillisZone(millis, ZoneOffset.UTC, ZoneOffset.systemDefault()) coroutineScope.launchNonCancellable { - val millis = state.value.selection.atStartOfDay(ZoneId.systemDefault()) - .toInstant() - .toEpochMilli() if (start) { - service.setRemoteStartDate(track, millis) + service.setRemoteStartDate(track, localMillis) } else { - service.setRemoteFinishDate(track, millis) + service.setRemoteFinishDate(track, localMillis) } } } @@ -503,10 +511,19 @@ private data class TrackDateSelectorScreen( fun confirmRemoveDate(navigator: Navigator) { navigator.push(TrackDateRemoverScreen(track, service.id, start)) } + } - data class State( - val selection: LocalDate, - ) + companion object { + private fun convertEpochMillisZone( + localMillis: Long, + from: ZoneId, + to: ZoneId, + ): Long { + return LocalDateTime.ofInstant(Instant.ofEpochMilli(localMillis), from) + .atZone(to) + .toInstant() + .toEpochMilli() + } } } diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/AlertDialog.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/AlertDialog.kt index 74f5ec8f0..9bbce88bf 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/AlertDialog.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/AlertDialog.kt @@ -2,7 +2,9 @@ package tachiyomi.presentation.core.components.material import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.sizeIn import androidx.compose.material3.LocalContentColor @@ -20,71 +22,109 @@ fun AlertDialogContent( modifier: Modifier = Modifier, icon: (@Composable () -> Unit)? = null, title: (@Composable () -> Unit)? = null, - text: @Composable (() -> Unit)? = null, + text: @Composable () -> Unit, +) { + AlertDialogContent( + modifier = modifier, + icon = icon, + title = title, + content = { + Column { + CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) { + val textStyle = MaterialTheme.typography.bodyMedium + ProvideTextStyle(textStyle) { + Box( + Modifier + .weight(weight = 1f, fill = false) + .padding(horizontal = DialogPadding) + .padding(TextPadding) + .align(Alignment.Start), + ) { + text() + } + } + } + + Box( + modifier = Modifier + .padding( + start = DialogPadding, + end = DialogPadding, + bottom = DialogPadding, + ) + .align(Alignment.End), + ) { + CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) { + val textStyle = MaterialTheme.typography.labelLarge + ProvideTextStyle(value = textStyle, content = buttons) + } + } + } + }, + ) +} + +@Composable +fun AlertDialogContent( + modifier: Modifier = Modifier, + icon: (@Composable () -> Unit)? = null, + title: (@Composable () -> Unit)? = null, + content: @Composable (ColumnScope.() -> Unit)? = null, ) { Column( modifier = modifier - .sizeIn(minWidth = MinWidth, maxWidth = MaxWidth) - .padding(DialogPadding), + .sizeIn(minWidth = MinWidth, maxWidth = MaxWidth), ) { - icon?.let { - CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.secondary) { - Box( - Modifier - .padding(IconPadding) - .align(Alignment.CenterHorizontally), - ) { - icon() + if (icon != null || title != null) { + Column( + modifier = Modifier + .padding( + start = DialogPadding, + top = DialogPadding, + end = DialogPadding, + ) + .fillMaxWidth(), + ) { + icon?.let { + CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.secondary) { + Box( + Modifier + .padding(IconPadding) + .align(Alignment.CenterHorizontally), + ) { + icon() + } + } } - } - } - title?.let { - CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) { - val textStyle = MaterialTheme.typography.headlineSmall - ProvideTextStyle(textStyle) { - Box( - // Align the title to the center when an icon is present. - Modifier - .padding(TitlePadding) - .align( - if (icon == null) { - Alignment.Start - } else { - Alignment.CenterHorizontally - }, - ), - ) { - title() + title?.let { + CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) { + val textStyle = MaterialTheme.typography.headlineSmall + ProvideTextStyle(textStyle) { + Box( + // Align the title to the center when an icon is present. + Modifier + .padding(TitlePadding) + .align( + if (icon == null) { + Alignment.Start + } else { + Alignment.CenterHorizontally + }, + ), + ) { + title() + } + } } } } } - text?.let { - CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) { - val textStyle = MaterialTheme.typography.bodyMedium - ProvideTextStyle(textStyle) { - Box( - Modifier - .weight(weight = 1f, fill = false) - .padding(TextPadding) - .align(Alignment.Start), - ) { - text() - } - } - } - } - Box(modifier = Modifier.align(Alignment.End)) { - CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) { - val textStyle = MaterialTheme.typography.labelLarge - ProvideTextStyle(value = textStyle, content = buttons) - } - } + content?.invoke(this) } } // Paddings for each of the dialog's parts. -private val DialogPadding = PaddingValues(all = 24.dp) +private val DialogPadding = 24.dp private val IconPadding = PaddingValues(bottom = 16.dp) private val TitlePadding = PaddingValues(bottom = 16.dp) private val TextPadding = PaddingValues(bottom = 24.dp)