Browse Source

TrackDateSelectorScreen: Use M3 date picker (#9138)

Ivan Iskandar 2 years ago
parent
commit
ec3ce74af8

+ 36 - 48
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(
-                    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)
-                    },
+            Column {
+                DatePicker(
+                    state = pickerState,
+                    title = { Text(text = title) },
+                    dateValidator = dateValidator,
+                    showModeToggle = false,
                 )
-            }
-        },
-        thirdButton = if (onRemove != null) {
-            {
-                TextButton(onClick = onRemove) {
-                    Text(text = stringResource(R.string.action_remove))
+
+                Row(
+                    modifier = Modifier
+                        .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,
     )
 }
 

+ 57 - 40
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
-            },
-            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()
+            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
+                }
             },
-            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<Model.State>(
-        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()
+        }
     }
 }
 

+ 90 - 50
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,
 ) {
-    Column(
-        modifier = modifier
-            .sizeIn(minWidth = MinWidth, maxWidth = MaxWidth)
-            .padding(DialogPadding),
-    ) {
-        icon?.let {
-            CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.secondary) {
+    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
-                        .padding(IconPadding)
-                        .align(Alignment.CenterHorizontally),
+                    modifier = Modifier
+                        .padding(
+                            start = DialogPadding,
+                            end = DialogPadding,
+                            bottom = DialogPadding,
+                        )
+                        .align(Alignment.End),
                 ) {
-                    icon()
+                    CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) {
+                        val textStyle = MaterialTheme.typography.labelLarge
+                        ProvideTextStyle(value = textStyle, content = buttons)
+                    }
                 }
             }
-        }
-        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()
+        },
+    )
+}
+
+@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),
+    ) {
+        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()
+                        }
                     }
                 }
-            }
-        }
-        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()
+                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()
+                            }
+                        }
                     }
                 }
             }
         }
-        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)