mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-16 13:48:09 -07:00
Store every date in the database as an Instant
This commit is contained in:
@@ -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()),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+6
-6
@@ -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) },
|
||||
|
||||
+2
-2
@@ -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 ""
|
||||
|
||||
+3
-2
@@ -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(
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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<Date, Long> {
|
||||
override fun decode(databaseValue: Long): Date = Date(databaseValue)
|
||||
override fun encode(value: Date): Long = value.time
|
||||
object InstantColumnAdapter : ColumnAdapter<Instant, Long> {
|
||||
override fun decode(databaseValue: Long): Instant = Instant.fromEpochMilliseconds(databaseValue)
|
||||
override fun encode(value: Instant): Long = value.toEpochMilliseconds()
|
||||
}
|
||||
|
||||
private const val LIST_OF_STRINGS_SEPARATOR = ", "
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<RestoredHistory>) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -100,11 +100,10 @@ class MangaRepositoryImpl(
|
||||
includedCategories: List<Long>,
|
||||
): Flow<List<Manga>> {
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<UpdatesWithRelations> {
|
||||
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<List<UpdatesWithRelations>> {
|
||||
return getUpdatesByReadStatus(read = read, after = after, limit = limit).subscribeToList()
|
||||
}
|
||||
|
||||
private fun getUpdatesByReadStatus(read: Boolean, after: Long, limit: Long): Query<UpdatesWithRelations> {
|
||||
private fun getUpdatesByReadStatus(read: Boolean, after: Instant, limit: Long): Query<UpdatesWithRelations> {
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<String>,
|
||||
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;
|
||||
@@ -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;
|
||||
|
||||
@@ -3,11 +3,10 @@ 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)
|
||||
@@ -22,9 +21,7 @@ SELECT
|
||||
)
|
||||
)
|
||||
ELSE (SELECT max(history.read_at) FROM history WHERE history.manga_id = M.id)
|
||||
END,
|
||||
0
|
||||
) AS lastRead,
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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<Long, Long>,
|
||||
window: ClosedRange<Instant>?,
|
||||
): 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<Long, Long> {
|
||||
fun getWindow(localDateTime: LocalDate, timeZone: TimeZone): ClosedRange<Instant> {
|
||||
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<Chapter>, 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, Long>,
|
||||
): Long {
|
||||
if (manga.nextUpdate in window.first.rangeTo(window.second + 1)) {
|
||||
window: ClosedRange<Instant>,
|
||||
): 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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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?,
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,7 +11,7 @@ class GetUpdates(
|
||||
private val repository: UpdatesRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(read: Boolean, after: Long): List<UpdatesWithRelations> {
|
||||
suspend fun await(read: Boolean, after: Instant): List<UpdatesWithRelations> {
|
||||
return repository.awaitWithRead(read, after, limit = 500)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class GetUpdates(
|
||||
excludedCategories: List<Long>,
|
||||
): Flow<List<UpdatesWithRelations>> {
|
||||
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<List<UpdatesWithRelations>> {
|
||||
fun subscribe(read: Boolean, after: Instant): Flow<List<UpdatesWithRelations>> {
|
||||
return repository.subscribeWithRead(read, after, limit = 500)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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<UpdatesWithRelations>
|
||||
suspend fun awaitWithRead(read: Boolean, after: Instant, limit: Long): List<UpdatesWithRelations>
|
||||
|
||||
fun subscribeAll(
|
||||
after: Long,
|
||||
after: Instant,
|
||||
limit: Long,
|
||||
unread: Boolean?,
|
||||
started: Boolean?,
|
||||
@@ -18,5 +19,5 @@ interface UpdatesRepository {
|
||||
excludedCategories: List<Long>,
|
||||
): Flow<List<UpdatesWithRelations>>
|
||||
|
||||
fun subscribeWithRead(read: Boolean, after: Long, limit: Long): Flow<List<UpdatesWithRelations>>
|
||||
fun subscribeWithRead(read: Boolean, after: Instant, limit: Long): Flow<List<UpdatesWithRelations>>
|
||||
}
|
||||
|
||||
@@ -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<Chapter>.lastUploadDate() =
|
||||
last().dateUpload.toDuration(DurationUnit.MILLISECONDS)
|
||||
last().dateUpload!! - Instant.fromEpochMilliseconds(0)
|
||||
}
|
||||
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user