From 44c1e1915df4835c0e2d563aab9b48ae0030e892 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Sat, 10 Oct 2026 01:13:08 +0600 Subject: [PATCH] Store every date in the database as an Instant --- .../interactor/SyncChaptersWithSource.kt | 23 +- .../eu/kanade/domain/chapter/model/Chapter.kt | 10 +- .../domain/manga/interactor/UpdateManga.kt | 9 +- .../eu/kanade/domain/manga/model/Manga.kt | 7 +- .../domain/track/interactor/AddTracks.kt | 9 +- .../eu/kanade/domain/track/model/Track.kt | 10 +- .../presentation/components/DateText.kt | 7 +- .../history/HistoryviewModelStateProvider.kt | 6 +- .../HistoryWithRelationsProvider.kt | 12 +- .../presentation/track/TrackInfoDialogHome.kt | 8 +- .../TrackInfoDialogHomePreviewProvider.kt | 4 +- .../track/TrackerSearchPreviewProvider.kt | 9 +- .../create/creators/MangaBackupCreator.kt | 5 +- .../data/backup/models/BackupChapter.kt | 11 +- .../data/backup/models/BackupHistory.kt | 4 +- .../data/backup/models/BackupManga.kt | 8 +- .../data/backup/models/BackupTracking.kt | 10 +- .../tachiyomi/data/database/models/Chapter.kt | 6 +- .../data/library/LibraryUpdateWorker.kt | 10 +- .../source/browse/BrowseSourceViewModel.kt | 2 +- .../ui/download/DownloadQueueViewModel.kt | 2 +- .../tachiyomi/ui/history/HistoryViewModel.kt | 4 +- .../tachiyomi/ui/library/LibraryViewModel.kt | 8 +- .../ui/manga/track/TrackInfoDialog.kt | 26 +- .../tachiyomi/ui/reader/ReaderViewModel.kt | 10 +- .../kanade/tachiyomi/util/MangaExtensions.kt | 2 +- .../tachiyomi/util/lang/DateExtensions.kt | 15 +- .../migration/usecases/MigrateMangaUseCase.kt | 2 +- .../interactor/UpdateMangaFromRemote.kt | 9 +- .../feature/library/QueryNodeExtensions.kt | 6 +- .../mihon/core/common/extensions/Instant.kt | 14 + .../java/tachiyomi/data/DatabaseAdapter.kt | 8 +- .../java/tachiyomi/data/DatabaseBindings.kt | 12 +- .../data/backup/RestoreRepositoryImpl.kt | 20 +- .../data/chapter/ChapterRepositoryImpl.kt | 5 +- .../tachiyomi/data/history/HistoryMapper.kt | 8 +- .../java/tachiyomi/data/manga/MangaMapper.kt | 35 +-- .../data/manga/MangaRepositoryImpl.kt | 5 +- .../java/tachiyomi/data/track/TrackMapper.kt | 5 +- .../data/updates/UpdatesRepositoryImpl.kt | 15 +- .../main/sqldelight/tachiyomi/data/chapter.sq | 5 +- .../main/sqldelight/tachiyomi/data/history.sq | 10 +- .../main/sqldelight/tachiyomi/data/manga.sq | 9 +- .../sqldelight/tachiyomi/data/manga_track.sq | 5 +- .../sqldelight/tachiyomi/migrations/19.sqm | 271 ++++++++++++++++++ .../sqldelight/tachiyomi/view/historyView.sq | 4 +- .../sqldelight/tachiyomi/view/libraryView.sq | 39 ++- .../domain/backup/model/RestoredManga.kt | 4 +- .../tachiyomi/domain/chapter/model/Chapter.kt | 9 +- .../chapter/model/ChapterRemoteUpdate.kt | 3 +- .../domain/chapter/model/ChapterUpdate.kt | 3 +- .../domain/chapter/service/ChapterSort.kt | 4 +- .../tachiyomi/domain/history/model/History.kt | 4 +- .../domain/history/model/HistoryUpdate.kt | 4 +- .../history/model/HistoryWithRelations.kt | 4 +- .../domain/library/model/LibraryManga.kt | 7 +- .../domain/manga/interactor/FetchInterval.kt | 38 ++- .../tachiyomi/domain/manga/model/Manga.kt | 23 +- .../domain/manga/model/MangaCover.kt | 4 +- .../domain/manga/model/MangaRemoteUpdate.kt | 3 +- .../domain/manga/model/MangaUpdate.kt | 9 +- .../tachiyomi/domain/track/model/Track.kt | 5 +- .../domain/updates/interactor/GetUpdates.kt | 6 +- .../updates/model/UpdatesWithRelations.kt | 3 +- .../updates/repository/UpdatesRepository.kt | 7 +- .../manga/interactor/FetchIntervalTest.kt | 15 +- .../widget/BaseUpdatesGridGlanceWidget.kt | 2 +- .../presentation/widget/WidgetManager.kt | 2 +- 68 files changed, 599 insertions(+), 284 deletions(-) create mode 100644 core/common/src/main/kotlin/mihon/core/common/extensions/Instant.kt create mode 100644 data/src/main/sqldelight/tachiyomi/migrations/19.sqm 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 85dc9c352..56d53a211 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 @@ -27,6 +27,8 @@ import tachiyomi.source.local.isLocal import java.lang.Long.max import java.util.TreeSet import kotlin.time.Clock +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Instant @Inject class SyncChaptersWithSource( @@ -52,7 +54,7 @@ class SyncChaptersWithSource( manga: Manga, source: Source, manualFetch: Boolean = false, - fetchWindow: Pair = Pair(0, 0), + fetchWindow: ClosedRange? = null, ): List { if (rawSourceChapters.isEmpty() && !source.isLocal()) { throw NoChaptersException() @@ -60,7 +62,7 @@ class SyncChaptersWithSource( val timeZone = TimeZone.currentSystemDefault() val now = Clock.System.now().toLocalDateTime(timeZone) - val nowMillis = now.toInstant(timeZone).toEpochMilliseconds() + val nowInstant = now.toInstant(timeZone) val sourceChapters = rawSourceChapters .distinctBy { it.url } @@ -80,7 +82,7 @@ class SyncChaptersWithSource( // Used to not set upload date of older chapters // to a higher value than newer chapters - var maxSeenUploadDate = 0L + var maxSeenUploadDate: Instant? = null for (sourceChapter in sourceChapters) { var chapter = sourceChapter @@ -102,11 +104,10 @@ class SyncChaptersWithSource( val dbChapter = dbChaptersByUrl[chapter.url] if (dbChapter == null) { - val toAddChapter = if (chapter.dateUpload == 0L) { - val altDateUpload = if (maxSeenUploadDate == 0L) nowMillis else maxSeenUploadDate - chapter.copy(dateUpload = altDateUpload) + val toAddChapter = if (chapter.dateUpload == null) { + chapter.copy(dateUpload = maxSeenUploadDate ?: nowInstant) } else { - maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload) + maxSeenUploadDate = listOfNotNull(maxSeenUploadDate, sourceChapter.dateUpload).maxOrNull() chapter } newChapters.add(toAddChapter) @@ -132,7 +133,7 @@ class SyncChaptersWithSource( chapterNumber = chapter.chapterNumber, scanlator = chapter.scanlator, sourceOrder = chapter.sourceOrder, - dateUpload = chapter.dateUpload.takeIf { it != 0L }, + dateUpload = chapter.dateUpload, memo = chapter.memo, ), ) @@ -149,7 +150,9 @@ class SyncChaptersWithSource( // Return if there's nothing to add, delete, or update to avoid unnecessary db transactions. if (newChapters.isEmpty() && removedChapters.isEmpty() && updatedChapters.isEmpty()) { - if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) { + val isNextUpdateBeforeWindow = fetchWindow != null && + manga.nextUpdate.let { it == null || it < fetchWindow.start } + if (manualFetch || manga.fetchInterval == 0 || isNextUpdateBeforeWindow) { updateManga.awaitUpdateFetchInterval( manga, timeZone, @@ -188,7 +191,7 @@ class SyncChaptersWithSource( // Sources MUST return the chapters from most to less recent, which is common. var itemCount = newChapters.size val toAdd = newChapters.map { toAddItem -> - var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--) + var chapter = toAddItem.copy(dateFetch = nowInstant + (itemCount--).milliseconds) if (chapter.chapterNumber in readChapterNumbers && markDuplicateAsRead) { changedOrDuplicateReadUrls.add(chapter.url) diff --git a/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt b/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt index 28da13e37..9b69e3bae 100644 --- a/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt +++ b/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt @@ -2,6 +2,8 @@ package eu.kanade.domain.chapter.model import eu.kanade.tachiyomi.data.database.models.ChapterImpl import eu.kanade.tachiyomi.source.model.SChapter +import mihon.core.common.extensions.toEpochMillisOrZero +import mihon.core.common.extensions.toInstantOrNull import tachiyomi.domain.chapter.model.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter as DbChapter @@ -10,7 +12,7 @@ fun Chapter.toSChapter(): SChapter { return SChapter.create().also { it.url = url it.name = name - it.date_upload = dateUpload + it.date_upload = dateUpload.toEpochMillisOrZero() it.chapter_number = chapterNumber.toFloat() it.scanlator = scanlator it.memo = memo @@ -21,7 +23,7 @@ fun Chapter.copyFromSChapter(sChapter: SChapter): Chapter { return this.copy( name = sChapter.name, url = sChapter.url, - dateUpload = sChapter.date_upload, + dateUpload = sChapter.date_upload.toInstantOrNull(), chapterNumber = sChapter.chapter_number.toDouble(), scanlator = sChapter.scanlator?.ifBlank { null }?.trim(), memo = sChapter.memo, @@ -37,8 +39,8 @@ fun Chapter.toDbChapter(): DbChapter = ChapterImpl().also { it.read = read it.bookmark = bookmark it.last_page_read = lastPageRead.toInt() - it.date_fetch = dateFetch - it.date_upload = dateUpload + it.date_fetch = dateFetch.toEpochMilliseconds() + it.date_upload = dateUpload.toEpochMillisOrZero() it.chapter_number = chapterNumber.toFloat() it.source_order = sourceOrder.toInt() it.memo = memo 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 d998e0c30..c114ae3ce 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 @@ -9,6 +9,7 @@ import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.repository.MangaRepository import kotlin.time.Clock +import kotlin.time.Instant @Inject class UpdateManga( @@ -28,7 +29,7 @@ class UpdateManga( manga: Manga, timeZone: TimeZone = TimeZone.currentSystemDefault(), dateTime: LocalDateTime = Clock.System.now().toLocalDateTime(timeZone), - window: Pair = fetchInterval.getWindow(dateTime.date, timeZone), + window: ClosedRange? = fetchInterval.getWindow(dateTime.date, timeZone), ): Boolean { return mangaRepository.update( fetchInterval.withFetchInterval(manga, dateTime, timeZone, window), @@ -36,20 +37,20 @@ class UpdateManga( } suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean { - return mangaRepository.update(MangaUpdate(mangaId) { lastUpdate = Clock.System.now().toEpochMilliseconds() }) + return mangaRepository.update(MangaUpdate(mangaId) { lastUpdate = Clock.System.now() }) } suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean { return mangaRepository.update( MangaUpdate(mangaId) { - coverLastModified = Clock.System.now().toEpochMilliseconds() + coverLastModified = Clock.System.now() }, ) } suspend fun awaitUpdateFavorite(mangaId: Long, favorite: Boolean): Boolean { val update = when (favorite) { - true -> MangaUpdate(mangaId) { favoriteAt = Clock.System.now().toEpochMilliseconds() } + true -> MangaUpdate(mangaId) { favoriteAt = Clock.System.now() } false -> MangaUpdate(mangaId) { favoriteAt = null } } return mangaRepository.update(update) diff --git a/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt b/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt index a3c03ad35..33b89187b 100644 --- a/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt +++ b/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt @@ -16,7 +16,6 @@ import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.manga.model.Manga import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import kotlin.time.Instant // TODO: move these into the domain model val Manga.readingMode: Long @@ -90,11 +89,7 @@ fun getComicInfo( categories: List?, sourceName: String, ): ComicInfo { - val date = chapter.dateUpload - .takeIf { it != 0L } - ?.let { - Instant.fromEpochMilliseconds(it).toLocalDateTime(TimeZone.currentSystemDefault()) - } + val date = chapter.dateUpload?.toLocalDateTime(TimeZone.currentSystemDefault()) return ComicInfo( title = ComicInfo.Title(chapter.name), 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 5261331cf..315286bc9 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 @@ -18,6 +18,7 @@ import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.history.interactor.GetHistory import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.track.interactor.UpsertTrack +import kotlin.time.Instant @Inject class AddTracks( @@ -55,18 +56,18 @@ class AddTracks( tracker.setRemoteLastChapterRead(track.toDbTrack(), latestLocalReadChapterNumber.toInt()) } - if (track.startDate <= 0) { + if (track.startDate == null) { val firstReadChapterDate = getHistory.await(mangaId) - .mapNotNull { it.readAt?.takeIf { readAt -> readAt.time > 0 } } + .mapNotNull { it.readAt } .minOrNull() firstReadChapterDate?.let { - val startDate = firstReadChapterDate.time.convertEpochMillisZone( + val startDate = firstReadChapterDate.toEpochMilliseconds().convertEpochMillisZone( TimeZone.currentSystemDefault(), TimeZone.UTC, ) track = track.copy( - startDate = startDate, + startDate = Instant.fromEpochMilliseconds(startDate), ) tracker.setRemoteStartDate(track.toDbTrack(), startDate) } diff --git a/app/src/main/java/eu/kanade/domain/track/model/Track.kt b/app/src/main/java/eu/kanade/domain/track/model/Track.kt index f29901fad..6f352fbb9 100644 --- a/app/src/main/java/eu/kanade/domain/track/model/Track.kt +++ b/app/src/main/java/eu/kanade/domain/track/model/Track.kt @@ -1,5 +1,7 @@ package eu.kanade.domain.track.model +import mihon.core.common.extensions.toEpochMillisOrZero +import mihon.core.common.extensions.toInstantOrNull import tachiyomi.domain.track.model.Track import eu.kanade.tachiyomi.data.database.models.Track as DbTrack @@ -25,8 +27,8 @@ fun Track.toDbTrack(): DbTrack = DbTrack.create(trackerId).also { it.status = status it.score = score it.tracking_url = remoteUrl - it.started_reading_date = startDate - it.finished_reading_date = finishDate + it.started_reading_date = startDate.toEpochMillisOrZero() + it.finished_reading_date = finishDate.toEpochMillisOrZero() it.private = private } @@ -44,8 +46,8 @@ fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? { status = status, score = score, remoteUrl = tracking_url, - startDate = started_reading_date, - finishDate = finished_reading_date, + startDate = started_reading_date.toInstantOrNull(), + finishDate = finished_reading_date.toInstantOrNull(), private = private, ) } 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 9953f3861..2c16e5956 100644 --- a/app/src/main/java/eu/kanade/presentation/components/DateText.kt +++ b/app/src/main/java/eu/kanade/presentation/components/DateText.kt @@ -15,13 +15,10 @@ import kotlin.time.Instant @Composable fun relativeDateText( - dateEpochMillis: Long, + date: Instant?, ): String { return relativeDateText( - localDate = Instant.fromEpochMilliseconds(dateEpochMillis) - .toLocalDateTime(TimeZone.currentSystemDefault()) - .date - .takeIf { dateEpochMillis != 0L }, + localDate = date?.toLocalDateTime(TimeZone.currentSystemDefault())?.date, ) } 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 8f989fddc..4a75247e5 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryviewModelStateProvider.kt +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryviewModelStateProvider.kt @@ -6,12 +6,10 @@ import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import tachiyomi.domain.history.model.HistoryWithRelations import tachiyomi.domain.manga.model.MangaCover -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 { @@ -99,14 +97,14 @@ class HistoryviewModelStateProvider : PreviewParameterProvider { @@ -12,14 +12,14 @@ internal class HistoryWithRelationsProvider : PreviewParameterProvider Unit> { private val fullPageWithSecondSelected = @Composable { @@ -77,8 +74,6 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab } } - private val formatter: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) - private fun randTrackSearch() = TrackSearch().let { it.id = Random.nextLong() it.manga_id = Random.nextLong() @@ -94,7 +89,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(Clock.System.now().minus((1L..365).random().days).toJavaInstant())) + it.start_date = Clock.System.now().minus((1L..365).random().days).toLocalDate().toString() 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/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt index e90e541cb..22706e871 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt @@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.backup.models.toBackupChapter import eu.kanade.tachiyomi.data.backup.models.toBackupTracking import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import mihon.core.common.extensions.toByteArray +import mihon.core.common.extensions.toEpochMillisOrZero import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.chapter.repository.ChapterRepository import tachiyomi.domain.history.interactor.GetHistory @@ -66,7 +67,7 @@ class MangaBackupCreator( val history = historyByMangaId.mapNotNull { history -> // A chapter removed since its history was read takes that history with it val url = chapterUrlsById[history.chapterId] ?: return@mapNotNull null - BackupHistory(url, history.readAt?.time ?: 0L, history.readDuration) + BackupHistory(url, history.readAt.toEpochMillisOrZero(), history.readDuration) } if (history.isNotEmpty()) { mangaObject.history = history @@ -90,7 +91,7 @@ private fun Manga.toBackupManga() = thumbnailUrl = this.thumbnailUrl, favorite = this.favorite, source = this.source, - dateAdded = this.favoriteAt ?: 0L, + dateAdded = this.favoriteAt.toEpochMillisOrZero(), viewer = (this.viewerFlags.toInt() and ReadingMode.MASK), viewer_flags = this.viewerFlags.toInt(), chapterFlags = this.chapterFlags.toInt(), diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt index 20b7d2f0a..abb524297 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt @@ -4,8 +4,11 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber import mihon.core.common.extensions.JsonObjectEmptyBytes import mihon.core.common.extensions.toByteArray +import mihon.core.common.extensions.toEpochMillisOrZero +import mihon.core.common.extensions.toInstantOrNull import mihon.core.common.extensions.toJsonObject import tachiyomi.domain.chapter.model.Chapter +import kotlin.time.Instant @Serializable class BackupChapter( @@ -36,8 +39,8 @@ class BackupChapter( read = this@BackupChapter.read, bookmark = this@BackupChapter.bookmark, lastPageRead = this@BackupChapter.lastPageRead, - dateFetch = this@BackupChapter.dateFetch, - dateUpload = this@BackupChapter.dateUpload, + dateFetch = Instant.fromEpochMilliseconds(this@BackupChapter.dateFetch), + dateUpload = this@BackupChapter.dateUpload.toInstantOrNull(), sourceOrder = this@BackupChapter.sourceOrder, memo = this@BackupChapter.memo.toJsonObject(), ) @@ -52,8 +55,8 @@ fun Chapter.toBackupChapter() = BackupChapter( read = read, bookmark = bookmark, lastPageRead = lastPageRead, - dateFetch = dateFetch, - dateUpload = dateUpload, + dateFetch = dateFetch.toEpochMilliseconds(), + dateUpload = dateUpload.toEpochMillisOrZero(), sourceOrder = sourceOrder, memo = memo.toByteArray(), ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt index b5a6c616a..aed3f24a2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt @@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.data.backup.models import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber +import mihon.core.common.extensions.toInstantOrNull import tachiyomi.domain.history.model.History -import java.util.Date @Serializable data class BackupHistory( @@ -13,7 +13,7 @@ data class BackupHistory( ) { fun getHistoryImpl(): History { return History.create().copy( - readAt = Date(lastRead), + readAt = lastRead.toInstantOrNull(), readDuration = readDuration, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt index 95a126171..97132cb0a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt @@ -4,8 +4,10 @@ import eu.kanade.tachiyomi.source.model.UpdateStrategy import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber import mihon.core.common.extensions.JsonObjectEmptyBytes +import mihon.core.common.extensions.toInstantOrNull import mihon.core.common.extensions.toJsonObject import tachiyomi.domain.manga.model.Manga +import kotlin.time.Instant @Suppress("DEPRECATION") @Serializable @@ -61,9 +63,9 @@ class BackupManga( // For libraries older than dateAdded, the favorite timestamp is the only record of when // an entry was added. It was written by a trigger in seconds, unlike dateAdded. favoriteAt = if (this@BackupManga.favorite) { - this@BackupManga.dateAdded.takeIf { it != 0L } - ?: this@BackupManga.favoriteModifiedAt?.times(1000) - ?: 0L + this@BackupManga.dateAdded.toInstantOrNull() + ?: this@BackupManga.favoriteModifiedAt?.let { Instant.fromEpochSeconds(it) } + ?: Manga.UNKNOWN_FAVORITE_AT } else { null }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt index 67dc92b57..7b66f1457 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt @@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.data.backup.models import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber +import mihon.core.common.extensions.toEpochMillisOrZero +import mihon.core.common.extensions.toInstantOrNull import tachiyomi.domain.track.model.Track @Serializable @@ -46,8 +48,8 @@ data class BackupTracking( totalChapters = this@BackupTracking.totalChapters.toLong(), score = this@BackupTracking.score.toDouble(), status = this@BackupTracking.status.toLong(), - startDate = this@BackupTracking.startedReadingDate, - finishDate = this@BackupTracking.finishedReadingDate, + startDate = this@BackupTracking.startedReadingDate.toInstantOrNull(), + finishDate = this@BackupTracking.finishedReadingDate.toInstantOrNull(), remoteUrl = this@BackupTracking.trackingUrl, private = this@BackupTracking.private, ) @@ -64,8 +66,8 @@ fun Track.toBackupTracking() = BackupTracking( totalChapters = totalChapters.toInt(), score = score.toFloat(), status = status.toInt(), - startedReadingDate = startDate, - finishedReadingDate = finishDate, + startedReadingDate = startDate.toEpochMillisOrZero(), + finishedReadingDate = finishDate.toEpochMillisOrZero(), trackingUrl = remoteUrl, private = private, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt index ffd2b9ebf..f385198bf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt @@ -3,7 +3,9 @@ package eu.kanade.tachiyomi.data.database.models import eu.kanade.tachiyomi.source.model.SChapter +import mihon.core.common.extensions.toInstantOrNull import java.io.Serializable +import kotlin.time.Instant import tachiyomi.domain.chapter.model.Chapter as DomainChapter interface Chapter : SChapter, Serializable { @@ -34,11 +36,11 @@ fun Chapter.toDomainChapter(): DomainChapter? { read = read, bookmark = bookmark, lastPageRead = last_page_read.toLong(), - dateFetch = date_fetch, + dateFetch = Instant.fromEpochMilliseconds(date_fetch), sourceOrder = source_order.toLong(), url = url, name = name, - dateUpload = date_upload, + dateUpload = date_upload.toInstantOrNull(), chapterNumber = chapter_number.toDouble(), scanlator = scanlator, memo = memo, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateWorker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateWorker.kt index a101c9f96..2eb044dbd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateWorker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateWorker.kt @@ -76,6 +76,7 @@ import kotlin.concurrent.atomics.AtomicInt import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.concurrent.atomics.incrementAndFetch import kotlin.time.Clock +import kotlin.time.Instant @OptIn(ExperimentalAtomicApi::class) class LibraryUpdateWorker(private val context: Context, workerParams: WorkerParameters) : @@ -181,10 +182,10 @@ class LibraryUpdateWorker(private val context: Context, workerParams: WorkerPara val restrictions = libraryPreferences.autoUpdateMangaRestrictions.get() val skippedUpdates = mutableListOf>() val timeZone = TimeZone.currentSystemDefault() - val (_, fetchWindowUpperBound) = fetchInterval.getWindow( + val fetchWindowUpperBound = fetchInterval.getWindow( Clock.System.now().toLocalDateTime(timeZone).date, timeZone, - ) + ).endInclusive mangaToUpdate = listToUpdate .filter { @@ -214,7 +215,8 @@ class LibraryUpdateWorker(private val context: Context, workerParams: WorkerPara false } - MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && it.manga.nextUpdate > fetchWindowUpperBound -> { + MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && + it.manga.nextUpdate.let { next -> next != null && next > fetchWindowUpperBound } -> { skippedUpdates.add( it.manga to context.stringResource(MR.strings.skipped_reason_not_in_release_period), ) @@ -344,7 +346,7 @@ class LibraryUpdateWorker(private val context: Context, workerParams: WorkerPara * @param manga the manga to update. * @return a pair of the inserted and removed chapters. */ - private suspend fun updateManga(manga: Manga, fetchWindow: Pair): List { + private suspend fun updateManga(manga: Manga, fetchWindow: ClosedRange): List { val source = sourceManager.getOrStub(manga.source) val update = updateMangaFromRemote( 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 eecef476f..3dbc62730 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 @@ -244,7 +244,7 @@ class BrowseSourceViewModel( } else { setMangaDefaultChapterFlags.await(manga) addTracks.bindEnhancedTrackers(manga, sourceManager.getOrStub(manga.source)) - MangaUpdate(manga.id) { favoriteAt = Clock.System.now().toEpochMilliseconds() } + MangaUpdate(manga.id) { favoriteAt = Clock.System.now() } } updateManga.await(update) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueViewModel.kt index 1143bbf66..c30dd85d2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueViewModel.kt @@ -164,7 +164,7 @@ class DownloadQueueViewModel( downloadManager.cancelQueuedDownloads(downloads) } - fun > reorderQueue(selector: (DownloadItem) -> R, reverse: Boolean = false) { + fun > reorderQueue(selector: (DownloadItem) -> R?, reverse: Boolean = false) { val adapter = adapter ?: return val newDownloads = mutableListOf() adapter.headerItems.forEach { headerItem -> diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryViewModel.kt index 8291df989..d086aaeb5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryViewModel.kt @@ -104,8 +104,8 @@ class HistoryViewModel( private fun List.toHistoryUiModels(): List { return map { HistoryUiModel.Item(it) } .insertSeparators { before, after -> - val beforeDate = before?.item?.readAt?.time?.toLocalDate() - val afterDate = after?.item?.readAt?.time?.toLocalDate() + val beforeDate = before?.item?.readAt?.toLocalDate() + val afterDate = after?.item?.readAt?.toLocalDate() when { beforeDate != afterDate && afterDate != null -> HistoryUiModel.Header(afterDate) // Return null to avoid adding a separator between two items. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryViewModel.kt index e5d741d56..b2aa78393 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryViewModel.kt @@ -325,10 +325,10 @@ class LibraryViewModel( sortAlphabetically(manga1, manga2) } LibrarySort.Type.LastRead -> { - manga1.libraryManga.lastRead.compareTo(manga2.libraryManga.lastRead) + compareValues(manga1.libraryManga.lastRead, manga2.libraryManga.lastRead) } LibrarySort.Type.LastUpdate -> { - manga1.libraryManga.manga.lastUpdate.compareTo(manga2.libraryManga.manga.lastUpdate) + compareValues(manga1.libraryManga.manga.lastUpdate, manga2.libraryManga.manga.lastUpdate) } LibrarySort.Type.UnreadCount -> when { // Ensure unread content comes first @@ -341,10 +341,10 @@ class LibraryViewModel( manga1.libraryManga.totalChapters.compareTo(manga2.libraryManga.totalChapters) } LibrarySort.Type.LatestChapter -> { - manga1.libraryManga.latestUpload.compareTo(manga2.libraryManga.latestUpload) + compareValues(manga1.libraryManga.latestUpload, manga2.libraryManga.latestUpload) } LibrarySort.Type.ChapterFetchDate -> { - manga1.libraryManga.chapterFetchedAt.compareTo(manga2.libraryManga.chapterFetchedAt) + compareValues(manga1.libraryManga.chapterFetchedAt, manga2.libraryManga.chapterFetchedAt) } LibrarySort.Type.DateAdded -> { compareValues(manga1.libraryManga.manga.favoriteAt, manga2.libraryManga.manga.favoriteAt) 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 e10ecbe20..ec47a2a08 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 @@ -501,15 +501,15 @@ data class TrackDateSelectorScreen( // Disallow future dates if (targetDate > Clock.System.now().toLocalDateTime(TimeZone.UTC)) return false + val finishDate = track.finishDate?.toLocalDateTime(TimeZone.UTC) + val startDate = track.startDate?.toLocalDateTime(TimeZone.UTC) return when { // Disallow setting start date after finish date - start && track.finishDate > 0 -> { - val finishDate = Instant.fromEpochMilliseconds(track.finishDate).toLocalDateTime(TimeZone.UTC) + start && finishDate != null -> { targetDate <= finishDate } // Disallow setting finish date before start date - !start && track.startDate > 0 -> { - val startDate = Instant.fromEpochMilliseconds(track.startDate).toLocalDateTime(TimeZone.UTC) + !start && startDate != null -> { startDate <= targetDate } else -> { @@ -522,15 +522,15 @@ data class TrackDateSelectorScreen( // Disallow future years if (year > Clock.System.now().toLocalDateTime(TimeZone.UTC).year) return false + val finishDate = track.finishDate?.toLocalDateTime(TimeZone.UTC) + val startDate = track.startDate?.toLocalDateTime(TimeZone.UTC) return when { // Disallow setting start year after finish year - start && track.finishDate > 0 -> { - val finishDate = Instant.fromEpochMilliseconds(track.finishDate).toLocalDateTime(TimeZone.UTC) + start && finishDate != null -> { year <= finishDate.year } // Disallow setting finish year before start year - !start && track.startDate > 0 -> { - val startDate = Instant.fromEpochMilliseconds(track.startDate).toLocalDateTime(TimeZone.UTC) + !start && startDate != null -> { startDate.year <= year } else -> { @@ -548,9 +548,9 @@ data class TrackDateSelectorScreen( } val canRemove = if (start) { - track.startDate > 0 + track.startDate != null } else { - track.finishDate > 0 + track.finishDate != null } TrackDateSelector( title = if (start) { @@ -589,10 +589,8 @@ data class TrackDateSelectorScreen( // In UTC val initialSelection: Long get() { - val millis = (if (start) track.startDate else track.finishDate) - .takeIf { it != 0L } - ?: Clock.System.now().toEpochMilliseconds() - return millis.convertEpochMillisZone(TimeZone.currentSystemDefault(), TimeZone.UTC) + val date = (if (start) track.startDate else track.finishDate) ?: Clock.System.now() + return date.toEpochMilliseconds().convertEpochMillisZone(TimeZone.currentSystemDefault(), TimeZone.UTC) } // In UTC 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 8eb34b44c..2e34bb10f 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 @@ -90,8 +90,8 @@ import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.source.service.SourceManager import tachiyomi.source.local.image.LocalCoverManager import tachiyomi.source.local.isLocal -import java.util.Date import kotlin.time.Clock +import kotlin.time.Instant /** * Presenter used by the activity to perform background operations. @@ -187,7 +187,7 @@ class ReaderViewModel( /** * The time the chapter was started reading */ - private var chapterReadStartTime: Long? = null + private var chapterReadStartTime: Instant? = null private var chapterToDownload: Download? = null @@ -638,7 +638,7 @@ class ReaderViewModel( } fun restartReadTimer() { - chapterReadStartTime = Clock.System.now().toEpochMilliseconds() + chapterReadStartTime = Clock.System.now() } /** @@ -649,8 +649,8 @@ class ReaderViewModel( if (incognitoMode) return@let val chapterId = readerChapter.chapter.id!! - val endTime = Date() - val sessionReadDuration = chapterReadStartTime?.let { endTime.time - it } ?: 0 + val endTime = Clock.System.now() + val sessionReadDuration = chapterReadStartTime?.let { (endTime - it).inWholeMilliseconds } ?: 0 chapterReadStartTime = null upsertHistory.await(HistoryUpdate(chapterId, endTime, sessionReadDuration)) 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 e13bf9602..3a754b100 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt @@ -12,7 +12,7 @@ import kotlin.time.Clock fun Manga.removeCovers(coverCache: CoverCache): Manga { if (isLocal()) return this return if (coverCache.deleteFromCache(this, true) > 0) { - copy(coverLastModified = Clock.System.now().toEpochMilliseconds()) + copy(coverLastModified = Clock.System.now()) } 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 0e7fceafa..8d037d5ed 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 @@ -12,10 +12,8 @@ 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.format.DateTimeFormatter import java.time.format.FormatStyle -import java.util.Date import kotlin.math.absoluteValue import kotlin.time.Clock import kotlin.time.Instant @@ -27,8 +25,9 @@ fun LocalDateTime.toDateTimestampString(dateTimeFormatter: DateTimeFormatter): S return "$date $time" } -fun Date.toTimestampString(): String { - return DateFormat.getTimeInstance(DateFormat.SHORT).format(this) +fun Instant.toTimestampString(): String { + return DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) + .format(toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime()) } fun Long.convertEpochMillisZone( @@ -42,10 +41,14 @@ fun Long.convertEpochMillisZone( } fun Long.toLocalDate(): LocalDate { - return Instant.fromEpochMilliseconds(this).toLocalDateTime(TimeZone.currentSystemDefault()).date + return Instant.fromEpochMilliseconds(this).toLocalDate() } -fun Long.toJavaLocalDate(): java.time.LocalDate { +fun Instant.toLocalDate(): LocalDate { + return toLocalDateTime(TimeZone.currentSystemDefault()).date +} + +fun Instant.toJavaLocalDate(): java.time.LocalDate { return this.toLocalDate().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 85c4d3b8a..cc7f332c5 100644 --- a/app/src/main/java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt +++ b/app/src/main/java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt @@ -115,7 +115,7 @@ class MigrateMangaUseCase( } .takeIf { replace } val targetMangaUpdate = MangaUpdate(target.id) { - favoriteAt = current.favoriteAt?.takeIf { replace } ?: Clock.System.now().toEpochMilliseconds() + favoriteAt = current.favoriteAt?.takeIf { replace } ?: Clock.System.now() chapterFlags = current.chapterFlags viewerFlags = current.viewerFlags if (MigrationFlag.NOTES in flags) notes = current.notes 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 ab1fe2fa7..cacbdce87 100644 --- a/app/src/main/java/mihon/domain/source/interactor/UpdateMangaFromRemote.kt +++ b/app/src/main/java/mihon/domain/source/interactor/UpdateMangaFromRemote.kt @@ -22,6 +22,7 @@ import tachiyomi.domain.manga.repository.MangaRepository import tachiyomi.domain.source.service.SourceManager import tachiyomi.source.local.isLocal import kotlin.time.Clock +import kotlin.time.Instant @Inject class UpdateMangaFromRemote( @@ -38,7 +39,7 @@ class UpdateMangaFromRemote( fetchDetails: Boolean = false, fetchChapters: Boolean = false, manualFetch: Boolean = false, - fetchWindow: Pair = Pair(0, 0), + fetchWindow: ClosedRange? = null, ): Result { val source = sourceManager.getOrStub(manga.source) return invoke( @@ -56,7 +57,7 @@ class UpdateMangaFromRemote( fetchDetails: Boolean = false, fetchChapters: Boolean = false, manualFetch: Boolean = false, - fetchWindow: Pair = Pair(0, 0), + fetchWindow: ClosedRange? = null, ): Result { return try { val chapters = chapterRepository.getChapterByMangaId(manga.id) @@ -110,14 +111,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() -> Clock.System.now().toEpochMilliseconds() + localManga.isLocal() -> Clock.System.now() localManga.hasCustomCover(coverCache) -> { coverCache.deleteFromCache(localManga, false) null } else -> { coverCache.deleteFromCache(localManga, false) - Clock.System.now().toEpochMilliseconds() + Clock.System.now() } } diff --git a/app/src/main/java/mihon/feature/library/QueryNodeExtensions.kt b/app/src/main/java/mihon/feature/library/QueryNodeExtensions.kt index 24d648f6e..962106cf7 100644 --- a/app/src/main/java/mihon/feature/library/QueryNodeExtensions.kt +++ b/app/src/main/java/mihon/feature/library/QueryNodeExtensions.kt @@ -108,9 +108,9 @@ private fun FieldQueryNode.matches(item: LibraryItem): Boolean { private fun ComparisonQueryNode.matches(item: LibraryItem): Boolean { val manga = item.libraryManga.manga - fun compareDates(timestamp: Long, value: String): Boolean? { + fun compareDates(date: Instant, value: String): Boolean? { val inputDate = runCatching { LocalDate.parse(value) }.getOrNull() ?: return null - val mangaDate = Instant.fromEpochMilliseconds(timestamp).toLocalDateTime(TimeZone.currentSystemDefault()).date + val mangaDate = date.toLocalDateTime(TimeZone.currentSystemDefault()).date return queryComparator.apply(mangaDate, inputDate) } @@ -122,7 +122,7 @@ private fun ComparisonQueryNode.matches(item: LibraryItem): Boolean { ComparisonField.FETCH_INTERVAL -> value.toIntOrNull() ?.let { queryComparator.apply(abs(manga.fetchInterval), it) } - ComparisonField.NEXT_UPDATE -> compareDates(manga.nextUpdate, value) + ComparisonField.NEXT_UPDATE -> manga.nextUpdate?.let { compareDates(it, value) } ComparisonField.UNREAD -> { value.toLongOrNull()?.let { diff --git a/core/common/src/main/kotlin/mihon/core/common/extensions/Instant.kt b/core/common/src/main/kotlin/mihon/core/common/extensions/Instant.kt new file mode 100644 index 000000000..48de66996 --- /dev/null +++ b/core/common/src/main/kotlin/mihon/core/common/extensions/Instant.kt @@ -0,0 +1,14 @@ +package mihon.core.common.extensions + +import kotlin.time.Instant + +/** + * Reads epoch milliseconds from a format that writes 0 for a time it doesn't know, such as the source API, a tracker + * or a backup. + */ +fun Long.toInstantOrNull(): Instant? = takeIf { it != 0L }?.let(Instant::fromEpochMilliseconds) + +/** + * Writes epoch milliseconds to a format that takes 0 for a time it doesn't know. + */ +fun Instant?.toEpochMillisOrZero(): Long = this?.toEpochMilliseconds() ?: 0L diff --git a/data/src/main/java/tachiyomi/data/DatabaseAdapter.kt b/data/src/main/java/tachiyomi/data/DatabaseAdapter.kt index 33b495fe6..bdd372634 100644 --- a/data/src/main/java/tachiyomi/data/DatabaseAdapter.kt +++ b/data/src/main/java/tachiyomi/data/DatabaseAdapter.kt @@ -5,11 +5,11 @@ import eu.kanade.tachiyomi.source.model.UpdateStrategy import kotlinx.serialization.json.JsonObject import mihon.core.common.extensions.toByteArray import mihon.core.common.extensions.toJsonObject -import java.util.Date +import kotlin.time.Instant -object DateColumnAdapter : ColumnAdapter { - override fun decode(databaseValue: Long): Date = Date(databaseValue) - override fun encode(value: Date): Long = value.time +object InstantColumnAdapter : ColumnAdapter { + override fun decode(databaseValue: Long): Instant = Instant.fromEpochMilliseconds(databaseValue) + override fun encode(value: Instant): Long = value.toEpochMilliseconds() } private const val LIST_OF_STRINGS_SEPARATOR = ", " diff --git a/data/src/main/java/tachiyomi/data/DatabaseBindings.kt b/data/src/main/java/tachiyomi/data/DatabaseBindings.kt index df38cef17..f00fc39cb 100644 --- a/data/src/main/java/tachiyomi/data/DatabaseBindings.kt +++ b/data/src/main/java/tachiyomi/data/DatabaseBindings.kt @@ -67,15 +67,25 @@ object DatabaseBindings { return Database( driver = driver, historyAdapter = History.Adapter( - read_atAdapter = DateColumnAdapter, + read_atAdapter = InstantColumnAdapter, ), mangaAdapter = Manga.Adapter( remote_genreAdapter = StringListColumnAdapter, remote_update_strategyAdapter = UpdateStrategyColumnAdapter, remote_memoAdapter = MemoColumnAdapter, + user_favorite_atAdapter = InstantColumnAdapter, + state_chapter_last_updateAdapter = InstantColumnAdapter, + state_chapter_next_updateAdapter = InstantColumnAdapter, + state_cover_last_modifiedAdapter = InstantColumnAdapter, ), chapterAdapter = Chapter.Adapter( + remote_date_uploadAdapter = InstantColumnAdapter, remote_memoAdapter = MemoColumnAdapter, + state_date_fetchAdapter = InstantColumnAdapter, + ), + manga_trackAdapter = Manga_track.Adapter( + start_dateAdapter = InstantColumnAdapter, + finish_dateAdapter = InstantColumnAdapter, ), ) } diff --git a/data/src/main/java/tachiyomi/data/backup/RestoreRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/backup/RestoreRepositoryImpl.kt index 562c915c1..cf9151748 100644 --- a/data/src/main/java/tachiyomi/data/backup/RestoreRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/backup/RestoreRepositoryImpl.kt @@ -21,8 +21,8 @@ import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.repository.MangaRepository import tachiyomi.domain.track.model.Track import tachiyomi.domain.track.repository.TrackRepository -import java.util.Date import kotlin.math.max +import kotlin.time.Instant @Inject @SingleIn(AppScope::class) @@ -74,7 +74,10 @@ class RestoreRepositoryImpl( val details = if (dbManga.initialized || !manga.initialized) dbManga else manga return dbManga.copy( favoriteAt = if (dbManga.favorite || manga.favorite) { - listOfNotNull(dbManga.favoriteAt, manga.favoriteAt).filter { it > 0 }.minOrNull() ?: 0L + listOfNotNull(dbManga.favoriteAt, manga.favoriteAt) + .filter { it > Manga.UNKNOWN_FAVORITE_AT } + .minOrNull() + ?: Manga.UNKNOWN_FAVORITE_AT } else { null }, @@ -128,7 +131,7 @@ class RestoreRepositoryImpl( remoteStatus = manga.status, remoteCover = manga.thumbnailUrl, stateChapterLastUpdate = manga.lastUpdate, - stateChapterNextUpdate = 0L, + stateChapterNextUpdate = null, stateChapterFetchInterval = 0L, stateInitialized = manga.initialized, userReaderFlags = manga.viewerFlags, @@ -212,7 +215,7 @@ class RestoreRepositoryImpl( } private fun Chapter.forComparison() = - this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L) + this.copy(id = 0L, mangaId = 0L, dateFetch = Instant.DISTANT_PAST, dateUpload = null) private suspend fun restoreHistory(manga: Manga, restoredHistory: List) { if (restoredHistory.isEmpty()) return @@ -229,17 +232,16 @@ class RestoreRepositoryImpl( // each copy; they are one chapter now, read as late as any copy and for as long as all .groupBy { chapterIdsByUrl.getValue(it.chapterUrl) } .map { (chapterId, copies) -> - val readAt = copies.maxOf { it.readAt?.time ?: 0L } + val readAt = copies.mapNotNull { it.readAt }.maxOrNull() val readDuration = copies.sumOf { it.readDuration } val dbHistory = dbHistoryByChapterId[chapterId] // New history entry - ?: return@map Triple(chapterId, Date(readAt), readDuration) + ?: return@map Triple(chapterId, readAt, readDuration) - // Update history entry. 0 is kept rather than written as NULL, since it marks history - // the user removed. + // Update history entry Triple( chapterId, - Date(max(readAt, dbHistory.read_at?.time ?: 0L)), + listOfNotNull(readAt, dbHistory.read_at).maxOrNull(), max(readDuration, dbHistory.read_duration) - dbHistory.read_duration, ) } diff --git a/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt index 5642f1e7d..d6074a70b 100644 --- a/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt @@ -16,6 +16,7 @@ import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.ChapterRemoteUpdate import tachiyomi.domain.chapter.model.ChapterUpdate import tachiyomi.domain.chapter.repository.ChapterRepository +import kotlin.time.Instant @Inject @SingleIn(AppScope::class) @@ -158,13 +159,13 @@ class ChapterRepositoryImpl( remoteName: String, remoteScanlator: String?, remoteChapterNumber: Double, - remoteDateUpload: Long, + remoteDateUpload: Instant?, remoteOrder: Long, remoteMemo: JsonObject, userRead: Boolean, userBookmark: Boolean, userLastPageRead: Long, - stateDateFetch: Long, + stateDateFetch: Instant, ): Chapter = Chapter( id = id, mangaId = mangaId, diff --git a/data/src/main/java/tachiyomi/data/history/HistoryMapper.kt b/data/src/main/java/tachiyomi/data/history/HistoryMapper.kt index 709c5800d..ed0f970b6 100644 --- a/data/src/main/java/tachiyomi/data/history/HistoryMapper.kt +++ b/data/src/main/java/tachiyomi/data/history/HistoryMapper.kt @@ -3,13 +3,13 @@ package tachiyomi.data.history import tachiyomi.domain.history.model.History import tachiyomi.domain.history.model.HistoryWithRelations import tachiyomi.domain.manga.model.MangaCover -import java.util.Date +import kotlin.time.Instant object HistoryMapper { fun mapHistory( id: Long, chapterId: Long, - readAt: Date?, + readAt: Instant?, readDuration: Long, ): History = History( id = id, @@ -26,9 +26,9 @@ object HistoryMapper { thumbnailUrl: String?, sourceId: Long, isFavorite: Boolean, - coverLastModified: Long, + coverLastModified: Instant?, chapterNumber: Double, - readAt: Date?, + readAt: Instant?, readDuration: Long, ): HistoryWithRelations = HistoryWithRelations( id = historyId, diff --git a/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt b/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt index 9e8825bfb..de94512d3 100644 --- a/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt +++ b/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt @@ -5,6 +5,7 @@ import kotlinx.serialization.json.JsonObject import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.MangaWithChapterCount +import kotlin.time.Instant object MangaMapper { fun mapManga( @@ -20,21 +21,21 @@ object MangaMapper { remoteCover: String?, remoteUpdateStrategy: UpdateStrategy, remoteMemo: JsonObject, - userFavoriteAt: Long?, + userFavoriteAt: Instant?, userNotes: String, userReaderFlags: Long, userChapterFlags: Long, - stateChapterLastUpdate: Long?, - stateChapterNextUpdate: Long?, + stateChapterLastUpdate: Instant?, + stateChapterNextUpdate: Instant?, stateChapterFetchInterval: Long, - stateCoverLastModified: Long, + stateCoverLastModified: Instant?, stateInitialized: Boolean, ): Manga = Manga( id = id, source = sourceId, favoriteAt = userFavoriteAt, - lastUpdate = stateChapterLastUpdate ?: 0, - nextUpdate = stateChapterNextUpdate ?: 0, + lastUpdate = stateChapterLastUpdate, + nextUpdate = stateChapterNextUpdate, fetchInterval = stateChapterFetchInterval.toInt(), viewerFlags = userReaderFlags, chapterFlags = userChapterFlags, @@ -66,20 +67,20 @@ object MangaMapper { remoteCover: String?, remoteUpdateStrategy: UpdateStrategy, remoteMemo: JsonObject, - userFavoriteAt: Long, + userFavoriteAt: Instant, userNotes: String, userReaderFlags: Long, userChapterFlags: Long, - stateChapterLastUpdate: Long?, - stateChapterNextUpdate: Long?, + stateChapterLastUpdate: Instant?, + stateChapterNextUpdate: Instant?, stateChapterFetchInterval: Long, - stateCoverLastModified: Long, + stateCoverLastModified: Instant?, stateInitialized: Boolean, totalCount: Long, readCount: Double, - latestUpload: Long, - chapterFetchedAt: Long, - lastRead: Long, + latestUpload: Instant?, + chapterFetchedAt: Instant?, + lastRead: Instant?, bookmarkCount: Double, categories: String, ): LibraryManga = LibraryManga( @@ -128,14 +129,14 @@ object MangaMapper { remoteCover: String?, remoteUpdateStrategy: UpdateStrategy, remoteMemo: JsonObject, - userFavoriteAt: Long?, + userFavoriteAt: Instant?, userNotes: String, userReaderFlags: Long, userChapterFlags: Long, - stateChapterLastUpdate: Long?, - stateChapterNextUpdate: Long?, + stateChapterLastUpdate: Instant?, + stateChapterNextUpdate: Instant?, stateChapterFetchInterval: Long, - stateCoverLastModified: Long, + stateCoverLastModified: Instant?, stateInitialized: Boolean, chapterCount: Long, ): MangaWithChapterCount = MangaWithChapterCount( diff --git a/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt index 078c2bff8..82c1fb788 100644 --- a/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt @@ -100,11 +100,10 @@ class MangaRepositoryImpl( includedCategories: List, ): Flow> { val timeZone = TimeZone.currentSystemDefault() - val epochMillis = - Clock.System.now().toLocalDateTime(timeZone).date.atStartOfDayIn(timeZone).toEpochMilliseconds() + val startOfDay = Clock.System.now().toLocalDateTime(timeZone).date.atStartOfDayIn(timeZone) return database.mangaQueries .getUpcomingManga( - startOfDay = epochMillis, + startOfDay = startOfDay, statuses = statuses, includedEmpty = includedCategories.isEmpty(), includedCategories = includedCategories, diff --git a/data/src/main/java/tachiyomi/data/track/TrackMapper.kt b/data/src/main/java/tachiyomi/data/track/TrackMapper.kt index 183bedbcb..4e45f931d 100644 --- a/data/src/main/java/tachiyomi/data/track/TrackMapper.kt +++ b/data/src/main/java/tachiyomi/data/track/TrackMapper.kt @@ -1,6 +1,7 @@ package tachiyomi.data.track import tachiyomi.domain.track.model.Track +import kotlin.time.Instant object TrackMapper { fun mapTrack( @@ -15,8 +16,8 @@ object TrackMapper { totalChapters: Long, status: Long, score: Double, - startDate: Long, - finishDate: Long, + startDate: Instant?, + finishDate: Instant?, private: Boolean, ): Track = Track( id = id, diff --git a/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt index 5fe3ca017..8e90666a7 100644 --- a/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt @@ -13,6 +13,7 @@ import tachiyomi.data.subscribeToList import tachiyomi.domain.manga.model.MangaCover import tachiyomi.domain.updates.model.UpdatesWithRelations import tachiyomi.domain.updates.repository.UpdatesRepository +import kotlin.time.Instant @Inject @SingleIn(AppScope::class) @@ -23,14 +24,14 @@ class UpdatesRepositoryImpl( override suspend fun awaitWithRead( read: Boolean, - after: Long, + after: Instant, limit: Long, ): List { return getUpdatesByReadStatus(read = read, after = after, limit = limit).awaitAsList() } override fun subscribeAll( - after: Long, + after: Instant, limit: Long, unread: Boolean?, started: Boolean?, @@ -58,13 +59,13 @@ class UpdatesRepositoryImpl( override fun subscribeWithRead( read: Boolean, - after: Long, + after: Instant, limit: Long, ): Flow> { return getUpdatesByReadStatus(read = read, after = after, limit = limit).subscribeToList() } - private fun getUpdatesByReadStatus(read: Boolean, after: Long, limit: Long): Query { + private fun getUpdatesByReadStatus(read: Boolean, after: Instant, limit: Long): Query { return if (read) { database.updatesViewQueries.getReadUpdates( after = after, @@ -94,9 +95,9 @@ class UpdatesRepositoryImpl( sourceId: Long, favorite: Boolean, thumbnailUrl: String?, - coverLastModified: Long, - dateUpload: Long, - dateFetch: Long, + coverLastModified: Instant?, + dateUpload: Instant?, + dateFetch: Instant, excludedScanlator: String?, ): UpdatesWithRelations = UpdatesWithRelations( mangaId = mangaId, diff --git a/data/src/main/sqldelight/tachiyomi/data/chapter.sq b/data/src/main/sqldelight/tachiyomi/data/chapter.sq index 33aa3d321..3e47cb12d 100644 --- a/data/src/main/sqldelight/tachiyomi/data/chapter.sq +++ b/data/src/main/sqldelight/tachiyomi/data/chapter.sq @@ -1,4 +1,5 @@ import kotlin.Boolean; +import kotlin.time.Instant; import kotlinx.serialization.json.JsonObject; CREATE TABLE chapter( @@ -8,13 +9,13 @@ CREATE TABLE chapter( remote_name TEXT NOT NULL, remote_scanlator TEXT, remote_chapter_number REAL NOT NULL, - remote_date_upload INTEGER NOT NULL, + remote_date_upload INTEGER AS Instant, remote_order INTEGER NOT NULL, remote_memo BLOB AS JsonObject NOT NULL, user_read INTEGER AS Boolean NOT NULL, user_bookmark INTEGER AS Boolean NOT NULL, user_last_page_read INTEGER NOT NULL, - state_date_fetch INTEGER NOT NULL, + state_date_fetch INTEGER AS Instant NOT NULL, UNIQUE(manga_id, remote_url), FOREIGN KEY(manga_id) REFERENCES manga (id) ON DELETE CASCADE diff --git a/data/src/main/sqldelight/tachiyomi/data/history.sq b/data/src/main/sqldelight/tachiyomi/data/history.sq index 1607b04a0..189f112e4 100644 --- a/data/src/main/sqldelight/tachiyomi/data/history.sq +++ b/data/src/main/sqldelight/tachiyomi/data/history.sq @@ -1,10 +1,10 @@ -import java.util.Date; +import kotlin.time.Instant; CREATE TABLE history( id INTEGER NOT NULL PRIMARY KEY, manga_id INTEGER NOT NULL, chapter_id INTEGER NOT NULL UNIQUE, - read_at INTEGER AS Date, + read_at INTEGER AS Instant, read_duration INTEGER NOT NULL, FOREIGN KEY(manga_id) REFERENCES manga (id) ON DELETE CASCADE, @@ -22,12 +22,12 @@ WHERE manga_id = :mangaId; resetHistoryById: UPDATE history -SET read_at = 0 +SET read_at = NULL WHERE id = :id; resetHistoryByMangaId: UPDATE history -SET read_at = 0 +SET read_at = NULL WHERE manga_id = :mangaId; removeAllHistory: @@ -35,7 +35,7 @@ DELETE FROM history; removeResettedHistory: DELETE FROM history -WHERE read_at = 0; +WHERE read_at IS NULL; upsert: INSERT INTO history(manga_id, chapter_id, read_at, read_duration) diff --git a/data/src/main/sqldelight/tachiyomi/data/manga.sq b/data/src/main/sqldelight/tachiyomi/data/manga.sq index 5204c3de2..89d821e71 100644 --- a/data/src/main/sqldelight/tachiyomi/data/manga.sq +++ b/data/src/main/sqldelight/tachiyomi/data/manga.sq @@ -2,6 +2,7 @@ import eu.kanade.tachiyomi.source.model.UpdateStrategy; import kotlin.Boolean; import kotlin.String; import kotlin.collections.List; +import kotlin.time.Instant; import kotlinx.serialization.json.JsonObject; CREATE TABLE manga( @@ -17,14 +18,14 @@ CREATE TABLE manga( remote_cover TEXT, remote_update_strategy INTEGER AS UpdateStrategy NOT NULL, remote_memo BLOB AS JsonObject NOT NULL, - user_favorite_at INTEGER, + user_favorite_at INTEGER AS Instant, user_notes TEXT NOT NULL, user_reader_flags INTEGER NOT NULL, user_chapter_flags INTEGER NOT NULL, - state_chapter_last_update INTEGER, - state_chapter_next_update INTEGER, + state_chapter_last_update INTEGER AS Instant, + state_chapter_next_update INTEGER AS Instant, state_chapter_fetch_interval INTEGER NOT NULL, - state_cover_last_modified INTEGER NOT NULL, + state_cover_last_modified INTEGER AS Instant, state_initialized INTEGER AS Boolean NOT NULL, UNIQUE(source_id, remote_url) ); diff --git a/data/src/main/sqldelight/tachiyomi/data/manga_track.sq b/data/src/main/sqldelight/tachiyomi/data/manga_track.sq index 608fdc4a4..a41a596c2 100644 --- a/data/src/main/sqldelight/tachiyomi/data/manga_track.sq +++ b/data/src/main/sqldelight/tachiyomi/data/manga_track.sq @@ -1,4 +1,5 @@ import kotlin.Boolean; +import kotlin.time.Instant; CREATE TABLE manga_track( id INTEGER NOT NULL PRIMARY KEY, @@ -12,8 +13,8 @@ CREATE TABLE manga_track( total_chapters INTEGER NOT NULL, status INTEGER NOT NULL, score REAL NOT NULL, - start_date INTEGER NOT NULL, - finish_date INTEGER NOT NULL, + start_date INTEGER AS Instant, + finish_date INTEGER AS Instant, private INTEGER AS Boolean NOT NULL DEFAULT 0, UNIQUE (manga_id, tracker_id), FOREIGN KEY(manga_id) REFERENCES manga (id) diff --git a/data/src/main/sqldelight/tachiyomi/migrations/19.sqm b/data/src/main/sqldelight/tachiyomi/migrations/19.sqm new file mode 100644 index 000000000..a70074198 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/migrations/19.sqm @@ -0,0 +1,271 @@ +import eu.kanade.tachiyomi.source.model.UpdateStrategy; +import kotlin.Boolean; +import kotlin.String; +import kotlin.collections.List; +import kotlin.time.Instant; +import kotlinx.serialization.json.JsonObject; + +-- A time that isn't known was stored as 0, which reads as the start of 1970 once it is an instant, so +-- it is NULL instead. SQLite cannot drop NOT NULL from a column, so the tables holding one are rebuilt. + +-- Every view reads `manga` or `chapter`, so they are dropped here and recreated against the new tables below +DROP VIEW historyView; +DROP VIEW libraryView; +DROP VIEW updatesView; + +-- Each table is renamed aside and rebuilt under its own name, as in migration 17. Renaming a table also +-- repoints the foreign keys of the tables that reference it, which would leave them on the copy that is +-- dropped; the legacy behavior leaves them naming the table that takes its place. +PRAGMA legacy_alter_table = 1; + +-- --------------------------------------------------------------------------------------------------------- +-- table - manga +-- --------------------------------------------------------------------------------------------------------- + +ALTER TABLE manga RENAME TO manga_temp; + +CREATE TABLE manga( + id INTEGER NOT NULL PRIMARY KEY, + source_id INTEGER NOT NULL, + remote_url TEXT NOT NULL, + remote_title TEXT NOT NULL, + remote_author TEXT, + remote_artist TEXT, + remote_description TEXT, + remote_genre TEXT AS List, + remote_status INTEGER NOT NULL, + remote_cover TEXT, + remote_update_strategy INTEGER AS UpdateStrategy NOT NULL, + remote_memo BLOB AS JsonObject NOT NULL, + user_favorite_at INTEGER AS Instant, + user_notes TEXT NOT NULL, + user_reader_flags INTEGER NOT NULL, + user_chapter_flags INTEGER NOT NULL, + state_chapter_last_update INTEGER AS Instant, + state_chapter_next_update INTEGER AS Instant, + state_chapter_fetch_interval INTEGER NOT NULL, + state_cover_last_modified INTEGER AS Instant, + state_initialized INTEGER AS Boolean NOT NULL, + UNIQUE(source_id, remote_url) +); + +-- `user_favorite_at` keeps its 0: NULL already means the entry isn't in the library, and 0 is an entry +-- added before the time was recorded +INSERT INTO manga( + id, source_id, remote_url, remote_title, remote_author, remote_artist, remote_description, remote_genre, + remote_status, remote_cover, remote_update_strategy, remote_memo, user_favorite_at, user_notes, + user_reader_flags, user_chapter_flags, state_chapter_last_update, state_chapter_next_update, + state_chapter_fetch_interval, state_cover_last_modified, state_initialized +) +SELECT + id, source_id, remote_url, remote_title, remote_author, remote_artist, remote_description, remote_genre, + remote_status, remote_cover, remote_update_strategy, remote_memo, user_favorite_at, user_notes, + user_reader_flags, user_chapter_flags, nullif(state_chapter_last_update, 0), nullif(state_chapter_next_update, 0), + state_chapter_fetch_interval, nullif(state_cover_last_modified, 0), state_initialized +FROM manga_temp; + +DROP TABLE manga_temp; + +CREATE INDEX manga_user_favorite_at_index ON manga(user_favorite_at) WHERE user_favorite_at IS NOT NULL; + +-- --------------------------------------------------------------------------------------------------------- +-- table - chapter +-- --------------------------------------------------------------------------------------------------------- + +ALTER TABLE chapter RENAME TO chapter_temp; + +CREATE TABLE chapter( + id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + remote_url TEXT NOT NULL, + remote_name TEXT NOT NULL, + remote_scanlator TEXT, + remote_chapter_number REAL NOT NULL, + remote_date_upload INTEGER AS Instant, + remote_order INTEGER NOT NULL, + remote_memo BLOB AS JsonObject NOT NULL, + user_read INTEGER AS Boolean NOT NULL, + user_bookmark INTEGER AS Boolean NOT NULL, + user_last_page_read INTEGER NOT NULL, + state_date_fetch INTEGER AS Instant NOT NULL, + UNIQUE(manga_id, remote_url), + FOREIGN KEY(manga_id) REFERENCES manga (id) + ON DELETE CASCADE +); + +INSERT INTO chapter( + id, manga_id, remote_url, remote_name, remote_scanlator, remote_chapter_number, remote_date_upload, + remote_order, remote_memo, user_read, user_bookmark, user_last_page_read, state_date_fetch +) +SELECT + id, manga_id, remote_url, remote_name, remote_scanlator, remote_chapter_number, nullif(remote_date_upload, 0), + remote_order, remote_memo, user_read, user_bookmark, user_last_page_read, state_date_fetch +FROM chapter_temp; + +DROP TABLE chapter_temp; + +CREATE INDEX chapter_state_date_fetch_index ON chapter(state_date_fetch); +CREATE INDEX chapter_unread_state_date_fetch_index ON chapter(state_date_fetch) WHERE user_read = 0; + +-- --------------------------------------------------------------------------------------------------------- +-- table - manga_track +-- --------------------------------------------------------------------------------------------------------- + +ALTER TABLE manga_track RENAME TO manga_track_temp; + +CREATE TABLE manga_track( + id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + tracker_id INTEGER NOT NULL, + remote_id INTEGER NOT NULL, + remote_url TEXT NOT NULL, + library_id INTEGER, + title TEXT NOT NULL, + last_chapter_read REAL NOT NULL, + total_chapters INTEGER NOT NULL, + status INTEGER NOT NULL, + score REAL NOT NULL, + start_date INTEGER AS Instant, + finish_date INTEGER AS Instant, + private INTEGER AS Boolean NOT NULL DEFAULT 0, + UNIQUE (manga_id, tracker_id), + FOREIGN KEY(manga_id) REFERENCES manga (id) + ON DELETE CASCADE +); + +INSERT INTO manga_track( + id, manga_id, tracker_id, remote_id, remote_url, library_id, title, last_chapter_read, total_chapters, + status, score, start_date, finish_date, private +) +SELECT + id, manga_id, tracker_id, remote_id, remote_url, library_id, title, last_chapter_read, total_chapters, + status, score, nullif(start_date, 0), nullif(finish_date, 0), private +FROM manga_track_temp; + +DROP TABLE manga_track_temp; + +CREATE INDEX manga_track_tracker_id_remote_id_index ON manga_track(tracker_id, remote_id); + +PRAGMA legacy_alter_table = 0; + +-- --------------------------------------------------------------------------------------------------------- +-- table - history +-- --------------------------------------------------------------------------------------------------------- + +-- 0 marked history the user removed, which NULL marks now +UPDATE history +SET read_at = NULL +WHERE read_at = 0; + +-- --------------------------------------------------------------------------------------------------------- +-- views +-- --------------------------------------------------------------------------------------------------------- + +CREATE VIEW historyView AS +SELECT + history.id AS id, + manga.id AS mangaId, + chapter.id AS chapterId, + manga.remote_title AS title, + manga.remote_cover AS thumbnailUrl, + manga.source_id AS sourceId, + manga.user_favorite_at IS NOT NULL AS favorite, + manga.state_cover_last_modified AS coverLastModified, + chapter.remote_chapter_number AS chapterNumber, + history.read_at AS readAt, + history.read_duration AS readDuration, + max_read.read_at AS maxReadAt, + max_read.chapter_id AS maxReadAtChapterId +FROM manga +JOIN chapter +ON manga.id = chapter.manga_id +JOIN history +ON chapter.id = history.chapter_id +JOIN ( + SELECT manga_id, chapter_id, MAX(read_at) AS read_at + FROM history + GROUP BY manga_id +) AS max_read +ON chapter.manga_id = max_read.manga_id; + +-- An entry without chapters or reading history has no latest upload, fetch or read time, which is NULL +-- rather than 0 now +CREATE VIEW libraryView AS +SELECT + M.*, + coalesce(C.total, 0) AS totalCount, + coalesce(C.readCount, 0) AS readCount, + C.latestUpload AS latestUpload, + C.fetchedAt AS chapterFetchedAt, + -- Joining history to every chapter is the slowest part of the query, and only entries with excluded + -- scanlators need the chapters to leave theirs out + CASE WHEN EXISTS (SELECT 0 FROM excluded_scanlator WHERE excluded_scanlator.manga_id = M.id) + THEN ( + SELECT max(history.read_at) + FROM history + JOIN chapter + ON chapter.id = history.chapter_id + WHERE history.manga_id = M.id + AND NOT EXISTS ( + SELECT 0 FROM excluded_scanlator + WHERE excluded_scanlator.manga_id = M.id + AND excluded_scanlator.scanlator = chapter.remote_scanlator + ) + ) + ELSE (SELECT max(history.read_at) FROM history WHERE history.manga_id = M.id) + END AS lastRead, + coalesce(C.bookmarkCount, 0) AS bookmarkCount, + coalesce(MC.categories, '0') AS categories +FROM manga M +LEFT JOIN ( + SELECT + chapter.manga_id, + count(*) AS total, + sum(user_read) AS readCount, + max(chapter.remote_date_upload) AS latestUpload, + max(chapter.state_date_fetch) AS fetchedAt, + sum(chapter.user_bookmark) AS bookmarkCount + FROM chapter + LEFT JOIN excluded_scanlator + ON chapter.manga_id = excluded_scanlator.manga_id + AND chapter.remote_scanlator = excluded_scanlator.scanlator + WHERE excluded_scanlator.scanlator IS NULL + AND chapter.manga_id IN (SELECT id FROM manga WHERE user_favorite_at IS NOT NULL) + GROUP BY chapter.manga_id +) AS C +ON M.id = C.manga_id +LEFT JOIN ( + SELECT manga_id, group_concat(category_id) AS categories + FROM manga_category + WHERE manga_id IN (SELECT id FROM manga WHERE user_favorite_at IS NOT NULL) + GROUP BY manga_id +) AS MC +ON MC.manga_id = M.id +WHERE M.user_favorite_at IS NOT NULL; + +CREATE VIEW updatesView AS +SELECT + manga.id AS mangaId, + manga.remote_title AS mangaTitle, + chapter.id AS chapterId, + chapter.remote_name AS chapterName, + chapter.remote_scanlator AS scanlator, + chapter.remote_url AS chapterUrl, + chapter.user_read AS read, + chapter.user_bookmark AS bookmark, + chapter.user_last_page_read AS lastPageRead, + manga.source_id AS sourceId, + manga.user_favorite_at IS NOT NULL AS favorite, + manga.remote_cover AS thumbnailUrl, + manga.state_cover_last_modified AS coverLastModified, + chapter.remote_date_upload AS dateUpload, + chapter.state_date_fetch AS dateFetch, + excluded_scanlator.scanlator AS excludedScanlator +FROM manga JOIN chapter +ON manga.id = chapter.manga_id +LEFT JOIN excluded_scanlator +ON manga.id = excluded_scanlator.manga_id +AND chapter.remote_scanlator = excluded_scanlator.scanlator +WHERE user_favorite_at IS NOT NULL +AND state_date_fetch > user_favorite_at +ORDER BY state_date_fetch DESC; diff --git a/data/src/main/sqldelight/tachiyomi/view/historyView.sq b/data/src/main/sqldelight/tachiyomi/view/historyView.sq index ec2fba9e9..7c7b3ff49 100644 --- a/data/src/main/sqldelight/tachiyomi/view/historyView.sq +++ b/data/src/main/sqldelight/tachiyomi/view/historyView.sq @@ -39,7 +39,7 @@ chapterNumber, readAt, readDuration FROM historyView -WHERE historyView.readAt > 0 +WHERE historyView.readAt IS NOT NULL AND maxReadAtChapterId = historyView.chapterId AND lower(historyView.title) LIKE ('%' || :query || '%') ORDER BY readAt DESC; @@ -62,6 +62,6 @@ JOIN manga ON manga.id = history.manga_id JOIN chapter ON chapter.id = history.chapter_id -WHERE history.read_at > 0 +WHERE history.read_at IS NOT NULL ORDER BY history.read_at DESC LIMIT 1; diff --git a/data/src/main/sqldelight/tachiyomi/view/libraryView.sq b/data/src/main/sqldelight/tachiyomi/view/libraryView.sq index 5717e3166..8c6b5b2d2 100644 --- a/data/src/main/sqldelight/tachiyomi/view/libraryView.sq +++ b/data/src/main/sqldelight/tachiyomi/view/libraryView.sq @@ -3,28 +3,25 @@ SELECT M.*, coalesce(C.total, 0) AS totalCount, coalesce(C.readCount, 0) AS readCount, - coalesce(C.latestUpload, 0) AS latestUpload, - coalesce(C.fetchedAt, 0) AS chapterFetchedAt, + C.latestUpload AS latestUpload, + C.fetchedAt AS chapterFetchedAt, -- Joining history to every chapter is the slowest part of the query, and only entries with excluded -- scanlators need the chapters to leave theirs out - coalesce( - CASE WHEN EXISTS (SELECT 0 FROM excluded_scanlator WHERE excluded_scanlator.manga_id = M.id) - THEN ( - SELECT max(history.read_at) - FROM history - JOIN chapter - ON chapter.id = history.chapter_id - WHERE history.manga_id = M.id - AND NOT EXISTS ( - SELECT 0 FROM excluded_scanlator - WHERE excluded_scanlator.manga_id = M.id - AND excluded_scanlator.scanlator = chapter.remote_scanlator - ) + CASE WHEN EXISTS (SELECT 0 FROM excluded_scanlator WHERE excluded_scanlator.manga_id = M.id) + THEN ( + SELECT max(history.read_at) + FROM history + JOIN chapter + ON chapter.id = history.chapter_id + WHERE history.manga_id = M.id + AND NOT EXISTS ( + SELECT 0 FROM excluded_scanlator + WHERE excluded_scanlator.manga_id = M.id + AND excluded_scanlator.scanlator = chapter.remote_scanlator ) - ELSE (SELECT max(history.read_at) FROM history WHERE history.manga_id = M.id) - END, - 0 - ) AS lastRead, + ) + ELSE (SELECT max(history.read_at) FROM history WHERE history.manga_id = M.id) + END AS lastRead, coalesce(C.bookmarkCount, 0) AS bookmarkCount, coalesce(MC.categories, '0') AS categories FROM manga M @@ -33,8 +30,8 @@ LEFT JOIN ( chapter.manga_id, count(*) AS total, sum(user_read) AS readCount, - coalesce(max(chapter.remote_date_upload), 0) AS latestUpload, - coalesce(max(chapter.state_date_fetch), 0) AS fetchedAt, + max(chapter.remote_date_upload) AS latestUpload, + max(chapter.state_date_fetch) AS fetchedAt, sum(chapter.user_bookmark) AS bookmarkCount FROM chapter LEFT JOIN excluded_scanlator diff --git a/domain/src/main/java/tachiyomi/domain/backup/model/RestoredManga.kt b/domain/src/main/java/tachiyomi/domain/backup/model/RestoredManga.kt index 8e9cdac1e..c8f37b722 100644 --- a/domain/src/main/java/tachiyomi/domain/backup/model/RestoredManga.kt +++ b/domain/src/main/java/tachiyomi/domain/backup/model/RestoredManga.kt @@ -3,7 +3,7 @@ package tachiyomi.domain.backup.model import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.track.model.Track -import java.util.Date +import kotlin.time.Instant data class RestoredManga( val manga: Manga, @@ -16,6 +16,6 @@ data class RestoredManga( data class RestoredHistory( val chapterUrl: String, - val readAt: Date?, + val readAt: Instant?, val readDuration: Long, ) diff --git a/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt b/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt index e6c8d42a2..f8b807c1d 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt @@ -2,6 +2,7 @@ package tachiyomi.domain.chapter.model import kotlinx.serialization.json.JsonObject import mihon.core.common.extensions.EMPTY +import kotlin.time.Instant data class Chapter( val id: Long, @@ -9,11 +10,11 @@ data class Chapter( val read: Boolean, val bookmark: Boolean, val lastPageRead: Long, - val dateFetch: Long, + val dateFetch: Instant, val sourceOrder: Long, val url: String, val name: String, - val dateUpload: Long, + val dateUpload: Instant?, val chapterNumber: Double, val scanlator: String?, val memo: JsonObject, @@ -38,11 +39,11 @@ data class Chapter( read = false, bookmark = false, lastPageRead = 0, - dateFetch = 0, + dateFetch = Instant.fromEpochMilliseconds(0), sourceOrder = 0, url = "", name = "", - dateUpload = -1, + dateUpload = null, chapterNumber = -1.0, scanlator = null, memo = JsonObject.EMPTY, diff --git a/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterRemoteUpdate.kt b/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterRemoteUpdate.kt index 125d5daac..c093ada0b 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterRemoteUpdate.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterRemoteUpdate.kt @@ -1,13 +1,14 @@ package tachiyomi.domain.chapter.model import kotlinx.serialization.json.JsonObject +import kotlin.time.Instant data class ChapterRemoteUpdate( val id: Long, val name: String, val scanlator: String?, val chapterNumber: Double, - val dateUpload: Long?, + val dateUpload: Instant?, val sourceOrder: Long, val memo: JsonObject, ) diff --git a/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt b/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt index e87886fd1..ad5d08046 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt @@ -1,12 +1,13 @@ package tachiyomi.domain.chapter.model import mihon.domain.common.PartialUpdate +import kotlin.time.Instant class ChapterUpdate(val id: Long, block: ChapterUpdate.() -> Unit) : PartialUpdate() { var read: Boolean? by field(null) var bookmark: Boolean? by field(null) var lastPageRead: Long? by field(null) - var dateFetch: Long? by field(null) + var dateFetch: Instant? by field(null) init { block() diff --git a/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterSort.kt b/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterSort.kt index 795805555..7836eee29 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterSort.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterSort.kt @@ -21,8 +21,8 @@ fun getChapterSort( false -> { c1, c2 -> c1.chapterNumber.compareTo(c2.chapterNumber) } } Manga.CHAPTER_SORTING_UPLOAD_DATE -> when (sortDescending) { - true -> { c1, c2 -> c2.dateUpload.compareTo(c1.dateUpload) } - false -> { c1, c2 -> c1.dateUpload.compareTo(c2.dateUpload) } + true -> { c1, c2 -> compareValues(c2.dateUpload, c1.dateUpload) } + false -> { c1, c2 -> compareValues(c1.dateUpload, c2.dateUpload) } } Manga.CHAPTER_SORTING_ALPHABET -> when (sortDescending) { true -> { c1, c2 -> c2.name.compareToWithCollator(c1.name) } diff --git a/domain/src/main/java/tachiyomi/domain/history/model/History.kt b/domain/src/main/java/tachiyomi/domain/history/model/History.kt index 41b58b637..39480c82b 100644 --- a/domain/src/main/java/tachiyomi/domain/history/model/History.kt +++ b/domain/src/main/java/tachiyomi/domain/history/model/History.kt @@ -1,11 +1,11 @@ package tachiyomi.domain.history.model -import java.util.Date +import kotlin.time.Instant data class History( val id: Long, val chapterId: Long, - val readAt: Date?, + val readAt: Instant?, val readDuration: Long, ) { companion object { diff --git a/domain/src/main/java/tachiyomi/domain/history/model/HistoryUpdate.kt b/domain/src/main/java/tachiyomi/domain/history/model/HistoryUpdate.kt index 62738c33b..3b6c3f081 100644 --- a/domain/src/main/java/tachiyomi/domain/history/model/HistoryUpdate.kt +++ b/domain/src/main/java/tachiyomi/domain/history/model/HistoryUpdate.kt @@ -1,9 +1,9 @@ package tachiyomi.domain.history.model -import java.util.Date +import kotlin.time.Instant data class HistoryUpdate( val chapterId: Long, - val readAt: Date, + val readAt: Instant, val sessionReadDuration: Long, ) diff --git a/domain/src/main/java/tachiyomi/domain/history/model/HistoryWithRelations.kt b/domain/src/main/java/tachiyomi/domain/history/model/HistoryWithRelations.kt index 68d93dd95..28711a194 100644 --- a/domain/src/main/java/tachiyomi/domain/history/model/HistoryWithRelations.kt +++ b/domain/src/main/java/tachiyomi/domain/history/model/HistoryWithRelations.kt @@ -1,7 +1,7 @@ package tachiyomi.domain.history.model import tachiyomi.domain.manga.model.MangaCover -import java.util.Date +import kotlin.time.Instant data class HistoryWithRelations( val id: Long, @@ -9,7 +9,7 @@ data class HistoryWithRelations( val mangaId: Long, val title: String, val chapterNumber: Double, - val readAt: Date?, + val readAt: Instant?, val readDuration: Long, val coverData: MangaCover, ) diff --git a/domain/src/main/java/tachiyomi/domain/library/model/LibraryManga.kt b/domain/src/main/java/tachiyomi/domain/library/model/LibraryManga.kt index 2fda140aa..92a548e28 100644 --- a/domain/src/main/java/tachiyomi/domain/library/model/LibraryManga.kt +++ b/domain/src/main/java/tachiyomi/domain/library/model/LibraryManga.kt @@ -1,6 +1,7 @@ package tachiyomi.domain.library.model import tachiyomi.domain.manga.model.Manga +import kotlin.time.Instant data class LibraryManga( val manga: Manga, @@ -8,9 +9,9 @@ data class LibraryManga( val totalChapters: Long, val readCount: Long, val bookmarkCount: Long, - val latestUpload: Long, - val chapterFetchedAt: Long, - val lastRead: Long, + val latestUpload: Instant?, + val chapterFetchedAt: Instant?, + val lastRead: Instant?, ) { val id: Long = manga.id 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 c62f07af2..f08866a6f 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt @@ -15,6 +15,7 @@ import tachiyomi.domain.manga.model.MangaUpdate import kotlin.math.absoluteValue import kotlin.time.Clock import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Instant @Inject @@ -26,17 +27,13 @@ class FetchInterval( manga: Manga, dateTime: LocalDateTime, timeZone: TimeZone, - window: Pair, + window: ClosedRange?, ): MangaUpdate { val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval( chapters = getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true), zone = timeZone, ) - val currentWindow = if (window.first == 0L && window.second == 0L) { - getWindow(Clock.System.now().toLocalDateTime(timeZone).date, timeZone) - } else { - window - } + val currentWindow = window ?: getWindow(Clock.System.now().toLocalDateTime(timeZone).date, timeZone) val nextUpdate = calculateNextUpdate(manga, interval, dateTime, timeZone, currentWindow) return MangaUpdate(manga.id) { @@ -45,22 +42,19 @@ class FetchInterval( } } - fun getWindow(localDateTime: LocalDate, timeZone: TimeZone): Pair { + fun getWindow(localDateTime: LocalDate, timeZone: TimeZone): ClosedRange { val today = localDateTime.atStartOfDayIn(timeZone) - val lowerBound = today - GRACE_PERIOD.days - val upperBound = today + GRACE_PERIOD.days - return Pair(lowerBound.toEpochMilliseconds(), upperBound.toEpochMilliseconds()) + return (today - GRACE_PERIOD.days)..(today + GRACE_PERIOD.days) } 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 } + .mapNotNull { it.dateUpload } + .sortedDescending() .map { - Instant.fromEpochMilliseconds(it.dateUpload) - .toLocalDateTime(zone) + it.toLocalDateTime(zone) .date .atStartOfDayIn(zone) } @@ -69,10 +63,10 @@ class FetchInterval( .toList() val fetchDates = chapters.asSequence() - .sortedByDescending { it.dateFetch } + .map { it.dateFetch } + .sortedDescending() .map { - Instant.fromEpochMilliseconds(it.dateFetch) - .toLocalDateTime(zone) + it.toLocalDateTime(zone) .date .atStartOfDayIn(zone) } @@ -103,13 +97,13 @@ class FetchInterval( interval: Int, dateTime: LocalDateTime, timeZone: TimeZone, - window: Pair, - ): Long { - if (manga.nextUpdate in window.first.rangeTo(window.second + 1)) { + window: ClosedRange, + ): Instant { + if (manga.nextUpdate != null && manga.nextUpdate in window.start..window.endInclusive + 1.milliseconds) { return manga.nextUpdate } - val instant = if (manga.lastUpdate > 0) Instant.fromEpochMilliseconds(manga.lastUpdate) else Clock.System.now() + val instant = manga.lastUpdate ?: Clock.System.now() val latestDate = instant.toLocalDateTime(timeZone).date.atStartOfDayIn(timeZone) val daysSinceLatest = (dateTime.toInstant(timeZone) - latestDate).inWholeDays @@ -119,7 +113,7 @@ class FetchInterval( ) val offsetDays = ((cycle + 1) * interval.absoluteValue.toLong()).days - return latestDate.plus(offsetDays).toEpochMilliseconds() + return latestDate.plus(offsetDays) } private fun increaseInterval(delta: Int, daysSinceLatest: Long, increaseWhenOver: Int): Int { 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 be790b87a..48bdde7af 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt @@ -19,13 +19,13 @@ import java.io.Serializable as JavaSerializable data class Manga( val id: Long, val source: Long, - val lastUpdate: Long, - val nextUpdate: Long, + val lastUpdate: Instant?, + val nextUpdate: Instant?, val fetchInterval: Int, - val favoriteAt: Long?, + val favoriteAt: Instant?, val viewerFlags: Long, val chapterFlags: Long, - val coverLastModified: Long, + val coverLastModified: Instant?, val url: String, val title: String, val artist: String?, @@ -44,9 +44,7 @@ data class Manga( get() = favoriteAt != null val expectedNextUpdate: Instant? - get() = nextUpdate - .takeIf { status != SManga.COMPLETED.toLong() } - ?.let { Instant.fromEpochMilliseconds(it) } + get() = nextUpdate?.takeIf { status != SManga.COMPLETED.toLong() } val sorting: Long get() = chapterFlags and CHAPTER_SORTING_MASK @@ -111,18 +109,23 @@ data class Manga( const val CHAPTER_DISPLAY_NUMBER = 0x00100000L const val CHAPTER_DISPLAY_MASK = 0x00100000L + /** + * [favoriteAt] of an entry added to the library before the time it was added was recorded. + */ + val UNKNOWN_FAVORITE_AT = Instant.fromEpochMilliseconds(0) + fun create() = Manga( id = -1L, url = "", title = "", source = -1L, - lastUpdate = 0L, - nextUpdate = 0L, + lastUpdate = null, + nextUpdate = null, fetchInterval = 0, favoriteAt = null, viewerFlags = 0L, chapterFlags = 0L, - coverLastModified = 0L, + coverLastModified = null, artist = null, author = null, description = null, diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/MangaCover.kt b/domain/src/main/java/tachiyomi/domain/manga/model/MangaCover.kt index d2ca3b103..4827f55a1 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/model/MangaCover.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/model/MangaCover.kt @@ -1,5 +1,7 @@ package tachiyomi.domain.manga.model +import kotlin.time.Instant + /** * Contains the required data for MangaCoverFetcher */ @@ -8,7 +10,7 @@ data class MangaCover( val sourceId: Long, val isMangaFavorite: Boolean, val url: String?, - val lastModified: Long, + val lastModified: Instant?, ) fun Manga.asMangaCover(): MangaCover { diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/MangaRemoteUpdate.kt b/domain/src/main/java/tachiyomi/domain/manga/model/MangaRemoteUpdate.kt index 70225ba58..7eefaca14 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/model/MangaRemoteUpdate.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/model/MangaRemoteUpdate.kt @@ -2,6 +2,7 @@ package tachiyomi.domain.manga.model import eu.kanade.tachiyomi.source.model.UpdateStrategy import kotlinx.serialization.json.JsonObject +import kotlin.time.Instant data class MangaRemoteUpdate( val id: Long, @@ -15,5 +16,5 @@ data class MangaRemoteUpdate( val updateStrategy: UpdateStrategy, val memo: JsonObject, val initialized: Boolean, - val coverLastModified: Long?, + val coverLastModified: Instant?, ) diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt b/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt index 8fa6ccfa8..3f94aac7b 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt @@ -1,15 +1,16 @@ package tachiyomi.domain.manga.model import mihon.domain.common.PartialUpdate +import kotlin.time.Instant class MangaUpdate(val id: Long, block: MangaUpdate.() -> Unit) : PartialUpdate() { - var favoriteAt: Long? by field(null) - var lastUpdate: Long? by field(null) - var nextUpdate: Long? by field(null) + var favoriteAt: Instant? by field(null) + var lastUpdate: Instant? by field(null) + var nextUpdate: Instant? by field(null) var fetchInterval: Int? by field(null) var viewerFlags: Long? by field(null) var chapterFlags: Long? by field(null) - var coverLastModified: Long? by field(null) + var coverLastModified: Instant? by field(null) var notes: String? by field(null) init { diff --git a/domain/src/main/java/tachiyomi/domain/track/model/Track.kt b/domain/src/main/java/tachiyomi/domain/track/model/Track.kt index 54765790c..5d10fcae8 100644 --- a/domain/src/main/java/tachiyomi/domain/track/model/Track.kt +++ b/domain/src/main/java/tachiyomi/domain/track/model/Track.kt @@ -1,6 +1,7 @@ package tachiyomi.domain.track.model import java.io.Serializable +import kotlin.time.Instant data class Track( val id: Long, @@ -14,7 +15,7 @@ data class Track( val status: Long, val score: Double, val remoteUrl: String, - val startDate: Long, - val finishDate: Long, + val startDate: Instant?, + val finishDate: Instant?, val private: Boolean, ) : Serializable 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 59e11f7fa..2cfa61049 100644 --- a/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt +++ b/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt @@ -11,7 +11,7 @@ class GetUpdates( private val repository: UpdatesRepository, ) { - suspend fun await(read: Boolean, after: Long): List { + suspend fun await(read: Boolean, after: Instant): List { return repository.awaitWithRead(read, after, limit = 500) } @@ -25,7 +25,7 @@ class GetUpdates( excludedCategories: List, ): Flow> { return repository.subscribeAll( - instant.toEpochMilliseconds(), + instant, limit = 500, unread = unread, started = started, @@ -36,7 +36,7 @@ class GetUpdates( ) } - fun subscribe(read: Boolean, after: Long): Flow> { + fun subscribe(read: Boolean, after: Instant): Flow> { return repository.subscribeWithRead(read, after, limit = 500) } } diff --git a/domain/src/main/java/tachiyomi/domain/updates/model/UpdatesWithRelations.kt b/domain/src/main/java/tachiyomi/domain/updates/model/UpdatesWithRelations.kt index 4bdb441bf..e9a3275ad 100644 --- a/domain/src/main/java/tachiyomi/domain/updates/model/UpdatesWithRelations.kt +++ b/domain/src/main/java/tachiyomi/domain/updates/model/UpdatesWithRelations.kt @@ -1,6 +1,7 @@ package tachiyomi.domain.updates.model import tachiyomi.domain.manga.model.MangaCover +import kotlin.time.Instant data class UpdatesWithRelations( val mangaId: Long, @@ -13,6 +14,6 @@ data class UpdatesWithRelations( val bookmark: Boolean, val lastPageRead: Long, val sourceId: Long, - val dateFetch: Long, + val dateFetch: Instant, val coverData: MangaCover, ) diff --git a/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt b/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt index a991f6822..5ebead2dd 100644 --- a/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt +++ b/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt @@ -2,13 +2,14 @@ package tachiyomi.domain.updates.repository import kotlinx.coroutines.flow.Flow import tachiyomi.domain.updates.model.UpdatesWithRelations +import kotlin.time.Instant interface UpdatesRepository { - suspend fun awaitWithRead(read: Boolean, after: Long, limit: Long): List + suspend fun awaitWithRead(read: Boolean, after: Instant, limit: Long): List fun subscribeAll( - after: Long, + after: Instant, limit: Long, unread: Boolean?, started: Boolean?, @@ -18,5 +19,5 @@ interface UpdatesRepository { excludedCategories: List, ): Flow> - fun subscribeWithRead(read: Boolean, after: Long, limit: Long): Flow> + fun subscribeWithRead(read: Boolean, after: Instant, limit: Long): Flow> } 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 7d204aa1b..d17b98178 100644 --- a/domain/src/test/java/tachiyomi/domain/manga/interactor/FetchIntervalTest.kt +++ b/domain/src/test/java/tachiyomi/domain/manga/interactor/FetchIntervalTest.kt @@ -12,8 +12,7 @@ import tachiyomi.domain.chapter.model.Chapter 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.Instant @Execution(ExecutionMode.CONCURRENT) class FetchIntervalTest { @@ -21,8 +20,8 @@ class FetchIntervalTest { private val testTime = LocalDateTime.parse("2020-01-01T00:00:00") private val testTimeZone = TimeZone.UTC private var chapter = Chapter.create().copy( - dateFetch = testTime.toInstant(testTimeZone).toEpochMilliseconds(), - dateUpload = testTime.toInstant(testTimeZone).toEpochMilliseconds(), + dateFetch = testTime.toInstant(testTimeZone), + dateUpload = testTime.toInstant(testTimeZone), ) private val fetchInterval = FetchInterval(mockk()) @@ -35,7 +34,7 @@ class FetchIntervalTest { fetchInterval.calculateInterval(chaptersWithUploadDate, testTimeZone) shouldBe 7 val chaptersWithoutUploadDate = chaptersWithUploadDate.map { - it.copy(dateUpload = 0L) + it.copy(dateUpload = null) } fetchInterval.calculateInterval(chaptersWithoutUploadDate, testTimeZone) shouldBe 7 } @@ -119,7 +118,7 @@ class FetchIntervalTest { fetchInterval.calculateInterval(chaptersWithUploadDate, testTimeZone) shouldBe 1 val chaptersWithoutUploadDate = chaptersWithUploadDate.map { - it.copy(dateUpload = 0L) + it.copy(dateUpload = null) } fetchInterval.calculateInterval(chaptersWithoutUploadDate, testTimeZone) shouldBe 1 } @@ -133,10 +132,10 @@ class FetchIntervalTest { } private fun chapterWithTime(chapter: Chapter, duration: Duration): Chapter { - val newTime = testTime.toInstant(testTimeZone).plus(duration).toEpochMilliseconds() + val newTime = testTime.toInstant(testTimeZone) + duration return chapter.copy(dateFetch = newTime, dateUpload = newTime) } private fun List.lastUploadDate() = - last().dateUpload.toDuration(DurationUnit.MILLISECONDS) + last().dateUpload!! - Instant.fromEpochMilliseconds(0) } 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 a2b8dff68..d656329b2 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/BaseUpdatesGridGlanceWidget.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/BaseUpdatesGridGlanceWidget.kt @@ -96,7 +96,7 @@ abstract class BaseUpdatesGridGlanceWidget : GlanceAppWidget() { val flow = remember { getUpdates - .subscribe(false, DateLimit.toEpochMilliseconds()) + .subscribe(false, DateLimit) .map { rawData -> rawData.prepareData(context, rowCount, columnCount) } 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 7105eacd0..442d92497 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/WidgetManager.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/WidgetManager.kt @@ -24,7 +24,7 @@ class WidgetManager( context(context: Context) fun init(scope: LifecycleCoroutineScope) { combine( - getUpdates.subscribe(read = false, after = BaseUpdatesGridGlanceWidget.DateLimit.toEpochMilliseconds()), + getUpdates.subscribe(read = false, after = BaseUpdatesGridGlanceWidget.DateLimit), securityPreferences.useAuthenticator.changes(), transform = { a, b -> a to b }, )