Store every date in the database as an Instant

This commit is contained in:
AntsyLich
2026-10-10 17:37:43 +06:00
parent 8972882a34
commit 44c1e1915d
68 changed files with 599 additions and 284 deletions
@@ -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<Long, Long> = Pair(0, 0),
fetchWindow: ClosedRange<Instant>? = null,
): List<Chapter> {
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)
@@ -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
@@ -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<Long, Long> = fetchInterval.getWindow(dateTime.date, timeZone),
window: ClosedRange<Instant>? = 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)
@@ -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<String>?,
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),
@@ -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)
}
@@ -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,
)
}
@@ -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,
)
}
@@ -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<HistoryViewModel.State> {
@@ -99,14 +97,14 @@ class HistoryviewModelStateProvider : PreviewParameterProvider<HistoryViewModel.
mangaId = Random.nextLong(),
title = "Test Title",
chapterNumber = Random.nextDouble(),
readAt = Date.from(Clock.System.now().toJavaInstant()),
readAt = Clock.System.now(),
readDuration = Random.nextLong(),
coverData = MangaCover(
mangaId = Random.nextLong(),
sourceId = Random.nextLong(),
isMangaFavorite = Random.nextBoolean(),
url = "https://example.com/cover.png",
lastModified = Random.nextLong(),
lastModified = Instant.fromEpochMilliseconds(Random.nextLong()),
),
),
),
@@ -2,7 +2,7 @@ package eu.kanade.presentation.history.components
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import tachiyomi.domain.history.model.HistoryWithRelations
import java.util.Date
import kotlin.time.Instant
internal class HistoryWithRelationsProvider : PreviewParameterProvider<HistoryWithRelations> {
@@ -12,14 +12,14 @@ internal class HistoryWithRelationsProvider : PreviewParameterProvider<HistoryWi
mangaId = 3L,
title = "Test Title",
chapterNumber = 10.2,
readAt = Date(1697247357L),
readAt = Instant.fromEpochMilliseconds(1697247357L),
readDuration = 123L,
coverData = tachiyomi.domain.manga.model.MangaCover(
mangaId = 3L,
sourceId = 4L,
isMangaFavorite = false,
url = "https://example.com/cover.png",
lastModified = 5L,
lastModified = Instant.fromEpochMilliseconds(5L),
),
)
@@ -36,7 +36,7 @@ internal class HistoryWithRelationsProvider : PreviewParameterProvider<HistoryWi
sourceId = 4L,
isMangaFavorite = false,
url = "https://example.com/cover.png",
lastModified = 5L,
lastModified = Instant.fromEpochMilliseconds(5L),
),
)
@@ -46,14 +46,14 @@ internal class HistoryWithRelationsProvider : PreviewParameterProvider<HistoryWi
mangaId = 3L,
title = "Test Title",
chapterNumber = -2.0,
readAt = Date(1697247357L),
readAt = Instant.fromEpochMilliseconds(1697247357L),
readDuration = 123L,
coverData = tachiyomi.domain.manga.model.MangaCover(
mangaId = 3L,
sourceId = 4L,
isMangaFavorite = false,
url = "https://example.com/cover.png",
lastModified = 5L,
lastModified = Instant.fromEpochMilliseconds(5L),
),
)
@@ -113,13 +113,13 @@ fun TrackInfoDialogHome(
onScoreClick = { onScoreClick(item) }
.takeIf { supportsScoring },
startDate = remember(item.track.startDate) {
dateFormat.format(item.track.startDate.toLocalDate().toJavaLocalDate())
item.track.startDate?.let { dateFormat.format(it.toJavaLocalDate()) }
}
.takeIf { supportsReadingDates && item.track.startDate != 0L },
?.takeIf { supportsReadingDates },
onStartDateClick = { onStartDateEdit(item) } // TODO
.takeIf { supportsReadingDates },
endDate = dateFormat.format(item.track.finishDate.toJavaLocalDate())
.takeIf { supportsReadingDates && item.track.finishDate != 0L },
endDate = item.track.finishDate?.let { dateFormat.format(it.toJavaLocalDate()) }
?.takeIf { supportsReadingDates },
onEndDateClick = { onEndDateEdit(item) }
.takeIf { supportsReadingDates },
onNewSearch = { onNewSearch(item) },
@@ -23,8 +23,8 @@ internal class TrackInfoDialogHomePreviewProvider :
status = 1L,
score = 2.0,
remoteUrl = "https://example.com",
startDate = 0L,
finishDate = 0L,
startDate = null,
finishDate = null,
private = false,
)
private val privateTrack = aTrack.copy(private = true)
@@ -5,13 +5,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import eu.kanade.tachiyomi.util.lang.toLocalDate
import kotlin.random.Random
import kotlin.time.Clock
import kotlin.time.Duration.Companion.days
import kotlin.time.toJavaInstant
internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composable () -> Unit> {
private val fullPageWithSecondSelected = @Composable {
@@ -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 ""
@@ -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(),
@@ -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(),
)
@@ -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,
)
}
@@ -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
},
@@ -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,
)
@@ -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,
@@ -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<Pair<Manga, String?>>()
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<Long, Long>): List<Chapter> {
private suspend fun updateManga(manga: Manga, fetchWindow: ClosedRange<Instant>): List<Chapter> {
val source = sourceManager.getOrStub(manga.source)
val update = updateMangaFromRemote(
@@ -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)
@@ -164,7 +164,7 @@ class DownloadQueueViewModel(
downloadManager.cancelQueuedDownloads(downloads)
}
fun <R : Comparable<R>> reorderQueue(selector: (DownloadItem) -> R, reverse: Boolean = false) {
fun <R : Comparable<R>> reorderQueue(selector: (DownloadItem) -> R?, reverse: Boolean = false) {
val adapter = adapter ?: return
val newDownloads = mutableListOf<Download>()
adapter.headerItems.forEach { headerItem ->
@@ -104,8 +104,8 @@ class HistoryViewModel(
private fun List<HistoryWithRelations>.toHistoryUiModels(): List<HistoryUiModel> {
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.
@@ -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)
@@ -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
@@ -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))
@@ -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
}
@@ -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()
}
@@ -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
@@ -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<Long, Long> = Pair(0, 0),
fetchWindow: ClosedRange<Instant>? = null,
): Result<RemoteMangaUpdate> {
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<Long, Long> = Pair(0, 0),
fetchWindow: ClosedRange<Instant>? = null,
): Result<RemoteMangaUpdate> {
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()
}
}
@@ -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 {