mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-16 13:48:09 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81e0754c0d | ||
|
|
e287f08789 | ||
|
|
a73271aefa | ||
|
|
f7a1ecd251 | ||
|
|
f80c91081f | ||
|
|
55be95dd5d | ||
|
|
d7f3ceef5c | ||
|
|
1d8a2b05dd | ||
|
|
6d69903a56 |
@@ -30,7 +30,7 @@ body:
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.20.2](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.20.3](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
@@ -52,7 +52,7 @@ body:
|
||||
label: Mihon version
|
||||
description: You can find your Mihon version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "0.20.2"
|
||||
Example: "0.20.3"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -95,7 +95,7 @@ body:
|
||||
required: true
|
||||
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.20.2](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.20.3](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
required: true
|
||||
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
||||
required: true
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
|
||||
- name: Check code format
|
||||
run: ./gradlew spotlessCheck
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
|
||||
- name: Build
|
||||
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
with:
|
||||
cache-disabled: true
|
||||
|
||||
|
||||
+19
-1
@@ -12,6 +12,23 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.20.3] - 2026-08-04
|
||||
### Added
|
||||
- Add Category filtering for the Updates tab ([@MajorTanya](https://github.com/MajorTanya)) ([#3589](https://github.com/mihonapp/mihon/pull/3589))
|
||||
- Add Category filtering for the Upcoming calendar ([@MajorTanya](https://github.com/MajorTanya)) ([#3607](https://github.com/mihonapp/mihon/pull/3607))
|
||||
|
||||
### Changed
|
||||
- Update default user agent to Chrome 149 ([@AntsyLich](https://github.com/AntsyLich)) ([#3678](https://github.com/mihonapp/mihon/pull/3678))
|
||||
|
||||
### Improved
|
||||
- Spoof `Sec-CH-UA` client hints in WebView to match the user agent ([@AntsyLich](https://github.com/AntsyLich)) ([#3678](https://github.com/mihonapp/mihon/pull/3678))
|
||||
|
||||
### Fixed
|
||||
- Fixed extension installation with shizuku installer ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#3676](https://github.com/mihonapp/mihon/pull/3676))
|
||||
- Fixed `X-Requested-With` spoofing leaking to unrelated callers ([@AntsyLich](https://github.com/AntsyLich)) ([#3678](https://github.com/mihonapp/mihon/pull/3678))
|
||||
- Fixed reader loading indefinitely in some scenarios ([@AntsyLich](https://github.com/AntsyLich)) ([#3686](https://github.com/mihonapp/mihon/pull/3686))
|
||||
- Fix MyAnimeList error for list entries with partial start/finish reading dates by setting those dates to either Jan 1st or 1st of the month ([@MajorTanya](https://github.com/MajorTanya)) ([#3573](https://github.com/mihonapp/mihon/pull/3573))
|
||||
|
||||
## [v0.20.2] - 2026-08-01
|
||||
### Added
|
||||
- Support resumable image downloads if supported by source ([@xMohnad](https://github.com/xMohnad)) ([#3167](https://github.com/mihonapp/mihon/pull/3167))
|
||||
@@ -601,7 +618,8 @@ Same as v0.19.6
|
||||
- Branding to Mihon ([@AntsyLich](https://github.com/AntsyLich))
|
||||
- Minimum supported Android version to 8 ([@AntsyLich](https://github.com/AntsyLich)) ([`dfb3091`](https://github.com/mihonapp/mihon/commit/dfb3091e380dda3e9bfb64bf5c9a685cf3a03d0e))
|
||||
|
||||
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.20.2...main
|
||||
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.20.3...main
|
||||
[v0.20.3]: https://github.com/mihonapp/mihon/compare/v0.20.2...v0.20.3
|
||||
[v0.20.2]: https://github.com/mihonapp/mihon/compare/v0.20.1...v0.20.2
|
||||
[v0.20.1]: https://github.com/mihonapp/mihon/compare/v0.20.0...v0.20.1
|
||||
[v0.20.0]: https://github.com/mihonapp/mihon/compare/v0.19.9...v0.20.0
|
||||
|
||||
@@ -32,8 +32,8 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "app.mihon"
|
||||
|
||||
versionCode = 27
|
||||
versionName = "0.20.2"
|
||||
versionCode = 28
|
||||
versionName = "0.20.3"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getLatestCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getLatestCommitSha()}\"")
|
||||
@@ -243,6 +243,8 @@ dependencies {
|
||||
|
||||
implementation(libs.sqldelight.async)
|
||||
|
||||
implementation(libs.kotlinx.datetime)
|
||||
|
||||
// AndroidX libraries
|
||||
implementation(libs.androidx.annotation)
|
||||
implementation(libs.androidx.appCompat)
|
||||
|
||||
Vendored
+9
@@ -86,3 +86,12 @@
|
||||
# Firebase
|
||||
-keep class com.google.firebase.installations.** { *; }
|
||||
-keep interface com.google.firebase.installations.** { *; }
|
||||
|
||||
# KotlinX Datetime
|
||||
-keep,allowoptimization class kotlinx.datetime.** { public protected *; }
|
||||
|
||||
# Methods called by Shizuku only
|
||||
-keepclassmembers class mihon.app.shizuku.ShellInterface {
|
||||
public <init>();
|
||||
public void destroy();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+5
-2
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+5
-7
@@ -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 ""
|
||||
|
||||
@@ -14,12 +14,19 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import eu.kanade.presentation.category.visualName
|
||||
import eu.kanade.presentation.components.TabbedDialog
|
||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||
import eu.kanade.tachiyomi.ui.updates.UpdatesSettingsViewModel
|
||||
import tachiyomi.core.common.preference.TriState
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -27,6 +34,7 @@ import tachiyomi.presentation.core.components.SettingsItemsPaddings
|
||||
import tachiyomi.presentation.core.components.TriStateItem
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
|
||||
@Composable
|
||||
@@ -38,14 +46,18 @@ fun UpdatesFilterDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
tabTitles = listOf(
|
||||
stringResource(MR.strings.action_filter),
|
||||
stringResource(MR.strings.categories),
|
||||
),
|
||||
) {
|
||||
) { page ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(vertical = TabbedDialogPaddings.Vertical)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
FilterSheet(viewModel = viewModel)
|
||||
when (page) {
|
||||
0 -> FilterSheet(viewModel = viewModel)
|
||||
1 -> CategoryFilterSheet(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,3 +120,55 @@ private fun ColumnScope.FilterSheet(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.CategoryFilterSheet(
|
||||
viewModel: UpdatesSettingsViewModel,
|
||||
) {
|
||||
Text(
|
||||
stringResource(MR.strings.pref_filter_update_categories_details),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = SettingsItemsPaddings.Horizontal,
|
||||
vertical = SettingsItemsPaddings.Vertical,
|
||||
),
|
||||
)
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(MaterialTheme.padding.extraSmall))
|
||||
|
||||
val allCategories by viewModel.getCategories.subscribe().collectAsState(initial = emptyList())
|
||||
|
||||
if (allCategories.isEmpty()) {
|
||||
// since it includes the system category, this should only happen when loading is required
|
||||
LoadingScreen(modifier = Modifier.padding(16.dp))
|
||||
return
|
||||
}
|
||||
|
||||
val excluded by viewModel.updatesPreferences.filterExcludedCategories.collectAsState()
|
||||
val included by viewModel.updatesPreferences.filterIncludedCategories.collectAsState()
|
||||
|
||||
val selected = remember {
|
||||
allCategories.map { category ->
|
||||
when (category.id) {
|
||||
in included -> TriState.ENABLED_IS
|
||||
in excluded -> TriState.ENABLED_NOT
|
||||
else -> TriState.DISABLED
|
||||
}
|
||||
}.toMutableStateList()
|
||||
}
|
||||
|
||||
Column {
|
||||
allCategories.fastForEachIndexed { idx, category ->
|
||||
val state = selected[idx]
|
||||
TriStateItem(
|
||||
label = category.visualName,
|
||||
state = state,
|
||||
onClick = {
|
||||
selected[idx] = state.next()
|
||||
viewModel.cycleCategory(category)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -51,6 +51,7 @@ import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.getHtml
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
import eu.kanade.tachiyomi.util.system.setUserAgent
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
@@ -72,6 +73,7 @@ fun WebViewScreenContent(
|
||||
onNavigateUp: () -> Unit,
|
||||
initialTitle: String?,
|
||||
url: String,
|
||||
defaultUserAgentProvider: () -> String,
|
||||
onShare: (String) -> Unit,
|
||||
onOpenInBrowser: (String) -> Unit,
|
||||
onClearCookies: (String) -> Unit,
|
||||
@@ -340,9 +342,7 @@ fun WebViewScreenContent(
|
||||
WebView.setWebContentsDebuggingEnabled(true)
|
||||
}
|
||||
|
||||
headers["user-agent"]?.let {
|
||||
webView.settings.userAgentString = it
|
||||
}
|
||||
webView.setUserAgent(headers["user-agent"] ?: defaultUserAgentProvider())
|
||||
},
|
||||
onDispose = { webView ->
|
||||
val window = windowStack.items.find { it.webView == webView }
|
||||
|
||||
@@ -8,7 +8,6 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import android.os.Looper
|
||||
import android.webkit.WebView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
@@ -231,7 +230,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
override fun getPackageName(): String {
|
||||
try {
|
||||
// Override the value passed as X-Requested-With in WebView requests
|
||||
val stackTrace = Looper.getMainLooper().thread.stackTrace
|
||||
val stackTrace = Thread.currentThread().stackTrace
|
||||
val isChromiumCall = stackTrace.any { trace ->
|
||||
trace.className.lowercase() in setOf("org.chromium.base.buildinfo", "org.chromium.base.apkinfo") &&
|
||||
trace.methodName.lowercase() in setOf("getall", "getpackagename", "<init>")
|
||||
|
||||
@@ -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()
|
||||
|
||||
+7
-9
@@ -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
|
||||
}
|
||||
|
||||
@@ -245,7 +245,13 @@ class MyAnimeListApi(
|
||||
}
|
||||
|
||||
private fun parseDate(isoDate: String): Long {
|
||||
return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(isoDate)?.time ?: 0L
|
||||
val pattern = when (isoDate.length) {
|
||||
10 -> "yyyy-MM-dd"
|
||||
7 -> "yyyy-MM"
|
||||
4 -> "yyyy"
|
||||
else -> throw IllegalArgumentException("Unsupported date format: \"$isoDate\"")
|
||||
}
|
||||
return SimpleDateFormat(pattern, Locale.US).parse(isoDate)?.time ?: 0L
|
||||
}
|
||||
|
||||
private fun convertToIsoDate(epochTime: Long): String? {
|
||||
|
||||
@@ -17,6 +17,7 @@ import tachiyomi.domain.backup.service.BackupPreferences
|
||||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.storage.service.StoragePreferences
|
||||
import tachiyomi.domain.upcoming.service.UpcomingPreferences
|
||||
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
@@ -47,6 +48,9 @@ class PreferenceModule(val app: Application) : InjektModule {
|
||||
addSingletonFactory {
|
||||
LibraryPreferences(get())
|
||||
}
|
||||
addSingletonFactory {
|
||||
UpcomingPreferences(get())
|
||||
}
|
||||
addSingletonFactory {
|
||||
UpdatesPreferences(get())
|
||||
}
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
@@ -98,7 +98,6 @@ import tachiyomi.core.common.Constants
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.core.common.util.lang.launchNonCancellable
|
||||
import tachiyomi.core.common.util.lang.withUIContext
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
@@ -171,26 +170,17 @@ class ReaderActivity : BaseActivity() {
|
||||
setContentView(binding.root)
|
||||
binding.setComposeOverlay()
|
||||
|
||||
if (viewModel.needsInit()) {
|
||||
val manga = intent.extras?.getLong("manga", -1) ?: -1L
|
||||
val chapter = intent.extras?.getLong("chapter", -1) ?: -1L
|
||||
if (manga == -1L || chapter == -1L) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
NotificationReceiver.dismissNotification(this, manga.hashCode(), Notifications.ID_NEW_CHAPTERS)
|
||||
|
||||
lifecycleScope.launch {
|
||||
val initResult = viewModel.init(manga, chapter)
|
||||
if (!initResult.getOrDefault(false)) {
|
||||
val exception = initResult.exceptionOrNull() ?: IllegalStateException("Unknown err")
|
||||
withUIContext {
|
||||
setInitialChapterError(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!viewModel.hasValidArgs) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
NotificationReceiver.dismissNotification(
|
||||
this,
|
||||
viewModel.mangaId.hashCode(),
|
||||
Notifications.ID_NEW_CHAPTERS,
|
||||
)
|
||||
|
||||
config = ReaderConfig()
|
||||
setMenuVisibility(viewModel.state.value.menuVisible)
|
||||
|
||||
@@ -200,6 +190,13 @@ class ReaderActivity : BaseActivity() {
|
||||
.onEach { if (!it) finish() }
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
viewModel.state
|
||||
.map { it.initError }
|
||||
.distinctUntilChanged()
|
||||
.filterNotNull()
|
||||
.onEach(::setInitialChapterError)
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
viewModel.state
|
||||
.map { it.isLoadingAdjacentChapter }
|
||||
.distinctUntilChanged()
|
||||
|
||||
@@ -53,6 +53,7 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.preference.toggle
|
||||
@@ -76,8 +77,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.
|
||||
@@ -106,6 +108,14 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
private val mutableState = MutableStateFlow(State())
|
||||
val state = mutableState.asStateFlow()
|
||||
|
||||
/**
|
||||
* Ids of the manga and chapter the reader was launched with, taken from the activity intent.
|
||||
*/
|
||||
val mangaId = savedState.get<Long>("manga") ?: -1L
|
||||
private val initialChapterId = savedState.get<Long>("chapter") ?: -1L
|
||||
|
||||
val hasValidArgs = mangaId != -1L && initialChapterId != -1L
|
||||
|
||||
private val eventChannel = Channel<Event>()
|
||||
val eventFlow = eventChannel.receiveAsFlow()
|
||||
|
||||
@@ -243,6 +253,10 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
chapterId = currentChapter.chapter.id!!
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
if (hasValidArgs) {
|
||||
viewModelScope.launch { init() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
@@ -264,41 +278,28 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this presenter is initialized yet.
|
||||
* Initializes this presenter with the [mangaId] and [initialChapterId] the reader was launched
|
||||
* with. This method will fetch the manga from the database and initialize the initial chapter.
|
||||
* Failures are reported through [State.initError].
|
||||
*/
|
||||
fun needsInit(): Boolean {
|
||||
return manga == null
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this presenter with the given [mangaId] and [initialChapterId]. This method will
|
||||
* fetch the manga from the database and initialize the initial chapter.
|
||||
*/
|
||||
suspend fun init(mangaId: Long, initialChapterId: Long): Result<Boolean> {
|
||||
if (!needsInit()) return Result.success(true)
|
||||
return withIOContext {
|
||||
private suspend fun init() {
|
||||
withIOContext {
|
||||
try {
|
||||
val manga = getManga.await(mangaId)
|
||||
if (manga != null) {
|
||||
sourceManager.isInitialized.first { it }
|
||||
mutableState.update { it.copy(manga = manga) }
|
||||
if (chapterId == -1L) chapterId = initialChapterId
|
||||
val manga = getManga.await(mangaId) ?: error("Requested manga of id $mangaId not found")
|
||||
sourceManager.isInitialized.first { it }
|
||||
mutableState.update { it.copy(manga = manga) }
|
||||
if (chapterId == -1L) chapterId = initialChapterId
|
||||
|
||||
val context = Injekt.get<Application>()
|
||||
val source = sourceManager.getOrStub(manga.source)
|
||||
loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source)
|
||||
val context = Injekt.get<Application>()
|
||||
val source = sourceManager.getOrStub(manga.source)
|
||||
loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source)
|
||||
|
||||
loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id })
|
||||
Result.success(true)
|
||||
} else {
|
||||
// Unlikely but okay
|
||||
Result.success(false)
|
||||
}
|
||||
loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id })
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) {
|
||||
throw e
|
||||
}
|
||||
Result.failure(e)
|
||||
mutableState.update { it.copy(initError = e) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -582,7 +583,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
fun restartReadTimer() {
|
||||
chapterReadStartTime = Instant.now().toEpochMilli()
|
||||
chapterReadStartTime = Clock.System.now().toEpochMilliseconds()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -947,6 +948,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
@Immutable
|
||||
data class State(
|
||||
val manga: Manga? = null,
|
||||
val initError: Throwable? = null,
|
||||
val viewerChapters: ViewerChapters? = null,
|
||||
val bookmarked: Boolean = false,
|
||||
val isLoadingAdjacentChapter: Boolean = false,
|
||||
|
||||
@@ -4,14 +4,32 @@ import androidx.lifecycle.ViewModel
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.TriState
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class UpdatesSettingsViewModel(
|
||||
val updatesPreferences: UpdatesPreferences = Injekt.get(),
|
||||
val getCategories: GetCategories = Injekt.get(),
|
||||
) : ViewModel() {
|
||||
|
||||
val includedCategories = updatesPreferences.filterIncludedCategories
|
||||
val excludedCategories = updatesPreferences.filterExcludedCategories
|
||||
|
||||
fun cycleCategory(category: Category) {
|
||||
when (category.id) {
|
||||
in includedCategories.get() -> {
|
||||
includedCategories.getAndSet { it - category.id }
|
||||
excludedCategories.getAndSet { it + category.id }
|
||||
}
|
||||
|
||||
in excludedCategories.get() -> excludedCategories.getAndSet { it - category.id }
|
||||
else -> includedCategories.getAndSet { it + category.id }
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleFilter(preference: (UpdatesPreferences) -> Preference<TriState>) {
|
||||
preference(updatesPreferences).getAndSet {
|
||||
it.next()
|
||||
|
||||
@@ -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)
|
||||
@@ -90,6 +93,8 @@ class UpdatesViewModel(
|
||||
started = it.filterStarted.toBooleanOrNull(),
|
||||
bookmarked = it.filterBookmarked.toBooleanOrNull(),
|
||||
hideExcludedScanlators = it.filterExcludedScanlators,
|
||||
includedCategories = it.filterIncludedCategories,
|
||||
excludedCategories = it.filterExcludedCategories,
|
||||
).distinctUntilChanged()
|
||||
},
|
||||
downloadCache.changes,
|
||||
@@ -420,13 +425,18 @@ class UpdatesViewModel(
|
||||
updatesPreferences.filterStarted.changes(),
|
||||
updatesPreferences.filterBookmarked.changes(),
|
||||
updatesPreferences.filterExcludedScanlators.changes(),
|
||||
) { downloaded, unread, started, bookmarked, excludedScanlators ->
|
||||
updatesPreferences.filterIncludedCategories.changes(),
|
||||
updatesPreferences.filterExcludedCategories.changes(),
|
||||
) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
ItemPreferences(
|
||||
filterDownloaded = downloaded,
|
||||
filterUnread = unread,
|
||||
filterStarted = started,
|
||||
filterBookmarked = bookmarked,
|
||||
filterExcludedScanlators = excludedScanlators,
|
||||
filterDownloaded = it[0] as TriState,
|
||||
filterUnread = it[1] as TriState,
|
||||
filterStarted = it[2] as TriState,
|
||||
filterBookmarked = it[3] as TriState,
|
||||
filterExcludedScanlators = it[4] as Boolean,
|
||||
filterIncludedCategories = it[5] as List<Long>,
|
||||
filterExcludedCategories = it[6] as List<Long>,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -442,6 +452,8 @@ class UpdatesViewModel(
|
||||
val filterStarted: TriState,
|
||||
val filterBookmarked: TriState,
|
||||
val filterExcludedScanlators: Boolean,
|
||||
val filterIncludedCategories: List<Long>,
|
||||
val filterExcludedCategories: List<Long>,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
|
||||
@@ -72,6 +72,7 @@ class WebViewActivity : BaseActivity() {
|
||||
initialTitle = intent.extras?.getString(TITLE_KEY),
|
||||
url = url,
|
||||
headers = headers,
|
||||
defaultUserAgentProvider = network::defaultUserAgentProvider,
|
||||
onUrlChange = { assistUrl = it },
|
||||
onShare = this::shareWebpage,
|
||||
onOpenInBrowser = this::openInBrowser,
|
||||
|
||||
@@ -36,6 +36,7 @@ class WebViewScreen(
|
||||
initialTitle = initialTitle,
|
||||
url = url,
|
||||
headers = viewModel.headers,
|
||||
defaultUserAgentProvider = viewModel::defaultUserAgentProvider,
|
||||
onUrlChange = { assistUrl = it },
|
||||
onShare = { viewModel.shareWebpage(context, it) },
|
||||
onOpenInBrowser = { viewModel.openInBrowser(context, it) },
|
||||
|
||||
@@ -67,4 +67,6 @@ class WebViewViewModel(
|
||||
logcat { "Cleared $cleared cookies for: $url" }
|
||||
}
|
||||
}
|
||||
|
||||
fun defaultUserAgentProvider() = network.defaultUserAgentProvider()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,38 @@
|
||||
package mihon.feature.upcoming
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.category.visualName
|
||||
import eu.kanade.presentation.components.TabbedDialog
|
||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||
import tachiyomi.core.common.preference.TriState
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.SettingsItemsPaddings
|
||||
import tachiyomi.presentation.core.components.TriStateItem
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
|
||||
class UpcomingScreen : Screen() {
|
||||
|
||||
@@ -18,10 +43,93 @@ class UpcomingScreen : Screen() {
|
||||
val viewModel = viewModel<UpcomingViewModel>()
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
when (state.dialog) {
|
||||
is UpcomingViewModel.Dialog.FilterSheet -> {
|
||||
UpcomingFilterDialog(
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
|
||||
null -> {}
|
||||
}
|
||||
|
||||
UpcomingScreenContent(
|
||||
state = state,
|
||||
setSelectedYearMonth = viewModel::setSelectedYearMonth,
|
||||
onClickUpcoming = { navigator.push(MangaScreen(it.id)) },
|
||||
hasActiveFilters = state.hasActiveFilters,
|
||||
onClickFilter = viewModel::showFilterDialog,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.CategoryFilterSheet(
|
||||
viewModel: UpcomingViewModel,
|
||||
) {
|
||||
Text(
|
||||
stringResource(MR.strings.pref_filter_upcoming_categories_details),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = SettingsItemsPaddings.Horizontal,
|
||||
vertical = SettingsItemsPaddings.Vertical,
|
||||
),
|
||||
)
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(MaterialTheme.padding.extraSmall))
|
||||
|
||||
val allCategories by viewModel.getCategories.subscribe().collectAsState(initial = emptyList())
|
||||
|
||||
if (allCategories.isEmpty()) {
|
||||
LoadingScreen(modifier = Modifier.padding(16.dp))
|
||||
return
|
||||
}
|
||||
|
||||
val excluded by viewModel.upcomingPreferences.filterExcludedCategories.collectAsState()
|
||||
val included by viewModel.upcomingPreferences.filterIncludedCategories.collectAsState()
|
||||
|
||||
val selected = remember {
|
||||
allCategories.map { category ->
|
||||
when (category.id) {
|
||||
in included -> TriState.ENABLED_IS
|
||||
in excluded -> TriState.ENABLED_NOT
|
||||
else -> TriState.DISABLED
|
||||
}
|
||||
}.toMutableStateList()
|
||||
}
|
||||
|
||||
Column {
|
||||
allCategories.fastForEachIndexed { idx, category ->
|
||||
val state = selected[idx]
|
||||
TriStateItem(
|
||||
label = category.visualName,
|
||||
state = state,
|
||||
onClick = {
|
||||
selected[idx] = state.next()
|
||||
viewModel.cycleCategory(category)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpcomingFilterDialog(
|
||||
viewModel: UpcomingViewModel,
|
||||
) {
|
||||
TabbedDialog(
|
||||
onDismissRequest = viewModel::resetDialog,
|
||||
tabTitles = listOf(
|
||||
stringResource(MR.strings.categories),
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(vertical = TabbedDialogPaddings.Vertical)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
CategoryFilterSheet(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -23,9 +23,12 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
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,14 +39,15 @@ 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
|
||||
import tachiyomi.presentation.core.theme.active
|
||||
|
||||
@Composable
|
||||
fun UpcomingScreenContent(
|
||||
state: UpcomingViewModel.State,
|
||||
setSelectedYearMonth: (YearMonth) -> Unit,
|
||||
onClickUpcoming: (manga: Manga) -> Unit,
|
||||
onClickFilter: () -> Unit,
|
||||
hasActiveFilters: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -56,7 +60,12 @@ fun UpcomingScreenContent(
|
||||
}
|
||||
}
|
||||
Scaffold(
|
||||
topBar = { UpcomingToolbar() },
|
||||
topBar = {
|
||||
UpcomingToolbar(
|
||||
hasFilters = hasActiveFilters,
|
||||
onClickFilter = onClickFilter,
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
) { paddingValues ->
|
||||
if (isTabletUi()) {
|
||||
@@ -86,7 +95,10 @@ fun UpcomingScreenContent(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UpcomingToolbar() {
|
||||
private fun UpcomingToolbar(
|
||||
hasFilters: Boolean,
|
||||
onClickFilter: () -> Unit,
|
||||
) {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
@@ -94,12 +106,21 @@ private fun UpcomingToolbar() {
|
||||
title = stringResource(MR.strings.label_upcoming),
|
||||
navigateUp = navigator::pop,
|
||||
actions = {
|
||||
IconButton(onClick = { uriHandler.openUri(Constants.URL_HELP_UPCOMING) }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.HelpOutline,
|
||||
contentDescription = stringResource(MR.strings.upcoming_guide),
|
||||
)
|
||||
}
|
||||
AppBarActions(
|
||||
listOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_filter),
|
||||
icon = Icons.Outlined.FilterList,
|
||||
iconTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current,
|
||||
onClick = onClickFilter,
|
||||
),
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.upcoming_guide),
|
||||
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
||||
onClick = { uriHandler.openUri(Constants.URL_HELP_UPCOMING) },
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -171,6 +192,7 @@ private fun UpcomingScreenSmallImpl(
|
||||
onClick = { onClickUpcoming(item.manga) },
|
||||
)
|
||||
}
|
||||
|
||||
is UpcomingUIModel.Header -> {
|
||||
DateHeading(
|
||||
date = item.date,
|
||||
@@ -222,6 +244,7 @@ private fun UpcomingScreenLargeImpl(
|
||||
onClick = { onClickUpcoming(item.manga) },
|
||||
)
|
||||
}
|
||||
|
||||
is UpcomingUIModel.Header -> {
|
||||
DateHeading(
|
||||
date = item.date,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,37 +1,63 @@
|
||||
package mihon.feature.upcoming
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
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.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
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.core.common.preference.getAndSet
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.upcoming.service.UpcomingPreferences
|
||||
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(),
|
||||
val getCategories: GetCategories = Injekt.get(),
|
||||
val upcomingPreferences: UpcomingPreferences = Injekt.get(),
|
||||
) : StateViewModel<UpcomingViewModel.State>(State()) {
|
||||
|
||||
val excludedCategories = upcomingPreferences.filterExcludedCategories
|
||||
val includedCategories = upcomingPreferences.filterIncludedCategories
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
getUpcomingManga.subscribe().collectLatest {
|
||||
mutableState.update { state ->
|
||||
val upcomingItems = it.toUpcomingUIModels()
|
||||
state.copy(
|
||||
items = upcomingItems,
|
||||
events = upcomingItems.toEvents(),
|
||||
headerIndexes = upcomingItems.getHeaderIndexes(),
|
||||
viewModelScope.launchIO {
|
||||
getUpcomingItemPreferenceFlow()
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest {
|
||||
getUpcomingManga.subscribe(
|
||||
excludedCategories = it.filterExcludedCategories,
|
||||
includedCategories = it.filterIncludedCategories,
|
||||
)
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
.collectLatest {
|
||||
mutableState.update { state ->
|
||||
val upcomingItems = it.toUpcomingUIModels()
|
||||
state.copy(
|
||||
items = upcomingItems,
|
||||
events = upcomingItems.toEvents(),
|
||||
headerIndexes = upcomingItems.getHeaderIndexes(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,8 +67,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 }
|
||||
@@ -72,10 +104,57 @@ class UpcomingViewModel(
|
||||
mutableState.update { it.copy(selectedYearMonth = yearMonth) }
|
||||
}
|
||||
|
||||
private fun getUpcomingItemPreferenceFlow(): Flow<ItemPreferences> {
|
||||
return combine(
|
||||
upcomingPreferences.filterExcludedCategories.changes(),
|
||||
upcomingPreferences.filterIncludedCategories.changes(),
|
||||
) { excluded, included ->
|
||||
ItemPreferences(
|
||||
filterExcludedCategories = excluded,
|
||||
filterIncludedCategories = included,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetDialog() {
|
||||
mutableState.update { it.copy(dialog = null) }
|
||||
}
|
||||
|
||||
fun showFilterDialog() {
|
||||
mutableState.update { it.copy(dialog = Dialog.FilterSheet) }
|
||||
}
|
||||
|
||||
fun cycleCategory(category: Category) {
|
||||
when (category.id) {
|
||||
in includedCategories.get() -> {
|
||||
includedCategories.getAndSet { it - category.id }
|
||||
excludedCategories.getAndSet { it + category.id }
|
||||
}
|
||||
|
||||
in excludedCategories.get() -> excludedCategories.getAndSet { it - category.id }
|
||||
else -> includedCategories.getAndSet { it + category.id }
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
private data class ItemPreferences(
|
||||
val filterExcludedCategories: List<Long>,
|
||||
val filterIncludedCategories: List<Long>,
|
||||
)
|
||||
|
||||
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(),
|
||||
val hasActiveFilters: Boolean = false,
|
||||
val dialog: Dialog? = null,
|
||||
)
|
||||
|
||||
sealed interface Dialog {
|
||||
data object FilterSheet : Dialog
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {},
|
||||
)
|
||||
|
||||
@@ -41,6 +41,7 @@ dependencies {
|
||||
api(libs.kotlinx.serialization.jsonOkio)
|
||||
|
||||
api(libs.androidx.preference)
|
||||
implementation(libs.androidx.webkit)
|
||||
|
||||
implementation(libs.jsoup)
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class NetworkHelper(
|
||||
private val context: Context,
|
||||
@@ -20,9 +21,9 @@ class NetworkHelper(
|
||||
private val clientBuilder: OkHttpClient.Builder = run {
|
||||
val builder = OkHttpClient.Builder()
|
||||
.cookieJar(cookieJar)
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.callTimeout(2, TimeUnit.MINUTES)
|
||||
.connectTimeout(30.seconds)
|
||||
.readTimeout(30.seconds)
|
||||
.callTimeout(2.minutes)
|
||||
.cache(
|
||||
Cache(
|
||||
directory = File(context.cacheDir, "network_cache"),
|
||||
|
||||
@@ -14,6 +14,6 @@ class NetworkPreferences(
|
||||
|
||||
val defaultUserAgent: Preference<String> = preferenceStore.getString(
|
||||
"default_user_agent",
|
||||
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Mobile Safari/537.36",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
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_BODY: RequestBody = FormBody.Builder().build()
|
||||
|
||||
|
||||
+2
-1
@@ -8,6 +8,7 @@ import android.widget.Toast
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
import eu.kanade.tachiyomi.util.system.setUserAgent
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Interceptor
|
||||
@@ -84,7 +85,7 @@ abstract class WebViewInterceptor(
|
||||
return WebView(context).apply {
|
||||
setDefaultSettings()
|
||||
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
|
||||
settings.userAgentString = request.header("User-Agent") ?: defaultUserAgentProvider()
|
||||
setUserAgent(request.header("User-Agent") ?: defaultUserAgentProvider())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ import android.content.pm.PackageManager
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import androidx.webkit.UserAgentMetadata
|
||||
import androidx.webkit.WebSettingsCompat
|
||||
import androidx.webkit.WebViewFeature
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
@@ -99,6 +102,53 @@ fun WebView.setDefaultSettings() {
|
||||
CookieManager.getInstance().acceptThirdPartyCookies(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user agent along with the matching user agent metadata, which Chromium uses to populate
|
||||
* the `Sec-CH-UA` client hints. Without this the hints keep advertising the real WebView brand and
|
||||
* version, contradicting the spoofed user agent.
|
||||
*/
|
||||
fun WebView.setUserAgent(userAgent: String) {
|
||||
settings.userAgentString = userAgent
|
||||
|
||||
if (!WebViewFeature.isFeatureSupported(WebViewFeature.USER_AGENT_METADATA)) return
|
||||
|
||||
val versionMatch = CHROME_VERSION_REGEX.find(userAgent) ?: return
|
||||
val majorVersion = versionMatch.groupValues[1]
|
||||
val fullVersion = majorVersion + versionMatch.groupValues[2].ifEmpty { ".0.0.0" }
|
||||
|
||||
try {
|
||||
val metadata = WebSettingsCompat.getUserAgentMetadata(settings)
|
||||
val brandVersionList = metadata.brandVersionList.map { brandVersion ->
|
||||
val brand = when (brandVersion.brand) {
|
||||
WEBVIEW_BRAND -> CHROME_BRAND
|
||||
CHROMIUM_BRAND -> CHROMIUM_BRAND
|
||||
else -> return@map brandVersion
|
||||
}
|
||||
|
||||
UserAgentMetadata.BrandVersion.Builder()
|
||||
.setBrand(brand)
|
||||
.setMajorVersion(majorVersion)
|
||||
.setFullVersion(fullVersion)
|
||||
.build()
|
||||
}
|
||||
|
||||
WebSettingsCompat.setUserAgentMetadata(
|
||||
settings,
|
||||
UserAgentMetadata.Builder(metadata)
|
||||
.setBrandVersionList(brandVersionList)
|
||||
.setFullVersion(fullVersion)
|
||||
.build(),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to set user agent metadata" }
|
||||
}
|
||||
}
|
||||
|
||||
private const val WEBVIEW_BRAND = "Android WebView"
|
||||
private const val CHROMIUM_BRAND = "Chromium"
|
||||
private const val CHROME_BRAND = "Google Chrome"
|
||||
private val CHROME_VERSION_REGEX = """Chrome/(\d+)(\.[\d.]+)?""".toRegex()
|
||||
|
||||
private fun WebView.getWebViewMajorVersion(): Int {
|
||||
val uaRegexMatch = """.*Chrome/(\d+)\..*""".toRegex().matchEntire(getDefaultUserAgentString())
|
||||
return if (uaRegexMatch != null && uaRegexMatch.groupValues.size > 1) {
|
||||
|
||||
@@ -38,5 +38,7 @@ dependencies {
|
||||
|
||||
implementation(libs.injekt)
|
||||
|
||||
implementation(libs.kotlinx.datetime)
|
||||
|
||||
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.awaitAsOneOrNull
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.atStartOfDayIn
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
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.MangaWithChapterCount
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
import kotlin.time.Clock
|
||||
|
||||
class MangaRepositoryImpl(
|
||||
private val database: Database,
|
||||
@@ -85,10 +87,24 @@ class MangaRepositoryImpl(
|
||||
.awaitAsList()
|
||||
}
|
||||
|
||||
override suspend fun getUpcomingManga(statuses: Set<Long>): Flow<List<Manga>> {
|
||||
val epochMillis = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toEpochSecond() * 1000
|
||||
override suspend fun getUpcomingManga(
|
||||
statuses: Set<Long>,
|
||||
excludedCategories: List<Long>,
|
||||
includedCategories: List<Long>,
|
||||
): Flow<List<Manga>> {
|
||||
val timeZone = TimeZone.currentSystemDefault()
|
||||
val epochMillis =
|
||||
Clock.System.now().toLocalDateTime(timeZone).date.atStartOfDayIn(timeZone).toEpochMilliseconds()
|
||||
return database.mangasQueries
|
||||
.getUpcomingManga(epochMillis, statuses, MangaMapper::mapManga)
|
||||
.getUpcomingManga(
|
||||
startOfDay = epochMillis,
|
||||
statuses = statuses,
|
||||
includedEmpty = includedCategories.isEmpty(),
|
||||
includedCategories = includedCategories,
|
||||
excludedEmpty = excludedCategories.isEmpty(),
|
||||
excludedCategories = excludedCategories,
|
||||
mapper = MangaMapper::mapManga,
|
||||
)
|
||||
.subscribeToList()
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ class UpdatesRepositoryImpl(
|
||||
started: Boolean?,
|
||||
bookmarked: Boolean?,
|
||||
hideExcludedScanlators: Boolean,
|
||||
includedCategories: List<Long>,
|
||||
excludedCategories: List<Long>,
|
||||
): Flow<List<UpdatesWithRelations>> {
|
||||
return database.updatesViewQueries
|
||||
.getRecentUpdatesWithFilters(
|
||||
@@ -44,6 +46,10 @@ class UpdatesRepositoryImpl(
|
||||
started = started?.toLong(),
|
||||
bookmarked = bookmarked,
|
||||
hideExcludedScanlators = hideExcludedScanlators.toLong(),
|
||||
includedEmpty = includedCategories.isEmpty(),
|
||||
excludedEmpty = excludedCategories.isEmpty(),
|
||||
includedCategories = includedCategories,
|
||||
excludedCategories = excludedCategories,
|
||||
mapper = ::mapUpdatesWithRelations,
|
||||
)
|
||||
.subscribeToList()
|
||||
|
||||
@@ -164,6 +164,33 @@ FROM mangas
|
||||
WHERE next_update >= :startOfDay
|
||||
AND favorite = 1
|
||||
AND status IN :statuses
|
||||
AND (
|
||||
-- includedEmpty being true expresses "don't care" state and bypasses the membership filter
|
||||
:includedEmpty
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM mangas_categories
|
||||
WHERE mangas_categories.manga_id = mangas._id
|
||||
AND COALESCE(mangas_categories.category_id, 0) IN :includedCategories
|
||||
)
|
||||
OR (0 IN :includedCategories AND NOT EXISTS (
|
||||
SELECT 1 FROM mangas_categories WHERE mangas_categories.manga_id = mangas._id)
|
||||
)
|
||||
)
|
||||
AND (
|
||||
-- excludedEmpty being true expresses "don't care" state and bypasses the membership filter
|
||||
:excludedEmpty
|
||||
OR (
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM mangas_categories
|
||||
WHERE mangas_categories.manga_id = mangas._id
|
||||
AND COALESCE(mangas_categories.category_id, 0) IN :excludedCategories
|
||||
)
|
||||
AND (
|
||||
0 NOT IN :excludedCategories
|
||||
OR EXISTS (SELECT 1 FROM mangas_categories WHERE mangas_categories.manga_id = mangas._id)
|
||||
)
|
||||
)
|
||||
)
|
||||
ORDER BY next_update ASC;
|
||||
|
||||
resetViewerFlags:
|
||||
|
||||
@@ -46,6 +46,33 @@ AND (:bookmarked IS NULL OR bookmark = :bookmarked)
|
||||
AND (
|
||||
(excludedScanlator IS NULL OR :hideExcludedScanlators = 0)
|
||||
)
|
||||
AND (
|
||||
-- includedEmpty being true expresses "don't care" state and bypasses the membership filter
|
||||
:includedEmpty
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM mangas_categories
|
||||
WHERE mangas_categories.manga_id = mangaId
|
||||
AND COALESCE(mangas_categories.category_id, 0) IN :includedCategories
|
||||
)
|
||||
OR (0 IN :includedCategories AND NOT EXISTS (
|
||||
SELECT 1 FROM mangas_categories mc WHERE mc.manga_id = mangaId)
|
||||
)
|
||||
)
|
||||
AND (
|
||||
-- excludedEmpty being true expresses "don't care" state and bypasses the membership filter
|
||||
:excludedEmpty
|
||||
OR (
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM mangas_categories
|
||||
WHERE mangas_categories.manga_id = mangaId
|
||||
AND COALESCE(mangas_categories.category_id, 0) IN :excludedCategories
|
||||
)
|
||||
AND (
|
||||
0 NOT IN :excludedCategories
|
||||
OR EXISTS (SELECT 1 FROM mangas_categories WHERE mangas_categories.manga_id = mangaId)
|
||||
)
|
||||
)
|
||||
)
|
||||
LIMIT :limit;
|
||||
|
||||
getUpdatesByReadStatus:
|
||||
|
||||
@@ -22,6 +22,8 @@ dependencies {
|
||||
implementation(libs.bundles.kotlinx.coroutines)
|
||||
implementation(libs.bundles.serialization)
|
||||
|
||||
implementation(libs.kotlinx.datetime)
|
||||
|
||||
implementation(libs.unifile)
|
||||
|
||||
api(libs.sqldelight.androidxPaging)
|
||||
|
||||
@@ -14,7 +14,14 @@ class GetUpcomingManga(
|
||||
SManga.PUBLISHING_FINISHED.toLong(),
|
||||
)
|
||||
|
||||
suspend fun subscribe(): Flow<List<Manga>> {
|
||||
return mangaRepository.getUpcomingManga(includedStatuses)
|
||||
suspend fun subscribe(
|
||||
excludedCategories: List<Long>,
|
||||
includedCategories: List<Long>,
|
||||
): Flow<List<Manga>> {
|
||||
return mangaRepository.getUpcomingManga(
|
||||
includedStatuses,
|
||||
excludedCategories = excludedCategories,
|
||||
includedCategories = includedCategories,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
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.model.Chapter
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
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.time.Clock
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Instant
|
||||
|
||||
class FetchInterval(
|
||||
private val getChaptersByMangaId: GetChaptersByMangaId,
|
||||
@@ -16,40 +22,42 @@ class FetchInterval(
|
||||
|
||||
suspend fun toMangaUpdate(
|
||||
manga: Manga,
|
||||
dateTime: ZonedDateTime,
|
||||
dateTime: LocalDateTime,
|
||||
timeZone: TimeZone,
|
||||
window: Pair<Long, Long>,
|
||||
): MangaUpdate {
|
||||
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(
|
||||
chapters = getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true),
|
||||
zone = dateTime.zone,
|
||||
zone = timeZone,
|
||||
)
|
||||
val currentWindow = if (window.first == 0L && window.second == 0L) {
|
||||
getWindow(ZonedDateTime.now())
|
||||
getWindow(Clock.System.now().toLocalDateTime(timeZone).date, timeZone)
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
|
||||
fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
|
||||
val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone)
|
||||
val lowerBound = today.minusDays(GRACE_PERIOD)
|
||||
val upperBound = today.plusDays(GRACE_PERIOD)
|
||||
return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1)
|
||||
fun getWindow(localDateTime: LocalDate, timeZone: TimeZone): Pair<Long, Long> {
|
||||
val today = localDateTime.atStartOfDayIn(timeZone)
|
||||
val lowerBound = today - GRACE_PERIOD.days
|
||||
val upperBound = today + GRACE_PERIOD.days
|
||||
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 uploadDates = chapters.asSequence()
|
||||
.filter { it.dateUpload > 0L }
|
||||
.sortedByDescending { it.dateUpload }
|
||||
.map {
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
Instant.fromEpochMilliseconds(it.dateUpload)
|
||||
.toLocalDateTime(zone)
|
||||
.date
|
||||
.atStartOfDayIn(zone)
|
||||
}
|
||||
.distinct()
|
||||
.take(chapterWindow)
|
||||
@@ -58,9 +66,10 @@ class FetchInterval(
|
||||
val fetchDates = chapters.asSequence()
|
||||
.sortedByDescending { it.dateFetch }
|
||||
.map {
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
Instant.fromEpochMilliseconds(it.dateFetch)
|
||||
.toLocalDateTime(zone)
|
||||
.date
|
||||
.atStartOfDayIn(zone)
|
||||
}
|
||||
.distinct()
|
||||
.take(chapterWindow)
|
||||
@@ -69,13 +78,13 @@ class FetchInterval(
|
||||
val interval = when {
|
||||
// Enough upload date from source
|
||||
uploadDates.size >= 3 -> {
|
||||
val ranges = uploadDates.windowed(2).map { x -> x[1].until(x[0], ChronoUnit.DAYS) }.sorted()
|
||||
ranges[(ranges.size - 1) / 2].toInt()
|
||||
val ranges = uploadDates.windowed(2).map { x -> x[1].daysUntil(x[0], zone) }.sorted()
|
||||
ranges[(ranges.size - 1) / 2]
|
||||
}
|
||||
// Enough fetch date from client
|
||||
fetchDates.size >= 3 -> {
|
||||
val ranges = fetchDates.windowed(2).map { x -> x[1].until(x[0], ChronoUnit.DAYS) }.sorted()
|
||||
ranges[(ranges.size - 1) / 2].toInt()
|
||||
val ranges = fetchDates.windowed(2).map { x -> x[1].daysUntil(x[0], zone) }.sorted()
|
||||
ranges[(ranges.size - 1) / 2]
|
||||
}
|
||||
// Default to 7 days
|
||||
else -> 7
|
||||
@@ -87,34 +96,34 @@ class FetchInterval(
|
||||
private fun calculateNextUpdate(
|
||||
manga: Manga,
|
||||
interval: Int,
|
||||
dateTime: ZonedDateTime,
|
||||
dateTime: LocalDateTime,
|
||||
timeZone: TimeZone,
|
||||
window: Pair<Long, Long>,
|
||||
): Long {
|
||||
if (manga.nextUpdate in window.first.rangeTo(window.second + 1)) {
|
||||
return manga.nextUpdate
|
||||
}
|
||||
|
||||
val latestDate = ZonedDateTime.ofInstant(
|
||||
if (manga.lastUpdate > 0) Instant.ofEpochMilli(manga.lastUpdate) else Instant.now(),
|
||||
dateTime.zone,
|
||||
)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, dateTime).toInt()
|
||||
val cycle = timeSinceLatest.floorDiv(
|
||||
val instant = if (manga.lastUpdate > 0) Instant.fromEpochMilliseconds(manga.lastUpdate) else Clock.System.now()
|
||||
val latestDate = instant.toLocalDateTime(timeZone).date.atStartOfDayIn(timeZone)
|
||||
|
||||
val daysSinceLatest = (dateTime.toInstant(timeZone) - latestDate).inWholeDays
|
||||
val cycle = daysSinceLatest.floorDiv(
|
||||
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
|
||||
|
||||
// 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) {
|
||||
increaseInterval(delta * 2, timeSinceLatest, increaseWhenOver)
|
||||
increaseInterval(delta * 2, daysSinceLatest, increaseWhenOver)
|
||||
} else {
|
||||
delta
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import kotlinx.serialization.json.JsonObject
|
||||
import mihon.core.common.extensions.EMPTY
|
||||
import tachiyomi.core.common.preference.TriState
|
||||
import java.io.ObjectStreamException
|
||||
import java.time.Instant
|
||||
import kotlin.time.Instant
|
||||
import java.io.Serializable as JavaSerializable
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
@@ -47,7 +47,7 @@ data class Manga(
|
||||
val expectedNextUpdate: Instant?
|
||||
get() = nextUpdate
|
||||
.takeIf { status != SManga.COMPLETED.toLong() }
|
||||
?.let { Instant.ofEpochMilli(it) }
|
||||
?.let { Instant.fromEpochMilliseconds(it) }
|
||||
|
||||
val sorting: Long
|
||||
get() = chapterFlags and CHAPTER_SORTING_MASK
|
||||
|
||||
@@ -28,7 +28,11 @@ interface MangaRepository {
|
||||
|
||||
suspend fun getDuplicateLibraryManga(id: Long, title: String): List<MangaWithChapterCount>
|
||||
|
||||
suspend fun getUpcomingManga(statuses: Set<Long>): Flow<List<Manga>>
|
||||
suspend fun getUpcomingManga(
|
||||
statuses: Set<Long>,
|
||||
excludedCategories: List<Long>,
|
||||
includedCategories: List<Long>,
|
||||
): Flow<List<Manga>>
|
||||
|
||||
suspend fun resetViewerFlags(): Boolean
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package tachiyomi.domain.upcoming.service
|
||||
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.core.common.preference.getLongArray
|
||||
|
||||
class UpcomingPreferences(
|
||||
preferenceStore: PreferenceStore,
|
||||
) {
|
||||
|
||||
val filterIncludedCategories: Preference<List<Long>> = preferenceStore.getLongArray(
|
||||
"pref_filter_upcoming_included_categories",
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
val filterExcludedCategories: Preference<List<Long>> = preferenceStore.getLongArray(
|
||||
"pref_filter_upcoming_excluded_categories",
|
||||
emptyList(),
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package tachiyomi.domain.updates.interactor
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import tachiyomi.domain.updates.model.UpdatesWithRelations
|
||||
import tachiyomi.domain.updates.repository.UpdatesRepository
|
||||
import java.time.Instant
|
||||
import kotlin.time.Instant
|
||||
|
||||
class GetUpdates(
|
||||
private val repository: UpdatesRepository,
|
||||
@@ -19,14 +19,18 @@ class GetUpdates(
|
||||
started: Boolean?,
|
||||
bookmarked: Boolean?,
|
||||
hideExcludedScanlators: Boolean,
|
||||
includedCategories: List<Long>,
|
||||
excludedCategories: List<Long>,
|
||||
): Flow<List<UpdatesWithRelations>> {
|
||||
return repository.subscribeAll(
|
||||
instant.toEpochMilli(),
|
||||
instant.toEpochMilliseconds(),
|
||||
limit = 500,
|
||||
unread = unread,
|
||||
started = started,
|
||||
bookmarked = bookmarked,
|
||||
hideExcludedScanlators = hideExcludedScanlators,
|
||||
includedCategories = includedCategories,
|
||||
excludedCategories = excludedCategories,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ interface UpdatesRepository {
|
||||
started: Boolean?,
|
||||
bookmarked: Boolean?,
|
||||
hideExcludedScanlators: Boolean,
|
||||
includedCategories: List<Long>,
|
||||
excludedCategories: List<Long>,
|
||||
): Flow<List<UpdatesWithRelations>>
|
||||
|
||||
fun subscribeWithRead(read: Boolean, after: Long, limit: Long): Flow<List<UpdatesWithRelations>>
|
||||
|
||||
@@ -4,6 +4,7 @@ import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.core.common.preference.TriState
|
||||
import tachiyomi.core.common.preference.getEnum
|
||||
import tachiyomi.core.common.preference.getLongArray
|
||||
|
||||
class UpdatesPreferences(
|
||||
preferenceStore: PreferenceStore,
|
||||
@@ -33,4 +34,14 @@ class UpdatesPreferences(
|
||||
"pref_filter_updates_hide_excluded_scanlators",
|
||||
false,
|
||||
)
|
||||
|
||||
val filterIncludedCategories: Preference<List<Long>> = preferenceStore.getLongArray(
|
||||
"pref_filter_updates_included_categories",
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
val filterExcludedCategories: Preference<List<Long>> = preferenceStore.getLongArray(
|
||||
"pref_filter_updates_excluded_categories",
|
||||
emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,27 +2,27 @@ package tachiyomi.domain.manga.interactor
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
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.parallel.Execution
|
||||
import org.junit.jupiter.api.parallel.ExecutionMode
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
import kotlin.time.toJavaDuration
|
||||
|
||||
@Execution(ExecutionMode.CONCURRENT)
|
||||
class FetchIntervalTest {
|
||||
|
||||
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
|
||||
private val testZoneId = ZoneOffset.UTC
|
||||
private val testTime = LocalDateTime.parse("2020-01-01T00:00:00")
|
||||
private val testTimeZone = TimeZone.UTC
|
||||
private var chapter = Chapter.create().copy(
|
||||
dateFetch = testTime.toEpochSecond() * 1000,
|
||||
dateUpload = testTime.toEpochSecond() * 1000,
|
||||
dateFetch = testTime.toInstant(testTimeZone).toEpochMilliseconds(),
|
||||
dateUpload = testTime.toInstant(testTimeZone).toEpochMilliseconds(),
|
||||
)
|
||||
|
||||
private val fetchInterval = FetchInterval(mockk())
|
||||
@@ -32,12 +32,12 @@ class FetchIntervalTest {
|
||||
val chaptersWithUploadDate = (1..50).map {
|
||||
chapterWithTime(chapter, 1.days)
|
||||
}
|
||||
fetchInterval.calculateInterval(chaptersWithUploadDate, testZoneId) shouldBe 7
|
||||
fetchInterval.calculateInterval(chaptersWithUploadDate, testTimeZone) shouldBe 7
|
||||
|
||||
val chaptersWithoutUploadDate = chaptersWithUploadDate.map {
|
||||
it.copy(dateUpload = 0L)
|
||||
}
|
||||
fetchInterval.calculateInterval(chaptersWithoutUploadDate, testZoneId) shouldBe 7
|
||||
fetchInterval.calculateInterval(chaptersWithoutUploadDate, testTimeZone) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -51,7 +51,7 @@ class FetchIntervalTest {
|
||||
|
||||
val chapters = oldChapters + newChapters
|
||||
|
||||
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1
|
||||
fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -66,7 +66,7 @@ class FetchIntervalTest {
|
||||
|
||||
val chapters = oldChapters + newChapters
|
||||
|
||||
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 7
|
||||
fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -74,7 +74,7 @@ class FetchIntervalTest {
|
||||
val chapters = (1..10).map {
|
||||
chapterWithTime(chapter, 10.hours)
|
||||
}
|
||||
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 7
|
||||
fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -84,7 +84,7 @@ class FetchIntervalTest {
|
||||
} + (1..5).map {
|
||||
chapterWithTime(chapter, 2.days)
|
||||
}
|
||||
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 7
|
||||
fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -92,7 +92,7 @@ class FetchIntervalTest {
|
||||
val chapters = (1..20).map {
|
||||
chapterWithTime(chapter, it.days)
|
||||
}
|
||||
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1
|
||||
fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -100,7 +100,7 @@ class FetchIntervalTest {
|
||||
val chapters = (1..20).map {
|
||||
chapterWithTime(chapter, (15 * it).hours)
|
||||
}
|
||||
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1
|
||||
fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -108,7 +108,7 @@ class FetchIntervalTest {
|
||||
val chapters = (1..20).map {
|
||||
chapterWithTime(chapter, (2 * it).days)
|
||||
}
|
||||
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 2
|
||||
fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 2
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -116,12 +116,12 @@ class FetchIntervalTest {
|
||||
val chaptersWithUploadDate = (1..5).map {
|
||||
chapterWithTime(chapter, (25 * it).hours)
|
||||
}
|
||||
fetchInterval.calculateInterval(chaptersWithUploadDate, testZoneId) shouldBe 1
|
||||
fetchInterval.calculateInterval(chaptersWithUploadDate, testTimeZone) shouldBe 1
|
||||
|
||||
val chaptersWithoutUploadDate = chaptersWithUploadDate.map {
|
||||
it.copy(dateUpload = 0L)
|
||||
}
|
||||
fetchInterval.calculateInterval(chaptersWithoutUploadDate, testZoneId) shouldBe 1
|
||||
fetchInterval.calculateInterval(chaptersWithoutUploadDate, testTimeZone) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -129,11 +129,11 @@ class FetchIntervalTest {
|
||||
val chapters = (1..20).map {
|
||||
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 {
|
||||
val newTime = testTime.plus(duration.toJavaDuration()).toEpochSecond() * 1000
|
||||
val newTime = testTime.toInstant(testTimeZone).plus(duration).toEpochMilliseconds()
|
||||
return chapter.copy(dateFetch = newTime, dateUpload = newTime)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package mihon.gradle
|
||||
|
||||
import org.gradle.api.Project
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration.Companion.nanoseconds
|
||||
import kotlin.time.Instant
|
||||
|
||||
// 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
|
||||
@@ -18,19 +17,18 @@ fun Project.getLatestCommitSha(): String {
|
||||
// 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;
|
||||
* 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 {
|
||||
return if (useLatestCommitTime) {
|
||||
val epoch = exec("git log -1 --format=%ct").toLong()
|
||||
Instant.ofEpochSecond(epoch).atOffset(ZoneOffset.UTC).format(BUILD_TIME_FORMATTER)
|
||||
Instant.fromEpochSeconds(epoch).toString()
|
||||
} else {
|
||||
LocalDateTime.now(ZoneOffset.UTC).format(BUILD_TIME_FORMATTER)
|
||||
val now = Clock.System.now()
|
||||
(now - now.nanosecondsOfSecond.nanoseconds).toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ androidx-test-espresso = "3.7.0"
|
||||
androidx-test-junit = "1.3.0"
|
||||
androidx-test-uiautomator = "2.4.0"
|
||||
androidx-viewPager = "1.1.0"
|
||||
androidx-webkit = "1.16.0"
|
||||
androidx-work = "2.11.2"
|
||||
archive = "1.1.6"
|
||||
coil = "3.5.0"
|
||||
@@ -45,6 +46,7 @@ junit = "6.1.2"
|
||||
kotest-assertions = "6.2.3"
|
||||
kotlin-gradle = "2.4.10"
|
||||
kotlinx-coroutines = "1.11.0"
|
||||
kotlinx-datetime = "0.8.0"
|
||||
kotlinx-serialization = "1.11.0"
|
||||
ktlint = "1.8.0"
|
||||
leakCanary = "2.14"
|
||||
@@ -112,6 +114,7 @@ androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core",
|
||||
androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-junit" }
|
||||
androidx-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "androidx-test-uiautomator" }
|
||||
androidx-viewPager = { module = "androidx.viewpager:viewpager", version.ref = "androidx-viewPager" }
|
||||
androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "androidx-webkit" }
|
||||
androidx-work = { module = "androidx.work:work-runtime", version.ref = "androidx-work" }
|
||||
archive = { module = "me.zhanghai.android.libarchive:library", version.ref = "archive" }
|
||||
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
|
||||
@@ -142,6 +145,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-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-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-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" }
|
||||
|
||||
@@ -861,11 +861,13 @@
|
||||
<string name="relative_time_span_never">Never</string>
|
||||
<string name="action_view_upcoming">View Upcoming Updates</string>
|
||||
<string name="action_filter_excluded_scanlators">Filter excluded scanlators</string>
|
||||
<string name="pref_filter_update_categories_details">Updates in excluded categories will not be shown even if they are also in any included categories.</string>
|
||||
|
||||
<!-- Upcoming -->
|
||||
<string name="upcoming_guide">Upcoming Guide</string>
|
||||
<string name="upcoming_calendar_next">Next Month</string>
|
||||
<string name="upcoming_calendar_prev">Previous Month</string>
|
||||
<string name="pref_filter_upcoming_categories_details">Titles in excluded categories are not shown, even if they are also in any of the included categories.</string>
|
||||
|
||||
<!-- History -->
|
||||
<string name="recent_manga_time">Ch. %1$s - %2$s</string>
|
||||
|
||||
@@ -18,6 +18,8 @@ dependencies {
|
||||
implementation(libs.androidx.glance.appWidget)
|
||||
implementation(libs.material)
|
||||
|
||||
implementation(libs.kotlinx.datetime)
|
||||
|
||||
implementation(libs.coil.core)
|
||||
|
||||
api(libs.injekt)
|
||||
|
||||
+7
-4
@@ -34,6 +34,9 @@ import coil3.transform.RoundedCornersTransformation
|
||||
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
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.domain.manga.model.MangaCover
|
||||
import tachiyomi.domain.updates.interactor.GetUpdates
|
||||
@@ -46,8 +49,8 @@ import tachiyomi.presentation.widget.util.appWidgetBackgroundRadius
|
||||
import tachiyomi.presentation.widget.util.calculateRowAndColumnCount
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
abstract class BaseUpdatesGridGlanceWidget(
|
||||
private val context: Context = Injekt.get<Application>(),
|
||||
@@ -90,7 +93,7 @@ abstract class BaseUpdatesGridGlanceWidget(
|
||||
|
||||
val flow = remember {
|
||||
getUpdates
|
||||
.subscribe(false, DateLimit.toEpochMilli())
|
||||
.subscribe(false, DateLimit.toEpochMilliseconds())
|
||||
.map { rawData ->
|
||||
rawData.prepareData(rowCount, columnCount)
|
||||
}
|
||||
@@ -153,6 +156,6 @@ abstract class BaseUpdatesGridGlanceWidget(
|
||||
|
||||
companion object {
|
||||
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) {
|
||||
combine(
|
||||
getUpdates.subscribe(read = false, after = BaseUpdatesGridGlanceWidget.DateLimit.toEpochMilli()),
|
||||
getUpdates.subscribe(read = false, after = BaseUpdatesGridGlanceWidget.DateLimit.toEpochMilliseconds()),
|
||||
securityPreferences.useAuthenticator.changes(),
|
||||
transform = { a, b -> a to b },
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user