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 }, )