Replace java.time APIs with kotlin(x).(date)time (#3001)

* Replace java.time APIs with kotlin(x).(date)time

kotlinx-datetime explicitly says in its README that it does not cover
i18n, so we have to stick with using their toJavaX converters wherever
we localise dates and times.

Yes, some of these replacements are... questionable. But I wanted to
try and maximise the replacement for now.

* Bump kotlinx-datetime to 0.8.0

* More kotlin.time/kotlinx-datetime replacements

* Remove redundant init block & make attributes val

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