TrackDateSelectorScreen: Use M3 date picker (#9138)

This commit is contained in:
Ivan Iskandar 2023-02-25 03:22:23 +07:00 committed by GitHub
parent 83a4e34095
commit ec3ce74af8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 185 additions and 140 deletions

View file

@ -3,6 +3,7 @@ package eu.kanade.presentation.track
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer 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.lazy.rememberLazyListState
import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.DatePicker
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.WheelDatePicker
import tachiyomi.presentation.core.components.WheelTextPicker import tachiyomi.presentation.core.components.WheelTextPicker
import tachiyomi.presentation.core.components.material.AlertDialogContent import tachiyomi.presentation.core.components.material.AlertDialogContent
import tachiyomi.presentation.core.components.material.Divider import tachiyomi.presentation.core.components.material.Divider
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.isScrolledToEnd import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrolledToStart import tachiyomi.presentation.core.util.isScrolledToStart
import java.time.LocalDate
import java.time.format.TextStyle
import java.util.Locale
@Composable @Composable
fun TrackStatusSelector( fun TrackStatusSelector(
@ -140,53 +134,47 @@ fun TrackScoreSelector(
@Composable @Composable
fun TrackDateSelector( fun TrackDateSelector(
title: String, title: String,
minDate: LocalDate?, initialSelectedDateMillis: Long,
maxDate: LocalDate?, dateValidator: (Long) -> Boolean,
selection: LocalDate, onConfirm: (Long) -> Unit,
onSelectionChange: (LocalDate) -> Unit,
onConfirm: () -> Unit,
onRemove: (() -> Unit)?, onRemove: (() -> Unit)?,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
) { ) {
BaseSelector( val pickerState = rememberDatePickerState(
title = title, initialSelectedDateMillis = initialSelectedDateMillis,
)
AlertDialogContent(
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars),
content = { content = {
Row( Column {
modifier = Modifier.align(Alignment.Center), DatePicker(
verticalAlignment = Alignment.CenterVertically, state = pickerState,
) { title = { Text(text = title) },
var internalSelection by remember { mutableStateOf(selection) } dateValidator = dateValidator,
Text( showModeToggle = false,
)
Row(
modifier = Modifier modifier = Modifier
.weight(1f) .fillMaxWidth()
.padding(end = 16.dp), .padding(start = 12.dp, top = 8.dp, end = 12.dp, bottom = 24.dp),
text = internalSelection.dayOfWeek horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small, Alignment.End),
.getDisplayName(TextStyle.SHORT, Locale.getDefault()), ) {
textAlign = TextAlign.Center, if (onRemove != null) {
style = MaterialTheme.typography.titleMedium, TextButton(onClick = onRemove) {
) Text(text = stringResource(R.string.action_remove))
WheelDatePicker( }
startDate = selection, Spacer(modifier = Modifier.weight(1f))
minDate = minDate, }
maxDate = maxDate, TextButton(onClick = onDismissRequest) {
onSelectionChanged = { Text(text = stringResource(android.R.string.cancel))
internalSelection = it }
onSelectionChange(it) TextButton(onClick = { onConfirm(pickerState.selectedDateMillis!!) }) {
}, Text(text = stringResource(android.R.string.ok))
) }
}
},
thirdButton = if (onRemove != null) {
{
TextButton(onClick = onRemove) {
Text(text = stringResource(R.string.action_remove))
} }
} }
} else {
null
}, },
onConfirm = onConfirm,
onDismissRequest = onDismissRequest,
) )
} }

View file

@ -77,7 +77,9 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.time.Instant import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId import java.time.ZoneId
import java.time.ZoneOffset
data class TrackInfoDialogHomeScreen( data class TrackInfoDialogHomeScreen(
private val mangaId: Long, private val mangaId: Long,
@ -432,7 +434,6 @@ private data class TrackDateSelectorScreen(
start = start, start = start,
) )
} }
val state by sm.state.collectAsState()
val canRemove = if (start) { val canRemove = if (start) {
track.started_reading_date > 0 track.started_reading_date > 0
@ -445,22 +446,35 @@ private data class TrackDateSelectorScreen(
} else { } else {
stringResource(R.string.track_finished_reading_date) stringResource(R.string.track_finished_reading_date)
}, },
minDate = if (!start && track.started_reading_date > 0) { initialSelectedDateMillis = sm.initialSelection,
// Disallow end date to be set earlier than start date dateValidator = { utcMillis ->
Instant.ofEpochMilli(track.started_reading_date).atZone(ZoneId.systemDefault()).toLocalDate() val dateToCheck = Instant.ofEpochMilli(utcMillis)
} else { .atZone(ZoneOffset.systemDefault())
null .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) { onConfirm = { sm.setDate(it); navigator.pop() },
// 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() },
onRemove = { sm.confirmRemoveDate(navigator) }.takeIf { canRemove }, onRemove = { sm.confirmRemoveDate(navigator) }.takeIf { canRemove },
onDismissRequest = navigator::pop, onDismissRequest = navigator::pop,
) )
@ -470,32 +484,26 @@ private data class TrackDateSelectorScreen(
private val track: Track, private val track: Track,
private val service: TrackService, private val service: TrackService,
private val start: Boolean, private val start: Boolean,
) : StateScreenModel<Model.State>( ) : ScreenModel {
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(),
),
) {
fun setSelection(selection: LocalDate) { // In UTC
mutableState.update { it.copy(selection = selection) } 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 { coroutineScope.launchNonCancellable {
val millis = state.value.selection.atStartOfDay(ZoneId.systemDefault())
.toInstant()
.toEpochMilli()
if (start) { if (start) {
service.setRemoteStartDate(track, millis) service.setRemoteStartDate(track, localMillis)
} else { } else {
service.setRemoteFinishDate(track, millis) service.setRemoteFinishDate(track, localMillis)
} }
} }
} }
@ -503,10 +511,19 @@ private data class TrackDateSelectorScreen(
fun confirmRemoveDate(navigator: Navigator) { fun confirmRemoveDate(navigator: Navigator) {
navigator.push(TrackDateRemoverScreen(track, service.id, start)) navigator.push(TrackDateRemoverScreen(track, service.id, start))
} }
}
data class State( companion object {
val selection: LocalDate, private fun convertEpochMillisZone(
) localMillis: Long,
from: ZoneId,
to: ZoneId,
): Long {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(localMillis), from)
.atZone(to)
.toInstant()
.toEpochMilli()
}
} }
} }

View file

@ -2,7 +2,9 @@ package tachiyomi.presentation.core.components.material
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
@ -20,71 +22,109 @@ fun AlertDialogContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
icon: (@Composable () -> Unit)? = null, icon: (@Composable () -> Unit)? = null,
title: (@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( Column(
modifier = modifier modifier = modifier
.sizeIn(minWidth = MinWidth, maxWidth = MaxWidth) .sizeIn(minWidth = MinWidth, maxWidth = MaxWidth),
.padding(DialogPadding),
) { ) {
icon?.let { if (icon != null || title != null) {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.secondary) { Column(
Box( modifier = Modifier
Modifier .padding(
.padding(IconPadding) start = DialogPadding,
.align(Alignment.CenterHorizontally), top = DialogPadding,
) { end = DialogPadding,
icon() )
.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) {
title?.let { val textStyle = MaterialTheme.typography.headlineSmall
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) { ProvideTextStyle(textStyle) {
val textStyle = MaterialTheme.typography.headlineSmall Box(
ProvideTextStyle(textStyle) { // Align the title to the center when an icon is present.
Box( Modifier
// Align the title to the center when an icon is present. .padding(TitlePadding)
Modifier .align(
.padding(TitlePadding) if (icon == null) {
.align( Alignment.Start
if (icon == null) { } else {
Alignment.Start Alignment.CenterHorizontally
} else { },
Alignment.CenterHorizontally ),
}, ) {
), title()
) { }
title() }
} }
} }
} }
} }
text?.let { content?.invoke(this)
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)
}
}
} }
} }
// Paddings for each of the dialog's parts. // 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 IconPadding = PaddingValues(bottom = 16.dp)
private val TitlePadding = PaddingValues(bottom = 16.dp) private val TitlePadding = PaddingValues(bottom = 16.dp)
private val TextPadding = PaddingValues(bottom = 24.dp) private val TextPadding = PaddingValues(bottom = 24.dp)