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
@@ -10,6 +10,9 @@ import eu.kanade.tachiyomi.data.download.DownloadProvider
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import tachiyomi.data.chapter.ChapterSanitizer
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
@@ -23,8 +26,8 @@ import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.model.Manga
import tachiyomi.source.local.isLocal
import java.lang.Long.max
import java.time.ZonedDateTime
import java.util.TreeSet
import kotlin.time.Clock
class SyncChaptersWithSource(
private val downloadManager: DownloadManager,
@@ -57,8 +60,9 @@ class SyncChaptersWithSource(
throw NoChaptersException()
}
val now = ZonedDateTime.now()
val nowMillis = now.toInstant().toEpochMilli()
val timeZone = TimeZone.currentSystemDefault()
val now = Clock.System.now().toLocalDateTime(timeZone)
val nowMillis = now.toInstant(timeZone).toEpochMilliseconds()
val sourceChapters = rawSourceChapters
.distinctBy { it.url }
@@ -145,6 +149,7 @@ class SyncChaptersWithSource(
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
updateManga.awaitUpdateFetchInterval(
manga,
timeZone,
now,
fetchWindow,
)
@@ -217,7 +222,7 @@ class SyncChaptersWithSource(
val chapterUpdates = updatedChapters.map { it.toChapterUpdate() }
updateChapter.awaitAll(chapterUpdates)
}
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
updateManga.awaitUpdateFetchInterval(manga, timeZone, now, fetchWindow)
// Set this manga as updated since chapters were changed
// Note that last_update actually represents last time the chapter list changed at all
@@ -1,11 +1,13 @@
package eu.kanade.domain.manga.interactor
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.repository.MangaRepository
import java.time.Instant
import java.time.ZonedDateTime
import kotlin.time.Clock
class UpdateManga(
private val mangaRepository: MangaRepository,
@@ -22,25 +24,31 @@ class UpdateManga(
suspend fun awaitUpdateFetchInterval(
manga: Manga,
dateTime: ZonedDateTime = ZonedDateTime.now(),
window: Pair<Long, Long> = fetchInterval.getWindow(dateTime),
timeZone: TimeZone = TimeZone.currentSystemDefault(),
dateTime: LocalDateTime = Clock.System.now().toLocalDateTime(timeZone),
window: Pair<Long, Long> = fetchInterval.getWindow(dateTime.date, timeZone),
): Boolean {
return mangaRepository.update(
fetchInterval.toMangaUpdate(manga, dateTime, window),
fetchInterval.toMangaUpdate(manga, dateTime, timeZone, window),
)
}
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Instant.now().toEpochMilli()))
return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Clock.System.now().toEpochMilliseconds()))
}
suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean {
return mangaRepository.update(MangaUpdate(id = mangaId, coverLastModified = Instant.now().toEpochMilli()))
return mangaRepository.update(
MangaUpdate(
id = mangaId,
coverLastModified = Clock.System.now().toEpochMilliseconds(),
),
)
}
suspend fun awaitUpdateFavorite(mangaId: Long, favorite: Boolean): Boolean {
val dateAdded = when (favorite) {
true -> Instant.now().toEpochMilli()
true -> Clock.System.now().toEpochMilliseconds()
false -> 0
}
return mangaRepository.update(
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
import kotlinx.datetime.TimeZone
import logcat.LogPriority
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.lang.withNonCancellableContext
@@ -18,7 +19,6 @@ import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.track.interactor.InsertTrack
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.ZoneOffset
class AddTracks(
private val insertTrack: InsertTrack,
@@ -62,8 +62,8 @@ class AddTracks(
firstReadChapterDate?.let {
val startDate = firstReadChapterDate.time.convertEpochMillisZone(
ZoneOffset.systemDefault(),
ZoneOffset.UTC,
TimeZone.currentSystemDefault(),
TimeZone.UTC,
)
track = track.copy(
startDate = startDate,
@@ -5,23 +5,23 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.util.lang.toRelativeString
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import kotlin.time.Instant
@Composable
fun relativeDateText(
dateEpochMillis: Long,
): String {
return relativeDateText(
localDate = LocalDate.ofInstant(
Instant.ofEpochMilli(dateEpochMillis),
ZoneId.systemDefault(),
)
localDate = Instant.fromEpochMilliseconds(dateEpochMillis)
.toLocalDateTime(TimeZone.currentSystemDefault())
.date
.takeIf { dateEpochMillis != 0L },
)
}
@@ -19,6 +19,7 @@ import eu.kanade.presentation.components.relativeDateText
import eu.kanade.presentation.history.components.HistoryItem
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import eu.kanade.tachiyomi.ui.history.HistoryViewModel
import kotlinx.datetime.LocalDate
import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn
@@ -27,7 +28,6 @@ import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import java.time.LocalDate
@Composable
fun HistoryScreen(
@@ -2,13 +2,16 @@ package eu.kanade.presentation.history
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import eu.kanade.tachiyomi.ui.history.HistoryViewModel
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.domain.manga.model.MangaCover
import java.time.Instant
import java.time.LocalDate
import java.time.temporal.ChronoUnit
import java.util.Date
import kotlin.random.Random
import kotlin.time.Clock
import kotlin.time.Duration.Companion.days
import kotlin.time.Instant
import kotlin.time.toJavaInstant
class HistoryviewModelStateProvider : PreviewParameterProvider<HistoryViewModel.State> {
@@ -18,9 +21,9 @@ class HistoryviewModelStateProvider : PreviewParameterProvider<HistoryViewModel.
listOf(HistoryUiModelExamples.headerToday)
.asSequence()
.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.header { it.minus(2, ChronoUnit.DAYS) })
.plus(HistoryUiModelExamples.header { it.minus(2.days) })
.plus(HistoryUiModelExamples.items().take(7))
.toList(),
dialog = null,
@@ -72,10 +75,12 @@ class HistoryviewModelStateProvider : PreviewParameterProvider<HistoryViewModel.
private object HistoryUiModelExamples {
val headerToday = header()
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 }) =
HistoryUiModel.Header(LocalDate.from(instantBuilder(Instant.now())))
HistoryUiModel.Header(
instantBuilder(Clock.System.now()).toLocalDateTime(TimeZone.currentSystemDefault()).date,
)
fun items() = sequence {
var count = 1
@@ -94,7 +99,7 @@ class HistoryviewModelStateProvider : PreviewParameterProvider<HistoryViewModel.
mangaId = Random.nextLong(),
title = "Test Title",
chapterNumber = Random.nextDouble(),
readAt = Date.from(Instant.now()),
readAt = Date.from(Clock.System.now().toJavaInstant()),
readDuration = Random.nextLong(),
coverData = MangaCover(
mangaId = Random.nextLong(),
@@ -75,7 +75,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.shouldExpandFAB
import tachiyomi.source.local.isLocal
import java.time.Instant
import kotlin.time.Instant
@Composable
fun MangaScreen(
@@ -20,15 +20,17 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import kotlinx.datetime.TimeZone
import kotlinx.datetime.daysUntil
import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.WheelTextPicker
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
import java.time.Instant
import java.time.temporal.ChronoUnit
import kotlin.math.absoluteValue
import kotlin.time.Clock
import kotlin.time.Instant
@Composable
fun DeleteChaptersDialog(
@@ -72,8 +74,8 @@ fun SetIntervalDialog(
val nextUpdateDays = remember(nextUpdate) {
return@remember if (nextUpdate != null) {
val now = Instant.now()
now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0)
val now = Clock.System.now()
now.daysUntil(nextUpdate, TimeZone.currentSystemDefault()).coerceAtLeast(0)
} else {
null
}
@@ -91,6 +91,8 @@ import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.system.copyToClipboard
import kotlinx.datetime.TimeZone
import kotlinx.datetime.daysUntil
import org.intellij.markdown.MarkdownElementTypes
import org.intellij.markdown.MarkdownTokenTypes
import org.intellij.markdown.ast.findChildOfType
@@ -105,9 +107,9 @@ import tachiyomi.presentation.core.util.clickableNoIndication
import tachiyomi.presentation.core.util.secondaryItemAlpha
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant
import java.time.temporal.ChronoUnit
import kotlin.math.roundToInt
import kotlin.time.Clock
import kotlin.time.Instant
@Composable
fun MangaInfoBox(
@@ -189,8 +191,8 @@ fun MangaActionRow(
// TODO: show something better when using custom interval
val nextUpdateDays = remember(nextUpdate) {
return@remember if (nextUpdate != null) {
val now = Instant.now()
now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0)
val now = Clock.System.now()
now.daysUntil(nextUpdate, TimeZone.currentSystemDefault()).coerceAtLeast(0)
} else {
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.AppThemePreferenceWidget
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.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.LocalDate
import kotlin.time.Clock
object SettingsAppearanceScreen : SearchableSettings {
@@ -99,7 +102,7 @@ object SettingsAppearanceScreen : SearchableSettings {
val context = LocalContext.current
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 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.updaterEnabled
import kotlinx.coroutines.launch
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import logcat.LogPriority
import tachiyomi.core.common.Constants
import tachiyomi.core.common.util.lang.withIOContext
@@ -59,9 +61,7 @@ import tachiyomi.presentation.core.icons.Reddit
import tachiyomi.presentation.core.icons.X
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import kotlin.time.Instant
object AboutScreen : Screen() {
@@ -273,16 +273,14 @@ object AboutScreen : Screen() {
internal fun getFormattedBuildTime(): String {
return try {
LocalDateTime.ofInstant(
Instant.parse(BuildConfig.BUILD_TIME),
ZoneId.systemDefault(),
)
Instant.parse(BuildConfig.BUILD_TIME)
.toLocalDateTime(TimeZone.currentSystemDefault())
.toDateTimestampString(
UiPreferences.dateFormat(
Injekt.get<UiPreferences>().dateFormat.get(),
),
)
} catch (e: Exception) {
} catch (_: Exception) {
BuildConfig.BUILD_TIME
}
}
@@ -38,15 +38,15 @@ import eu.kanade.tachiyomi.util.system.workManager
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.plus
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import kotlin.time.Instant
class WorkerInfoScreen : Screen() {
@@ -165,10 +165,8 @@ class WorkerInfoScreen : Screen() {
}
appendLine("State: ${workInfo.state}")
if (workInfo.state == WorkInfo.State.ENQUEUED) {
val timestamp = LocalDateTime.ofInstant(
Instant.ofEpochMilli(workInfo.nextScheduleTimeMillis),
ZoneId.systemDefault(),
)
val timestamp = Instant.fromEpochMilliseconds(workInfo.nextScheduleTimeMillis)
.toLocalDateTime(TimeZone.currentSystemDefault())
.toDateTimestampString(
UiPreferences.dateFormat(
Injekt.get<UiPreferences>().dateFormat.get(),
@@ -56,8 +56,10 @@ import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import eu.kanade.presentation.track.components.TrackLogoIcon
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
import eu.kanade.tachiyomi.util.lang.toJavaLocalDate
import eu.kanade.tachiyomi.util.lang.toLocalDate
import eu.kanade.tachiyomi.util.system.copyToClipboard
import kotlinx.datetime.toJavaLocalDate
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import java.time.format.DateTimeFormatter
@@ -110,11 +112,13 @@ fun TrackInfoDialogHome(
.takeIf { supportsScoring && item.track.score != 0.0 },
onScoreClick = { onScoreClick(item) }
.takeIf { supportsScoring },
startDate = remember(item.track.startDate) { dateFormat.format(item.track.startDate.toLocalDate()) }
startDate = remember(item.track.startDate) {
dateFormat.format(item.track.startDate.toLocalDate().toJavaLocalDate())
}
.takeIf { supportsReadingDates && item.track.startDate != 0L },
onStartDateClick = { onStartDateEdit(item) } // TODO
.takeIf { supportsReadingDates },
endDate = dateFormat.format(item.track.finishDate.toLocalDate())
endDate = dateFormat.format(item.track.finishDate.toJavaLocalDate())
.takeIf { supportsReadingDates && item.track.finishDate != 0L },
onEndDateClick = { onEndDateEdit(item) }
.takeIf { supportsReadingDates },
@@ -6,11 +6,12 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import java.text.SimpleDateFormat
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.Date
import java.util.Locale
import kotlin.random.Random
import kotlin.time.Clock
import kotlin.time.Duration.Companion.days
import kotlin.time.toJavaInstant
internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composable () -> Unit> {
private val fullPageWithSecondSelected = @Composable {
@@ -93,7 +94,7 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
it.finished_reading_date = 0L
it.tracking_url = "https://example.com/tracker-example"
it.cover_url = "https://example.com/cover.png"
it.start_date = formatter.format(Date.from(Instant.now().minus((1L..365).random(), ChronoUnit.DAYS)))
it.start_date = formatter.format(Date.from(Clock.System.now().minus((1L..365).random().days).toJavaInstant()))
it.summary = lorem((0..40).random()).joinToString()
it.publishing_status = if (Random.nextBoolean()) "Finished" else ""
it.publishing_type = if (Random.nextBoolean()) "Oneshot" else ""
@@ -32,6 +32,7 @@ import eu.kanade.tachiyomi.ui.updates.UpdatesItem
import eu.kanade.tachiyomi.ui.updates.UpdatesViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.datetime.LocalDate
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.PullRefresh
@@ -40,7 +41,6 @@ import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.theme.active
import java.time.LocalDate
import kotlin.time.Duration.Companion.seconds
@Composable
@@ -7,7 +7,7 @@ import androidx.compose.runtime.ReadOnlyComposable
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import java.time.Instant
import kotlin.time.Clock
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
@@ -29,7 +29,7 @@ fun Duration.toDurationString(context: Context, fallback: String): String {
@Composable
@ReadOnlyComposable
fun relativeTimeSpanString(epochMillis: Long): String {
val now = Instant.now().toEpochMilli()
val now = Clock.System.now().toEpochMilliseconds()
return when {
epochMillis <= 0L -> stringResource(MR.strings.relative_time_span_never)
now - epochMillis < 1.minutes.inWholeMilliseconds -> stringResource(
@@ -18,7 +18,7 @@ import tachiyomi.core.common.storage.displayablePath
import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.milliseconds
class BackupNotifier(private val context: Context) {
@@ -149,10 +149,8 @@ class BackupNotifier(private val context: Context) {
val timeString = context.stringResource(
MR.strings.restore_duration,
TimeUnit.MILLISECONDS.toMinutes(time),
TimeUnit.MILLISECONDS.toSeconds(time) - TimeUnit.MINUTES.toSeconds(
TimeUnit.MILLISECONDS.toMinutes(time),
),
time.milliseconds.inWholeMinutes,
time.milliseconds.inWholeSeconds - (time.milliseconds.inWholeMinutes * 60),
)
with(completeNotificationBuilder) {
@@ -33,9 +33,9 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.time.Instant
import java.util.Date
import java.util.Locale
import kotlin.time.Clock
class BackupCreator(
private val context: Context,
@@ -108,7 +108,7 @@ class BackupCreator(
BackupFileValidator(context).validate(fileUri)
if (isAutoBackup) {
backupPreferences.lastAutoBackupTimestamp.set(Instant.now().toEpochMilli())
backupPreferences.lastAutoBackupTimestamp.set(Clock.System.now().toEpochMilliseconds())
}
return fileUri.toString()
@@ -9,6 +9,8 @@ import eu.kanade.tachiyomi.data.backup.models.BackupChapter
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.backup.models.BackupTracking
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import tachiyomi.data.Database
import tachiyomi.data.MemoColumnAdapter
import tachiyomi.data.UpdateStrategyColumnAdapter
@@ -23,9 +25,9 @@ import tachiyomi.domain.track.interactor.InsertTrack
import tachiyomi.domain.track.model.Track
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.ZonedDateTime
import java.util.Date
import kotlin.math.max
import kotlin.time.Clock
class MangaRestorer(
private val database: Database = Injekt.get(),
@@ -38,13 +40,9 @@ class MangaRestorer(
fetchInterval: FetchInterval = Injekt.get(),
) {
private var now = ZonedDateTime.now()
private var currentFetchWindow = fetchInterval.getWindow(now)
init {
now = ZonedDateTime.now()
currentFetchWindow = fetchInterval.getWindow(now)
}
private val timeZone = TimeZone.currentSystemDefault()
private val now = Clock.System.now().toLocalDateTime(timeZone)
private val currentFetchWindow = fetchInterval.getWindow(now.date, timeZone)
suspend fun sortByNew(backupMangas: List<BackupManga>): List<BackupManga> {
val urlsBySource = database.mangasQueries
@@ -286,7 +284,7 @@ class MangaRestorer(
restoreTracking(manga, tracks)
restoreHistory(manga, history)
restoreExcludedScanlators(manga, excludedScanlators)
updateManga.awaitUpdateFetchInterval(manga, now, currentFetchWindow)
updateManga.awaitUpdateFetchInterval(manga, timeZone, now, currentFetchWindow)
return manga
}
@@ -35,6 +35,8 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import logcat.LogPriority
import mihon.domain.chapter.interactor.FilterChaptersForDownload
import mihon.domain.source.interactor.UpdateMangaFromRemote
@@ -64,14 +66,13 @@ import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.time.Instant
import java.time.ZonedDateTime
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.concurrent.atomics.incrementAndFetch
import kotlin.time.Clock
@OptIn(ExperimentalAtomicApi::class)
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
@@ -108,7 +109,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
setForegroundSafely()
libraryPreferences.lastUpdatedTimestamp.set(Instant.now().toEpochMilli())
libraryPreferences.lastUpdatedTimestamp.set(Clock.System.now().toEpochMilliseconds())
val categoryId = inputData.getLong(KEY_CATEGORY, -1L)
addMangaToQueue(categoryId)
@@ -167,7 +168,11 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val restrictions = libraryPreferences.autoUpdateMangaRestrictions.get()
val skippedUpdates = mutableListOf<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
.filter {
@@ -234,7 +239,8 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val newUpdates = CopyOnWriteArrayList<Pair<Manga, Array<Chapter>>>()
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
val hasDownloads = AtomicBoolean(false)
val fetchWindow = fetchInterval.getWindow(ZonedDateTime.now())
val timeZone = TimeZone.currentSystemDefault()
val fetchWindow = fetchInterval.getWindow(Clock.System.now().toLocalDateTime(timeZone).date, timeZone)
coroutineScope {
mangaToUpdate.groupBy { it.manga.source }.values
@@ -24,7 +24,7 @@ import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.InputStream
import java.time.Instant
import kotlin.time.Clock
class ImageSaver(
val context: Context,
@@ -85,7 +85,7 @@ class ImageSaver(
MediaStore.MediaColumns.RELATIVE_PATH to relativePath,
MediaStore.MediaColumns.DISPLAY_NAME to if (isMimeTypeSupported) image.name else filename,
MediaStore.MediaColumns.MIME_TYPE to type.mime,
MediaStore.MediaColumns.DATE_MODIFIED to Instant.now().epochSecond,
MediaStore.MediaColumns.DATE_MODIFIED to Clock.System.now().epochSeconds,
)
val picture = findUriOrDefault(relativePath, filename) {
@@ -15,6 +15,9 @@ import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs
import kotlinx.datetime.TimeZone
import kotlinx.datetime.number
import kotlinx.datetime.toLocalDateTime
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
@@ -25,10 +28,8 @@ import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.common.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Instant
import tachiyomi.domain.track.model.Track as DomainTrack
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
@@ -326,11 +327,11 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
}
}
val dateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(dateValue), ZoneId.systemDefault())
val dateTime = Instant.fromEpochMilliseconds(dateValue).toLocalDateTime(TimeZone.currentSystemDefault())
return buildJsonObject {
put("year", dateTime.year)
put("month", dateTime.monthValue)
put("day", dateTime.dayOfMonth)
put("month", dateTime.month.number)
put("day", dateTime.day)
}
}
@@ -1,8 +1,9 @@
package eu.kanade.tachiyomi.data.track.anilist.dto
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
import kotlinx.serialization.Serializable
import java.time.LocalDate
import java.time.ZoneId
@Serializable
data class ALFuzzyDate(
@@ -11,10 +12,9 @@ data class ALFuzzyDate(
val day: Int?,
) {
fun toEpochMilli(): Long = try {
LocalDate.of(year!!, month!!, day!!)
.atStartOfDay(ZoneId.systemDefault())
.toInstant()
.toEpochMilli()
LocalDate(year!!, month!!, day!!)
.atStartOfDayIn(TimeZone.currentSystemDefault())
.toEpochMilliseconds()
} catch (_: Exception) {
0L
}
@@ -49,7 +49,7 @@ import tachiyomi.domain.source.interactor.GetRemoteManga
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant
import kotlin.time.Clock
import eu.kanade.tachiyomi.source.model.Filter as SourceModelFilter
class BrowseSourceViewModel(
@@ -226,7 +226,7 @@ class BrowseSourceViewModel(
favorite = !manga.favorite,
dateAdded = when (manga.favorite) {
true -> 0
false -> Instant.now().toEpochMilli()
false -> Clock.System.now().toEpochMilliseconds()
},
)
@@ -56,7 +56,6 @@ import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
import eu.kanade.tachiyomi.util.lang.toLocalDate
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toast
@@ -66,6 +65,8 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.i18n.stringResource
@@ -85,9 +86,8 @@ import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneOffset
import kotlin.time.Clock
import kotlin.time.Instant
data class TrackInfoDialogHomeScreen(
private val mangaId: Long,
@@ -513,20 +513,20 @@ private data class TrackDateSelectorScreen(
@Transient
private val selectableDates = object : SelectableDates {
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
val targetDate = Instant.ofEpochMilli(utcTimeMillis).toLocalDate(ZoneOffset.UTC)
val targetDate = Instant.fromEpochMilliseconds(utcTimeMillis).toLocalDateTime(TimeZone.UTC)
// Disallow future dates
if (targetDate > LocalDate.now(ZoneOffset.UTC)) return false
if (targetDate > Clock.System.now().toLocalDateTime(TimeZone.UTC)) return false
return when {
// Disallow setting start date after finish date
start && track.finishDate > 0 -> {
val finishDate = Instant.ofEpochMilli(track.finishDate).toLocalDate(ZoneOffset.UTC)
val finishDate = Instant.fromEpochMilliseconds(track.finishDate).toLocalDateTime(TimeZone.UTC)
targetDate <= finishDate
}
// Disallow setting finish date before start date
!start && track.startDate > 0 -> {
val startDate = Instant.ofEpochMilli(track.startDate).toLocalDate(ZoneOffset.UTC)
val startDate = Instant.fromEpochMilliseconds(track.startDate).toLocalDateTime(TimeZone.UTC)
startDate <= targetDate
}
else -> {
@@ -537,17 +537,17 @@ private data class TrackDateSelectorScreen(
override fun isSelectableYear(year: Int): Boolean {
// Disallow future years
if (year > LocalDate.now(ZoneOffset.UTC).year) return false
if (year > Clock.System.now().toLocalDateTime(TimeZone.UTC).year) return false
return when {
// Disallow setting start year after finish year
start && track.finishDate > 0 -> {
val finishDate = Instant.ofEpochMilli(track.finishDate).toLocalDate(ZoneOffset.UTC)
val finishDate = Instant.fromEpochMilliseconds(track.finishDate).toLocalDateTime(TimeZone.UTC)
year <= finishDate.year
}
// Disallow setting finish year before start year
!start && track.startDate > 0 -> {
val startDate = Instant.ofEpochMilli(track.startDate).toLocalDate(ZoneOffset.UTC)
val startDate = Instant.fromEpochMilliseconds(track.startDate).toLocalDateTime(TimeZone.UTC)
startDate.year <= year
}
else -> {
@@ -618,14 +618,14 @@ private data class TrackDateSelectorScreen(
get() {
val millis = (if (start) track.startDate else track.finishDate)
.takeIf { it != 0L }
?: Instant.now().toEpochMilli()
return millis.convertEpochMillisZone(ZoneOffset.systemDefault(), ZoneOffset.UTC)
?: Clock.System.now().toEpochMilliseconds()
return millis.convertEpochMillisZone(TimeZone.currentSystemDefault(), TimeZone.UTC)
}
// In UTC
fun setDate(millis: Long) {
// Convert to local time
val localMillis = millis.convertEpochMillisZone(ZoneOffset.UTC, ZoneOffset.systemDefault())
val localMillis = millis.convertEpochMillisZone(TimeZone.UTC, TimeZone.currentSystemDefault())
viewModelScope.launchNonCancellable {
if (start) {
tracker.setRemoteStartDate(track.toDbTrack(), localMillis)
@@ -76,8 +76,9 @@ import tachiyomi.domain.source.service.SourceManager
import tachiyomi.source.local.isLocal
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant
import java.util.Date
import kotlin.getValue
import kotlin.time.Clock
/**
* Presenter used by the activity to perform background operations.
@@ -582,7 +583,7 @@ class ReaderViewModel @JvmOverloads constructor(
}
fun restartReadTimer() {
chapterReadStartTime = Instant.now().toEpochMilli()
chapterReadStartTime = Clock.System.now().toEpochMilliseconds()
}
/**
@@ -31,6 +31,9 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.TimeZone
import kotlinx.datetime.minus
import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.preference.TriState
@@ -49,7 +52,7 @@ import tachiyomi.domain.updates.model.UpdatesWithRelations
import tachiyomi.domain.updates.service.UpdatesPreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.ZonedDateTime
import kotlin.time.Clock
class UpdatesViewModel(
private val sourceManager: SourceManager = Injekt.get(),
@@ -77,7 +80,7 @@ class UpdatesViewModel(
init {
viewModelScope.launchIO {
// Set date limit for recent chapters
val limit = ZonedDateTime.now().minusMonths(3).toInstant()
val limit = Clock.System.now().minus(3, DateTimeUnit.MONTH, TimeZone.currentSystemDefault())
combine(
// needed for SQL filters (unread, started, bookmarked, etc)
@@ -10,12 +10,14 @@ import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.datetime.TimeZone
import kotlinx.datetime.offsetAt
import kotlinx.datetime.toLocalDateTime
import tachiyomi.core.common.util.lang.withNonCancellableContext
import tachiyomi.core.common.util.lang.withUIContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.OffsetDateTime
import java.time.ZoneId
import kotlin.time.Clock
class CrashLogUtil(
private val context: Context,
@@ -35,12 +37,14 @@ class CrashLogUtil(
val uri = file.getUriCompat(context)
context.startActivity(uri.toShareIntent(context, "text/plain"))
} catch (e: Throwable) {
} catch (_: Throwable) {
withUIContext { context.toast("Failed to get logs") }
}
}
fun getDebugInfo(): String {
val now = Clock.System.now()
val tz = TimeZone.currentSystemDefault()
return """
App ID: ${BuildConfig.APPLICATION_ID}
App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE}, ${BuildConfig.BUILD_TIME})
@@ -51,7 +55,7 @@ class CrashLogUtil(
Device name: ${Build.DEVICE} (${Build.PRODUCT})
Device model: ${Build.MODEL}
WebView: ${WebViewUtil.getVersion(context)}
Current time: ${OffsetDateTime.now(ZoneId.systemDefault())}
Current time: ${now.toLocalDateTime(tz)}${tz.offsetAt(now)}
""".trimIndent()
}
@@ -9,12 +9,12 @@ import tachiyomi.source.local.isLocal
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.InputStream
import java.time.Instant
import kotlin.time.Clock
fun Manga.removeCovers(coverCache: CoverCache = Injekt.get()): Manga {
if (isLocal()) return this
return if (coverCache.deleteFromCache(this, true) > 0) {
copy(coverLastModified = Instant.now().toEpochMilli())
copy(coverLastModified = Clock.System.now().toEpochMilliseconds())
} else {
this
}
@@ -1,23 +1,29 @@
package eu.kanade.tachiyomi.util.lang
import android.content.Context
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.minus
import kotlinx.datetime.toInstant
import kotlinx.datetime.toJavaLocalDate
import kotlinx.datetime.toJavaLocalDateTime
import kotlinx.datetime.toLocalDateTime
import tachiyomi.core.common.i18n.pluralStringResource
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.i18n.MR
import java.text.DateFormat
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.time.temporal.ChronoUnit
import java.util.Date
import kotlin.math.absoluteValue
import kotlin.time.Clock
import kotlin.time.Instant
fun LocalDateTime.toDateTimestampString(dateTimeFormatter: DateTimeFormatter): String {
val date = dateTimeFormatter.format(this)
val time = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).format(this)
val javaLocalDateTime = this.toJavaLocalDateTime()
val date = dateTimeFormatter.format(javaLocalDateTime)
val time = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).format(javaLocalDateTime)
return "$date $time"
}
@@ -26,21 +32,21 @@ fun Date.toTimestampString(): String {
}
fun Long.convertEpochMillisZone(
from: ZoneId,
to: ZoneId,
from: TimeZone,
to: TimeZone,
): Long {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(this), from)
.atZone(to)
.toInstant()
.toEpochMilli()
return Instant.fromEpochMilliseconds(this)
.toLocalDateTime(from)
.toInstant(to)
.toEpochMilliseconds()
}
fun Long.toLocalDate(): LocalDate {
return LocalDate.ofInstant(Instant.ofEpochMilli(this), ZoneId.systemDefault())
return Instant.fromEpochMilliseconds(this).toLocalDateTime(TimeZone.currentSystemDefault()).date
}
fun Instant.toLocalDate(zoneId: ZoneId = ZoneId.systemDefault()): LocalDate {
return LocalDate.ofInstant(this, zoneId)
fun Long.toJavaLocalDate(): java.time.LocalDate {
return this.toLocalDate().toJavaLocalDate()
}
fun LocalDate.toRelativeString(
@@ -49,23 +55,25 @@ fun LocalDate.toRelativeString(
dateFormat: DateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT),
): String {
if (!relative) {
return dateFormat.format(this)
return dateFormat.format(this.toJavaLocalDate())
}
val now = LocalDate.now()
val difference = ChronoUnit.DAYS.between(this, now)
val today = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
val difference = (today - this).days
return when {
difference < -7 -> dateFormat.format(this)
difference < -7 -> dateFormat.format(this.toJavaLocalDate())
difference < 0 -> context.pluralStringResource(
MR.plurals.upcoming_relative_time,
difference.toInt().absoluteValue,
difference.toInt().absoluteValue,
difference.absoluteValue,
difference.absoluteValue,
)
difference < 1 -> context.stringResource(MR.strings.relative_time_today)
difference < 7 -> context.pluralStringResource(
MR.plurals.relative_time,
difference.toInt(),
difference.toInt(),
difference,
difference,
)
else -> dateFormat.format(this)
else -> dateFormat.format(this.toJavaLocalDate())
}
}
@@ -20,7 +20,7 @@ import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack
import java.time.Instant
import kotlin.time.Clock
class MigrateMangaUseCase(
private val sourcePreferences: SourcePreferences,
@@ -124,7 +124,7 @@ class MigrateMangaUseCase(
favorite = true,
chapterFlags = current.chapterFlags,
viewerFlags = current.viewerFlags,
dateAdded = if (replace) current.dateAdded else Instant.now().toEpochMilli(),
dateAdded = if (replace) current.dateAdded else Clock.System.now().toEpochMilliseconds(),
notes = if (MigrationFlag.NOTES in flags) current.notes else null,
)
@@ -20,7 +20,7 @@ import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.source.local.isLocal
import java.time.Instant
import kotlin.time.Clock
class UpdateMangaFromRemote(
private val sourceManager: SourceManager,
@@ -107,14 +107,14 @@ class UpdateMangaFromRemote(
// Never refresh covers if the url is empty to avoid "losing" existing covers
remoteManga.thumbnail_url.isNullOrEmpty() -> null
!manualFetch && localManga.thumbnailUrl == remoteManga.thumbnail_url -> null
localManga.isLocal() -> Instant.now().toEpochMilli()
localManga.isLocal() -> Clock.System.now().toEpochMilliseconds()
localManga.hasCustomCover(coverCache) -> {
coverCache.deleteFromCache(localManga, false)
null
}
else -> {
coverCache.deleteFromCache(localManga, false)
Instant.now().toEpochMilli()
Clock.System.now().toEpochMilliseconds()
}
}
@@ -1,6 +1,9 @@
package mihon.feature.library
import eu.kanade.tachiyomi.ui.library.LibraryItem
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import mihon.domain.library.model.search.AndNode
import mihon.domain.library.model.search.ComparisonField
import mihon.domain.library.model.search.ComparisonQueryNode
@@ -12,10 +15,8 @@ import mihon.domain.library.model.search.NotNode
import mihon.domain.library.model.search.OrNode
import mihon.domain.library.model.search.QueryNode
import tachiyomi.source.local.LocalSource
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import kotlin.math.abs
import kotlin.time.Instant
fun QueryNode.matches(item: LibraryItem): Boolean {
return when (this) {
@@ -109,7 +110,7 @@ private fun ComparisonQueryNode.matches(item: LibraryItem): Boolean {
fun compareDates(timestamp: Long, value: String): Boolean? {
val inputDate = runCatching { LocalDate.parse(value) }.getOrNull() ?: return null
val mangaDate = Instant.ofEpochMilli(timestamp).atZone(ZoneId.systemDefault()).toLocalDate()
val mangaDate = Instant.fromEpochMilliseconds(timestamp).toLocalDateTime(TimeZone.currentSystemDefault()).date
return queryComparator.apply(mangaDate, inputDate)
}
@@ -26,6 +26,8 @@ import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.relativeDateText
import eu.kanade.presentation.util.isTabletUi
import kotlinx.coroutines.launch
import kotlinx.datetime.LocalDate
import kotlinx.datetime.YearMonth
import mihon.feature.upcoming.components.UpcomingItem
import mihon.feature.upcoming.components.calendar.Calendar
import tachiyomi.core.common.Constants
@@ -36,8 +38,6 @@ import tachiyomi.presentation.core.components.TwoPanelBox
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import java.time.LocalDate
import java.time.YearMonth
@Composable
fun UpcomingScreenContent(
@@ -1,7 +1,7 @@
package mihon.feature.upcoming
import kotlinx.datetime.LocalDate
import tachiyomi.domain.manga.model.Manga
import java.time.LocalDate
sealed interface UpcomingUIModel {
data class Header(val date: LocalDate, val mangaCount: Int) : UpcomingUIModel
@@ -4,17 +4,20 @@ import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMapIndexedNotNull
import androidx.lifecycle.viewModelScope
import eu.kanade.core.util.insertSeparatorsReversed
import eu.kanade.tachiyomi.util.lang.toLocalDate
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.YearMonth
import kotlinx.datetime.toLocalDateTime
import kotlinx.datetime.yearMonth
import mihon.core.viewmodel.StateViewModel
import mihon.domain.upcoming.interactor.GetUpcomingManga
import tachiyomi.domain.manga.model.Manga
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.LocalDate
import java.time.YearMonth
import kotlin.time.Clock
class UpcomingViewModel(
private val getUpcomingManga: GetUpcomingManga = Injekt.get(),
@@ -41,8 +44,14 @@ class UpcomingViewModel(
.insertSeparatorsReversed { before, after ->
if (after != null) mangaCount++
val beforeDate = before?.manga?.expectedNextUpdate?.toLocalDate()
val afterDate = after?.manga?.expectedNextUpdate?.toLocalDate()
val beforeDate = before?.manga
?.expectedNextUpdate
?.toLocalDateTime(TimeZone.currentSystemDefault())
?.date
val afterDate = after?.manga
?.expectedNextUpdate
?.toLocalDateTime(TimeZone.currentSystemDefault())
?.date
if (beforeDate != afterDate && afterDate != null) {
UpcomingUIModel.Header(afterDate, mangaCount).also { mangaCount = 0 }
@@ -73,7 +82,10 @@ class UpcomingViewModel(
}
data class State(
val selectedYearMonth: YearMonth = YearMonth.now(),
val selectedYearMonth: YearMonth = Clock.System.now()
.toLocalDateTime(TimeZone.currentSystemDefault())
.date
.yearMonth,
val items: List<UpcomingUIModel> = listOf(),
val events: 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 io.woong.compose.grid.SimpleGridCells
import io.woong.compose.grid.VerticalGrid
import kotlinx.datetime.LocalDate
import kotlinx.datetime.YearMonth
import kotlinx.datetime.minusMonth
import kotlinx.datetime.plusMonth
import kotlinx.datetime.toJavaDayOfWeek
import mihon.core.designsystem.utils.isExpandedWidthWindow
import mihon.core.designsystem.utils.isMediumWidthWindow
import tachiyomi.presentation.core.components.material.padding
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.YearMonth
import java.time.format.TextStyle
import java.time.temporal.WeekFields
import java.util.Locale
@@ -47,8 +50,8 @@ fun Calendar(
) {
CalenderHeader(
yearMonth = selectedYearMonth,
onPreviousClick = { setSelectedYearMonth(selectedYearMonth.minusMonths(1L)) },
onNextClick = { setSelectedYearMonth(selectedYearMonth.plusMonths(1L)) },
onPreviousClick = { setSelectedYearMonth(selectedYearMonth.minusMonth()) },
onNextClick = { setSelectedYearMonth(selectedYearMonth.plusMonth()) },
modifier = Modifier
.fillMaxWidth()
.padding(vertical = MaterialTheme.padding.small)
@@ -74,8 +77,8 @@ private fun CalendarGrid(
.map { DayOfWeek.of((localeFirstDayOfWeek - 1 + it) % DAYS_OF_WEEK + 1) }
}
val emptyFieldCount = weekDays.indexOf(selectedYearMonth.atDay(1).dayOfWeek)
val daysInMonth = selectedYearMonth.lengthOfMonth()
val emptyFieldCount = weekDays.indexOf(selectedYearMonth.firstDay.dayOfWeek.toJavaDayOfWeek())
val daysInMonth = selectedYearMonth.numberOfDays
VerticalGrid(
columns = SimpleGridCells.Fixed(DAYS_OF_WEEK),
@@ -98,7 +101,7 @@ private fun CalendarGrid(
}
repeat(emptyFieldCount) { Box { } }
repeat(daysInMonth) { dayIndex ->
val localDate = selectedYearMonth.atDay(dayIndex + 1)
val localDate = LocalDate(selectedYearMonth.year, selectedYearMonth.month, dayIndex + 1)
CalendarDay(
date = localDate,
onDayClick = { onClickDay(localDate) },
@@ -19,8 +19,11 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import tachiyomi.presentation.core.components.material.DISABLED_ALPHA
import java.time.LocalDate
import kotlin.time.Clock
private const val MAX_EVENTS = 3
@@ -31,7 +34,7 @@ fun CalendarDay(
onDayClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val today = remember { LocalDate.now() }
val today = remember { Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date }
Box(
modifier = modifier
@@ -54,10 +57,10 @@ fun CalendarDay(
contentAlignment = Alignment.Center,
) {
Text(
text = date.dayOfMonth.toString(),
text = date.day.toString(),
textAlign = TextAlign.Center,
fontSize = 16.sp,
color = if (date.isBefore(today)) {
color = if (date < today) {
MaterialTheme.colorScheme.onBackground.copy(alpha = DISABLED_ALPHA)
} else {
MaterialTheme.colorScheme.onBackground
@@ -24,11 +24,16 @@ import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import kotlinx.datetime.TimeZone
import kotlinx.datetime.YearMonth
import kotlinx.datetime.toJavaYearMonth
import kotlinx.datetime.toLocalDateTime
import kotlinx.datetime.yearMonth
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import java.time.YearMonth
import java.time.format.DateTimeFormatter
import java.util.Locale
import kotlin.time.Clock
@Composable
fun CalenderHeader(
@@ -88,14 +93,14 @@ private fun AnimatedContentTransitionScope<YearMonth>.getAnimation(): ContentTra
@ReadOnlyComposable
private fun getTitleText(monthYear: YearMonth): String {
val formatter = DateTimeFormatter.ofPattern("MMMM yyyy", Locale.getDefault())
return formatter.format(monthYear)
return formatter.format(monthYear.toJavaYearMonth())
}
@Preview
@Composable
private fun CalenderHeaderPreview() {
CalenderHeader(
yearMonth = YearMonth.now(),
yearMonth = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date.yearMonth,
onNextClick = {},
onPreviousClick = {},
)