From 6d69903a561b3b9445578eb9b2479f064f4926e8 Mon Sep 17 00:00:00 2001 From: MajorTanya <39014446+MajorTanya@users.noreply.github.com> Date: Sun, 2 Aug 2026 12:58:25 +0200 Subject: [PATCH] Replace java.time APIs with kotlin(x).(date)time (#3001) * Replace java.time APIs with kotlin(x).(date)time kotlinx-datetime explicitly says in its README that it does not cover i18n, so we have to stick with using their toJavaX converters wherever we localise dates and times. Yes, some of these replacements are... questionable. But I wanted to try and maximise the replacement for now. * Bump kotlinx-datetime to 0.8.0 * More kotlin.time/kotlinx-datetime replacements * Remove redundant init block & make attributes val * Replace new use of java.time.Instant --- app/build.gradle.kts | 2 + app/proguard-rules.pro | 3 + .../interactor/SyncChaptersWithSource.kt | 13 ++- .../domain/manga/interactor/UpdateManga.kt | 24 ++++-- .../domain/track/interactor/AddTracks.kt | 6 +- .../presentation/components/DateText.kt | 14 +-- .../presentation/history/HistoryScreen.kt | 2 +- .../history/HistoryviewModelStateProvider.kt | 21 +++-- .../kanade/presentation/manga/MangaScreen.kt | 2 +- .../manga/components/MangaDialogs.kt | 10 ++- .../manga/components/MangaInfoHeader.kt | 10 ++- .../screen/SettingsAppearanceScreen.kt | 7 +- .../more/settings/screen/about/AboutScreen.kt | 14 ++- .../settings/screen/debug/WorkerInfoScreen.kt | 12 ++- .../presentation/track/TrackInfoDialogHome.kt | 8 +- .../track/TrackerSearchPreviewProvider.kt | 7 +- .../presentation/updates/UpdatesScreen.kt | 2 +- .../eu/kanade/presentation/util/TimeUtils.kt | 4 +- .../tachiyomi/data/backup/BackupNotifier.kt | 8 +- .../data/backup/create/BackupCreator.kt | 4 +- .../backup/restore/restorers/MangaRestorer.kt | 16 ++-- .../data/library/LibraryUpdateJob.kt | 16 ++-- .../kanade/tachiyomi/data/saver/ImageSaver.kt | 4 +- .../data/track/anilist/AnilistApi.kt | 13 +-- .../data/track/anilist/dto/ALFuzzyDate.kt | 12 +-- .../source/browse/BrowseSourceViewModel.kt | 4 +- .../ui/manga/track/TrackInfoDialog.kt | 28 +++--- .../tachiyomi/ui/reader/ReaderViewModel.kt | 5 +- .../tachiyomi/ui/updates/UpdatesViewModel.kt | 7 +- .../eu/kanade/tachiyomi/util/CrashLogUtil.kt | 12 ++- .../kanade/tachiyomi/util/MangaExtensions.kt | 4 +- .../tachiyomi/util/lang/DateExtensions.kt | 58 +++++++------ .../migration/usecases/MigrateMangaUseCase.kt | 4 +- .../interactor/UpdateMangaFromRemote.kt | 6 +- .../feature/library/QueryNodeExtensions.kt | 9 +- .../feature/upcoming/UpcomingScreenContent.kt | 4 +- .../mihon/feature/upcoming/UpcomingUIModel.kt | 2 +- .../feature/upcoming/UpcomingViewModel.kt | 24 ++++-- .../upcoming/components/calendar/Calendar.kt | 17 ++-- .../components/calendar/CalendarDay.kt | 11 ++- .../components/calendar/CalendarHeader.kt | 11 ++- .../kanade/tachiyomi/network/NetworkHelper.kt | 9 +- .../eu/kanade/tachiyomi/network/Requests.kt | 4 +- data/build.gradle.kts | 2 + .../data/manga/MangaRepositoryImpl.kt | 10 ++- domain/build.gradle.kts | 2 + .../domain/manga/interactor/FetchInterval.kt | 85 ++++++++++--------- .../tachiyomi/domain/manga/model/Manga.kt | 4 +- .../domain/updates/interactor/GetUpdates.kt | 4 +- .../manga/interactor/FetchIntervalTest.kt | 40 ++++----- .../src/main/kotlin/mihon/gradle/Commands.kt | 16 ++-- gradle/libs.versions.toml | 2 + presentation-widget/build.gradle.kts | 2 + .../widget/BaseUpdatesGridGlanceWidget.kt | 11 ++- .../presentation/widget/WidgetManager.kt | 2 +- 55 files changed, 365 insertions(+), 268 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 81cebc96c..6604fc256 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -243,6 +243,8 @@ dependencies { implementation(libs.sqldelight.async) + implementation(libs.kotlinx.datetime) + // AndroidX libraries implementation(libs.androidx.annotation) implementation(libs.androidx.appCompat) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f5f87a933..0a2ae9e56 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -86,3 +86,6 @@ # Firebase -keep class com.google.firebase.installations.** { *; } -keep interface com.google.firebase.installations.** { *; } + +# KotlinX Datetime +-keep,allowoptimization class kotlinx.datetime.** { public protected *; } diff --git a/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt b/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt index fdf31cf77..f094e076a 100644 --- a/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt +++ b/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt @@ -10,6 +10,9 @@ import eu.kanade.tachiyomi.data.download.DownloadProvider import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.online.HttpSource +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant +import kotlinx.datetime.toLocalDateTime import tachiyomi.data.chapter.ChapterSanitizer import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter @@ -23,8 +26,8 @@ import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.manga.model.Manga import tachiyomi.source.local.isLocal import java.lang.Long.max -import java.time.ZonedDateTime import java.util.TreeSet +import kotlin.time.Clock class SyncChaptersWithSource( private val downloadManager: DownloadManager, @@ -57,8 +60,9 @@ class SyncChaptersWithSource( throw NoChaptersException() } - val now = ZonedDateTime.now() - val nowMillis = now.toInstant().toEpochMilli() + val timeZone = TimeZone.currentSystemDefault() + val now = Clock.System.now().toLocalDateTime(timeZone) + val nowMillis = now.toInstant(timeZone).toEpochMilliseconds() val sourceChapters = rawSourceChapters .distinctBy { it.url } @@ -145,6 +149,7 @@ class SyncChaptersWithSource( if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) { updateManga.awaitUpdateFetchInterval( manga, + timeZone, now, fetchWindow, ) @@ -217,7 +222,7 @@ class SyncChaptersWithSource( val chapterUpdates = updatedChapters.map { it.toChapterUpdate() } updateChapter.awaitAll(chapterUpdates) } - updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow) + updateManga.awaitUpdateFetchInterval(manga, timeZone, now, fetchWindow) // Set this manga as updated since chapters were changed // Note that last_update actually represents last time the chapter list changed at all diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt index 7c93e8748..20c51b3a7 100644 --- a/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt @@ -1,11 +1,13 @@ package eu.kanade.domain.manga.interactor +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.repository.MangaRepository -import java.time.Instant -import java.time.ZonedDateTime +import kotlin.time.Clock class UpdateManga( private val mangaRepository: MangaRepository, @@ -22,25 +24,31 @@ class UpdateManga( suspend fun awaitUpdateFetchInterval( manga: Manga, - dateTime: ZonedDateTime = ZonedDateTime.now(), - window: Pair = fetchInterval.getWindow(dateTime), + timeZone: TimeZone = TimeZone.currentSystemDefault(), + dateTime: LocalDateTime = Clock.System.now().toLocalDateTime(timeZone), + window: Pair = fetchInterval.getWindow(dateTime.date, timeZone), ): Boolean { return mangaRepository.update( - fetchInterval.toMangaUpdate(manga, dateTime, window), + fetchInterval.toMangaUpdate(manga, dateTime, timeZone, window), ) } suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean { - return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Instant.now().toEpochMilli())) + return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Clock.System.now().toEpochMilliseconds())) } suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean { - return mangaRepository.update(MangaUpdate(id = mangaId, coverLastModified = Instant.now().toEpochMilli())) + return mangaRepository.update( + MangaUpdate( + id = mangaId, + coverLastModified = Clock.System.now().toEpochMilliseconds(), + ), + ) } suspend fun awaitUpdateFavorite(mangaId: Long, favorite: Boolean): Boolean { val dateAdded = when (favorite) { - true -> Instant.now().toEpochMilli() + true -> Clock.System.now().toEpochMilliseconds() false -> 0 } return mangaRepository.update( diff --git a/app/src/main/java/eu/kanade/domain/track/interactor/AddTracks.kt b/app/src/main/java/eu/kanade/domain/track/interactor/AddTracks.kt index a5b6a0e1c..6357f4093 100644 --- a/app/src/main/java/eu/kanade/domain/track/interactor/AddTracks.kt +++ b/app/src/main/java/eu/kanade/domain/track/interactor/AddTracks.kt @@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone +import kotlinx.datetime.TimeZone import logcat.LogPriority import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withNonCancellableContext @@ -18,7 +19,6 @@ import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.track.interactor.InsertTrack import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.time.ZoneOffset class AddTracks( private val insertTrack: InsertTrack, @@ -62,8 +62,8 @@ class AddTracks( firstReadChapterDate?.let { val startDate = firstReadChapterDate.time.convertEpochMillisZone( - ZoneOffset.systemDefault(), - ZoneOffset.UTC, + TimeZone.currentSystemDefault(), + TimeZone.UTC, ) track = track.copy( startDate = startDate, 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 d041695ba..85e70bb03 100644 --- a/app/src/main/java/eu/kanade/presentation/components/DateText.kt +++ b/app/src/main/java/eu/kanade/presentation/components/DateText.kt @@ -5,23 +5,23 @@ 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 kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.time.Instant -import java.time.LocalDate -import java.time.ZoneId +import kotlin.time.Instant @Composable fun relativeDateText( dateEpochMillis: Long, ): String { return relativeDateText( - localDate = LocalDate.ofInstant( - Instant.ofEpochMilli(dateEpochMillis), - ZoneId.systemDefault(), - ) + localDate = Instant.fromEpochMilliseconds(dateEpochMillis) + .toLocalDateTime(TimeZone.currentSystemDefault()) + .date .takeIf { dateEpochMillis != 0L }, ) } 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 6199aca86..869284174 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt @@ -19,6 +19,7 @@ import eu.kanade.presentation.components.relativeDateText import eu.kanade.presentation.history.components.HistoryItem import eu.kanade.presentation.theme.TachiyomiPreviewTheme import eu.kanade.tachiyomi.ui.history.HistoryViewModel +import kotlinx.datetime.LocalDate import tachiyomi.domain.history.model.HistoryWithRelations import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.FastScrollLazyColumn @@ -27,7 +28,6 @@ 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.time.LocalDate @Composable fun HistoryScreen( diff --git a/app/src/main/java/eu/kanade/presentation/history/HistoryviewModelStateProvider.kt b/app/src/main/java/eu/kanade/presentation/history/HistoryviewModelStateProvider.kt index 8f52fbce9..8f989fddc 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryviewModelStateProvider.kt +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryviewModelStateProvider.kt @@ -2,13 +2,16 @@ package eu.kanade.presentation.history import androidx.compose.ui.tooling.preview.PreviewParameterProvider import eu.kanade.tachiyomi.ui.history.HistoryViewModel +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime 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 +import kotlin.time.Clock +import kotlin.time.Duration.Companion.days +import kotlin.time.Instant +import kotlin.time.toJavaInstant class HistoryviewModelStateProvider : PreviewParameterProvider { @@ -18,9 +21,9 @@ class HistoryviewModelStateProvider : PreviewParameterProvider Instant = { it }) = - HistoryUiModel.Header(LocalDate.from(instantBuilder(Instant.now()))) + HistoryUiModel.Header( + instantBuilder(Clock.System.now()).toLocalDateTime(TimeZone.currentSystemDefault()).date, + ) fun items() = sequence { var count = 1 @@ -94,7 +99,7 @@ class HistoryviewModelStateProvider : PreviewParameterProvider().dateFormat.get(), ), ) - } catch (e: Exception) { + } catch (_: Exception) { BuildConfig.BUILD_TIME } } 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 60fe53f64..3b21b38d3 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 @@ -38,15 +38,15 @@ import eu.kanade.tachiyomi.util.system.workManager import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.Scaffold 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.time.Instant -import java.time.LocalDateTime -import java.time.ZoneId +import kotlin.time.Instant class WorkerInfoScreen : Screen() { @@ -165,10 +165,8 @@ class WorkerInfoScreen : Screen() { } appendLine("State: ${workInfo.state}") if (workInfo.state == WorkInfo.State.ENQUEUED) { - val timestamp = LocalDateTime.ofInstant( - Instant.ofEpochMilli(workInfo.nextScheduleTimeMillis), - ZoneId.systemDefault(), - ) + val timestamp = Instant.fromEpochMilliseconds(workInfo.nextScheduleTimeMillis) + .toLocalDateTime(TimeZone.currentSystemDefault()) .toDateTimestampString( UiPreferences.dateFormat( Injekt.get().dateFormat.get(), 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 28bcd36e6..21b30cc4c 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt @@ -56,8 +56,10 @@ 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.toJavaLocalDate import eu.kanade.tachiyomi.util.lang.toLocalDate import eu.kanade.tachiyomi.util.system.copyToClipboard +import kotlinx.datetime.toJavaLocalDate import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource import java.time.format.DateTimeFormatter @@ -110,11 +112,13 @@ fun TrackInfoDialogHome( .takeIf { supportsScoring && item.track.score != 0.0 }, onScoreClick = { onScoreClick(item) } .takeIf { supportsScoring }, - startDate = remember(item.track.startDate) { dateFormat.format(item.track.startDate.toLocalDate()) } + startDate = remember(item.track.startDate) { + dateFormat.format(item.track.startDate.toLocalDate().toJavaLocalDate()) + } .takeIf { supportsReadingDates && item.track.startDate != 0L }, onStartDateClick = { onStartDateEdit(item) } // TODO .takeIf { supportsReadingDates }, - endDate = dateFormat.format(item.track.finishDate.toLocalDate()) + endDate = dateFormat.format(item.track.finishDate.toJavaLocalDate()) .takeIf { supportsReadingDates && item.track.finishDate != 0L }, onEndDateClick = { onEndDateEdit(item) } .takeIf { supportsReadingDates }, diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackerSearchPreviewProvider.kt b/app/src/main/java/eu/kanade/presentation/track/TrackerSearchPreviewProvider.kt index 11a5c8dcc..deb6cb261 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackerSearchPreviewProvider.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackerSearchPreviewProvider.kt @@ -6,11 +6,12 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.datasource.LoremIpsum import eu.kanade.tachiyomi.data.track.model.TrackSearch import java.text.SimpleDateFormat -import java.time.Instant -import java.time.temporal.ChronoUnit import java.util.Date import java.util.Locale import kotlin.random.Random +import kotlin.time.Clock +import kotlin.time.Duration.Companion.days +import kotlin.time.toJavaInstant internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composable () -> Unit> { private val fullPageWithSecondSelected = @Composable { @@ -93,7 +94,7 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab it.finished_reading_date = 0L it.tracking_url = "https://example.com/tracker-example" it.cover_url = "https://example.com/cover.png" - it.start_date = formatter.format(Date.from(Instant.now().minus((1L..365).random(), ChronoUnit.DAYS))) + it.start_date = formatter.format(Date.from(Clock.System.now().minus((1L..365).random().days).toJavaInstant())) it.summary = lorem((0..40).random()).joinToString() it.publishing_status = if (Random.nextBoolean()) "Finished" else "" it.publishing_type = if (Random.nextBoolean()) "Oneshot" else "" 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 8e5c36f5f..fbd94fe07 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt @@ -32,6 +32,7 @@ import eu.kanade.tachiyomi.ui.updates.UpdatesItem import eu.kanade.tachiyomi.ui.updates.UpdatesViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.datetime.LocalDate import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.material.PullRefresh @@ -40,7 +41,6 @@ import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.theme.active -import java.time.LocalDate import kotlin.time.Duration.Companion.seconds @Composable diff --git a/app/src/main/java/eu/kanade/presentation/util/TimeUtils.kt b/app/src/main/java/eu/kanade/presentation/util/TimeUtils.kt index 643f19f6e..949480463 100644 --- a/app/src/main/java/eu/kanade/presentation/util/TimeUtils.kt +++ b/app/src/main/java/eu/kanade/presentation/util/TimeUtils.kt @@ -7,7 +7,7 @@ import androidx.compose.runtime.ReadOnlyComposable import tachiyomi.core.common.i18n.stringResource import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource -import java.time.Instant +import kotlin.time.Clock import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes @@ -29,7 +29,7 @@ fun Duration.toDurationString(context: Context, fallback: String): String { @Composable @ReadOnlyComposable fun relativeTimeSpanString(epochMillis: Long): String { - val now = Instant.now().toEpochMilli() + val now = Clock.System.now().toEpochMilliseconds() return when { epochMillis <= 0L -> stringResource(MR.strings.relative_time_span_never) now - epochMillis < 1.minutes.inWholeMilliseconds -> stringResource( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt index d5381b8d1..000242c0b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt @@ -18,7 +18,7 @@ import tachiyomi.core.common.storage.displayablePath import tachiyomi.i18n.MR import uy.kohesive.injekt.injectLazy import java.io.File -import java.util.concurrent.TimeUnit +import kotlin.time.Duration.Companion.milliseconds class BackupNotifier(private val context: Context) { @@ -149,10 +149,8 @@ class BackupNotifier(private val context: Context) { val timeString = context.stringResource( MR.strings.restore_duration, - TimeUnit.MILLISECONDS.toMinutes(time), - TimeUnit.MILLISECONDS.toSeconds(time) - TimeUnit.MINUTES.toSeconds( - TimeUnit.MILLISECONDS.toMinutes(time), - ), + time.milliseconds.inWholeMinutes, + time.milliseconds.inWholeSeconds - (time.milliseconds.inWholeMinutes * 60), ) with(completeNotificationBuilder) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt index 3f6a0d9ce..a7f457093 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt @@ -33,9 +33,9 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.FileOutputStream import java.text.SimpleDateFormat -import java.time.Instant import java.util.Date import java.util.Locale +import kotlin.time.Clock class BackupCreator( private val context: Context, @@ -108,7 +108,7 @@ class BackupCreator( BackupFileValidator(context).validate(fileUri) if (isAutoBackup) { - backupPreferences.lastAutoBackupTimestamp.set(Instant.now().toEpochMilli()) + backupPreferences.lastAutoBackupTimestamp.set(Clock.System.now().toEpochMilliseconds()) } return fileUri.toString() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt index 2e3af2a17..5257fdeb8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt @@ -9,6 +9,8 @@ import eu.kanade.tachiyomi.data.backup.models.BackupChapter import eu.kanade.tachiyomi.data.backup.models.BackupHistory import eu.kanade.tachiyomi.data.backup.models.BackupManga import eu.kanade.tachiyomi.data.backup.models.BackupTracking +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import tachiyomi.data.Database import tachiyomi.data.MemoColumnAdapter import tachiyomi.data.UpdateStrategyColumnAdapter @@ -23,9 +25,9 @@ import tachiyomi.domain.track.interactor.InsertTrack import tachiyomi.domain.track.model.Track import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.time.ZonedDateTime import java.util.Date import kotlin.math.max +import kotlin.time.Clock class MangaRestorer( private val database: Database = Injekt.get(), @@ -38,13 +40,9 @@ class MangaRestorer( fetchInterval: FetchInterval = Injekt.get(), ) { - private var now = ZonedDateTime.now() - private var currentFetchWindow = fetchInterval.getWindow(now) - - init { - now = ZonedDateTime.now() - currentFetchWindow = fetchInterval.getWindow(now) - } + private val timeZone = TimeZone.currentSystemDefault() + private val now = Clock.System.now().toLocalDateTime(timeZone) + private val currentFetchWindow = fetchInterval.getWindow(now.date, timeZone) suspend fun sortByNew(backupMangas: List): List { val urlsBySource = database.mangasQueries @@ -286,7 +284,7 @@ class MangaRestorer( restoreTracking(manga, tracks) restoreHistory(manga, history) restoreExcludedScanlators(manga, excludedScanlators) - updateManga.awaitUpdateFetchInterval(manga, now, currentFetchWindow) + updateManga.awaitUpdateFetchInterval(manga, timeZone, now, currentFetchWindow) return manga } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt index 7543d4bfd..f208966fd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt @@ -35,6 +35,8 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.ensureActive import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import logcat.LogPriority import mihon.domain.chapter.interactor.FilterChaptersForDownload import mihon.domain.source.interactor.UpdateMangaFromRemote @@ -64,14 +66,13 @@ import tachiyomi.i18n.MR import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.File -import java.time.Instant -import java.time.ZonedDateTime import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.TimeUnit import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.AtomicInt import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.concurrent.atomics.incrementAndFetch +import kotlin.time.Clock @OptIn(ExperimentalAtomicApi::class) class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) : @@ -108,7 +109,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet setForegroundSafely() - libraryPreferences.lastUpdatedTimestamp.set(Instant.now().toEpochMilli()) + libraryPreferences.lastUpdatedTimestamp.set(Clock.System.now().toEpochMilliseconds()) val categoryId = inputData.getLong(KEY_CATEGORY, -1L) addMangaToQueue(categoryId) @@ -167,7 +168,11 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet val restrictions = libraryPreferences.autoUpdateMangaRestrictions.get() val skippedUpdates = mutableListOf>() - val (_, fetchWindowUpperBound) = fetchInterval.getWindow(ZonedDateTime.now()) + val timeZone = TimeZone.currentSystemDefault() + val (_, fetchWindowUpperBound) = fetchInterval.getWindow( + Clock.System.now().toLocalDateTime(timeZone).date, + timeZone, + ) mangaToUpdate = listToUpdate .filter { @@ -234,7 +239,8 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet val newUpdates = CopyOnWriteArrayList>>() val failedUpdates = CopyOnWriteArrayList>() val hasDownloads = AtomicBoolean(false) - val fetchWindow = fetchInterval.getWindow(ZonedDateTime.now()) + val timeZone = TimeZone.currentSystemDefault() + val fetchWindow = fetchInterval.getWindow(Clock.System.now().toLocalDateTime(timeZone).date, timeZone) coroutineScope { mangaToUpdate.groupBy { it.manga.source }.values diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt index 144c6f1fb..be52184a5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt @@ -24,7 +24,7 @@ import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.File import java.io.InputStream -import java.time.Instant +import kotlin.time.Clock class ImageSaver( val context: Context, @@ -85,7 +85,7 @@ class ImageSaver( MediaStore.MediaColumns.RELATIVE_PATH to relativePath, MediaStore.MediaColumns.DISPLAY_NAME to if (isMimeTypeSupported) image.name else filename, MediaStore.MediaColumns.MIME_TYPE to type.mime, - MediaStore.MediaColumns.DATE_MODIFIED to Instant.now().epochSecond, + MediaStore.MediaColumns.DATE_MODIFIED to Clock.System.now().epochSeconds, ) val picture = findUriOrDefault(relativePath, filename) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt index c0d8dd7b6..eed713629 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt @@ -15,6 +15,9 @@ import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.parseAs +import kotlinx.datetime.TimeZone +import kotlinx.datetime.number +import kotlinx.datetime.toLocalDateTime import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonObject @@ -25,10 +28,8 @@ import okhttp3.OkHttpClient import okhttp3.RequestBody.Companion.toRequestBody import tachiyomi.core.common.util.lang.withIOContext import uy.kohesive.injekt.injectLazy -import java.time.Instant -import java.time.ZoneId -import java.time.ZonedDateTime import kotlin.time.Duration.Companion.minutes +import kotlin.time.Instant import tachiyomi.domain.track.model.Track as DomainTrack class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { @@ -326,11 +327,11 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { } } - val dateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(dateValue), ZoneId.systemDefault()) + val dateTime = Instant.fromEpochMilliseconds(dateValue).toLocalDateTime(TimeZone.currentSystemDefault()) return buildJsonObject { put("year", dateTime.year) - put("month", dateTime.monthValue) - put("day", dateTime.dayOfMonth) + put("month", dateTime.month.number) + put("day", dateTime.day) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALFuzzyDate.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALFuzzyDate.kt index 7dbd8c296..1ae59602e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALFuzzyDate.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALFuzzyDate.kt @@ -1,8 +1,9 @@ package eu.kanade.tachiyomi.data.track.anilist.dto +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.atStartOfDayIn import kotlinx.serialization.Serializable -import java.time.LocalDate -import java.time.ZoneId @Serializable data class ALFuzzyDate( @@ -11,10 +12,9 @@ data class ALFuzzyDate( val day: Int?, ) { fun toEpochMilli(): Long = try { - LocalDate.of(year!!, month!!, day!!) - .atStartOfDay(ZoneId.systemDefault()) - .toInstant() - .toEpochMilli() + LocalDate(year!!, month!!, day!!) + .atStartOfDayIn(TimeZone.currentSystemDefault()) + .toEpochMilliseconds() } catch (_: Exception) { 0L } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceViewModel.kt index d8262ab3a..bfbf9865c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceViewModel.kt @@ -49,7 +49,7 @@ import tachiyomi.domain.source.interactor.GetRemoteManga import tachiyomi.domain.source.service.SourceManager import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.time.Instant +import kotlin.time.Clock import eu.kanade.tachiyomi.source.model.Filter as SourceModelFilter class BrowseSourceViewModel( @@ -226,7 +226,7 @@ class BrowseSourceViewModel( favorite = !manga.favorite, dateAdded = when (manga.favorite) { true -> 0 - false -> Instant.now().toEpochMilli() + false -> Clock.System.now().toEpochMilliseconds() }, ) 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 ef79c078c..930cd57e3 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 @@ -56,7 +56,6 @@ import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone -import eu.kanade.tachiyomi.util.lang.toLocalDate import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.toast @@ -66,6 +65,8 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import logcat.LogPriority import mihon.core.viewmodel.StateViewModel import tachiyomi.core.common.i18n.stringResource @@ -85,9 +86,8 @@ import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.time.Instant -import java.time.LocalDate -import java.time.ZoneOffset +import kotlin.time.Clock +import kotlin.time.Instant data class TrackInfoDialogHomeScreen( private val mangaId: Long, @@ -513,20 +513,20 @@ private data class TrackDateSelectorScreen( @Transient private val selectableDates = object : SelectableDates { override fun isSelectableDate(utcTimeMillis: Long): Boolean { - val targetDate = Instant.ofEpochMilli(utcTimeMillis).toLocalDate(ZoneOffset.UTC) + val targetDate = Instant.fromEpochMilliseconds(utcTimeMillis).toLocalDateTime(TimeZone.UTC) // Disallow future dates - if (targetDate > LocalDate.now(ZoneOffset.UTC)) return false + if (targetDate > Clock.System.now().toLocalDateTime(TimeZone.UTC)) return false return when { // Disallow setting start date after finish date start && track.finishDate > 0 -> { - val finishDate = Instant.ofEpochMilli(track.finishDate).toLocalDate(ZoneOffset.UTC) + val finishDate = Instant.fromEpochMilliseconds(track.finishDate).toLocalDateTime(TimeZone.UTC) targetDate <= finishDate } // Disallow setting finish date before start date !start && track.startDate > 0 -> { - val startDate = Instant.ofEpochMilli(track.startDate).toLocalDate(ZoneOffset.UTC) + val startDate = Instant.fromEpochMilliseconds(track.startDate).toLocalDateTime(TimeZone.UTC) startDate <= targetDate } else -> { @@ -537,17 +537,17 @@ private data class TrackDateSelectorScreen( override fun isSelectableYear(year: Int): Boolean { // Disallow future years - if (year > LocalDate.now(ZoneOffset.UTC).year) return false + if (year > Clock.System.now().toLocalDateTime(TimeZone.UTC).year) return false return when { // Disallow setting start year after finish year start && track.finishDate > 0 -> { - val finishDate = Instant.ofEpochMilli(track.finishDate).toLocalDate(ZoneOffset.UTC) + val finishDate = Instant.fromEpochMilliseconds(track.finishDate).toLocalDateTime(TimeZone.UTC) year <= finishDate.year } // Disallow setting finish year before start year !start && track.startDate > 0 -> { - val startDate = Instant.ofEpochMilli(track.startDate).toLocalDate(ZoneOffset.UTC) + val startDate = Instant.fromEpochMilliseconds(track.startDate).toLocalDateTime(TimeZone.UTC) startDate.year <= year } else -> { @@ -618,14 +618,14 @@ private data class TrackDateSelectorScreen( get() { val millis = (if (start) track.startDate else track.finishDate) .takeIf { it != 0L } - ?: Instant.now().toEpochMilli() - return millis.convertEpochMillisZone(ZoneOffset.systemDefault(), ZoneOffset.UTC) + ?: Clock.System.now().toEpochMilliseconds() + return millis.convertEpochMillisZone(TimeZone.currentSystemDefault(), TimeZone.UTC) } // In UTC fun setDate(millis: Long) { // Convert to local time - val localMillis = millis.convertEpochMillisZone(ZoneOffset.UTC, ZoneOffset.systemDefault()) + val localMillis = millis.convertEpochMillisZone(TimeZone.UTC, TimeZone.currentSystemDefault()) viewModelScope.launchNonCancellable { if (start) { tracker.setRemoteStartDate(track.toDbTrack(), localMillis) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index 4c227636a..e45794f8b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -76,8 +76,9 @@ import tachiyomi.domain.source.service.SourceManager import tachiyomi.source.local.isLocal import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.time.Instant import java.util.Date +import kotlin.getValue +import kotlin.time.Clock /** * Presenter used by the activity to perform background operations. @@ -582,7 +583,7 @@ class ReaderViewModel @JvmOverloads constructor( } fun restartReadTimer() { - chapterReadStartTime = Instant.now().toEpochMilli() + chapterReadStartTime = Clock.System.now().toEpochMilliseconds() } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesViewModel.kt index 68bc0e5e1..e19ecacdb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesViewModel.kt @@ -31,6 +31,9 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.TimeZone +import kotlinx.datetime.minus import logcat.LogPriority import mihon.core.viewmodel.StateViewModel import tachiyomi.core.common.preference.TriState @@ -49,7 +52,7 @@ import tachiyomi.domain.updates.model.UpdatesWithRelations import tachiyomi.domain.updates.service.UpdatesPreferences import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.time.ZonedDateTime +import kotlin.time.Clock class UpdatesViewModel( private val sourceManager: SourceManager = Injekt.get(), @@ -77,7 +80,7 @@ class UpdatesViewModel( init { viewModelScope.launchIO { // Set date limit for recent chapters - val limit = ZonedDateTime.now().minusMonths(3).toInstant() + val limit = Clock.System.now().minus(3, DateTimeUnit.MONTH, TimeZone.currentSystemDefault()) combine( // needed for SQL filters (unread, started, bookmarked, etc) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt index ec3f17b38..48cdb1bd1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt @@ -10,12 +10,14 @@ import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.createFileInCacheDir import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toast +import kotlinx.datetime.TimeZone +import kotlinx.datetime.offsetAt +import kotlinx.datetime.toLocalDateTime import tachiyomi.core.common.util.lang.withNonCancellableContext import tachiyomi.core.common.util.lang.withUIContext import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.time.OffsetDateTime -import java.time.ZoneId +import kotlin.time.Clock class CrashLogUtil( private val context: Context, @@ -35,12 +37,14 @@ class CrashLogUtil( val uri = file.getUriCompat(context) context.startActivity(uri.toShareIntent(context, "text/plain")) - } catch (e: Throwable) { + } catch (_: Throwable) { withUIContext { context.toast("Failed to get logs") } } } fun getDebugInfo(): String { + val now = Clock.System.now() + val tz = TimeZone.currentSystemDefault() return """ App ID: ${BuildConfig.APPLICATION_ID} App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE}, ${BuildConfig.BUILD_TIME}) @@ -51,7 +55,7 @@ class CrashLogUtil( Device name: ${Build.DEVICE} (${Build.PRODUCT}) Device model: ${Build.MODEL} WebView: ${WebViewUtil.getVersion(context)} - Current time: ${OffsetDateTime.now(ZoneId.systemDefault())} + Current time: ${now.toLocalDateTime(tz)}${tz.offsetAt(now)} """.trimIndent() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt index d0f8e4ba6..c39357b25 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt @@ -9,12 +9,12 @@ import tachiyomi.source.local.isLocal import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.InputStream -import java.time.Instant +import kotlin.time.Clock fun Manga.removeCovers(coverCache: CoverCache = Injekt.get()): Manga { if (isLocal()) return this return if (coverCache.deleteFromCache(this, true) > 0) { - copy(coverLastModified = Instant.now().toEpochMilli()) + copy(coverLastModified = Clock.System.now().toEpochMilliseconds()) } else { this } 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 8827ae4a6..ef2761820 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 @@ -1,23 +1,29 @@ package eu.kanade.tachiyomi.util.lang import android.content.Context +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.minus +import kotlinx.datetime.toInstant +import kotlinx.datetime.toJavaLocalDate +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toLocalDateTime import tachiyomi.core.common.i18n.pluralStringResource 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.Date import kotlin.math.absoluteValue +import kotlin.time.Clock +import kotlin.time.Instant fun LocalDateTime.toDateTimestampString(dateTimeFormatter: DateTimeFormatter): String { - val date = dateTimeFormatter.format(this) - val time = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).format(this) + val javaLocalDateTime = this.toJavaLocalDateTime() + val date = dateTimeFormatter.format(javaLocalDateTime) + val time = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).format(javaLocalDateTime) return "$date $time" } @@ -26,21 +32,21 @@ fun Date.toTimestampString(): String { } fun Long.convertEpochMillisZone( - from: ZoneId, - to: ZoneId, + from: TimeZone, + to: TimeZone, ): Long { - return LocalDateTime.ofInstant(Instant.ofEpochMilli(this), from) - .atZone(to) - .toInstant() - .toEpochMilli() + return Instant.fromEpochMilliseconds(this) + .toLocalDateTime(from) + .toInstant(to) + .toEpochMilliseconds() } fun Long.toLocalDate(): LocalDate { - return LocalDate.ofInstant(Instant.ofEpochMilli(this), ZoneId.systemDefault()) + return Instant.fromEpochMilliseconds(this).toLocalDateTime(TimeZone.currentSystemDefault()).date } -fun Instant.toLocalDate(zoneId: ZoneId = ZoneId.systemDefault()): LocalDate { - return LocalDate.ofInstant(this, zoneId) +fun Long.toJavaLocalDate(): java.time.LocalDate { + return this.toLocalDate().toJavaLocalDate() } fun LocalDate.toRelativeString( @@ -49,23 +55,25 @@ fun LocalDate.toRelativeString( dateFormat: DateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT), ): String { if (!relative) { - return dateFormat.format(this) + return dateFormat.format(this.toJavaLocalDate()) } - val now = LocalDate.now() - val difference = ChronoUnit.DAYS.between(this, now) + val today = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date + val difference = (today - this).days return when { - difference < -7 -> dateFormat.format(this) + difference < -7 -> dateFormat.format(this.toJavaLocalDate()) difference < 0 -> context.pluralStringResource( MR.plurals.upcoming_relative_time, - difference.toInt().absoluteValue, - difference.toInt().absoluteValue, + difference.absoluteValue, + difference.absoluteValue, ) + difference < 1 -> context.stringResource(MR.strings.relative_time_today) difference < 7 -> context.pluralStringResource( MR.plurals.relative_time, - difference.toInt(), - difference.toInt(), + difference, + difference, ) - else -> dateFormat.format(this) + + else -> dateFormat.format(this.toJavaLocalDate()) } } diff --git a/app/src/main/java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt b/app/src/main/java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt index e272eb66c..cf639e45d 100644 --- a/app/src/main/java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt +++ b/app/src/main/java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt @@ -20,7 +20,7 @@ import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.track.interactor.GetTracks import tachiyomi.domain.track.interactor.InsertTrack -import java.time.Instant +import kotlin.time.Clock class MigrateMangaUseCase( private val sourcePreferences: SourcePreferences, @@ -124,7 +124,7 @@ class MigrateMangaUseCase( favorite = true, chapterFlags = current.chapterFlags, viewerFlags = current.viewerFlags, - dateAdded = if (replace) current.dateAdded else Instant.now().toEpochMilli(), + dateAdded = if (replace) current.dateAdded else Clock.System.now().toEpochMilliseconds(), notes = if (MigrationFlag.NOTES in flags) current.notes else null, ) diff --git a/app/src/main/java/mihon/domain/source/interactor/UpdateMangaFromRemote.kt b/app/src/main/java/mihon/domain/source/interactor/UpdateMangaFromRemote.kt index d81914f78..d016eaa11 100644 --- a/app/src/main/java/mihon/domain/source/interactor/UpdateMangaFromRemote.kt +++ b/app/src/main/java/mihon/domain/source/interactor/UpdateMangaFromRemote.kt @@ -20,7 +20,7 @@ import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.repository.MangaRepository import tachiyomi.domain.source.service.SourceManager import tachiyomi.source.local.isLocal -import java.time.Instant +import kotlin.time.Clock class UpdateMangaFromRemote( private val sourceManager: SourceManager, @@ -107,14 +107,14 @@ class UpdateMangaFromRemote( // Never refresh covers if the url is empty to avoid "losing" existing covers remoteManga.thumbnail_url.isNullOrEmpty() -> null !manualFetch && localManga.thumbnailUrl == remoteManga.thumbnail_url -> null - localManga.isLocal() -> Instant.now().toEpochMilli() + localManga.isLocal() -> Clock.System.now().toEpochMilliseconds() localManga.hasCustomCover(coverCache) -> { coverCache.deleteFromCache(localManga, false) null } else -> { coverCache.deleteFromCache(localManga, false) - Instant.now().toEpochMilli() + Clock.System.now().toEpochMilliseconds() } } diff --git a/app/src/main/java/mihon/feature/library/QueryNodeExtensions.kt b/app/src/main/java/mihon/feature/library/QueryNodeExtensions.kt index af49b7bc4..40a14ec6c 100644 --- a/app/src/main/java/mihon/feature/library/QueryNodeExtensions.kt +++ b/app/src/main/java/mihon/feature/library/QueryNodeExtensions.kt @@ -1,6 +1,9 @@ package mihon.feature.library import eu.kanade.tachiyomi.ui.library.LibraryItem +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import mihon.domain.library.model.search.AndNode import mihon.domain.library.model.search.ComparisonField import mihon.domain.library.model.search.ComparisonQueryNode @@ -12,10 +15,8 @@ import mihon.domain.library.model.search.NotNode import mihon.domain.library.model.search.OrNode import mihon.domain.library.model.search.QueryNode import tachiyomi.source.local.LocalSource -import java.time.Instant -import java.time.LocalDate -import java.time.ZoneId import kotlin.math.abs +import kotlin.time.Instant fun QueryNode.matches(item: LibraryItem): Boolean { return when (this) { @@ -109,7 +110,7 @@ private fun ComparisonQueryNode.matches(item: LibraryItem): Boolean { fun compareDates(timestamp: Long, value: String): Boolean? { val inputDate = runCatching { LocalDate.parse(value) }.getOrNull() ?: return null - val mangaDate = Instant.ofEpochMilli(timestamp).atZone(ZoneId.systemDefault()).toLocalDate() + val mangaDate = Instant.fromEpochMilliseconds(timestamp).toLocalDateTime(TimeZone.currentSystemDefault()).date return queryComparator.apply(mangaDate, inputDate) } diff --git a/app/src/main/java/mihon/feature/upcoming/UpcomingScreenContent.kt b/app/src/main/java/mihon/feature/upcoming/UpcomingScreenContent.kt index 3e098f2c2..e20202901 100644 --- a/app/src/main/java/mihon/feature/upcoming/UpcomingScreenContent.kt +++ b/app/src/main/java/mihon/feature/upcoming/UpcomingScreenContent.kt @@ -26,6 +26,8 @@ import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.relativeDateText import eu.kanade.presentation.util.isTabletUi import kotlinx.coroutines.launch +import kotlinx.datetime.LocalDate +import kotlinx.datetime.YearMonth import mihon.feature.upcoming.components.UpcomingItem import mihon.feature.upcoming.components.calendar.Calendar import tachiyomi.core.common.Constants @@ -36,8 +38,6 @@ import tachiyomi.presentation.core.components.TwoPanelBox import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource -import java.time.LocalDate -import java.time.YearMonth @Composable fun UpcomingScreenContent( diff --git a/app/src/main/java/mihon/feature/upcoming/UpcomingUIModel.kt b/app/src/main/java/mihon/feature/upcoming/UpcomingUIModel.kt index 758e1686e..9048061b5 100644 --- a/app/src/main/java/mihon/feature/upcoming/UpcomingUIModel.kt +++ b/app/src/main/java/mihon/feature/upcoming/UpcomingUIModel.kt @@ -1,7 +1,7 @@ package mihon.feature.upcoming +import kotlinx.datetime.LocalDate import tachiyomi.domain.manga.model.Manga -import java.time.LocalDate sealed interface UpcomingUIModel { data class Header(val date: LocalDate, val mangaCount: Int) : UpcomingUIModel diff --git a/app/src/main/java/mihon/feature/upcoming/UpcomingViewModel.kt b/app/src/main/java/mihon/feature/upcoming/UpcomingViewModel.kt index 0ec3d0a4e..e07caca27 100644 --- a/app/src/main/java/mihon/feature/upcoming/UpcomingViewModel.kt +++ b/app/src/main/java/mihon/feature/upcoming/UpcomingViewModel.kt @@ -4,17 +4,20 @@ import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMapIndexedNotNull import androidx.lifecycle.viewModelScope import eu.kanade.core.util.insertSeparatorsReversed -import eu.kanade.tachiyomi.util.lang.toLocalDate import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.YearMonth +import kotlinx.datetime.toLocalDateTime +import kotlinx.datetime.yearMonth import mihon.core.viewmodel.StateViewModel import mihon.domain.upcoming.interactor.GetUpcomingManga import tachiyomi.domain.manga.model.Manga import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.time.LocalDate -import java.time.YearMonth +import kotlin.time.Clock class UpcomingViewModel( private val getUpcomingManga: GetUpcomingManga = Injekt.get(), @@ -41,8 +44,14 @@ class UpcomingViewModel( .insertSeparatorsReversed { before, after -> if (after != null) mangaCount++ - val beforeDate = before?.manga?.expectedNextUpdate?.toLocalDate() - val afterDate = after?.manga?.expectedNextUpdate?.toLocalDate() + val beforeDate = before?.manga + ?.expectedNextUpdate + ?.toLocalDateTime(TimeZone.currentSystemDefault()) + ?.date + val afterDate = after?.manga + ?.expectedNextUpdate + ?.toLocalDateTime(TimeZone.currentSystemDefault()) + ?.date if (beforeDate != afterDate && afterDate != null) { UpcomingUIModel.Header(afterDate, mangaCount).also { mangaCount = 0 } @@ -73,7 +82,10 @@ class UpcomingViewModel( } data class State( - val selectedYearMonth: YearMonth = YearMonth.now(), + val selectedYearMonth: YearMonth = Clock.System.now() + .toLocalDateTime(TimeZone.currentSystemDefault()) + .date + .yearMonth, val items: List = listOf(), val events: Map = mapOf(), val headerIndexes: Map = mapOf(), diff --git a/app/src/main/java/mihon/feature/upcoming/components/calendar/Calendar.kt b/app/src/main/java/mihon/feature/upcoming/components/calendar/Calendar.kt index a955a8956..06cbba380 100644 --- a/app/src/main/java/mihon/feature/upcoming/components/calendar/Calendar.kt +++ b/app/src/main/java/mihon/feature/upcoming/components/calendar/Calendar.kt @@ -19,12 +19,15 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.util.fastForEach import io.woong.compose.grid.SimpleGridCells import io.woong.compose.grid.VerticalGrid +import kotlinx.datetime.LocalDate +import kotlinx.datetime.YearMonth +import kotlinx.datetime.minusMonth +import kotlinx.datetime.plusMonth +import kotlinx.datetime.toJavaDayOfWeek import mihon.core.designsystem.utils.isExpandedWidthWindow import mihon.core.designsystem.utils.isMediumWidthWindow import tachiyomi.presentation.core.components.material.padding import java.time.DayOfWeek -import java.time.LocalDate -import java.time.YearMonth import java.time.format.TextStyle import java.time.temporal.WeekFields import java.util.Locale @@ -47,8 +50,8 @@ fun Calendar( ) { CalenderHeader( yearMonth = selectedYearMonth, - onPreviousClick = { setSelectedYearMonth(selectedYearMonth.minusMonths(1L)) }, - onNextClick = { setSelectedYearMonth(selectedYearMonth.plusMonths(1L)) }, + onPreviousClick = { setSelectedYearMonth(selectedYearMonth.minusMonth()) }, + onNextClick = { setSelectedYearMonth(selectedYearMonth.plusMonth()) }, modifier = Modifier .fillMaxWidth() .padding(vertical = MaterialTheme.padding.small) @@ -74,8 +77,8 @@ private fun CalendarGrid( .map { DayOfWeek.of((localeFirstDayOfWeek - 1 + it) % DAYS_OF_WEEK + 1) } } - val emptyFieldCount = weekDays.indexOf(selectedYearMonth.atDay(1).dayOfWeek) - val daysInMonth = selectedYearMonth.lengthOfMonth() + val emptyFieldCount = weekDays.indexOf(selectedYearMonth.firstDay.dayOfWeek.toJavaDayOfWeek()) + val daysInMonth = selectedYearMonth.numberOfDays VerticalGrid( columns = SimpleGridCells.Fixed(DAYS_OF_WEEK), @@ -98,7 +101,7 @@ private fun CalendarGrid( } repeat(emptyFieldCount) { Box { } } repeat(daysInMonth) { dayIndex -> - val localDate = selectedYearMonth.atDay(dayIndex + 1) + val localDate = LocalDate(selectedYearMonth.year, selectedYearMonth.month, dayIndex + 1) CalendarDay( date = localDate, onDayClick = { onClickDay(localDate) }, diff --git a/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarDay.kt b/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarDay.kt index 3a8f3e6fb..79a7b982f 100644 --- a/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarDay.kt +++ b/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarDay.kt @@ -19,8 +19,11 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import tachiyomi.presentation.core.components.material.DISABLED_ALPHA -import java.time.LocalDate +import kotlin.time.Clock private const val MAX_EVENTS = 3 @@ -31,7 +34,7 @@ fun CalendarDay( onDayClick: () -> Unit, modifier: Modifier = Modifier, ) { - val today = remember { LocalDate.now() } + val today = remember { Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date } Box( modifier = modifier @@ -54,10 +57,10 @@ fun CalendarDay( contentAlignment = Alignment.Center, ) { Text( - text = date.dayOfMonth.toString(), + text = date.day.toString(), textAlign = TextAlign.Center, fontSize = 16.sp, - color = if (date.isBefore(today)) { + color = if (date < today) { MaterialTheme.colorScheme.onBackground.copy(alpha = DISABLED_ALPHA) } else { MaterialTheme.colorScheme.onBackground diff --git a/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarHeader.kt b/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarHeader.kt index b2cbbee6e..6d0b23942 100644 --- a/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarHeader.kt +++ b/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarHeader.kt @@ -24,11 +24,16 @@ import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import kotlinx.datetime.TimeZone +import kotlinx.datetime.YearMonth +import kotlinx.datetime.toJavaYearMonth +import kotlinx.datetime.toLocalDateTime +import kotlinx.datetime.yearMonth import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource -import java.time.YearMonth import java.time.format.DateTimeFormatter import java.util.Locale +import kotlin.time.Clock @Composable fun CalenderHeader( @@ -88,14 +93,14 @@ private fun AnimatedContentTransitionScope.getAnimation(): ContentTra @ReadOnlyComposable private fun getTitleText(monthYear: YearMonth): String { val formatter = DateTimeFormatter.ofPattern("MMMM yyyy", Locale.getDefault()) - return formatter.format(monthYear) + return formatter.format(monthYear.toJavaYearMonth()) } @Preview @Composable private fun CalenderHeaderPreview() { CalenderHeader( - yearMonth = YearMonth.now(), + yearMonth = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date.yearMonth, onNextClick = {}, onPreviousClick = {}, ) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt index 885a11108..9580db6c9 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -8,7 +8,8 @@ import okhttp3.Cache import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import java.io.File -import java.util.concurrent.TimeUnit +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds class NetworkHelper( private val context: Context, @@ -20,9 +21,9 @@ class NetworkHelper( private val clientBuilder: OkHttpClient.Builder = run { val builder = OkHttpClient.Builder() .cookieJar(cookieJar) - .connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .callTimeout(2, TimeUnit.MINUTES) + .connectTimeout(30.seconds) + .readTimeout(30.seconds) + .callTimeout(2.minutes) .cache( Cache( directory = File(context.cacheDir, "network_cache"), diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/Requests.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/Requests.kt index cb1fcb3b3..ea19570ae 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/Requests.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/Requests.kt @@ -9,9 +9,9 @@ import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Request import okhttp3.RequestBody -import java.util.concurrent.TimeUnit.MINUTES +import kotlin.time.Duration.Companion.minutes -private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build() +private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10.minutes).build() private val DEFAULT_HEADERS = Headers.Builder().build() private val DEFAULT_BODY: RequestBody = FormBody.Builder().build() diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 3612653b7..566345ad0 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -38,5 +38,7 @@ dependencies { implementation(libs.injekt) + implementation(libs.kotlinx.datetime) + api(libs.bundles.sqldelight) } diff --git a/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt index 0b10eca1e..6ad2104dd 100644 --- a/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt @@ -4,6 +4,9 @@ import app.cash.sqldelight.async.coroutines.awaitAsList import app.cash.sqldelight.async.coroutines.awaitAsOne import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.TimeZone +import kotlinx.datetime.atStartOfDayIn +import kotlinx.datetime.toLocalDateTime import logcat.LogPriority import tachiyomi.core.common.util.system.logcat import tachiyomi.data.Database @@ -18,8 +21,7 @@ import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.model.MangaWithChapterCount import tachiyomi.domain.manga.repository.MangaRepository -import java.time.LocalDate -import java.time.ZoneId +import kotlin.time.Clock class MangaRepositoryImpl( private val database: Database, @@ -86,7 +88,9 @@ class MangaRepositoryImpl( } override suspend fun getUpcomingManga(statuses: Set): Flow> { - val epochMillis = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toEpochSecond() * 1000 + val timeZone = TimeZone.currentSystemDefault() + val epochMillis = + Clock.System.now().toLocalDateTime(timeZone).date.atStartOfDayIn(timeZone).toEpochMilliseconds() return database.mangasQueries .getUpcomingManga(epochMillis, statuses, MangaMapper::mapManga) .subscribeToList() diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index e397a0b6a..cd42533e4 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -22,6 +22,8 @@ dependencies { implementation(libs.bundles.kotlinx.coroutines) implementation(libs.bundles.serialization) + implementation(libs.kotlinx.datetime) + implementation(libs.unifile) api(libs.sqldelight.androidxPaging) diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt index ad1a0f757..bdc0ba606 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt @@ -1,14 +1,20 @@ package tachiyomi.domain.manga.interactor +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.atStartOfDayIn +import kotlinx.datetime.daysUntil +import kotlinx.datetime.toInstant +import kotlinx.datetime.toLocalDateTime import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.MangaUpdate -import java.time.Instant -import java.time.ZoneId -import java.time.ZonedDateTime -import java.time.temporal.ChronoUnit import kotlin.math.absoluteValue +import kotlin.time.Clock +import kotlin.time.Duration.Companion.days +import kotlin.time.Instant class FetchInterval( private val getChaptersByMangaId: GetChaptersByMangaId, @@ -16,40 +22,42 @@ class FetchInterval( suspend fun toMangaUpdate( manga: Manga, - dateTime: ZonedDateTime, + dateTime: LocalDateTime, + timeZone: TimeZone, window: Pair, ): MangaUpdate { val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval( chapters = getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true), - zone = dateTime.zone, + zone = timeZone, ) val currentWindow = if (window.first == 0L && window.second == 0L) { - getWindow(ZonedDateTime.now()) + getWindow(Clock.System.now().toLocalDateTime(timeZone).date, timeZone) } else { window } - val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow) + val nextUpdate = calculateNextUpdate(manga, interval, dateTime, timeZone, currentWindow) return MangaUpdate(id = manga.id, nextUpdate = nextUpdate, fetchInterval = interval) } - fun getWindow(dateTime: ZonedDateTime): Pair { - val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone) - val lowerBound = today.minusDays(GRACE_PERIOD) - val upperBound = today.plusDays(GRACE_PERIOD) - return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1) + fun getWindow(localDateTime: LocalDate, timeZone: TimeZone): Pair { + val today = localDateTime.atStartOfDayIn(timeZone) + val lowerBound = today - GRACE_PERIOD.days + val upperBound = today + GRACE_PERIOD.days + return Pair(lowerBound.toEpochMilliseconds(), upperBound.toEpochMilliseconds()) } - internal fun calculateInterval(chapters: List, zone: ZoneId): Int { + internal fun calculateInterval(chapters: List, zone: TimeZone): Int { val chapterWindow = if (chapters.size <= 8) 3 else 10 val uploadDates = chapters.asSequence() .filter { it.dateUpload > 0L } .sortedByDescending { it.dateUpload } .map { - ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zone) - .toLocalDate() - .atStartOfDay() + Instant.fromEpochMilliseconds(it.dateUpload) + .toLocalDateTime(zone) + .date + .atStartOfDayIn(zone) } .distinct() .take(chapterWindow) @@ -58,9 +66,10 @@ class FetchInterval( val fetchDates = chapters.asSequence() .sortedByDescending { it.dateFetch } .map { - ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zone) - .toLocalDate() - .atStartOfDay() + Instant.fromEpochMilliseconds(it.dateFetch) + .toLocalDateTime(zone) + .date + .atStartOfDayIn(zone) } .distinct() .take(chapterWindow) @@ -69,13 +78,13 @@ class FetchInterval( val interval = when { // Enough upload date from source uploadDates.size >= 3 -> { - val ranges = uploadDates.windowed(2).map { x -> x[1].until(x[0], ChronoUnit.DAYS) }.sorted() - ranges[(ranges.size - 1) / 2].toInt() + val ranges = uploadDates.windowed(2).map { x -> x[1].daysUntil(x[0], zone) }.sorted() + ranges[(ranges.size - 1) / 2] } // Enough fetch date from client fetchDates.size >= 3 -> { - val ranges = fetchDates.windowed(2).map { x -> x[1].until(x[0], ChronoUnit.DAYS) }.sorted() - ranges[(ranges.size - 1) / 2].toInt() + val ranges = fetchDates.windowed(2).map { x -> x[1].daysUntil(x[0], zone) }.sorted() + ranges[(ranges.size - 1) / 2] } // Default to 7 days else -> 7 @@ -87,34 +96,34 @@ class FetchInterval( private fun calculateNextUpdate( manga: Manga, interval: Int, - dateTime: ZonedDateTime, + dateTime: LocalDateTime, + timeZone: TimeZone, window: Pair, ): Long { if (manga.nextUpdate in window.first.rangeTo(window.second + 1)) { return manga.nextUpdate } - val latestDate = ZonedDateTime.ofInstant( - if (manga.lastUpdate > 0) Instant.ofEpochMilli(manga.lastUpdate) else Instant.now(), - dateTime.zone, - ) - .toLocalDate() - .atStartOfDay() - val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, dateTime).toInt() - val cycle = timeSinceLatest.floorDiv( + val instant = if (manga.lastUpdate > 0) Instant.fromEpochMilliseconds(manga.lastUpdate) else Clock.System.now() + val latestDate = instant.toLocalDateTime(timeZone).date.atStartOfDayIn(timeZone) + + val daysSinceLatest = (dateTime.toInstant(timeZone) - latestDate).inWholeDays + val cycle = daysSinceLatest.floorDiv( interval.absoluteValue.takeIf { interval < 0 } - ?: increaseInterval(interval, timeSinceLatest, increaseWhenOver = 10), + ?: increaseInterval(interval, daysSinceLatest, increaseWhenOver = 10), ) - return latestDate.plusDays((cycle + 1) * interval.absoluteValue.toLong()).toEpochSecond(dateTime.offset) * 1000 + + val offsetDays = ((cycle + 1) * interval.absoluteValue.toLong()).days + return latestDate.plus(offsetDays).toEpochMilliseconds() } - private fun increaseInterval(delta: Int, timeSinceLatest: Int, increaseWhenOver: Int): Int { + private fun increaseInterval(delta: Int, daysSinceLatest: Long, increaseWhenOver: Int): Int { if (delta >= MAX_INTERVAL) return MAX_INTERVAL // double delta again if missed more than 9 check in new delta - val cycle = timeSinceLatest.floorDiv(delta) + 1 + val cycle = daysSinceLatest.floorDiv(delta) + 1 return if (cycle > increaseWhenOver) { - increaseInterval(delta * 2, timeSinceLatest, increaseWhenOver) + increaseInterval(delta * 2, daysSinceLatest, increaseWhenOver) } else { delta } diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt index e48298f01..421c2f054 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt @@ -10,7 +10,7 @@ import kotlinx.serialization.json.JsonObject import mihon.core.common.extensions.EMPTY import tachiyomi.core.common.preference.TriState import java.io.ObjectStreamException -import java.time.Instant +import kotlin.time.Instant import java.io.Serializable as JavaSerializable @SuppressLint("UnsafeOptInUsageError") @@ -47,7 +47,7 @@ data class Manga( val expectedNextUpdate: Instant? get() = nextUpdate .takeIf { status != SManga.COMPLETED.toLong() } - ?.let { Instant.ofEpochMilli(it) } + ?.let { Instant.fromEpochMilliseconds(it) } val sorting: Long get() = chapterFlags and CHAPTER_SORTING_MASK diff --git a/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt b/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt index 9db97154f..8c4bea360 100644 --- a/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt +++ b/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt @@ -3,7 +3,7 @@ package tachiyomi.domain.updates.interactor import kotlinx.coroutines.flow.Flow import tachiyomi.domain.updates.model.UpdatesWithRelations import tachiyomi.domain.updates.repository.UpdatesRepository -import java.time.Instant +import kotlin.time.Instant class GetUpdates( private val repository: UpdatesRepository, @@ -21,7 +21,7 @@ class GetUpdates( hideExcludedScanlators: Boolean, ): Flow> { return repository.subscribeAll( - instant.toEpochMilli(), + instant.toEpochMilliseconds(), limit = 500, unread = unread, started = started, diff --git a/domain/src/test/java/tachiyomi/domain/manga/interactor/FetchIntervalTest.kt b/domain/src/test/java/tachiyomi/domain/manga/interactor/FetchIntervalTest.kt index c27a71c94..7d204aa1b 100644 --- a/domain/src/test/java/tachiyomi/domain/manga/interactor/FetchIntervalTest.kt +++ b/domain/src/test/java/tachiyomi/domain/manga/interactor/FetchIntervalTest.kt @@ -2,27 +2,27 @@ package tachiyomi.domain.manga.interactor import io.kotest.matchers.shouldBe import io.mockk.mockk +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant import org.junit.jupiter.api.Test import org.junit.jupiter.api.parallel.Execution import org.junit.jupiter.api.parallel.ExecutionMode import tachiyomi.domain.chapter.model.Chapter -import java.time.ZoneOffset -import java.time.ZonedDateTime import kotlin.time.Duration import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours import kotlin.time.DurationUnit import kotlin.time.toDuration -import kotlin.time.toJavaDuration @Execution(ExecutionMode.CONCURRENT) class FetchIntervalTest { - private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z") - private val testZoneId = ZoneOffset.UTC + private val testTime = LocalDateTime.parse("2020-01-01T00:00:00") + private val testTimeZone = TimeZone.UTC private var chapter = Chapter.create().copy( - dateFetch = testTime.toEpochSecond() * 1000, - dateUpload = testTime.toEpochSecond() * 1000, + dateFetch = testTime.toInstant(testTimeZone).toEpochMilliseconds(), + dateUpload = testTime.toInstant(testTimeZone).toEpochMilliseconds(), ) private val fetchInterval = FetchInterval(mockk()) @@ -32,12 +32,12 @@ class FetchIntervalTest { val chaptersWithUploadDate = (1..50).map { chapterWithTime(chapter, 1.days) } - fetchInterval.calculateInterval(chaptersWithUploadDate, testZoneId) shouldBe 7 + fetchInterval.calculateInterval(chaptersWithUploadDate, testTimeZone) shouldBe 7 val chaptersWithoutUploadDate = chaptersWithUploadDate.map { it.copy(dateUpload = 0L) } - fetchInterval.calculateInterval(chaptersWithoutUploadDate, testZoneId) shouldBe 7 + fetchInterval.calculateInterval(chaptersWithoutUploadDate, testTimeZone) shouldBe 7 } @Test @@ -51,7 +51,7 @@ class FetchIntervalTest { val chapters = oldChapters + newChapters - fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1 + fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 1 } @Test @@ -66,7 +66,7 @@ class FetchIntervalTest { val chapters = oldChapters + newChapters - fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 7 + fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 7 } @Test @@ -74,7 +74,7 @@ class FetchIntervalTest { val chapters = (1..10).map { chapterWithTime(chapter, 10.hours) } - fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 7 + fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 7 } @Test @@ -84,7 +84,7 @@ class FetchIntervalTest { } + (1..5).map { chapterWithTime(chapter, 2.days) } - fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 7 + fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 7 } @Test @@ -92,7 +92,7 @@ class FetchIntervalTest { val chapters = (1..20).map { chapterWithTime(chapter, it.days) } - fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1 + fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 1 } @Test @@ -100,7 +100,7 @@ class FetchIntervalTest { val chapters = (1..20).map { chapterWithTime(chapter, (15 * it).hours) } - fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1 + fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 1 } @Test @@ -108,7 +108,7 @@ class FetchIntervalTest { val chapters = (1..20).map { chapterWithTime(chapter, (2 * it).days) } - fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 2 + fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 2 } @Test @@ -116,12 +116,12 @@ class FetchIntervalTest { val chaptersWithUploadDate = (1..5).map { chapterWithTime(chapter, (25 * it).hours) } - fetchInterval.calculateInterval(chaptersWithUploadDate, testZoneId) shouldBe 1 + fetchInterval.calculateInterval(chaptersWithUploadDate, testTimeZone) shouldBe 1 val chaptersWithoutUploadDate = chaptersWithUploadDate.map { it.copy(dateUpload = 0L) } - fetchInterval.calculateInterval(chaptersWithoutUploadDate, testZoneId) shouldBe 1 + fetchInterval.calculateInterval(chaptersWithoutUploadDate, testTimeZone) shouldBe 1 } @Test @@ -129,11 +129,11 @@ class FetchIntervalTest { val chapters = (1..20).map { chapterWithTime(chapter, (43 * it).hours) } - fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 2 + fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 2 } private fun chapterWithTime(chapter: Chapter, duration: Duration): Chapter { - val newTime = testTime.plus(duration.toJavaDuration()).toEpochSecond() * 1000 + val newTime = testTime.toInstant(testTimeZone).plus(duration).toEpochMilliseconds() return chapter.copy(dateFetch = newTime, dateUpload = newTime) } diff --git a/gradle/build-logic/src/main/kotlin/mihon/gradle/Commands.kt b/gradle/build-logic/src/main/kotlin/mihon/gradle/Commands.kt index c56a3e269..76dcb044b 100644 --- a/gradle/build-logic/src/main/kotlin/mihon/gradle/Commands.kt +++ b/gradle/build-logic/src/main/kotlin/mihon/gradle/Commands.kt @@ -1,10 +1,9 @@ package mihon.gradle import org.gradle.api.Project -import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneOffset -import java.time.format.DateTimeFormatter +import kotlin.time.Clock +import kotlin.time.Duration.Companion.nanoseconds +import kotlin.time.Instant // Git is needed in your system PATH for these commands to work. // If it's not installed, you can return a random value as a workaround @@ -18,19 +17,18 @@ fun Project.getLatestCommitSha(): String { // return "1" } -private val BUILD_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'") - /** * @param useLatestCommitTime If `true`, the build time is based on the timestamp of the last Git commit; * otherwise, the current time is used. Both are in UTC. - * @return A formatted string representing the build time. The format used is defined by [BUILD_TIME_FORMATTER]. + * @return An ISO 8601 formatted string representing the build time. */ fun Project.getBuildTime(useLatestCommitTime: Boolean): String { return if (useLatestCommitTime) { val epoch = exec("git log -1 --format=%ct").toLong() - Instant.ofEpochSecond(epoch).atOffset(ZoneOffset.UTC).format(BUILD_TIME_FORMATTER) + Instant.fromEpochSeconds(epoch).toString() } else { - LocalDateTime.now(ZoneOffset.UTC).format(BUILD_TIME_FORMATTER) + val now = Clock.System.now() + (now - now.nanosecondsOfSecond.nanoseconds).toString() } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d078afb9a..77cb7882c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -45,6 +45,7 @@ junit = "6.1.2" kotest-assertions = "6.2.3" kotlin-gradle = "2.4.10" kotlinx-coroutines = "1.11.0" +kotlinx-datetime = "0.8.0" kotlinx-serialization = "1.11.0" ktlint = "1.8.0" leakCanary = "2.14" @@ -142,6 +143,7 @@ kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutine kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } kotlinx-serialization-jsonOkio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "kotlinx-serialization" } kotlinx-serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "kotlinx-serialization" } diff --git a/presentation-widget/build.gradle.kts b/presentation-widget/build.gradle.kts index d5bdef18c..97935148d 100644 --- a/presentation-widget/build.gradle.kts +++ b/presentation-widget/build.gradle.kts @@ -18,6 +18,8 @@ dependencies { implementation(libs.androidx.glance.appWidget) implementation(libs.material) + implementation(libs.kotlinx.datetime) + implementation(libs.coil.core) api(libs.injekt) diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/BaseUpdatesGridGlanceWidget.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/BaseUpdatesGridGlanceWidget.kt index 780a41a2f..11cf4efa6 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/BaseUpdatesGridGlanceWidget.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/BaseUpdatesGridGlanceWidget.kt @@ -34,6 +34,9 @@ import coil3.transform.RoundedCornersTransformation import eu.kanade.tachiyomi.core.security.SecurityPreferences import eu.kanade.tachiyomi.util.system.dpToPx import kotlinx.coroutines.flow.map +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.TimeZone +import kotlinx.datetime.minus import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.domain.manga.model.MangaCover import tachiyomi.domain.updates.interactor.GetUpdates @@ -46,8 +49,8 @@ import tachiyomi.presentation.widget.util.appWidgetBackgroundRadius import tachiyomi.presentation.widget.util.calculateRowAndColumnCount import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.time.Instant -import java.time.ZonedDateTime +import kotlin.time.Clock +import kotlin.time.Instant abstract class BaseUpdatesGridGlanceWidget( private val context: Context = Injekt.get(), @@ -90,7 +93,7 @@ abstract class BaseUpdatesGridGlanceWidget( val flow = remember { getUpdates - .subscribe(false, DateLimit.toEpochMilli()) + .subscribe(false, DateLimit.toEpochMilliseconds()) .map { rawData -> rawData.prepareData(rowCount, columnCount) } @@ -153,6 +156,6 @@ abstract class BaseUpdatesGridGlanceWidget( companion object { val DateLimit: Instant - get() = ZonedDateTime.now().minusMonths(3).toInstant() + get() = Clock.System.now().minus(3, DateTimeUnit.MONTH, TimeZone.currentSystemDefault()) } } diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/WidgetManager.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/WidgetManager.kt index 0c52d6c84..7280587c5 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/WidgetManager.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/WidgetManager.kt @@ -21,7 +21,7 @@ class WidgetManager( fun Context.init(scope: LifecycleCoroutineScope) { combine( - getUpdates.subscribe(read = false, after = BaseUpdatesGridGlanceWidget.DateLimit.toEpochMilli()), + getUpdates.subscribe(read = false, after = BaseUpdatesGridGlanceWidget.DateLimit.toEpochMilliseconds()), securityPreferences.useAuthenticator.changes(), transform = { a, b -> a to b }, )