From 7ff95e21babda98dd1b479912278d6029cd15f0d Mon Sep 17 00:00:00 2001 From: Maddie Witman Date: Fri, 16 Feb 2024 06:09:00 -0500 Subject: [PATCH] Refactor use of Java.util.date to Java.time.*, to fix localized date issues. (#402) * Add support for localdate based relative times * Update History Screen to use new localdate based relative times * Update Updates Screen to use new localdate based relative times * Cleaned up date util classes * Updated build time display * Code cleanup * Fixed crash in settings * Updated Preferences item * Worker Info works * Fixed Tracker date display * Code changes to pass detekt --- .../java/eu/kanade/domain/ui/UiPreferences.kt | 10 ++-- .../presentation/components/DateText.kt | 25 +++++---- .../presentation/history/HistoryScreen.kt | 4 +- .../HistoryScreenModelStateProvider.kt | 5 +- .../screen/SettingsAppearanceScreen.kt | 4 +- .../more/settings/screen/about/AboutScreen.kt | 19 ++----- .../settings/screen/debug/WorkerInfoScreen.kt | 20 +++++-- .../presentation/track/TrackInfoDialogHome.kt | 9 +-- .../TrackInfoDialogHomePreviewProvider.kt | 7 ++- .../presentation/updates/UpdatesScreen.kt | 4 +- .../ui/history/HistoryScreenModel.kt | 12 ++-- .../ui/updates/UpdatesScreenModel.kt | 14 ++--- .../tachiyomi/util/lang/DateExtensions.kt | 56 ++++++------------- 13 files changed, 89 insertions(+), 100 deletions(-) diff --git a/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt b/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt index 606a1048d4..2efefb5174 100644 --- a/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt +++ b/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt @@ -7,8 +7,8 @@ import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.core.common.preference.getEnum -import java.text.DateFormat -import java.text.SimpleDateFormat +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle import java.util.Locale class UiPreferences( @@ -31,9 +31,9 @@ class UiPreferences( fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC) companion object { - fun dateFormat(format: String): DateFormat = when (format) { - "" -> DateFormat.getDateInstance(DateFormat.SHORT) - else -> SimpleDateFormat(format, Locale.getDefault()) + fun dateFormat(format: String): DateTimeFormatter = when (format) { + "" -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) + else -> DateTimeFormatter.ofPattern(format, Locale.getDefault()) } } } diff --git a/app/src/main/java/eu/kanade/presentation/components/DateText.kt b/app/src/main/java/eu/kanade/presentation/components/DateText.kt index 9fa8c85d19..59708a0493 100644 --- a/app/src/main/java/eu/kanade/presentation/components/DateText.kt +++ b/app/src/main/java/eu/kanade/presentation/components/DateText.kt @@ -4,25 +4,31 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import eu.kanade.domain.ui.UiPreferences -import eu.kanade.tachiyomi.util.lang.toRelativeString +import eu.kanade.tachiyomi.util.lang.toRelativeSting import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.util.Date +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneId @Composable fun relativeDateText( dateEpochMillis: Long, ): String { return relativeDateText( - date = Date(dateEpochMillis).takeIf { dateEpochMillis > 0L }, + localDate = LocalDate.ofInstant( + Instant.ofEpochMilli(dateEpochMillis), + ZoneId.systemDefault(), + ) + .takeIf { dateEpochMillis > 0L }, ) } @Composable fun relativeDateText( - date: Date?, + localDate: LocalDate?, ): String { val context = LocalContext.current @@ -30,11 +36,10 @@ fun relativeDateText( val relativeTime = remember { preferences.relativeTime().get() } val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) } - return date - ?.toRelativeString( - context = context, - relative = relativeTime, - dateFormat = dateFormat, - ) + return localDate?.toRelativeSting( + context = context, + relative = relativeTime, + dateFormat = dateFormat, + ) ?: stringResource(MR.strings.not_applicable) } diff --git a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt index ec25769dca..c15604829c 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt @@ -28,7 +28,7 @@ import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.LoadingScreen -import java.util.Date +import java.time.LocalDate @Composable fun HistoryScreen( @@ -133,7 +133,7 @@ private fun HistoryScreenContent( } sealed interface HistoryUiModel { - data class Header(val date: Date) : HistoryUiModel + data class Header(val date: LocalDate) : HistoryUiModel data class Item(val item: HistoryWithRelations) : HistoryUiModel } diff --git a/app/src/main/java/eu/kanade/presentation/history/HistoryScreenModelStateProvider.kt b/app/src/main/java/eu/kanade/presentation/history/HistoryScreenModelStateProvider.kt index 2c07b81e0b..4ffa15dd1d 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreenModelStateProvider.kt +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreenModelStateProvider.kt @@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.ui.history.HistoryScreenModel import tachiyomi.domain.history.model.HistoryWithRelations import tachiyomi.domain.manga.model.MangaCover import java.time.Instant +import java.time.LocalDate import java.time.temporal.ChronoUnit import java.util.Date import kotlin.random.Random @@ -71,10 +72,10 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider Instant = { it }) = - HistoryUiModel.Header(Date.from(instantBuilder(Instant.now()))) + HistoryUiModel.Header(LocalDate.from(instantBuilder(Instant.now()))) fun items() = sequence { var count = 1 diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt index d1474bf6a0..10ad5e7bde 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt @@ -26,7 +26,7 @@ import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.collectAsState import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.time.Instant +import java.time.LocalDate object SettingsAppearanceScreen : SearchableSettings { @@ -101,7 +101,7 @@ object SettingsAppearanceScreen : SearchableSettings { val context = LocalContext.current val navigator = LocalNavigator.currentOrThrow - val now = remember { Instant.now().toEpochMilli() } + val now = remember { LocalDate.now() } val dateFormat by uiPreferences.dateFormat().collectAsState() val formattedNow = remember(dateFormat) { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt index 58f6905692..615687aff7 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt @@ -55,10 +55,10 @@ import tachiyomi.presentation.core.icons.Reddit import tachiyomi.presentation.core.icons.X import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.text.DateFormat -import java.text.SimpleDateFormat +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter import java.util.Locale -import java.util.TimeZone object AboutScreen : Screen() { @@ -269,16 +269,9 @@ object AboutScreen : Screen() { internal fun getFormattedBuildTime(): String { return try { - val inputDf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.US) - inputDf.timeZone = TimeZone.getTimeZone("UTC") - val buildTime = inputDf.parse(BuildConfig.BUILD_TIME) - - val outputDf = DateFormat.getDateTimeInstance( - DateFormat.MEDIUM, - DateFormat.SHORT, - Locale.getDefault(), - ) - outputDf.timeZone = TimeZone.getDefault() + val df = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'", Locale.US) + .withZone(ZoneId.of("UTC")) + val buildTime = LocalDateTime.from(df.parse(BuildConfig.BUILD_TIME)) buildTime!!.toDateTimestampString(UiPreferences.dateFormat(Injekt.get().dateFormat().get())) } catch (e: Exception) { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt index ea8db81e87..dbe82830da 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt @@ -42,7 +42,9 @@ import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.plus import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.util.Date +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId class WorkerInfoScreen : Screen() { @@ -149,11 +151,17 @@ class WorkerInfoScreen : Screen() { appendLine("State: ${workInfo.state}") if (workInfo.state == WorkInfo.State.ENQUEUED) { appendLine( - "Next scheduled run: ${Date(workInfo.nextScheduleTimeMillis).toDateTimestampString( - UiPreferences.dateFormat( - Injekt.get().dateFormat().get(), - ), - )}", + "Next scheduled run: ${ + LocalDateTime.ofInstant( + Instant.ofEpochMilli(workInfo.nextScheduleTimeMillis), + ZoneId.systemDefault(), + ) + .toDateTimestampString( + UiPreferences.dateFormat( + Injekt.get().dateFormat().get(), + ), + ) + }", ) appendLine("Attempt #${workInfo.runAttemptCount + 1}") } diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt index 11f2b9a06d..9935321803 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt @@ -52,17 +52,18 @@ import eu.kanade.presentation.theme.TachiyomiPreviewTheme import eu.kanade.presentation.track.components.TrackLogoIcon import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.ui.manga.track.TrackItem +import eu.kanade.tachiyomi.util.lang.toLocalDate import eu.kanade.tachiyomi.util.system.copyToClipboard import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource -import java.text.DateFormat +import java.time.format.DateTimeFormatter private const val UnsetStatusTextAlpha = 0.5F @Composable fun TrackInfoDialogHome( trackItems: List, - dateFormat: DateFormat, + dateFormat: DateTimeFormatter, onStatusClick: (TrackItem) -> Unit, onChapterClick: (TrackItem) -> Unit, onScoreClick: (TrackItem) -> Unit, @@ -104,11 +105,11 @@ fun TrackInfoDialogHome( .takeIf { supportsScoring && item.track.score != 0.0 }, onScoreClick = { onScoreClick(item) } .takeIf { supportsScoring }, - startDate = remember(item.track.startDate) { dateFormat.format(item.track.startDate) } + startDate = remember(item.track.startDate) { dateFormat.format(item.track.startDate.toLocalDate()) } .takeIf { supportsReadingDates && item.track.startDate != 0L }, onStartDateClick = { onStartDateEdit(item) } // TODO .takeIf { supportsReadingDates }, - endDate = dateFormat.format(item.track.finishDate) + endDate = dateFormat.format(item.track.finishDate.toLocalDate()) .takeIf { supportsReadingDates && item.track.finishDate != 0L }, onEndDateClick = { onEndDateEdit(item) } .takeIf { supportsReadingDates }, diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHomePreviewProvider.kt b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHomePreviewProvider.kt index 51b7ca3f8d..aa58403eac 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHomePreviewProvider.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHomePreviewProvider.kt @@ -5,7 +5,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import eu.kanade.tachiyomi.ui.manga.track.TrackItem import eu.kanade.test.DummyTracker import tachiyomi.domain.track.model.Track -import java.text.DateFormat +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle internal class TrackInfoDialogHomePreviewProvider : PreviewParameterProvider<@Composable () -> Unit> { @@ -46,7 +47,7 @@ internal class TrackInfoDialogHomePreviewProvider : trackItemWithoutTrack, trackItemWithTrack, ), - dateFormat = DateFormat.getDateInstance(), + dateFormat = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM), onStatusClick = {}, onChapterClick = {}, onScoreClick = {}, @@ -61,7 +62,7 @@ internal class TrackInfoDialogHomePreviewProvider : private val noTrackers = @Composable { TrackInfoDialogHome( trackItems = listOf(), - dateFormat = DateFormat.getDateInstance(), + dateFormat = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM), onStatusClick = {}, onChapterClick = {}, onScoreClick = {}, diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt index ca1e5632ca..ac97de83f2 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt @@ -36,7 +36,7 @@ import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.LoadingScreen -import java.util.Date +import java.time.LocalDate import kotlin.time.Duration.Companion.seconds @Composable @@ -206,6 +206,6 @@ private fun UpdatesBottomBar( } sealed interface UpdatesUiModel { - data class Header(val date: Date) : UpdatesUiModel + data class Header(val date: LocalDate) : UpdatesUiModel data class Item(val item: UpdatesItem) : UpdatesUiModel } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt index a344fcda34..7b0cdcf725 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt @@ -5,7 +5,7 @@ import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope import eu.kanade.core.util.insertSeparators import eu.kanade.presentation.history.HistoryUiModel -import eu.kanade.tachiyomi.util.lang.toDateKey +import eu.kanade.tachiyomi.util.lang.toLocalDate import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow @@ -28,7 +28,7 @@ import tachiyomi.domain.history.interactor.RemoveHistory import tachiyomi.domain.history.model.HistoryWithRelations import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.util.Date +import java.time.LocalDate class HistoryScreenModel( private val getHistory: GetHistory = Injekt.get(), @@ -60,10 +60,12 @@ class HistoryScreenModel( private fun List.toHistoryUiModels(): List { return map { HistoryUiModel.Item(it) } .insertSeparators { before, after -> - val beforeDate = before?.item?.readAt?.time?.toDateKey() ?: Date(0) - val afterDate = after?.item?.readAt?.time?.toDateKey() ?: Date(0) + val beforeDate = before?.item?.readAt?.time?.toLocalDate() ?: LocalDate.MIN + val afterDate = after?.item?.readAt?.time?.toLocalDate() ?: LocalDate.MIN when { - beforeDate.time != afterDate.time && afterDate.time != 0L -> HistoryUiModel.Header(afterDate) + beforeDate.isAfter(afterDate) + or afterDate.equals(LocalDate.MIN) + or beforeDate.equals(LocalDate.MIN) -> HistoryUiModel.Header(afterDate) // Return null to avoid adding a separator between two items. else -> null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt index f76ac45fe7..2b2c1c25a5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt @@ -16,7 +16,7 @@ import eu.kanade.tachiyomi.data.download.DownloadCache import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.library.LibraryUpdateJob -import eu.kanade.tachiyomi.util.lang.toDateKey +import eu.kanade.tachiyomi.util.lang.toLocalDate import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.mutate import kotlinx.collections.immutable.persistentListOf @@ -45,8 +45,8 @@ import tachiyomi.domain.updates.interactor.GetUpdates import tachiyomi.domain.updates.model.UpdatesWithRelations import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.time.LocalDate import java.time.ZonedDateTime -import java.util.Date class UpdatesScreenModel( private val sourceManager: SourceManager = Injekt.get(), @@ -374,12 +374,12 @@ class UpdatesScreenModel( return items .map { UpdatesUiModel.Item(it) } .insertSeparators { before, after -> - val beforeDate = before?.item?.update?.dateFetch?.toDateKey() ?: Date(0) - val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0) + val beforeDate = before?.item?.update?.dateFetch?.toLocalDate() ?: LocalDate.MIN + val afterDate = after?.item?.update?.dateFetch?.toLocalDate() ?: LocalDate.MIN when { - beforeDate.time != afterDate.time && afterDate.time != 0L -> { - UpdatesUiModel.Header(afterDate) - } + beforeDate.isAfter(afterDate) + or afterDate.equals(LocalDate.MIN) + or beforeDate.equals(LocalDate.MIN) -> UpdatesUiModel.Header(afterDate) // Return null to avoid adding a separator between two items. else -> null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt index a313f5a2df..e1a1ea62f8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt @@ -6,15 +6,17 @@ import tachiyomi.core.common.i18n.stringResource import tachiyomi.i18n.MR import java.text.DateFormat import java.time.Instant +import java.time.LocalDate import java.time.LocalDateTime import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle import java.time.temporal.ChronoUnit -import java.util.Calendar import java.util.Date -fun Date.toDateTimestampString(dateFormatter: DateFormat): String { - val date = dateFormatter.format(this) - val time = DateFormat.getTimeInstance(DateFormat.SHORT).format(this) +fun LocalDateTime.toDateTimestampString(dateTimeFormatter: DateTimeFormatter): String { + val date = dateTimeFormatter.format(this) + val time = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).format(this) return "$date $time" } @@ -32,52 +34,28 @@ fun Long.convertEpochMillisZone( .toEpochMilli() } -/** - * Get date as time key - * - * @param date desired date - * @return date as time key - */ -fun Long.toDateKey(): Date { - val instant = Instant.ofEpochMilli(this) - return Date.from(instant.truncatedTo(ChronoUnit.DAYS)) +fun Long.toLocalDate(): LocalDate { + return LocalDate.ofInstant(Instant.ofEpochMilli(this), ZoneId.systemDefault()) } -fun Date.toRelativeString( +fun LocalDate.toRelativeSting( context: Context, relative: Boolean = true, - dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT), + dateFormat: DateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT), ): String { if (!relative) { return dateFormat.format(this) } - val now = Date() - val difference = now.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) - - this.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) - val days = difference.floorDiv(MILLISECONDS_IN_DAY).toInt() + val now = LocalDate.now() + val difference = ChronoUnit.DAYS.between(this, now) return when { - difference < 0 -> dateFormat.format(this) - difference < MILLISECONDS_IN_DAY -> context.stringResource(MR.strings.relative_time_today) - difference < MILLISECONDS_IN_DAY.times(7) -> context.pluralStringResource( + difference < 0 -> difference.toString() + difference < 1 -> context.stringResource(MR.strings.relative_time_today) + difference < 7 -> context.pluralStringResource( MR.plurals.relative_time, - days, - days, + difference.toInt(), + difference.toInt(), ) else -> dateFormat.format(this) } } - -private const val MILLISECONDS_IN_DAY = 86_400_000L - -private val Date.timeWithOffset: Long - get() { - return Calendar.getInstance().run { - time = this@timeWithOffset - val dstOffset = get(Calendar.DST_OFFSET) - this@timeWithOffset.time + timeZone.rawOffset + dstOffset - } - } - -private fun Long.floorNearest(to: Long): Long { - return this.floorDiv(to) * to -}